remove old stacks
This commit is contained in:
21
src/view/error.rs
Normal file
21
src/view/error.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use maud::{html, Markup, DOCTYPE};
|
||||
|
||||
pub struct ErrorPage;
|
||||
|
||||
impl ErrorPage {
|
||||
#[tracing::instrument]
|
||||
pub fn build(message: &str) -> Markup {
|
||||
html!(
|
||||
(DOCTYPE)
|
||||
html {
|
||||
head {
|
||||
title { "Packager" }
|
||||
}
|
||||
body {
|
||||
h1 { "Error" }
|
||||
p { (message) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
328
src/view/home.rs
Normal file
328
src/view/home.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub struct Home;
|
||||
|
||||
impl Home {
|
||||
#[tracing::instrument]
|
||||
pub fn build() -> Markup {
|
||||
html!(
|
||||
div
|
||||
id="home"
|
||||
."p-8"
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-8"
|
||||
."flex-nowrap"
|
||||
{
|
||||
h1
|
||||
."text-2xl"
|
||||
."m-auto"
|
||||
."my-4"
|
||||
{
|
||||
"Welcome!"
|
||||
}
|
||||
section
|
||||
."border-2"
|
||||
."border-gray-200"
|
||||
."flex"
|
||||
."flex-row"
|
||||
{
|
||||
a
|
||||
href="/inventory/"
|
||||
."p-8"
|
||||
."w-1/5"
|
||||
."flex"
|
||||
."hover:bg-gray-200"
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."text-xl"
|
||||
{ "Inventory" }
|
||||
}
|
||||
div
|
||||
."p-8"
|
||||
."w-4/5"
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-3"
|
||||
{
|
||||
p {
|
||||
"The inventory contains all the items that you own."
|
||||
}
|
||||
p {
|
||||
"It is effectively a list of items, sectioned into
|
||||
arbitrary categories"
|
||||
}
|
||||
p {
|
||||
"Each item has some important data attached to it,
|
||||
like its weight"
|
||||
}
|
||||
}
|
||||
}
|
||||
section
|
||||
."border-2"
|
||||
."border-gray-200"
|
||||
."flex"
|
||||
."flex-row"
|
||||
{
|
||||
a
|
||||
href="/trips/"
|
||||
."p-8"
|
||||
."w-1/5"
|
||||
."flex"
|
||||
."hover:bg-gray-200"
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."text-xl"
|
||||
{ "Trips" }
|
||||
}
|
||||
div
|
||||
."p-8"
|
||||
."w-4/5"
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-6"
|
||||
{
|
||||
div
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-3"
|
||||
{
|
||||
p {
|
||||
"Trips is where it gets interesting, as you can put
|
||||
your inventory to good use"
|
||||
}
|
||||
p {
|
||||
r#"With trips, you record any trips you plan to do. A
|
||||
"trip" can be anything you want it to be. Anything
|
||||
from a multi-week hike, a high altitude mountaineering
|
||||
tour or just a visit to the library. Whenever it makes
|
||||
sense to do some planning, creating a trip makes sense."#
|
||||
}
|
||||
p {
|
||||
"Each trip has some metadata attached to it, like start-
|
||||
and end dates or the expected temperature."
|
||||
}
|
||||
}
|
||||
div
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-3"
|
||||
{
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."gap-2"
|
||||
."items-center"
|
||||
."justify-start"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."mdi-pound"
|
||||
."text-lg"
|
||||
."text-gray-300"
|
||||
{}
|
||||
h3 ."text-lg" {
|
||||
"States"
|
||||
}
|
||||
}
|
||||
p {
|
||||
"One of the most important parts of each trip is
|
||||
its " em{"state"} ", which determines certain
|
||||
actions on the trip and can have the following values:"
|
||||
}
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
{
|
||||
tr
|
||||
."border-b-2"
|
||||
."last:border-b-0"
|
||||
{
|
||||
td ."py-2" ."pr-4" ."border-r-2" {
|
||||
"Init"
|
||||
}
|
||||
td ."py-2" ."w-full" ."pl-4" {
|
||||
"The new trip was just created"
|
||||
}
|
||||
}
|
||||
tr
|
||||
."border-b-2"
|
||||
."last:border-b-0"
|
||||
{
|
||||
td ."py-2" ."pr-4" ."border-r-2" {
|
||||
"Planning"
|
||||
}
|
||||
td ."py-2" ."w-full" ."pl-4" {
|
||||
"Now, you actually start planning the trip.
|
||||
Setting the location, going through your
|
||||
items to decide what to take with you."
|
||||
}
|
||||
}
|
||||
tr
|
||||
."border-b-2"
|
||||
."last:border-b-0"
|
||||
{
|
||||
td ."py-2" ."pr-4" ."border-r-2" {
|
||||
"Planned"
|
||||
}
|
||||
td ."py-2" ."w-full" ."pl-4" {
|
||||
"You are done with the planning. It's time
|
||||
to pack up your stuff and get going."
|
||||
}
|
||||
}
|
||||
tr
|
||||
."border-b-2"
|
||||
."last:border-b-0"
|
||||
{
|
||||
td ."py-2" ."pr-4" ."border-r-2" {
|
||||
"Active"
|
||||
}
|
||||
td ."py-2" ."w-full" ."pl-4" {
|
||||
"The trip is finally underway!"
|
||||
}
|
||||
}
|
||||
tr
|
||||
."border-b-2"
|
||||
."last:border-b-0"
|
||||
{
|
||||
td ."py-2" ."pr-4" ."border-r-2" {
|
||||
"Review"
|
||||
}
|
||||
td ."py-2" ."w-full" ."pl-4" {
|
||||
div
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-2"
|
||||
{
|
||||
p {
|
||||
"You returned from your trip. It may make
|
||||
sense to take a look back and see what
|
||||
went well and what went not so well."
|
||||
}
|
||||
p {
|
||||
"Anything you missed? Any items that you
|
||||
took with you that turned out to be useless?
|
||||
Record it and you will remember on your next
|
||||
trip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tr
|
||||
."border-b-2"
|
||||
."last:border-b-0"
|
||||
{
|
||||
td ."py-2" ."pr-4" ."border-r-2" {
|
||||
"Done"
|
||||
}
|
||||
td ."py-2" ."w-full" ."pl-4" {
|
||||
"Your review is done and the trip can be laid to rest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-3"
|
||||
{
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."gap-2"
|
||||
."items-center"
|
||||
."justify-start"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."mdi-pound"
|
||||
."text-lg"
|
||||
."text-gray-300"
|
||||
{}
|
||||
h3 ."text-lg" {
|
||||
"Items"
|
||||
}
|
||||
}
|
||||
p {
|
||||
"Of course, you can use items defined in your
|
||||
inventory in your trips"
|
||||
}
|
||||
p {
|
||||
"Generally, all items are available to you in
|
||||
the same way as the inventory. For each item,
|
||||
there are two specific states for the trip: An
|
||||
item can be " em{"picked"} ", which means that
|
||||
you plan to take it on the trip, and it can
|
||||
be " em{"packed"} ", which means that you actually
|
||||
packed it into your bag (and therefore, you cannot
|
||||
forget it any more)"
|
||||
}
|
||||
}
|
||||
div
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-3"
|
||||
{
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."gap-2"
|
||||
."items-center"
|
||||
."justify-start"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."mdi-pound"
|
||||
."text-lg"
|
||||
."text-gray-300"
|
||||
{}
|
||||
h3 ."text-lg" {
|
||||
"Types & Presets"
|
||||
}
|
||||
}
|
||||
p {
|
||||
"Often, you will take a certain set of items to
|
||||
certain trips. Whenever you plan to sleep outdoors,
|
||||
it makes sense to take your sleeping bag and mat
|
||||
with you"
|
||||
}
|
||||
p {
|
||||
"To reflect this, you can attach " em {"types"} " "
|
||||
"to your trips. Types define arbitrary characteristics
|
||||
about a trip and reference a certain set of items."
|
||||
}
|
||||
p {
|
||||
"Here are some examples of types that might make sense:"
|
||||
}
|
||||
ul
|
||||
."list-disc"
|
||||
."list-inside"
|
||||
{
|
||||
li {
|
||||
r#""Biking": Make sure to pack your helmet and
|
||||
some repair tools"#
|
||||
}
|
||||
li {
|
||||
r#""Climbing": You certainly don't want to forget
|
||||
your climbing shoes"#
|
||||
}
|
||||
li {
|
||||
r#""Rainy": Pack a rain jacket and some waterproof
|
||||
shoes"#
|
||||
}
|
||||
}
|
||||
p {
|
||||
"Types are super flexible, it's up to you how to use
|
||||
them"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
636
src/view/inventory.rs
Normal file
636
src/view/inventory.rs
Normal file
@@ -0,0 +1,636 @@
|
||||
use maud::{html, Markup};
|
||||
|
||||
use crate::models;
|
||||
use crate::ClientState;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Inventory;
|
||||
|
||||
impl Inventory {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory",
|
||||
fields(component = "Inventory")
|
||||
skip(categories)
|
||||
)]
|
||||
pub fn build(
|
||||
active_category: Option<&models::inventory::Category>,
|
||||
categories: &Vec<models::inventory::Category>,
|
||||
edit_item_id: Option<Uuid>,
|
||||
) -> Markup {
|
||||
html!(
|
||||
div id="pkglist-item-manager" {
|
||||
div ."p-8" ."grid" ."grid-cols-4" ."gap-5" {
|
||||
div ."col-span-2" ."flex" ."flex-col" ."gap-8" {
|
||||
h1 ."text-2xl" ."text-center" { "Categories" }
|
||||
(InventoryCategoryList::build(active_category, categories))
|
||||
(InventoryNewCategoryForm::build())
|
||||
}
|
||||
div ."col-span-2" ."flex" ."flex-col" ."gap-8" {
|
||||
h1 ."text-2xl" ."text-center" { "Items" }
|
||||
@if let Some(active_category) = active_category {
|
||||
(InventoryItemList::build(edit_item_id, active_category.items()))
|
||||
}
|
||||
(InventoryNewItemForm::build(active_category, categories))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryCategoryList;
|
||||
|
||||
impl InventoryCategoryList {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_category_list",
|
||||
fields(component = "InventoryCategoryList"),
|
||||
skip(categories)
|
||||
)]
|
||||
pub fn build(
|
||||
active_category: Option<&models::inventory::Category>,
|
||||
categories: &Vec<models::inventory::Category>,
|
||||
) -> Markup {
|
||||
let biggest_category_weight: i64 = categories
|
||||
.iter()
|
||||
.map(models::inventory::Category::total_weight)
|
||||
.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::<i64>().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryItemList;
|
||||
|
||||
impl InventoryItemList {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_item_list",
|
||||
fields(component = "InventoryItemList"),
|
||||
skip(items)
|
||||
)]
|
||||
pub fn build(edit_item_id: Option<Uuid>, items: &Vec<models::inventory::Item>) -> Markup {
|
||||
let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1);
|
||||
html!(
|
||||
div #items {
|
||||
@if items.is_empty() {
|
||||
p ."text-lg" ."text-center" ."py-5" ."text-gray-400" { "[Empty]" }
|
||||
} @else {
|
||||
@if let Some(edit_item_id) = edit_item_id {
|
||||
form
|
||||
name="edit-item"
|
||||
id="edit-item"
|
||||
action={"/inventory/item/" (edit_item_id) "/edit"}
|
||||
target="_self"
|
||||
method="post"
|
||||
{}
|
||||
}
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
."table-fixed"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
thead ."bg-gray-200" {
|
||||
tr ."h-10" {
|
||||
th ."border" ."p-2" ."w-3/5" { "Name" }
|
||||
th ."border" ."p-2" { "Weight" }
|
||||
th ."border" ."p-2" ."w-10" {}
|
||||
th ."border" ."p-2" ."w-10" {}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for item in items {
|
||||
@if edit_item_id.map_or(false, |id| id == item.id) {
|
||||
tr ."h-10" {
|
||||
td ."border" ."bg-blue-300" ."px-2" ."py-0" {
|
||||
div ."h-full" ."w-full" ."flex" {
|
||||
input ."m-auto" ."px-1" ."block" ."w-full" ."bg-blue-100" ."hover:bg-white"
|
||||
type="text"
|
||||
id="edit-item-name"
|
||||
name="edit-item-name"
|
||||
form="edit-item"
|
||||
value=(item.name)
|
||||
{}
|
||||
}
|
||||
}
|
||||
td ."border" ."bg-blue-300" ."px-2" ."py-0" {
|
||||
div ."h-full" ."w-full" ."flex" {
|
||||
input ."m-auto" ."px-1"."block" ."w-full" ."bg-blue-100" ."hover:bg-white"
|
||||
type="number"
|
||||
id="edit-item-weight"
|
||||
name="edit-item-weight"
|
||||
form="edit-item"
|
||||
value=(item.weight)
|
||||
{}
|
||||
}
|
||||
}
|
||||
td
|
||||
."border-none"
|
||||
."bg-green-100"
|
||||
."hover:bg-green-200"
|
||||
."p-0"
|
||||
."h-full"
|
||||
{
|
||||
button
|
||||
."aspect-square"
|
||||
."flex"
|
||||
."w-full"
|
||||
."h-full"
|
||||
type="submit"
|
||||
form="edit-item"
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-content-save"
|
||||
."text-xl"
|
||||
{}
|
||||
}
|
||||
}
|
||||
td
|
||||
."border-none"
|
||||
."bg-red-100"
|
||||
."hover:bg-red-200"
|
||||
."p-0"
|
||||
."h-full"
|
||||
{
|
||||
a
|
||||
href=(format!("/inventory/item/{id}/cancel", id = item.id))
|
||||
."aspect-square"
|
||||
."flex"
|
||||
."w-full"
|
||||
."h-full"
|
||||
."p-0"
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-cancel"
|
||||
."text-xl"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
tr ."h-10" {
|
||||
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 f64) / (biggest_item_weight as f64) * 100.0))) {}
|
||||
}
|
||||
td
|
||||
."border-none"
|
||||
."p-0"
|
||||
."bg-blue-200"
|
||||
."hover:bg-blue-400"
|
||||
."w-8"
|
||||
."h-full"
|
||||
{
|
||||
a
|
||||
href=(format!("?edit_item={id}", id = item.id))
|
||||
."aspect-square"
|
||||
."flex"
|
||||
."w-full"
|
||||
{
|
||||
span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl" {}
|
||||
}
|
||||
}
|
||||
td
|
||||
."border-none"
|
||||
."p-0"
|
||||
."bg-red-200"
|
||||
."hover:bg-red-400"
|
||||
."w-8"
|
||||
."h-full"
|
||||
{
|
||||
a
|
||||
href=(format!("/inventory/item/{id}/delete", id = item.id))
|
||||
."aspect-square"
|
||||
."flex"
|
||||
."w-full"
|
||||
{
|
||||
span ."m-auto" ."mdi" ."mdi-delete" ."text-xl" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryNewItemFormName;
|
||||
|
||||
impl InventoryNewItemFormName {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_new_item_form_name",
|
||||
fields(component = "InventoryNewItemFormName")
|
||||
)]
|
||||
pub fn build(value: Option<&str>, error: bool) -> Markup {
|
||||
html!(
|
||||
div
|
||||
."grid"
|
||||
."grid-cols-[2fr,3fr]"
|
||||
."justify-items-center"
|
||||
."items-center"
|
||||
hx-post="/inventory/item/name/validate"
|
||||
hx-trigger="input delay:1s, loaded from:document"
|
||||
hx-target="this"
|
||||
hx-params="new-item-name"
|
||||
hx-swap="outerHTML"
|
||||
#abc
|
||||
{
|
||||
label for="name" .font-bold { "Name" }
|
||||
input
|
||||
type="text"
|
||||
id="new-item-name"
|
||||
name="new-item-name"
|
||||
x-on:input="(e) => {save_active = inventory_new_item_check_input()}"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."border-2"
|
||||
."border-red-500"[error]
|
||||
."border-gray-300"[!error]
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
."focus:border-gray-500"[!error]
|
||||
value=[value]
|
||||
{}
|
||||
@if error {
|
||||
div
|
||||
."col-start-2"
|
||||
."text-sm"
|
||||
."text-red-500"
|
||||
{ "name already exists" }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryNewItemFormWeight;
|
||||
|
||||
impl InventoryNewItemFormWeight {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_new_item_form_weight",
|
||||
fields(component = "InventoryNewItemFormWeight")
|
||||
)]
|
||||
pub fn build() -> Markup {
|
||||
html!(
|
||||
div
|
||||
."grid"
|
||||
."grid-cols-[2fr,3fr]"
|
||||
."justify-items-center"
|
||||
."items-center"
|
||||
{
|
||||
label for="weight" .font-bold { "Weight" }
|
||||
input
|
||||
type="number"
|
||||
id="new-item-weight"
|
||||
name="new-item-weight"
|
||||
min="0"
|
||||
x-on:input="(e) => {
|
||||
save_active = inventory_new_item_check_input();
|
||||
weight_error = !check_weight();
|
||||
}"
|
||||
x-bind:class="weight_error && 'border-red-500' || 'border-gray-300 focus:border-gray-500'"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."border-2"
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
{}
|
||||
span
|
||||
// x-on produces some errors, this works as well
|
||||
x-bind:class="!weight_error && 'hidden'"
|
||||
."col-start-2"
|
||||
."text-sm"
|
||||
."text-red-500"
|
||||
{ "invalid input" }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryNewItemFormCategory;
|
||||
|
||||
impl InventoryNewItemFormCategory {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_new_item_form_category",
|
||||
fields(component = "InventoryNewItemFormCategory"),
|
||||
skip(categories)
|
||||
)]
|
||||
pub fn build(
|
||||
active_category: Option<&models::inventory::Category>,
|
||||
categories: &Vec<models::inventory::Category>,
|
||||
) -> Markup {
|
||||
html!(
|
||||
div
|
||||
."grid"
|
||||
."grid-cols-[2fr,3fr]"
|
||||
."justify-items-center"
|
||||
."items-center"
|
||||
{
|
||||
label for="item-category" .font-bold ."w-1/2" .text-center { "Category" }
|
||||
select
|
||||
id="new-item-category-id"
|
||||
name="new-item-category-id"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
."focus:border-gray-500"
|
||||
autocomplete="off" // https://stackoverflow.com/a/10096033
|
||||
{
|
||||
@for category in categories {
|
||||
option value=(category.id) selected[active_category.map_or(false, |c| c.id == category.id)] {
|
||||
(category.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryNewItemForm;
|
||||
|
||||
impl InventoryNewItemForm {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_new_item_form",
|
||||
fields(component = "InventoryNewItemForm"),
|
||||
skip(categories)
|
||||
)]
|
||||
pub fn build(
|
||||
active_category: Option<&models::inventory::Category>,
|
||||
categories: &Vec<models::inventory::Category>,
|
||||
) -> Markup {
|
||||
html!(
|
||||
form
|
||||
x-data="{
|
||||
save_active: inventory_new_item_check_input(),
|
||||
weight_error: !check_weight(),
|
||||
}"
|
||||
name="new-item"
|
||||
hx-post="/inventory/item/"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#pkglist-item-manager"
|
||||
id="new-item"
|
||||
action="/inventory/item/"
|
||||
target="_self"
|
||||
method="post"
|
||||
."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" ."flex" ."flex-col" ."gap-8" {
|
||||
(InventoryNewItemFormName::build(None, false))
|
||||
(InventoryNewItemFormWeight::build())
|
||||
(InventoryNewItemFormCategory::build(active_category, categories))
|
||||
input type="submit" value="Add"
|
||||
x-bind:disabled="!save_active"
|
||||
."enabled:cursor-pointer"
|
||||
."disabled:opacity-50"
|
||||
."py-2"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."mx-auto"
|
||||
."w-full" {
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryNewCategoryForm;
|
||||
|
||||
impl InventoryNewCategoryForm {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_new_category_form",
|
||||
fields(component = "InventoryNewCategoryForm")
|
||||
)]
|
||||
pub fn build() -> Markup {
|
||||
html!(
|
||||
form
|
||||
x-data="{ save_active: document.getElementById('new-category-name').value.length != 0 }"
|
||||
name="new-category"
|
||||
id="new-category"
|
||||
action="/inventory/category/"
|
||||
target="_self"
|
||||
method="post"
|
||||
."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 category" }
|
||||
}
|
||||
div ."w-11/12" ."mx-auto" {
|
||||
div ."pb-8" {
|
||||
div ."flex" ."flex-row" ."justify-center" ."items-start"{
|
||||
label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" }
|
||||
span ."w-1/2" {
|
||||
input type="text" id="new-category-name" name="new-category-name"
|
||||
x-on:input="(e) => {save_active = e.target.value.length != 0 }"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
."focus:border-gray-500"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
input type="submit" value="Add"
|
||||
x-bind:disabled="!save_active"
|
||||
."enabled:cursor-pointer"
|
||||
."disabled:opacity-50"
|
||||
."py-2"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."mx-auto"
|
||||
."w-full" {
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InventoryItem;
|
||||
|
||||
impl InventoryItem {
|
||||
#[tracing::instrument(
|
||||
target = "packager::html::build",
|
||||
name = "build_inventory_item",
|
||||
fields(component = "InventoryItem")
|
||||
)]
|
||||
pub fn build(_state: &ClientState, item: &models::inventory::InventoryItem) -> Markup {
|
||||
html!(
|
||||
div ."p-8" {
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
tbody {
|
||||
tr ."h-10" ."even:bg-gray-100" ."hover:bg-gray-100" ."h-full" {
|
||||
td ."border" ."p-2" { "Name" }
|
||||
td ."border" ."p-2" { (item.name) }
|
||||
}
|
||||
tr ."h-10" ."even:bg-gray-100" ."hover:bg-gray-100" ."h-full" {
|
||||
td ."border" ."p-2" { "Description" }
|
||||
td ."border" ."p-2" { (item.description.clone().unwrap_or(String::new())) }
|
||||
}
|
||||
tr ."h-10" ."even:bg-gray-100" ."hover:bg-gray-100" ."h-full" {
|
||||
td ."border" ."p-2" { "Weight" }
|
||||
td ."border" ."p-2" { (item.weight.to_string()) }
|
||||
}
|
||||
tr ."h-10" ."even:bg-gray-100" ."hover:bg-gray-100" ."h-full" {
|
||||
td ."border" ."p-2" { "Category" }
|
||||
td ."border" ."p-2" { (item.category.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@match item.product {
|
||||
Some(ref product) => p { "this item is part of product" (product.name) },
|
||||
None => p { "this item is not part of a product" },
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
128
src/view/mod.rs
Normal file
128
src/view/mod.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::fmt;
|
||||
|
||||
use base64::Engine as _;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::Context;
|
||||
use maud::Markup;
|
||||
|
||||
pub mod error;
|
||||
pub mod home;
|
||||
pub mod inventory;
|
||||
pub mod root;
|
||||
pub mod trip;
|
||||
|
||||
pub use error::ErrorPage;
|
||||
pub use root::Root;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HtmxAction {
|
||||
Get(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for HtmxAction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Get(path) => write!(f, "{path}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FallbackAction {
|
||||
Get(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for FallbackAction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Get(path) => write!(f, "{path}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ComponentId(String);
|
||||
|
||||
impl ComponentId {
|
||||
#[tracing::instrument]
|
||||
// fn new() -> Self {
|
||||
// NOTE: this could also use a static AtomicUsize incrementing integer, which might be faster
|
||||
// Self(random::<u32>())
|
||||
// }
|
||||
#[tracing::instrument]
|
||||
fn html_id(&self) -> String {
|
||||
let id = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.0.as_bytes());
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
// 9 bytes is enough to be unique
|
||||
// If this is divisible by 3, it means that we can base64-encode it without
|
||||
// any "=" padding
|
||||
//
|
||||
// cannot panic, as the output for sha256 will always be bit
|
||||
let id = &id[..9];
|
||||
|
||||
// URL_SAFE because we cannot have slashes in the output
|
||||
base64::engine::general_purpose::URL_SAFE.encode(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ComponentId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.html_id())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HtmxTarget {
|
||||
Myself,
|
||||
Component(ComponentId),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HtmxComponent {
|
||||
id: ComponentId,
|
||||
action: HtmxAction,
|
||||
fallback_action: FallbackAction,
|
||||
target: HtmxTarget,
|
||||
}
|
||||
|
||||
impl HtmxComponent {
|
||||
fn target(&self) -> &ComponentId {
|
||||
match self.target {
|
||||
HtmxTarget::Myself => &self.id,
|
||||
HtmxTarget::Component(ref id) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Parent {
|
||||
Root,
|
||||
Component(ComponentId),
|
||||
}
|
||||
|
||||
impl From<Parent> for ComponentId {
|
||||
fn from(value: Parent) -> Self {
|
||||
match value {
|
||||
Parent::Root => ComponentId("/".into()),
|
||||
Parent::Component(c) => c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComponentId> for Parent {
|
||||
fn from(value: ComponentId) -> Self {
|
||||
Self::Component(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Component {
|
||||
type Args;
|
||||
|
||||
fn init(parent: Parent, args: Self::Args) -> Self;
|
||||
fn build(self, context: &Context) -> Markup;
|
||||
}
|
||||
217
src/view/root.rs
Normal file
217
src/view/root.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use crate::{Context, TopLevelPage};
|
||||
|
||||
use maud::{html, Markup, PreEscaped, DOCTYPE};
|
||||
|
||||
use super::{
|
||||
Component, ComponentId, FallbackAction, HtmxAction, HtmxComponent, HtmxTarget, Parent,
|
||||
};
|
||||
|
||||
pub struct Header;
|
||||
|
||||
impl Header {
|
||||
#[tracing::instrument]
|
||||
pub fn build() -> Markup {
|
||||
html!(
|
||||
head {
|
||||
title { "Packager" }
|
||||
script src="https://unpkg.com/htmx.org@1.9.4" {}
|
||||
script src="https://unpkg.com/alpinejs@3.12.3" defer {}
|
||||
script src="https://cdn.tailwindcss.com/3.3.3" {}
|
||||
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" {}
|
||||
link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {}
|
||||
script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) }
|
||||
meta name="htmx-config" content=r#"{"useTemplateFragments":true}"# {}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HeaderLink<'a> {
|
||||
htmx: HtmxComponent,
|
||||
args: HeaderLinkArgs<'a>,
|
||||
}
|
||||
|
||||
pub struct HeaderLinkArgs<'a> {
|
||||
pub item: TopLevelPage,
|
||||
pub active_page: Option<&'a TopLevelPage>,
|
||||
}
|
||||
|
||||
impl<'a> Component for HeaderLink<'a> {
|
||||
type Args = HeaderLinkArgs<'a>;
|
||||
|
||||
#[tracing::instrument(skip(args))]
|
||||
fn init(parent: Parent, args: Self::Args) -> Self {
|
||||
Self {
|
||||
htmx: HtmxComponent {
|
||||
id: ComponentId(format!("/header/component/{}", args.item.id())),
|
||||
action: HtmxAction::Get(args.item.path().to_string()),
|
||||
fallback_action: FallbackAction::Get(args.item.path().to_string()),
|
||||
target: HtmxTarget::Component(parent.into()),
|
||||
},
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn build(self, context: &Context) -> Markup {
|
||||
let active = self
|
||||
.args
|
||||
.active_page
|
||||
.map_or(false, |page| *page == self.args.item);
|
||||
html!(
|
||||
a
|
||||
href=(self.args.item.path())
|
||||
hx-get=(self.args.item.path())
|
||||
hx-target={ "#" (self.htmx.target().html_id()) }
|
||||
hx-swap="outerHtml"
|
||||
hx-push-url="true"
|
||||
#{"header-link-" (self.args.item.id())}
|
||||
."px-5"
|
||||
."flex"
|
||||
."h-full"
|
||||
."text-lg"
|
||||
."hover:bg-gray-300"
|
||||
|
||||
// invisible top border to fix alignment
|
||||
."border-t-gray-200"[active]
|
||||
."hover:border-t-gray-300"[active]
|
||||
|
||||
."border-b-gray-500"[active]
|
||||
."border-y-4"[active]
|
||||
."font-bold"[active]
|
||||
{ span ."m-auto" ."font-semibold" { (self.args.item.name()) }}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Body<'a> {
|
||||
htmx: HtmxComponent,
|
||||
args: BodyArgs<'a>,
|
||||
}
|
||||
|
||||
pub struct BodyArgs<'a> {
|
||||
pub body: &'a Markup,
|
||||
pub active_page: Option<&'a TopLevelPage>,
|
||||
}
|
||||
|
||||
impl<'a> Component for Body<'a> {
|
||||
type Args = BodyArgs<'a>;
|
||||
|
||||
#[tracing::instrument(skip(args))]
|
||||
fn init(parent: Parent, args: Self::Args) -> Self {
|
||||
Self {
|
||||
htmx: HtmxComponent {
|
||||
id: ComponentId("/body/".into()),
|
||||
action: HtmxAction::Get("/".into()),
|
||||
fallback_action: FallbackAction::Get("/".into()),
|
||||
target: HtmxTarget::Myself,
|
||||
},
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn build(self, context: &Context) -> Markup {
|
||||
html!(
|
||||
body #(self.htmx.id.html_id())
|
||||
{
|
||||
header
|
||||
#header
|
||||
."h-16"
|
||||
."bg-gray-200"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."flex-nowrap"
|
||||
."justify-between"
|
||||
."items-stretch"
|
||||
{
|
||||
a
|
||||
#home
|
||||
href=(self.htmx.fallback_action)
|
||||
hx-get=(self.htmx.action)
|
||||
hx-target={ "#" (self.htmx.target()) }
|
||||
hx-swap="outerHTML"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."gap-3"
|
||||
."px-5"
|
||||
."hover:bg-gray-300"
|
||||
{
|
||||
img ."h-12" src="/assets/luggage.svg" {}
|
||||
span
|
||||
."text-xl"
|
||||
."font-semibold"
|
||||
{ "Packager" }
|
||||
}
|
||||
nav
|
||||
."grow"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-center"
|
||||
."gap-x-10"
|
||||
."items-stretch"
|
||||
{
|
||||
(
|
||||
// todo make clone() unnecessary
|
||||
// make ComponentId take &str instead of owned string
|
||||
HeaderLink::init(
|
||||
self.htmx.id.clone().into(),
|
||||
HeaderLinkArgs {
|
||||
item: TopLevelPage::Inventory,
|
||||
active_page: self.args.active_page
|
||||
}
|
||||
).build(context)
|
||||
)
|
||||
(
|
||||
HeaderLink::init(
|
||||
self.htmx.id.clone().into(),
|
||||
HeaderLinkArgs {
|
||||
item: TopLevelPage::Trips,
|
||||
active_page: self.args.active_page
|
||||
}
|
||||
).build(context)
|
||||
)
|
||||
}
|
||||
a
|
||||
."flex"
|
||||
."flex-row"
|
||||
."items-center"
|
||||
."gap-3"
|
||||
."px-5"
|
||||
."bg-gray-200"
|
||||
."hover:bg-gray-300"
|
||||
href=(format!("/user/{}", context.user.id))
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-account"
|
||||
."text-3xl"
|
||||
{}
|
||||
p { (context.user.fullname)}
|
||||
}
|
||||
}
|
||||
(self.args.body)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Root;
|
||||
|
||||
impl Root {
|
||||
#[tracing::instrument]
|
||||
pub fn build(context: &Context, body: &Markup, active_page: Option<&TopLevelPage>) -> Markup {
|
||||
html!(
|
||||
(DOCTYPE)
|
||||
html {
|
||||
(Header::build())
|
||||
(Body::init(Parent::Root, BodyArgs {
|
||||
body,
|
||||
active_page
|
||||
}).build(context))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
1286
src/view/trip/mod.rs
Normal file
1286
src/view/trip/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
359
src/view/trip/packagelist.rs
Normal file
359
src/view/trip/packagelist.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use maud::{html, Markup};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models;
|
||||
|
||||
pub struct TripPackageListRowReady;
|
||||
|
||||
impl TripPackageListRowReady {
|
||||
#[tracing::instrument]
|
||||
pub fn build(trip_id: Uuid, item: &models::trips::TripItem) -> Markup {
|
||||
html!(
|
||||
li
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-between"
|
||||
."items-stretch"
|
||||
."bg-green-50"[item.packed]
|
||||
."bg-red-50"[!item.packed]
|
||||
."hover:bg-white"[!item.packed]
|
||||
."h-full"
|
||||
{
|
||||
span
|
||||
."p-2"
|
||||
{
|
||||
(item.item.name)
|
||||
}
|
||||
@if item.packed {
|
||||
a
|
||||
href={
|
||||
"/trips/" (trip_id)
|
||||
"/items/" (item.item.id)
|
||||
"/unpack"
|
||||
}
|
||||
hx-post={
|
||||
"/trips/" (trip_id)
|
||||
"/packagelist/item/"
|
||||
(item.item.id) "/unpack"
|
||||
}
|
||||
hx-target="closest li"
|
||||
hx-swap="outerHTML"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."aspect-square"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."m-auto"
|
||||
."text-xl"
|
||||
."mdi-check"
|
||||
{}
|
||||
}
|
||||
} @else {
|
||||
a
|
||||
href={
|
||||
"/trips/" (trip_id)
|
||||
"/items/" (item.item.id)
|
||||
"/pack"
|
||||
}
|
||||
hx-post={
|
||||
"/trips/" (trip_id)
|
||||
"/packagelist/item/"
|
||||
(item.item.id) "/pack"
|
||||
}
|
||||
hx-target="closest li"
|
||||
hx-swap="outerHTML"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."aspect-square"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."m-auto"
|
||||
."text-xl"
|
||||
."mdi-checkbox-blank-outline"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TripPackageListRowUnready;
|
||||
|
||||
impl TripPackageListRowUnready {
|
||||
#[tracing::instrument]
|
||||
pub fn build(trip_id: Uuid, item: &models::trips::TripItem) -> Markup {
|
||||
html!(
|
||||
li
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-between"
|
||||
."items-stretch"
|
||||
."bg-green-50"[item.ready]
|
||||
."bg-red-50"[!item.ready]
|
||||
."hover:bg-white"[!item.ready]
|
||||
."h-full"
|
||||
{
|
||||
span
|
||||
."p-2"
|
||||
{
|
||||
(item.item.name)
|
||||
}
|
||||
@if item.ready {
|
||||
a
|
||||
href={
|
||||
"/trips/" (trip_id)
|
||||
"/items/" (item.item.id)
|
||||
"/unready"
|
||||
}
|
||||
hx-post={
|
||||
"/trips/" (trip_id)
|
||||
"/packagelist/item/"
|
||||
(item.item.id) "/unready"
|
||||
}
|
||||
hx-target="closest li"
|
||||
hx-swap="outerHTML"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."aspect-square"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."m-auto"
|
||||
."text-xl"
|
||||
."mdi-check"
|
||||
{}
|
||||
}
|
||||
} @else {
|
||||
a
|
||||
href={
|
||||
"/trips/" (trip_id)
|
||||
"/items/" (item.item.id)
|
||||
"/ready"
|
||||
}
|
||||
hx-post={
|
||||
"/trips/" (trip_id)
|
||||
"/packagelist/item/"
|
||||
(item.item.id) "/ready"
|
||||
}
|
||||
hx-target="closest li"
|
||||
hx-swap="outerHTML"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."aspect-square"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."m-auto"
|
||||
."text-xl"
|
||||
."mdi-checkbox-blank-outline"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TripPackageListCategoryBlockReady;
|
||||
|
||||
impl TripPackageListCategoryBlockReady {
|
||||
#[tracing::instrument]
|
||||
pub fn build(trip: &models::trips::Trip, category: &models::trips::TripCategory) -> Markup {
|
||||
let empty = !category
|
||||
.items
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item.picked);
|
||||
|
||||
html!(
|
||||
div
|
||||
."inline-block"
|
||||
."w-full"
|
||||
."mb-5"
|
||||
."border"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."opacity-30"[empty]
|
||||
{
|
||||
div
|
||||
."bg-gray-100"
|
||||
."border-b-2"
|
||||
."border-gray-300"
|
||||
."p-3"
|
||||
{
|
||||
h3 { (category.category.name) }
|
||||
}
|
||||
@if empty {
|
||||
div
|
||||
."flex"
|
||||
."p-1"
|
||||
{
|
||||
span
|
||||
."text-sm"
|
||||
."m-auto"
|
||||
{
|
||||
"no items picked"
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
ul
|
||||
."flex"
|
||||
."flex-col"
|
||||
{
|
||||
@for item in category.items.as_ref().unwrap().iter().filter(|item| item.picked) {
|
||||
(TripPackageListRowReady::build(trip.id, item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TripPackageListCategoryBlockUnready;
|
||||
|
||||
impl TripPackageListCategoryBlockUnready {
|
||||
#[tracing::instrument]
|
||||
pub fn build(trip: &models::trips::Trip, category: &models::trips::TripCategory) -> Markup {
|
||||
let empty = !category
|
||||
.items
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item.picked);
|
||||
|
||||
html!(
|
||||
div
|
||||
."inline-block"
|
||||
."w-full"
|
||||
."mb-5"
|
||||
."border"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."opacity-30"[empty]
|
||||
{
|
||||
div
|
||||
."bg-gray-100"
|
||||
."border-b-2"
|
||||
."border-gray-300"
|
||||
."p-3"
|
||||
{
|
||||
h3 { (category.category.name) }
|
||||
}
|
||||
@if empty {
|
||||
div
|
||||
."flex"
|
||||
."p-1"
|
||||
{
|
||||
span
|
||||
."text-sm"
|
||||
."m-auto"
|
||||
{
|
||||
"no items picked"
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
ul
|
||||
."flex"
|
||||
."flex-col"
|
||||
{
|
||||
@for item in category.items.as_ref().unwrap().iter().filter(|item| item.picked && !item.ready) {
|
||||
(TripPackageListRowUnready::build(trip.id, item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
pub struct TripPackageList;
|
||||
|
||||
impl TripPackageList {
|
||||
#[tracing::instrument]
|
||||
pub fn build(trip: &models::trips::Trip) -> Markup {
|
||||
// let all_packed = trip.categories().iter().all(|category| {
|
||||
// category
|
||||
// .items
|
||||
// .as_ref()
|
||||
// .unwrap()
|
||||
// .iter()
|
||||
// .all(|item| !item.picked || item.packed)
|
||||
// });
|
||||
let has_unready_items: bool = trip.categories.as_ref().unwrap().iter().any(|category| {
|
||||
category
|
||||
.items
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item.picked && !item.ready)
|
||||
});
|
||||
html!(
|
||||
div
|
||||
."p-8"
|
||||
."flex"
|
||||
."flex-col"
|
||||
."gap-8"
|
||||
{
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-between"
|
||||
{
|
||||
h1 ."text-xl" {
|
||||
"Package list for "
|
||||
a
|
||||
href={"/trips/" (trip.id) "/"}
|
||||
."font-bold"
|
||||
{
|
||||
(trip.name)
|
||||
}
|
||||
}
|
||||
a
|
||||
href={"/trips/" (trip.id) "/packagelist/"}
|
||||
// disabled[!all_packed]
|
||||
// ."opacity-50"[!all_packed]
|
||||
."p-2"
|
||||
."border-2"
|
||||
."border-gray-500"
|
||||
."bg-blue-200"
|
||||
."hover:bg-blue-200"
|
||||
{
|
||||
"Finish packing"
|
||||
}
|
||||
}
|
||||
@if has_unready_items {
|
||||
p { "There are items that are not yet ready, get them!"}
|
||||
div
|
||||
."columns-3"
|
||||
."gap-5"
|
||||
{
|
||||
@for category in trip.categories() {
|
||||
@let empty = !category
|
||||
.items
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item.picked);
|
||||
@if !empty {
|
||||
(TripPackageListCategoryBlockUnready::build(trip, category))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p { "Pack the following things:" }
|
||||
div
|
||||
."columns-3"
|
||||
."gap-5"
|
||||
{
|
||||
@for category in trip.categories() {
|
||||
(TripPackageListCategoryBlockReady::build(trip, category))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
163
src/view/trip/types.rs
Normal file
163
src/view/trip/types.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use crate::models;
|
||||
use crate::ClientState;
|
||||
use maud::{html, Markup};
|
||||
|
||||
pub struct TypeList;
|
||||
|
||||
impl TypeList {
|
||||
#[tracing::instrument]
|
||||
pub fn build(state: &ClientState, trip_types: Vec<models::trips::TripsType>) -> Markup {
|
||||
html!(
|
||||
div ."p-8" ."flex" ."flex-col" ."gap-8" {
|
||||
h1 ."text-2xl" {"Trip Types"}
|
||||
|
||||
ul
|
||||
."flex"
|
||||
."flex-col"
|
||||
."items-stretch"
|
||||
."border-t"
|
||||
."border-l"
|
||||
."h-full"
|
||||
{
|
||||
@for trip_type in trip_types {
|
||||
li
|
||||
."border-b"
|
||||
."border-r"
|
||||
."flex"
|
||||
."flex-row"
|
||||
."justify-between"
|
||||
."items-stretch"
|
||||
{
|
||||
@if state.trip_type_edit.map_or(false, |id| id == trip_type.id) {
|
||||
form
|
||||
."hidden"
|
||||
id="edit-trip-type"
|
||||
action={ (trip_type.id) "/edit/name/submit" }
|
||||
target="_self"
|
||||
method="post"
|
||||
{}
|
||||
div
|
||||
."bg-blue-200"
|
||||
."p-2"
|
||||
."grow"
|
||||
{
|
||||
input
|
||||
."bg-blue-100"
|
||||
."hover:bg-white"
|
||||
."w-full"
|
||||
type="text"
|
||||
name="new-value"
|
||||
form="edit-trip-type"
|
||||
value=(trip_type.name)
|
||||
{}
|
||||
}
|
||||
div
|
||||
."flex"
|
||||
."flex-row"
|
||||
{
|
||||
a
|
||||
href="."
|
||||
."bg-red-200"
|
||||
."hover:bg-red-300"
|
||||
."w-8"
|
||||
."flex"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."mdi-cancel"
|
||||
."text-xl"
|
||||
."m-auto"
|
||||
{}
|
||||
}
|
||||
button
|
||||
type="submit"
|
||||
form="edit-trip-type"
|
||||
."bg-green-200"
|
||||
."hover:bg-green-300"
|
||||
."w-8"
|
||||
{
|
||||
span
|
||||
."mdi"
|
||||
."mdi-content-save"
|
||||
."text-xl"
|
||||
{}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
span
|
||||
."p-2"
|
||||
{
|
||||
(trip_type.name)
|
||||
}
|
||||
|
||||
div
|
||||
."bg-blue-100"
|
||||
."hover:bg-blue-200"
|
||||
."p-0"
|
||||
."w-8"
|
||||
{
|
||||
a
|
||||
href={ "?edit=" (trip_type.id) }
|
||||
.flex
|
||||
."w-full"
|
||||
."h-full"
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."mdi-pencil"
|
||||
."text-xl"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form
|
||||
name="new-trip-type"
|
||||
action="/trips/types/"
|
||||
target="_self"
|
||||
method="post"
|
||||
."mt-8" ."p-5" ."border-2" ."border-gray-200"
|
||||
{
|
||||
div ."mb-5" ."flex" ."flex-row" {
|
||||
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
|
||||
p ."inline" ."text-xl" { "Add new trip type" }
|
||||
}
|
||||
div ."w-11/12" ."m-auto" {
|
||||
div ."mx-auto" ."pb-8" {
|
||||
div ."flex" ."flex-row" ."justify-center" {
|
||||
label for="new-trip-type-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Name" }
|
||||
span ."w-1/2" {
|
||||
input
|
||||
type="text"
|
||||
id="new-trip-type-name"
|
||||
name="new-trip-type-name"
|
||||
."block"
|
||||
."w-full"
|
||||
."p-2"
|
||||
."bg-gray-50"
|
||||
."border-2"
|
||||
."focus:outline-none"
|
||||
."focus:bg-white"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
input
|
||||
type="submit"
|
||||
value="Add"
|
||||
."py-2"
|
||||
."border-2"
|
||||
."border-gray-300"
|
||||
."mx-auto"
|
||||
."w-full"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user