use maud, make inventory work

This commit is contained in:
2023-05-08 22:31:01 +02:00
parent 7dd39ca2a5
commit 3264af5c65
7 changed files with 483 additions and 263 deletions

View File

@@ -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!(
<div id="home" class=["p-8", "max-w-xl"]>
<p><a href="/inventory">"Inventory"</a></p>
<p><a href="/trips">"Trips"</a></p>
</div>
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<Tree> for Home {
fn into(self) -> Tree {
impl Into<Markup> for Home {
fn into(self) -> Markup {
self.doc
}
}

View File

@@ -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<Category>) -> Result<Self, Error> {
let doc = html!(
<div id="pkglist-item-manager">
<div class=["p-8", "grid", "grid-cols-4", "gap-3"]>
<div class=["col-span-2"]>
{<InventoryCategoryList as Into<Tree>>::into(InventoryCategoryList::build(&categories).await?)}
</div>
{if state.has_active_category { html!(
<div class=["col-span-2"]>
{<InventoryItemList as Into<Tree>>::into(InventoryItemList::build(categories.iter().find(|category| category.active).unwrap().items()).await?)}
</div>
)} else {
html!(<div></div>)
}}
</div>
</div>
div id="pkglist-item-manager" {
div ."p-8" ."grid" ."grid-cols-4" ."gap-3" {
div ."col-span-2" {
({<InventoryCategoryList as Into<Markup>>::into(InventoryCategoryList::build(&categories).await?)})
}
div ."col-span-2" {
h1 ."text-2xl" ."mb-5" ."text-center" { "Items" }
@if state.active_category_id.is_some() {
({<InventoryItemList as Into<Markup>>::into(InventoryItemList::build(categories.iter().find(|category| category.active).unwrap().items()).await?)})
}
({<InventoryNewItemForm as Into<Markup>>::into(InventoryNewItemForm::build(&state, &categories).await?)})
}
}
}
);
Ok(Self { doc })
}
}
impl Into<Tree> for Inventory {
fn into(self) -> Tree {
impl Into<Markup> 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<Class> =
["border", "p-0", "m-0", "font-bold"].try_into().unwrap();
let cls_td_inactive: SpacedSet<Class> = ["border", "p-0", "m-0"].try_into().unwrap();
let cls_tr_active: SpacedSet<Class> = [
"h-10",
"hover:bg-purple-100",
"m-3",
"h-full",
"outline",
"outline-2",
"outline-indigo-600",
]
.try_into()
.unwrap();
let cls_tr_inactive: SpacedSet<Class> = ["h-10", "hover:bg-purple-100", "m-3", "h-full"]
.try_into()
.unwrap();
let doc = html!(
<div>
<h1 class=["text-2xl", "mb-5"]>"Categories"</h1>
<table class=[
"table",
"table-auto",
"border-collapse",
"border-spacing-0",
"border",
"w-full",
]>
div {
h1 ."text-2xl" ."mb-5" ."text-center" { "Categories" }
table
."table"
."table-auto"
."border-collapse"
."border-spacing-0"
."border"
."w-full"
{
<colgroup>
<col style="width:50%"/>
<col style="width:50%"/>
</colgroup>
<thead class=["bg-gray-200"]>
<tr class=["h-10"]>
<th class=["border", "p-2"]>"Name"</th>
<th class=["border", "p-2"]>"Weight"</th>
</tr>
</thead>
<tbody>
{categories.iter().map(|category| html!(
<tr
class={if category.active {
cls_tr_active.clone()
} else {
cls_tr_inactive.clone()
}}
>
<td
class={if category.active {
cls_td_active.clone()
} else {
cls_td_inactive.clone()
}}
>
<a
colgroup {
col style="width:50%" {}
col style="width:50%" {}
}
thead ."bg-gray-200" {
tr ."h-10" {
th ."border" ."p-2" { "Name" }
th ."border" ."p-2" { "Weight" }
}
}
tbody {
@for category in categories {
tr class={@if category.active {
"h-10 hover:bg-purple-100 m-3 h-full outline outline-2 outline-indigo-300"
} @else {
"h-10 hover:bg-purple-100 m-3 h-full"
}} {
td
class=@if category.active {
"border p-0 m-0 font-bold"
} @else {
"border p-0 m-0"
} {
a
id="select-category"
href={
href=(
format!(
"/inventory/category/{id}",
id=category.id
)
)
// hx-post=(
// format!(
// "/inventory/category/{id}/items",
// id=category.id
// )
// )
// hx-swap="outerHTML"
// hx-target="#items"
."inline-block" ."p-2" ."m-0" ."w-full"
{
(category.name.clone())
}
class=["inline-block", "p-2", "m-0", "w-full"]
>
{text!(category.name.clone())}
</a>
</td>
<td class=["border", "p-0", "m-0"] style="position:relative;">
<a
id="select-category"
href={
format!(
"/inventory/category/{id}",
id=category.id
)
}
class=["inline-block", "p-2", "m-0", "w-full"]
>
<p>
{text!(category.total_weight().to_string())}
</p>
</a>
<div
class=["bg-blue-600", "h-1.5"]
style = {
}
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=(
@@ -148,89 +121,216 @@ impl InventoryCategoryList {
* 100.0
)
)
}
>
</div>
</td>
</tr>
))}
<tr class=["h-10", "hover:bg-purple-200", "bg-gray-300", "font-bold"]>
<td class=["border", "p-0", "m-0"]>
<p class=["p-2", "m-2"]>"Sum"</p>
</td>
<td class=["border", "p-0", "m-0"]>
<p class=["p-2", "m-2"]>
{text!(categories.iter().map(|category| category.total_weight()).sum::<u32>().to_string())}
</p>
</td>
</tr>
</tbody>
</table>
</div>
) {}
}
}
}
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::<u32>().to_string())
}
}
}
}
}
}
);
Ok(Self { doc })
}
}
impl Into<Tree> for InventoryCategoryList {
fn into(self) -> Tree {
impl Into<Markup> for InventoryCategoryList {
fn into(self) -> Markup {
self.doc
}
}
pub struct InventoryItemList {
doc: Tree,
doc: Markup,
}
impl InventoryItemList {
pub async fn build(items: &Vec<Item>) -> Result<Self, Error> {
let biggest_item_weight: u32 = items.iter().map(|item| item.weight).max().unwrap_or(1);
let doc = html!(
<div>
<h1 class=["text-2xl", "mb-5"]>"Items"</h1>
<table class=[
"table",
"table-auto",
"border-collapse",
"border-spacing-0",
"border",
"w-full",
]>
<thead class=["bg-gray-200"]>
<tr class=["h-10"]>
<th class=["border", "p-2"]>"Name"</th>
<th class=["border", "p-2"]>"Weight"</th>
</tr>
</thead>
<tbody>
{items.iter().map(|item| html!(
<tr class=["h-10", "even:bg-gray-100", "hover:bg-purple-100"]>
<td class=["border", "p-2"]>
<a
class=["p-2", "w-full", "inline-block"]
href={
format!("/inventory/item/{id}/", id=item.id)
}
>
{text!(item.name.clone())}
</a>
</td>
<td class=["border", "p-2"]>
{text!(item.weight.to_string())}
</td>
</tr>
))}
</tbody>
</table>
</div>
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<Tree> for InventoryItemList {
fn into(self) -> Tree {
impl Into<Markup> for InventoryItemList {
fn into(self) -> Markup {
self.doc
}
}
pub struct InventoryNewItemForm {
doc: Markup,
}
impl InventoryNewItemForm {
pub async fn build(state: &State, categories: &Vec<Category>) -> Result<Self, Error> {
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<Markup> for InventoryNewItemForm {
fn into(self) -> Markup {
self.doc
}
}
// impl InventoryItemList {
// pub fn to_string(self) -> String {
// self.doc.into_string()
// }
// }
//ItemList

View File

@@ -1,12 +1,4 @@
use axohtml::{
dom::DOMTree,
elements::FlowContent,
html,
types::{Class, SpacedSet},
unsafe_text,
};
type Tree = Box<dyn FlowContent<String>>;
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<String>,
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<Class> =
["text-lg", "font-bold", "underline"].try_into().unwrap();
let inactive_classes: SpacedSet<Class> = ["text-lg"].try_into().unwrap();
pub fn build(body: Markup, active_page: TopLevelPage) -> Self {
let doc = html!(
<html>
<head>
<title>"Packager"</title>
<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="true"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"/>
<script>{unsafe_text!(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))}</script>
</head>
<body>
<header class=[
"bg-gray-200",
"p-5",
"flex",
"flex-row",
"flex-nowrap",
"justify-between",
"items-center",
]>
<span class=["text-xl", "font-semibold"]>
<a href="/">"Packager"</a>
</span>
<nav class=["grow", "flex", "flex-row", "justify-center", "gap-x-6"]>
<a href="/inventory/" class={
match active_page {
TopLevelPage::Inventory => active_classes.clone(),
_ => inactive_classes.clone(),
}
}>"Inventory"</a>
<a href="/trips/" class={
match active_page {
TopLevelPage::Trips => active_classes,
_ => inactive_classes,
}
}>"Trips"</a>
</nav>
</header>
{body}
</body>
</html>
(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, "<!DOCTYPE html>\n");
doc
pub fn to_string(self) -> String {
self.doc.into_string()
}
}

View File

@@ -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<Trip>) -> Self {
let doc = html!(
<table>
<thead>
<tr>
<th>"ID"</th>
<th>"Name"</th>
</tr>
</thead>
<tbody>
{
package_lists.into_iter().map(|list| {
html!(
<tr>
<td>{text!(list.id.to_string())}</td>
<td>{text!(list.name)}</td>
</tr>
)
})
table {
thead {
td {
td { "ID" }
td { "Name" }
}
</tbody>
</table>
}
tbody {
@for list in package_lists {
tr {
td { (list.id.to_string()) }
td { (list.name) }
}
}
}
}
);
Self { doc }
}
}
impl Into<Tree> for TripList {
fn into(self) -> Tree {
impl Into<Markup> for TripList {
fn into(self) -> Markup {
self.doc
}
}