From dab809be5c09375d4b9260046d28abf7fa6bd8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:33:59 +0200 Subject: [PATCH] tmp --- rust/Cargo.lock | 10 ++ rust/Cargo.toml | 3 + rust/src/components/inventory.rs | 94 +++++++---- rust/src/components/mod.rs | 18 ++- rust/src/components/trip.rs | 264 ++++++++++++++++++++++++------- rust/src/main.rs | 101 +++++++++++- rust/src/models.rs | 61 ++++++- 7 files changed, 444 insertions(+), 107 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d976aa6..f572eb4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -734,6 +734,7 @@ dependencies = [ "hyper", "maud", "serde", + "serde_variant", "sqlx", "time", "tokio", @@ -1008,6 +1009,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a8ec0b2fd0506290348d9699c0e3eb2e3e8c0498b5a9a6158b3bd4d6970076" +dependencies = [ + "serde", +] + [[package]] name = "sha1" version = "0.10.5" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c34fd62..9b5d5c5 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -51,3 +51,6 @@ features = ["serde"] [dependencies.serde] version = "1.0.162" features = ["derive"] + +[dependencies.serde_variant] +version = "0.1" diff --git a/rust/src/components/inventory.rs b/rust/src/components/inventory.rs index bbb5417..bba2f1e 100644 --- a/rust/src/components/inventory.rs +++ b/rust/src/components/inventory.rs @@ -181,8 +181,8 @@ impl InventoryItemList { @if state.edit_item.map_or(false, |edit_item| edit_item == item.id) { tr ."h-10" { td ."border" ."bg-blue-300" ."px-2" ."py-0" { - div ."h-full" ."w-full" { - input ."px-1" ."block" ."w-full" ."bg-blue-100" ."hover:bg-white" + 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" @@ -192,8 +192,8 @@ impl InventoryItemList { } } td ."border" ."bg-blue-300" ."px-2" ."py-0" { - div ."h-full" ."w-full" { - input ."px-1"."block" ."w-full" ."bg-blue-100" ."hover:bg-white" + 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" @@ -202,18 +202,48 @@ impl InventoryItemList { {} } } - td ."border-none" ."bg-green-100" ."hover:bg-green-200" .flex ."p-0" { - div .aspect-square .w-full .h-full .flex { - button type="submit" form="edit-item" .m-auto .w-full .h-full { - span ."mdi" ."mdi-content-save" ."text-xl" .m-auto {} - } + 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" { - div .aspect-square .flex .w-full .h-full { - a href=(format!("/inventory/item/{id}/cancel", id = item.id)) .flex .m-auto .w-full .h-full { - span ."mdi" ."mdi-cancel" ."text-xl" .m-auto {} - } + td + ."border-none" + ."bg-red-100" + ."hover:bg-red-200" + ."p-0" + ."h-full" + { + a + ."aspect-square" + ."flex" + ."w-full" + ."h-full" + ."p-0" + href=(format!("/inventory/item/{id}/cancel", id = item.id)) + { + span + ."m-auto" + ."mdi" + ."mdi-cancel" + ."text-xl"; } } } @@ -243,17 +273,16 @@ impl InventoryItemList { ."p-0" ."bg-blue-200" ."hover:bg-blue-400" - ."cursor-pointer" ."w-8" - ."text-center" + ."h-full" { - div .aspect-square .flex .w-full .h-full { - a href = (format!("?edit_item={id}", id = item.id)) ."m-auto" - { - button { - span ."mdi" ."mdi-pencil" ."text-xl" {} - } - } + a + ."aspect-square" + ."flex" + ."w-full" + href=(format!("?edit_item={id}", id = item.id)) + { + span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl"; } } td @@ -261,18 +290,17 @@ impl InventoryItemList { ."p-0" ."bg-red-200" ."hover:bg-red-400" - ."cursor-pointer" ."w-8" - ."text-center" + ."h-full" + { + a + ."aspect-square" + ."flex" + ."w-full" + href=(format!("/inventory/item/{id}/delete", id = item.id)) { - div .aspect-square .flex .w-full .h-full { - a href = (format!("/inventory/item/{id}/delete", id = item.id)) ."m-auto" - { - button { - span ."mdi" ."mdi-delete" ."text-xl" {} - } - } - } + span ."m-auto" ."mdi" ."mdi-delete" ."text-xl"; + } } } } diff --git a/rust/src/components/mod.rs b/rust/src/components/mod.rs index 2d77338..cfc01e6 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/components/mod.rs @@ -38,7 +38,6 @@ impl Root { ."flex-nowrap" ."justify-between" ."items-center" - hx-boost="true" { span ."text-xl" ."font-semibold" { a href="/" { "Packager" } @@ -54,7 +53,9 @@ impl Root { }} { "Trips" } } } - div hx-boost="true" { + div + hx-boost="true" + { (body) } } @@ -67,12 +68,17 @@ pub struct ErrorPage; impl ErrorPage { pub fn build(message: &str) -> Markup { - Root::build( - html!( + html!( + (DOCTYPE) + html { + head { + title { "Packager" } + } + body { h1 { "Error" } p { (message) } - ), - &TopLevelPage::None, + } + } ) } } diff --git a/rust/src/components/trip.rs b/rust/src/components/trip.rs index bdb9d7e..34bf6ac 100644 --- a/rust/src/components/trip.rs +++ b/rust/src/components/trip.rs @@ -1,8 +1,12 @@ use crate::models; use crate::models::*; -use maud::{html, Markup}; +use maud::{html, Markup, PreEscaped}; +use uuid::Uuid; +use serde_variant::to_variant_name; + +use crate::ClientState; pub struct TripManager; impl TripManager { @@ -16,6 +20,22 @@ impl TripManager { } } +pub enum InputType { + Text, + Number, + Date, +} + +impl From for &'static str { + fn from(value: InputType) -> &'static str { + match value { + InputType::Text => "text", + InputType::Number => "number", + InputType::Date => "date", + } + } +} + pub struct TripTable; impl TripTable { @@ -42,31 +62,11 @@ impl TripTable { tbody { @for trip in trips { tr ."h-10" ."even:bg-gray-100" ."hover:bg-purple-100" ."h-full" { - td ."border" ."p-0" ."m-0" { - a ."inline-block" ."p-2" ."m-0" ."w-full" - href=(format!("/trip/{id}/", id=trip.id)) - { (trip.name) } - } - td ."border" ."p-0" ."m-0" { - a ."inline-block" ."p-2" ."m-0" ."w-full" - href=(format!("/trip/{id}/", id=trip.id)) - { (trip.start_date) } - } - td ."border" ."p-0" ."m-0" { - a ."inline-block" ."p-2" ."m-0" ."w-full" - href=(format!("/trip/{id}/", id=trip.id)) - { (trip.end_date) } - } - td ."border" ."p-0" ."m-0" { - a ."inline-block" ."p-2" ."m-0" ."w-full" - href=(format!("/trip/{id}/", id=trip.id)) - { ((trip.end_date - trip.start_date).whole_days()) } - } - td ."border" ."p-0" ."m-0" { - a ."inline-block" ."p-2" ."m-0" ."w-full" - href=(format!("/trip/{id}/", id=trip.id)) - { (trip.state.to_string()) } - } + (TripTableRow::build(trip.id, &trip.name)) + (TripTableRow::build(trip.id, &trip.date_start)) + (TripTableRow::build(trip.id, &trip.date_end)) + (TripTableRow::build(trip.id, (trip.date_end - trip.date_start).whole_days())) + (TripTableRow::build(trip.id, trip.state)) } } } @@ -75,6 +75,20 @@ impl TripTable { } } +pub struct TripTableRow; + +impl TripTableRow { + pub fn build(trip_id: Uuid, value: impl std::fmt::Display) -> Markup { + html!( + td ."border" ."p-0" ."m-0" { + a ."inline-block" ."p-2" ."m-0" ."w-full" + href=(format!("/trip/{id}/", id=trip_id)) + { (value) } + } + ) + } +} + pub struct NewTrip; impl NewTrip { @@ -177,14 +191,129 @@ impl NewTrip { pub struct Trip; impl Trip { - pub fn build(trip: &models::Trip) -> Markup { + pub fn build(state: &ClientState, trip: &models::Trip) -> Markup { html!( div ."p-8" { div ."flex" ."flex-row" ."items-center" ."gap-x-3" { h1 ."text-2xl" ."font-semibold"{ (trip.name) } } div ."my-6" { - (TripInfo::build(&trip)) + (TripInfo::build(state, &trip)) + } + div ."my-6" { + (TripComment::build(&trip)) + } + } + ) + } +} + +pub struct TripInfoRow; + +impl TripInfoRow { + pub fn build( + name: &str, + value: impl std::fmt::Display, + attribute_key: TripAttribute, + edit_attribute: Option<&TripAttribute>, + input_type: InputType, + ) -> Markup { + let edit = edit_attribute.map_or(false, |a| *a == attribute_key); + html!( + @if edit { + form + name="edit-trip" + id="edit-trip" + action=(format!("edit/{key}/submit", key=(to_variant_name(&attribute_key).unwrap()) )) + // hx-post=(format!("edit/{name}/submit")) + target="." + method="post" + ; + } + tr .h-full { + @if edit { + td ."border" ."p-2" { (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=(>::into(input_type)) + id="new-value" + name="new-value" + form="edit-trip" + value=(value) + ; + } + } + td + ."border-none" + ."bg-red-100" + ."hover:bg-red-200" + ."p-0" + ."h-full" + ."w-8" + { + a + ."aspect-square" + ."flex" + ."w-full" + ."h-full" + ."p-0" + href="." // strips query parameters + { + span + ."m-auto" + ."mdi" + ."mdi-cancel" + ."text-xl"; + } + } + td + ."border-none" + ."bg-green-100" + ."hover:bg-green-200" + ."p-0" + ."h-full" + ."w-8" + { + button + ."aspect-square" + ."flex" + ."w-full" + ."h-full" + type="submit" + form="edit-trip" + { + span + ."m-auto" + ."mdi" + ."mdi-content-save" + ."text-xl"; + } + } + } @else { + td ."border" ."p-2" { (name) } + td ."border" ."p-2" { (value) } + td + ."border-none" + ."bg-blue-100" + ."hover:bg-blue-200" + ."p-0" + ."w-8" + ."h-full" + { + a + .flex + ."w-full" + ."h-full" + href={ "?edit=" (to_variant_name(&attribute_key).unwrap()) } + { + span + ."m-auto" + ."mdi" + ."mdi-pencil" + ."text-xl"; + } + } } } ) @@ -194,7 +323,7 @@ impl Trip { pub struct TripInfo; impl TripInfo { - pub fn build(trip: &models::Trip) -> Markup { + pub fn build(state: &ClientState, trip: &models::Trip) -> Markup { html!( table ."table" @@ -205,31 +334,12 @@ impl TripInfo { ."w-full" { tbody { - tr { - td ."border" ."p-2" { "State" } - td ."border" ."p-2" { (trip.state.to_string()) } - } - tr { - td ."border" ."p-2" { "Location" } - td ."border" ."p-2" { (trip.location) } - } - tr { - td ."border" ."p-2" { "Start date" } - td ."border" ."p-2" { (trip.start_date) } - } - tr { - td ."border" ."p-2" { "End date" } - td ."border" ."p-2" { (trip.end_date) } - } - tr { - td ."border" ."p-2" { "Temp (min)" } - td ."border" ."p-2" { (trip.temp_min) } - } - tr { - td ."border" ."p-2" { "Temp (max)" } - td ."border" ."p-2" { (trip.temp_max) } - } - tr { + (TripInfoRow::build("Location", &trip.location, TripAttribute::Location, state.trip_edit_attribute.as_ref(), InputType::Text)) + (TripInfoRow::build("Start date", trip.date_start, TripAttribute::DateStart, state.trip_edit_attribute.as_ref(), InputType::Date)) + (TripInfoRow::build("End date", trip.date_end, TripAttribute::DateEnd, state.trip_edit_attribute.as_ref(), InputType::Date)) + (TripInfoRow::build("Temp (min)", trip.temp_min, TripAttribute::TempMin, state.trip_edit_attribute.as_ref(), InputType::Number)) + (TripInfoRow::build("Temp (max)", trip.temp_max, TripAttribute::TempMax, state.trip_edit_attribute.as_ref(), InputType::Number)) + tr .h-full { td ."border" ."p-2" { "Types" } td ."border" ."p-2" { ul @@ -305,3 +415,49 @@ impl TripInfo { ) } } + +pub struct TripComment; + +impl TripComment { + pub fn build(trip: &models::Trip) -> Markup { + html!( + h1 ."text-xl" ."mb-5" { "Comments" } + + form + id="edit-comment" + action="comment/submit" + target="_self" + method="post" + ; + + // https://stackoverflow.com/a/48460773 + textarea + #"comment" + ."border" ."w-full" ."h-48" + name="new-comment" + form="edit-comment" + autocomplete="off" + oninput=r#"this.style.height = "";this.style.height = this.scrollHeight + 2 + "px""# + { (trip.comment.as_ref().unwrap_or(&"".to_string())) } + script defer { (PreEscaped(r#"e = document.getElementById("comment"); e.style.height = e.scrollHeight + 2 + "px";"#)) } + + button + type="submit" + form="edit-comment" + ."mt-2" + ."border" + ."bg-green-200" + ."hover:bg-green-400" + ."cursor-pointer" + ."flex" + ."flex-column" + ."p-2" + ."gap-2" + ."items-center" + { + span ."mdi" ."mdi-content-save" ."text-xl"; + span { "Save" } + } + ) + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs index a28d2f8..a50c5c2 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -9,6 +9,8 @@ use axum::{ Form, Router, }; +use serde_variant::to_variant_name; + use sqlx::{ error::DatabaseError, query, @@ -42,6 +44,7 @@ pub struct AppState { pub struct ClientState { pub active_category_id: Option, pub edit_item: Option, + pub trip_edit_attribute: Option, } impl ClientState { @@ -49,6 +52,7 @@ impl ClientState { ClientState { active_category_id: None, edit_item: None, + trip_edit_attribute: None, } } } @@ -86,8 +90,13 @@ async fn main() -> Result<(), sqlx::Error> { .route("/trips/", get(trips)) .route("/trip/", post(trip_create)) .route("/trip/:id/", get(trip)) + .route("/trip/:id/comment/submit", post(trip_comment_set)) .route("/trip/:id/type/:id/add", get(trip_type_add)) .route("/trip/:id/type/:id/remove", get(trip_type_remove)) + .route( + "/trip/:id/edit/:attribute/submit", + post(trip_edit_attribute), + ) .route("/inventory/", get(inventory_inactive)) .route("/inventory/item/", post(inventory_item_create)) .route("/inventory/category/:id/", get(inventory_active)) @@ -408,9 +417,9 @@ struct NewTrip { #[serde(rename = "new-trip-name")] name: String, #[serde(rename = "new-trip-start-date")] - start_date: time::Date, + date_start: time::Date, #[serde(rename = "new-trip-end-date")] - end_date: time::Date, + date_end: time::Date, } async fn trip_create( @@ -420,14 +429,14 @@ async fn trip_create( let id = Uuid::new_v4(); query( "INSERT INTO trips - (id, name, start_date, end_date) + (id, name, date_start, date_end) VALUES (?, ?, ?, ?)", ) .bind(id.to_string()) .bind(&new_trip.name) - .bind(new_trip.start_date) - .bind(new_trip.end_date) + .bind(new_trip.date_start) + .bind(new_trip.date_end) .execute(&state.database_pool) .await .map_err(|e| match e { @@ -499,12 +508,20 @@ async fn trips( )) } +#[derive(Debug, Deserialize)] +struct TripQuery { + edit: Option, +} + async fn trip( Path(id): Path, - State(state): State, + State(mut state): State, + Query(trip_query): Query, ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { + state.client_state.trip_edit_attribute = trip_query.edit; + let mut trip: models::Trip = - query("SELECT id,name,start_date,end_date,state,location,temp_min,temp_max FROM trips WHERE id = ?") + query("SELECT id,name,date_start,date_end,state,location,temp_min,temp_max,comment FROM trips WHERE id = ?") .bind(id.to_string()) .fetch_one(&state.database_pool) .map_ok(std::convert::TryInto::try_into) @@ -529,7 +546,10 @@ async fn trip( Ok(( StatusCode::OK, - Root::build(components::Trip::build(&trip), &TopLevelPage::Trips), + Root::build( + components::Trip::build(&state.client_state, &trip), + &TopLevelPage::Trips, + ), )) } @@ -621,3 +641,68 @@ async fn trip_type_add( Ok(Redirect::to(&format!("/trip/{trip_id}/"))) } + +#[derive(Deserialize)] +struct CommentUpdate { + #[serde(rename = "new-comment")] + new_comment: String, +} + +async fn trip_comment_set( + Path(trip_id): Path, + State(state): State, + Form(comment_update): Form, +) -> Result { + let result = query( + "UPDATE trips + SET comment = ? + WHERE id = ?", + ) + .bind(comment_update.new_comment) + .bind(trip_id.to_string()) + .execute(&state.database_pool) + .await + .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; + + if result.rows_affected() == 0 { + Err(( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)), + )) + } else { + Ok(Redirect::to(&format!("/trip/{id}/", id = trip_id))) + } +} + +#[derive(Deserialize)] +struct TripUpdate { + #[serde(rename = "new-value")] + new_value: String, +} + +async fn trip_edit_attribute( + Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>, + State(state): State, + Form(trip_update): Form, +) -> Result { + let result = query(&format!( + "UPDATE trips + SET {attribute} = ? + WHERE id = ?", + attribute = to_variant_name(&attribute).unwrap() + )) + .bind(trip_update.new_value) + .bind(trip_id.to_string()) + .execute(&state.database_pool) + .await + .map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?; + + if result.rows_affected() == 0 { + Err(( + StatusCode::NOT_FOUND, + ErrorPage::build(&format!("trip with id {id} not found", id = trip_id)), + )) + } else { + Ok(Redirect::to(&format!("/trips/"))) + } +} diff --git a/rust/src/models.rs b/rust/src/models.rs index e630bb4..3bfc2f1 100644 --- a/rust/src/models.rs +++ b/rust/src/models.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use sqlx::{ database::Database, database::HasValueRef, @@ -90,39 +91,87 @@ impl fmt::Display for TripState { pub struct Trip { pub id: Uuid, pub name: String, - pub start_date: time::Date, - pub end_date: time::Date, + pub date_start: time::Date, + pub date_end: time::Date, pub state: TripState, pub location: String, pub temp_min: i32, pub temp_max: i32, + pub comment: Option, types: Option>, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum TripAttribute { + #[serde(rename = "date_start")] + DateStart, + #[serde(rename = "date_end")] + DateEnd, + #[serde(rename = "location")] + Location, + #[serde(rename = "temp_min")] + TempMin, + #[serde(rename = "temp_max")] + TempMax, +} + +// impl std::convert::Into<&'static str> for TripAttribute { +// fn into(self) -> &'static str { +// match self { +// Self::DateStart => "date_start", +// Self::DateEnd => "date_end", +// Self::Location => "location", +// Self::TempMin => "temp_min", +// Self::TempMax => "temp_max", +// } +// } +// } + +// impl std::convert::TryFrom<&str> for TripAttribute { +// type Error = Error; + +// fn try_from(value: &str) -> Result { +// Ok(match value { +// "date_start" => Self::DateStart, +// "date_end" => Self::DateEnd, +// "location" => Self::Location, +// "temp_min" => Self::TempMin, +// "temp_max" => Self::TempMax, +// _ => { +// return Err(Error::UnknownAttributeValue { +// attribute: value.to_string(), +// }) +// } +// }) +// } +// } + impl TryFrom for Trip { type Error = Error; fn try_from(row: SqliteRow) -> Result { let name: &str = row.try_get("name")?; let id: &str = row.try_get("id")?; - let start_date: time::Date = row.try_get("start_date")?; - let end_date: time::Date = row.try_get("end_date")?; + let date_start: time::Date = row.try_get("date_start")?; + let date_end: time::Date = row.try_get("date_end")?; let state: TripState = row.try_get("state")?; let location = row.try_get("location")?; let temp_min = row.try_get("temp_min")?; let temp_max = row.try_get("temp_max")?; + let comment = row.try_get("comment")?; let id: Uuid = Uuid::try_parse(id)?; Ok(Trip { id, name: name.to_string(), - start_date, - end_date, + date_start, + date_end, state, location, temp_min, temp_max, + comment, types: None, }) }