diff --git a/.cargo/config.toml b/.cargo/config.toml index bff29e6..3711c26 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,6 @@ -[build] -rustflags = ["--cfg", "tokio_unstable"] +[target.x86_64-unknown-linux-gnu] +rustflags = [ + "--codegen", "linker=clang", + "--codegen", "link-arg=--ld-path=/usr/bin/mold", + "--cfg", "tokio_unstable" +] diff --git a/src/elements/list.rs b/src/elements/list.rs new file mode 100644 index 0000000..fd13576 --- /dev/null +++ b/src/elements/list.rs @@ -0,0 +1,242 @@ +use maud::{html, Markup}; + +use super::HxConfig; + +pub struct Link<'a> { + pub text: &'a str, + pub href: String, + pub hx_config: Option, +} + +pub struct NumberWithBar { + pub value: i64, + pub max_value: i64, +} + +pub struct Icon { + pub icon: super::Icon, + pub href: String, + pub hx_config: Option, +} + +pub enum CellType<'a> { + Text(&'a str), + Link(Link<'a>), + NumberWithBar(NumberWithBar), + Icon(Icon), +} + +pub struct Cell<'a> { + pub cell_type: CellType<'a>, +} + +impl<'a> Cell<'a> { + fn render(self) -> Markup { + match self.cell_type { + CellType::Text(text) => html!( + td + ."border" + ."p-0" + ."m-0" + { + p { (text) } + } + ), + CellType::Link(link) => { + let (hx_post, hx_swap, hx_target) = if let Some(hx_config) = link.hx_config { + ( + Some(hx_config.hx_post), + Some(hx_config.hx_swap), + Some(hx_config.hx_target), + ) + } else { + (None, None, None) + }; + html!( + td + ."border" + ."p-0" + ."m-0" + { + a + ."inline-block" + ."p-2" + ."m-0" + ."w-full" + + href=(link.href) + hx-post=[hx_post] + hx-swap=[hx_swap] + hx-target=[hx_target] + { + (link.text) + } + } + ) + } + CellType::NumberWithBar(number) => html!( + td + ."border" + ."p-2" + ."m-0" + style="position:relative;" + { + p { + (number.value) + } + div ."bg-blue-600" ."h-1.5" + style=( + format!( + "width: {width}%;position:absolute;left:0;bottom:0;right:0;", + width=( + (number.value as f64) + / (number.max_value as f64) + * 100.0 + ) + ) + ) + {} + } + ), + CellType::Icon(icon) => html!( + td + ."border-none" + ."p-0" + .(icon.icon.background()) + .(icon.icon.background_hover()) + ."h-full" + ."w-10" + { + a + href=(icon.href) + ."aspect-square" + ."flex" + { + span + ."m-auto" + ."mdi" + ."text-xl" + .(icon.icon.mdi_class()) + {} + } + } + ), + } + } +} + +pub struct EditingConfig { + pub edit_href: String, + pub edit_hx_config: Option, + pub delete_href: String, + pub delete_hx_config: Option, +} + +pub trait Row { + fn is_active(&self) -> bool { + false + } + + fn is_edit(&self) -> bool { + false + } + + fn editing_config(&self) -> Option { + None + } + + fn cells(&self) -> Vec; +} + +pub struct Header<'c> { + pub cells: Vec>>, +} + +pub struct HeaderCell<'a> { + pub title: &'a str, +} + +impl<'c> HeaderCell<'c> { + fn title(&self) -> &str { + &self.title + } +} + +pub struct List<'hc, R> +where + R: Row, +{ + pub id: Option<&'static str>, + pub header: Header<'hc>, + pub rows: Vec, + pub editing_config: Option EditingConfig>>, +} + +impl<'hc, R> List<'hc, R> +where + R: Row, +{ + pub fn render(self) -> Markup { + html!( + table + id=[self.id] + ."table" + ."table-auto" + ."border-collapse" + ."border-spacing-0" + ."border" + ."w-full" + { + thead ."bg-gray-200" { + tr + ."h-10" + { + @for header_cell in self.header.cells.iter() { + th ."border" ."p-2" { (header_cell.as_ref().map_or("", |c| c.title())) } + } + @if self.editing_config.is_some() { + th {} + th {} + } + } + } + tbody { + @for row in self.rows.into_iter() { + @let active = row.is_active(); + tr + ."h-10" + ."hover:bg-gray-100" + ."outline"[active] + ."outline-2"[active] + ."outline-indigo-300"[active] + ."pointer-events-none"[active] + ."font-bold"[active] + { + @for cell in row.cells() { + (cell.render()) + } + } + @if let Some(ref edit_config) = self.editing_config { + @let edit_config = (*edit_config)(row); + (Cell { + cell_type: CellType::Icon(Icon { + icon: super::Icon::Edit, + href: edit_config.edit_href, + hx_config: edit_config.edit_hx_config, + }), + }.render()) + (Cell { + cell_type: CellType::Icon(Icon { + icon: super::Icon::Delete, + href: edit_config.delete_href, + hx_config: edit_config.delete_hx_config, + }), + }.render()) + + } + } + } + } + ) + } +} diff --git a/src/elements/mod.rs b/src/elements/mod.rs new file mode 100644 index 0000000..5e8ca41 --- /dev/null +++ b/src/elements/mod.rs @@ -0,0 +1,61 @@ +use std::fmt::Display; + +pub mod list; + +pub enum HxSwap { + OuterHtml, +} + +pub enum Icon { + Edit, + Delete, + Save, + Cancel, +} + +impl Icon { + pub fn mdi_class(&self) -> &'static str { + match self { + Icon::Edit => "mdi-pencil", + Icon::Delete => "mdi-delete", + Icon::Save => "mdi-content-save", + Icon::Cancel => "mdi-cancel", + } + } + + pub fn background(&self) -> &'static str { + match self { + Icon::Edit => "bg-blue-200", + Icon::Delete => "bg-red-200", + Icon::Save => "bg-green-100", + Icon::Cancel => "bg-red-100", + } + } + + pub fn background_hover(&self) -> &'static str { + match self { + Icon::Edit => "hover:bg-blue-400", + Icon::Delete => "hover:bg-red-400", + Icon::Save => "hover:bg-green-200", + Icon::Cancel => "hover:bg-red-200", + } + } +} + +impl Display for HxSwap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + HxSwap::OuterHtml => "outerHtml", + } + ) + } +} + +pub struct HxConfig { + pub hx_post: String, + pub hx_swap: HxSwap, + pub hx_target: &'static str, +} diff --git a/src/lib.rs b/src/lib.rs index 9c79c04..09edea1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use std::fmt; pub mod auth; pub mod cli; pub mod components; +pub mod elements; pub mod error; pub mod htmx; pub mod models; diff --git a/src/view/inventory.rs b/src/view/inventory.rs index a64c573..deef3f0 100644 --- a/src/view/inventory.rs +++ b/src/view/inventory.rs @@ -2,6 +2,14 @@ use maud::{html, Markup}; use crate::models; use crate::ClientState; +use crate::{ + elements::{ + self, + list::{self, List}, + }, + models::inventory::Item, +}; + use uuid::Uuid; pub struct Inventory; @@ -58,96 +66,64 @@ impl InventoryCategoryList { .max() .unwrap_or(1); - html!( - table - #category-list - ."table" - ."table-auto" - ."border-collapse" - ."border-spacing-0" - ."border" - ."w-full" - { - - colgroup { - col style="width:50%" {} - col style="width:50%" {} - } - thead ."bg-gray-200" { - tr ."h-10" { - th ."border" ."p-2" ."w-3/5" { "Name" } - th ."border" ."p-2" { "Weight" } - } - } - tbody { - @for category in categories { - @let active = active_category.map_or(false, |c| category.id == c.id); - tr - ."h-10" - ."hover:bg-gray-100" - ."m-3" - ."h-full" - ."outline"[active] - ."outline-2"[active] - ."outline-indigo-300"[active] - ."pointer-events-none"[active] - { - - td - class=@if active_category.map_or(false, |c| category.id == c.id) { - "border p-0 m-0 font-bold" - } @else { - "border p-0 m-0" - } { - a - id="select-category" - href={ - "/inventory/category/" - (category.id) "/" - } - hx-post={ - "/inventory/categories/" - (category.id) - "/select" - } - hx-swap="outerHTML" - hx-target="#pkglist-item-manager" - ."inline-block" ."p-2" ."m-0" ."w-full" - { - (category.name.clone()) - } - } - td ."border" ."p-2" ."m-0" style="position:relative;" { - p { - (category.total_weight().to_string()) - } - div ."bg-blue-600" ."h-1.5" - style=( - format!( - "width: {width}%;position:absolute;left:0;bottom:0;right:0;", - width=( - (category.total_weight() as f64) - / (biggest_category_weight as f64) - * 100.0 - ) - ) - ) {} - } - } - } - tr ."h-10" ."bg-gray-300" ."font-bold" { - td ."border" ."p-0" ."m-0" { - p ."p-2" ."m-2" { "Sum" } - } - td ."border" ."p-0" ."m-0" { - p ."p-2" ."m-2" { - (categories.iter().map(models::inventory::Category::total_weight).sum::().to_string()) - } - } - } - } + struct Row<'a> { + category: &'a models::inventory::Category, + active: bool, + biggest_category_weight: i64, + } + impl<'a> list::Row for Row<'a> { + fn is_active(&self) -> bool { + self.active } - ) + + fn cells(&self) -> Vec { + vec![ + list::Cell { + cell_type: list::CellType::Link(list::Link { + text: &self.category.name, + href: format!("/inventory/category/{}", self.category.id), + hx_config: Some(elements::HxConfig { + hx_post: format!( + "/inventory/categories/{}/select", + self.category.id + ), + hx_swap: elements::HxSwap::OuterHtml, + hx_target: "#pkglist-item-manager", + }), + }), + }, + list::Cell { + cell_type: list::CellType::NumberWithBar(list::NumberWithBar { + value: self.category.total_weight(), + max_value: self.biggest_category_weight, + }), + }, + ] + } + } + + List { + id: Some("category-list"), + editing_config: None, + header: list::Header { + cells: vec![ + Some(list::HeaderCell { title: "Name" }), + Some(list::HeaderCell { title: "Weight" }), + ], + }, + rows: categories + .iter() + .map(|category| { + let active = active_category.map_or(false, |c| category.id == c.id); + Row { + category, + active, + biggest_category_weight, + } + }) + .collect(), + } + .render() } } @@ -162,6 +138,60 @@ impl InventoryItemList { )] pub fn build(edit_item_id: Option, items: &Vec) -> Markup { let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1); + + struct Row<'a> { + item: &'a Item, + biggest_item_weight: i64, + } + + impl<'a> list::Row for Row<'a> { + fn cells(&self) -> Vec { + vec![ + list::Cell { + cell_type: list::CellType::Link(list::Link { + text: &self.item.name, + href: format!("/inventory/item/{id}/", id = self.item.id), + hx_config: None, + }), + }, + list::Cell { + cell_type: list::CellType::NumberWithBar(list::NumberWithBar { + value: self.item.weight, + max_value: self.biggest_item_weight, + }), + }, + ] + } + } + + fn editing_config(row: Row) -> list::EditingConfig { + list::EditingConfig { + edit_href: format!("?edit_item={id}", id = row.item.id), + edit_hx_config: None, + + delete_href: format!("/inventory/item/{id}/delete", id = row.item.id), + delete_hx_config: None, + } + } + + let table = list::List { + id: None, + editing_config: Some(Box::new(editing_config)), + header: list::Header { + cells: vec![ + Some(list::HeaderCell { title: "Name" }), + Some(list::HeaderCell { title: "Weight" }), + ], + }, + rows: items + .iter() + .map(|item| Row { + item, + biggest_item_weight, + }) + .collect(), + }; + html!( div #items { @if items.is_empty() { @@ -176,6 +206,7 @@ impl InventoryItemList { method="post" {} } + (table.render()) table ."table" ."table-auto" @@ -352,7 +383,6 @@ impl InventoryNewItemFormName { hx-target="this" hx-params="new-item-name" hx-swap="outerHTML" - #abc { label for="name" .font-bold { "Name" } input