From 3264af5c65e8ec51769d271011b1df4a00af3b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Mon, 8 May 2023 22:31:01 +0200 Subject: [PATCH] use maud, make inventory work --- rust/Cargo.lock | 74 ++++++ rust/Cargo.toml | 3 + rust/src/components/home.rs | 23 +- rust/src/components/inventory.rs | 428 +++++++++++++++++++------------ rust/src/components/mod.rs | 104 ++++---- rust/src/components/triplist.rs | 43 ++-- rust/src/main.rs | 71 ++++- 7 files changed, 483 insertions(+), 263 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index bf584b3..03a86ea 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -540,6 +540,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.143" @@ -644,6 +650,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -669,6 +685,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "packager" version = "0.1.0" @@ -682,6 +704,7 @@ dependencies = [ "tokio", "tower", "tracing", + "tracing-subscriber", "uuid", ] @@ -947,6 +970,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1152,6 +1184,16 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.21" @@ -1319,6 +1361,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1392,6 +1460,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9a6f5be..50bff63 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -20,6 +20,9 @@ version = "0.4.13" [dependencies.tracing] version = "0.1.37" +[dependencies.tracing-subscriber] +version = "0.3" + [dependencies.maud] version = "0.25" diff --git a/rust/src/components/home.rs b/rust/src/components/home.rs index 7669e69..3b78bc0 100644 --- a/rust/src/components/home.rs +++ b/rust/src/components/home.rs @@ -1,25 +1,28 @@ -use super::Tree; -use axohtml::html; +use maud::{html, Markup}; pub struct Home { - doc: Tree, + doc: Markup, } impl Home { pub fn build() -> Self { - let doc = html!( -
-

"Inventory"

-

"Trips"

-
+ let doc: Markup = html!( + div id="home" class={"p-8" "max-w-xl"} { + p { + a href="/inventory" { "Inventory" } + } + p { + a href="/trips" { "Trips" } + } + } ); Self { doc } } } -impl Into for Home { - fn into(self) -> Tree { +impl Into for Home { + fn into(self) -> Markup { self.doc } } diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index 727ea5a..57254f9 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -1,47 +1,45 @@ -use super::Tree; -use axohtml::{ - html, text, - types::{Class, SpacedSet}, -}; +use maud::{html, Markup}; use crate::models::*; use crate::State; +use uuid::uuid; pub struct Inventory { - doc: Tree, + doc: Markup, } impl Inventory { pub async fn build(state: State, categories: Vec) -> Result { let doc = html!( -
-
-
- {>::into(InventoryCategoryList::build(&categories).await?)} -
- {if state.has_active_category { html!( -
- {>::into(InventoryItemList::build(categories.iter().find(|category| category.active).unwrap().items()).await?)} -
- )} else { - html!(
) - }} -
-
+ div id="pkglist-item-manager" { + div ."p-8" ."grid" ."grid-cols-4" ."gap-3" { + div ."col-span-2" { + ({>::into(InventoryCategoryList::build(&categories).await?)}) + } + div ."col-span-2" { + h1 ."text-2xl" ."mb-5" ."text-center" { "Items" } + @if state.active_category_id.is_some() { + ({>::into(InventoryItemList::build(categories.iter().find(|category| category.active).unwrap().items()).await?)}) + } + ({>::into(InventoryNewItemForm::build(&state, &categories).await?)}) + + } + } + } ); Ok(Self { doc }) } } -impl Into for Inventory { - fn into(self) -> Tree { +impl Into for Inventory { + fn into(self) -> Markup { self.doc } } pub struct InventoryCategoryList { - doc: Tree, + doc: Markup, } impl InventoryCategoryList { @@ -52,94 +50,69 @@ impl InventoryCategoryList { .max() .unwrap_or(1); - let cls_td_active: SpacedSet = - ["border", "p-0", "m-0", "font-bold"].try_into().unwrap(); - let cls_td_inactive: SpacedSet = ["border", "p-0", "m-0"].try_into().unwrap(); - - let cls_tr_active: SpacedSet = [ - "h-10", - "hover:bg-purple-100", - "m-3", - "h-full", - "outline", - "outline-2", - "outline-indigo-600", - ] - .try_into() - .unwrap(); - let cls_tr_inactive: SpacedSet = ["h-10", "hover:bg-purple-100", "m-3", "h-full"] - .try_into() - .unwrap(); - let doc = html!( -
-

"Categories"

- + div { + h1 ."text-2xl" ."mb-5" ."text-center" { "Categories" } + table + ."table" + ."table-auto" + ."border-collapse" + ."border-spacing-0" + ."border" + ."w-full" + { - - - - - - - - - - - - {categories.iter().map(|category| html!( - - - - - ))} - - - - - -
"Name""Weight"
- - {text!(category.name.clone())} - - - -

- {text!(category.total_weight().to_string())} -

-
-
-
-
-

"Sum"

-
-

- {text!(categories.iter().map(|category| category.total_weight()).sum::().to_string())} -

-
-
+ ) {} + } + } + } + tr ."h-10" ."hover:bg-purple-200" ."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(|category| category.total_weight()).sum::().to_string()) + } + } + } + } + } + } ); Ok(Self { doc }) } } -impl Into for InventoryCategoryList { - fn into(self) -> Tree { +impl Into for InventoryCategoryList { + fn into(self) -> Markup { self.doc } } pub struct InventoryItemList { - doc: Tree, + doc: Markup, } impl InventoryItemList { pub async fn build(items: &Vec) -> Result { + let biggest_item_weight: u32 = items.iter().map(|item| item.weight).max().unwrap_or(1); let doc = html!( -
-

"Items"

- - - - - - - - - {items.iter().map(|item| html!( - - - - - ))} - -
"Name""Weight"
- - {text!(item.name.clone())} - - - {text!(item.weight.to_string())} -
-
+ div #items { + @if items.is_empty() { + p ."text-lg" ."text-center" ."py-5" ."text-gray-400" { "[Empty]" } + } @else { + table + ."table" + ."table-auto" + ."border-collapse" + ."border-spacing-0" + ."border" + ."w-full" + { + thead ."bg-gray-200" { + tr ."h-10" { + th ."border" ."p-2" { "Name" } + th ."border" ."p-2" { "Weight" } + } + } + tbody { + @for item in items { + tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" { + td ."border" ."p-0" { + a + ."p-2" ."w-full" ."inline-block" + href=( + format!("/inventory/item/{id}/", id=item.id) + ) { + + (item.name.clone()) + } + } + td ."border" ."p-2" style="position:relative;" { + p { (item.weight.to_string()) } + div ."bg-blue-600" ."h-1.5" style=(format!(" + width: {width}%; + position:absolute; + left:0; + bottom:0; + right:0;", width=(item.weight as f32 / biggest_item_weight as f32 * 100.0))) {} + } + } + } + } + } + } + } ); Ok(Self { doc }) } } -impl Into for InventoryItemList { - fn into(self) -> Tree { +impl Into for InventoryItemList { + fn into(self) -> Markup { self.doc } } + +pub struct InventoryNewItemForm { + doc: Markup, +} + +impl InventoryNewItemForm { + pub async fn build(state: &State, categories: &Vec) -> Result { + let doc = html!( + + form + name="new-item" + id="new-item" + action="/inventory/item/" + target="_self" + method="post" + ."mt-8" ."p-5" ."border-2" ."border-gray-200" { + div ."mb-5" ."flex" ."flex-row" ."items-center" { + span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {} + p ."inline" ."text-xl" { "Add new item" } + } + div ."w-11/12" ."mx-auto" { + div ."pb-8" { + div ."flex" ."flex-row" ."justify-center" ."items-start"{ + label for="item-name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" } + span ."w-1/2" { + input type="text" id="item-name" name="name" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."rounded" + ."focus:outline-none" + ."focus:bg-white" + ."focus:border-purple-500" + { + } + } + } + } + div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" { + label for="item-weight" .font-bold ."w-1/2" .text-center { "Weight" } + span ."w-1/2" { + input + type="text" + id="item-weight" + name="weight" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."border-gray-300" + ."rounded" + ."focus:outline-none" + ."focus:bg-white" + ."focus:border-purple-500" + { + } + } + } + div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" { + label for="item-category" .font-bold ."w-1/2" .text-center { "Category" } + span ."w-1/2" { + select + id="item-category" + name="category" + ."block" + ."w-full" + ."p-2" + ."bg-gray-50" + ."border-2" + ."border-gray-300" + ."rounded" + ."focus:outline-none" + ."focus:bg-white" + ."focus:border-purple-500" { + @for category in categories { + @if state.active_category_id.map_or(false, |id| id == category.id) { + + option value=(category.id) selected="true" { + (category.name) + } + } @else { + option value=(category.id) { + (category.name) + } + } + } + } + } + } + input type="submit" value="Add" + ."py-2" + ."border-2" + ."rounded" + ."border-gray-300" + ."mx-auto" + ."w-full" { + } + } + } + ); + + Ok(Self { doc }) + } +} + +impl Into for InventoryNewItemForm { + fn into(self) -> Markup { + self.doc + } +} +// impl InventoryItemList { +// pub fn to_string(self) -> String { +// self.doc.into_string() +// } +// } +//ItemList diff --git a/rust/src/components/mod.rs b/rust/src/components/mod.rs index b3c6e11..3305666 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/components/mod.rs @@ -1,12 +1,4 @@ -use axohtml::{ - dom::DOMTree, - elements::FlowContent, - html, - types::{Class, SpacedSet}, - unsafe_text, -}; - -type Tree = Box>; +use maud::{html, Markup, DOCTYPE}; pub mod home; pub mod inventory; @@ -17,7 +9,7 @@ pub use inventory::*; pub use triplist::*; pub struct Root { - doc: DOMTree, + doc: Markup, } pub enum TopLevelPage { @@ -27,60 +19,54 @@ pub enum TopLevelPage { } impl Root { - pub fn build(body: Tree, active_page: TopLevelPage) -> Self { - let active_classes: SpacedSet = - ["text-lg", "font-bold", "underline"].try_into().unwrap(); - let inactive_classes: SpacedSet = ["text-lg"].try_into().unwrap(); - + pub fn build(body: Markup, active_page: TopLevelPage) -> Self { let doc = html!( - - - "Packager" - - - -
- - "Packager" - - -
- {body} - - + (DOCTYPE) + html { + head { + title { "Packager" } + script src="https://unpkg.com/htmx.org@1.7.0" {} + script src="https://cdn.tailwindcss.com" {} + script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js" defer {} + link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"; + script { (include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js"))) } + } + body { + header + ."bg-gray-200" + ."p-5" + ."flex" + ."flex-row" + ."flex-nowrap" + ."justify-between" + ."items-center" + hx-boost="true" + { + span ."text-xl" ."font-semibold" { + a href="/" { "Packager" } + } + nav ."grow" ."flex" ."flex-row" ."justify-center" ."gap-x-6" { + a href="/inventory/" class={@match active_page { + TopLevelPage::Inventory => "text-lg font-bold underline", + _ => "text-lg", + }} { "Inventory" } + a href="/trips/" class={@match active_page { + TopLevelPage::Trips => "text-lg font-bold underline", + _ => "text-lg", + }} { "Trips" } + } + } + div hx-boost="true" { + (body) + } + } + } ); Self { doc } } - pub fn to_string(&self) -> String { - let mut doc = self.doc.to_string(); - doc.insert_str(0, "\n"); - doc + pub fn to_string(self) -> String { + self.doc.into_string() } } diff --git a/rust/src/components/triplist.rs b/rust/src/components/triplist.rs index fd5aa62..4ed5a74 100644 --- a/rust/src/components/triplist.rs +++ b/rust/src/components/triplist.rs @@ -1,43 +1,38 @@ -use super::Tree; use crate::models::*; -use axohtml::{html, text}; +use maud::{html, Markup}; pub struct TripList { - doc: Tree, + doc: Markup, } impl TripList { pub fn build(package_lists: Vec) -> Self { let doc = html!( - - - - - - - - - { - package_lists.into_iter().map(|list| { - html!( - - - - - ) - }) + table { + thead { + td { + td { "ID" } + td { "Name" } } - -
"ID""Name"
{text!(list.id.to_string())}{text!(list.name)}
+ } + tbody { + @for list in package_lists { + tr { + td { (list.id.to_string()) } + td { (list.name) } + } + } + } + } ); Self { doc } } } -impl Into for TripList { - fn into(self) -> Tree { +impl Into for TripList { + fn into(self) -> Markup { self.doc } } diff --git a/rust/src/main.rs b/rust/src/main.rs index d3284df..0f9464c 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,6 +1,15 @@ -use axum::{extract::Path, http::StatusCode, response::Html, routing::get, Router}; +#![allow(unused_imports)] +use axum::{ + extract::Path, + http::StatusCode, + response::Html, + routing::{get, post}, + Router, +}; use sqlx::sqlite::SqlitePoolOptions; +use tracing_subscriber; + use futures::TryStreamExt; use uuid::Uuid; @@ -13,25 +22,34 @@ use crate::components::*; use crate::models::*; pub struct State { - pub has_active_category: bool, + pub active_category_id: Option, } impl State { pub fn new() -> Self { State { - has_active_category: false, + active_category_id: None, } } } #[tokio::main] async fn main() -> Result<(), sqlx::Error> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .init(); + // build our application with a route let app = Router::new() .route("/", get(root)) .route("/trips/", get(trips)) .route("/inventory/", get(inventory_inactive)) - .route("/inventory/category/:id", get(inventory_active)); + .route("/inventory/category/:id", get(inventory_active)) + // .route( + // "/inventory/category/:id/items", + // post(htmx_inventory_category_items), + // ); + ; let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); tracing::debug!("listening on {}", addr); @@ -64,13 +82,13 @@ async fn inventory( active_id: Option, ) -> Result<(StatusCode, Html), (StatusCode, Html)> { let mut state: State = State::new(); - state.has_active_category = active_id.is_some(); - let active_id = active_id .map(|id| Uuid::try_parse(&id)) .transpose() .map_err(|e| (StatusCode::BAD_REQUEST, Html::from(e.to_string())))?; + state.active_category_id = active_id; + let pool = SqlitePoolOptions::new() .max_connections(5) .connect("sqlite:///home/hannes-private/sync/items/items.sqlite") @@ -145,3 +163,44 @@ async fn trips() -> Result<(StatusCode, Html), (StatusCode, Html Html::from(Root::build(TripList::build(trips).into(), TopLevelPage::Trips).to_string()), )) } + +// async fn htmx_inventory_category_items( +// Path(id): Path, +// ) -> Result<(StatusCode, Html), (StatusCode, Html)> { +// let pool = SqlitePoolOptions::new() +// .max_connections(5) +// .connect("sqlite:///home/hannes-private/sync/items/items.sqlite") +// .await +// .unwrap(); + +// let items = sqlx::query(&format!( +// "SELECT +// i.id, i.name, i.description, i.weight, i.category_id +// FROM inventoryitemcategories AS c +// LEFT JOIN inventoryitems AS i +// ON i.category_id = c.id WHERE c.id = '{id}';", +// id = id, +// )) +// .fetch(&pool) +// .map_ok(|row| row.try_into()) +// .try_collect::>>() +// .await +// // we have two error handling lines here. these are distinct errors +// // this one is the SQL error that may arise during the query +// .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))? +// .into_iter() +// .collect::, models::Error>>() +// // and this one is the model mapping error that may arise e.g. during +// // reading of the rows +// .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))?; + +// Ok(( +// StatusCode::OK, +// Html::from( +// InventoryItemList::build(&items) +// .await +// .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Html::from(e.to_string())))? +// .to_string(), +// ), +// )) +// }