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!(
-
+ 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"
+ {
-
-
-
-
-
-
- | "Name" |
- "Weight" |
-
-
-
- {categories.iter().map(|category| html!(
-
- |
-
- {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"
-
-
-
- | "Name" |
- "Weight" |
-
-
-
- {items.iter().map(|item| html!(
-
- |
-
- {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"
-
-
-
-
-
-
-
-
- {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!(
-
-
-
- | "ID" |
- "Name" |
-
-
-
- {
- package_lists.into_iter().map(|list| {
- html!(
-
- | {text!(list.id.to_string())} |
- {text!(list.name)} |
-
- )
- })
+ table {
+ thead {
+ td {
+ td { "ID" }
+ td { "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(),
+// ),
+// ))
+// }