diff --git a/rust/src/error.rs b/rust/src/error.rs index a0e3e72..61dabd3 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -1,7 +1,7 @@ use std::fmt; -use crate::components; use crate::models; +use crate::view; use axum::{ http::StatusCode, @@ -74,38 +74,34 @@ impl IntoResponse for Error { Self::Model(ref model_error) => match model_error { models::Error::Database(_) => ( StatusCode::INTERNAL_SERVER_ERROR, - components::ErrorPage::build(&format!("{}", self)), + view::ErrorPage::build(&format!("{}", self)), ), models::Error::Query(error) => match error { - models::QueryError::NotFound { description } => ( - StatusCode::NOT_FOUND, - components::ErrorPage::build(&description), - ), + models::QueryError::NotFound { description } => { + (StatusCode::NOT_FOUND, view::ErrorPage::build(&description)) + } _ => ( StatusCode::BAD_REQUEST, - components::ErrorPage::build(&format!("{}", error)), + view::ErrorPage::build(&format!("{}", error)), ), }, }, Self::Request(request_error) => match request_error { RequestError::RefererNotFound => ( StatusCode::BAD_REQUEST, - components::ErrorPage::build("no referer header found"), + view::ErrorPage::build("no referer header found"), ), RequestError::RefererInvalid { message } => ( StatusCode::BAD_REQUEST, - components::ErrorPage::build(&format!( - "referer could not be converted: {}", - message - )), + view::ErrorPage::build(&format!("referer could not be converted: {}", message)), ), RequestError::EmptyFormElement { name } => ( StatusCode::UNPROCESSABLE_ENTITY, - components::ErrorPage::build(&format!("empty form element: {}", name)), + view::ErrorPage::build(&format!("empty form element: {}", name)), ), RequestError::NotFound { message } => ( StatusCode::NOT_FOUND, - components::ErrorPage::build(&format!("not found: {}", message)), + view::ErrorPage::build(&format!("not found: {}", message)), ), }, } diff --git a/rust/src/main.rs b/rust/src/main.rs index 86b68d0..86ee65b 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -10,13 +10,14 @@ use serde::Deserialize; use uuid::Uuid; +use std::fmt; use std::net::SocketAddr; -mod components; mod error; mod html; mod models; mod sqlite; +mod view; use error::{Error, RequestError, StartError}; @@ -62,6 +63,52 @@ impl Default for ClientState { } } +struct UriPath(String); + +impl fmt::Display for UriPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl<'a> Into<&'a str> for &'a UriPath { + fn into(self) -> &'a str { + self.0.as_str() + } +} + +#[derive(PartialEq, Eq)] +pub enum TopLevelPage { + Inventory, + Trips, +} + +impl TopLevelPage { + fn id(&self) -> &'static str { + match self { + Self::Inventory => "inventory", + Self::Trips => "trips", + } + } + + fn path(&self) -> UriPath { + UriPath( + match self { + Self::Inventory => "/inventory/", + Self::Trips => "/trips/", + } + .to_string(), + ) + } + + fn name(&self) -> &'static str { + match self { + Self::Inventory => "Inventory", + Self::Trips => "Trips", + } + } +} + enum HtmxEvents { TripItemEdited, } @@ -143,7 +190,7 @@ async fn main() -> Result<(), StartError> { }), ) .nest( - "/trips/", + (&TopLevelPage::Trips.path()).into(), Router::new() .route("/", get(trips).post(trip_create)) .route("/types/", get(trips_types).post(trip_type_create)) @@ -183,7 +230,7 @@ async fn main() -> Result<(), StartError> { ), ) .nest( - "/inventory/", + (&TopLevelPage::Inventory.path()).into(), Router::new() .route("/", get(inventory_inactive)) .route("/categories/:id/select", post(inventory_category_select)) @@ -216,10 +263,7 @@ async fn main() -> Result<(), StartError> { } async fn root() -> impl IntoResponse { - components::Root::build( - &components::home::Home::build(), - &components::TopLevelPage::None, - ) + view::Root::build(&view::home::Home::build(), None) } #[derive(Deserialize, Default)] @@ -251,13 +295,13 @@ async fn inventory_active( }) .transpose()?; - Ok(components::Root::build( - &components::inventory::Inventory::build( + Ok(view::Root::build( + &view::inventory::Inventory::build( active_category, &inventory.categories, state.client_state.edit_item, ), - &components::TopLevelPage::Inventory, + Some(&TopLevelPage::Inventory), )) } @@ -270,13 +314,13 @@ async fn inventory_inactive( let inventory = models::Inventory::load(&state.database_pool).await?; - Ok(components::Root::build( - &components::inventory::Inventory::build( + Ok(view::Root::build( + &view::inventory::Inventory::build( None, &inventory.categories, state.client_state.edit_item, ), - &components::TopLevelPage::Inventory, + Some(&TopLevelPage::Inventory), )) } @@ -304,7 +348,7 @@ async fn inventory_item_validate_name( ) -> Result { let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?; - Ok(components::inventory::InventoryNewItemFormName::build( + Ok(view::inventory::InventoryNewItemFormName::build( Some(&new_item.name), exists, )) @@ -342,7 +386,7 @@ async fn inventory_item_create( .unwrap(), ); - Ok(components::inventory::Inventory::build( + Ok(view::inventory::Inventory::build( active_category, &inventory.categories, state.client_state.edit_item, @@ -460,9 +504,9 @@ async fn trip_create( async fn trips(State(state): State) -> Result { let trips = models::Trip::all(&state.database_pool).await?; - Ok(components::Root::build( - &components::trip::TripManager::build(trips), - &components::TopLevelPage::Trips, + Ok(view::Root::build( + &view::trip::TripManager::build(trips), + Some(&TopLevelPage::Trips), )) } @@ -507,13 +551,13 @@ async fn trip( }) .transpose()?; - Ok(components::Root::build( - &components::trip::Trip::build( + Ok(view::Root::build( + &view::trip::Trip::build( &trip, state.client_state.trip_edit_attribute, active_category, ), - &components::TopLevelPage::Trips, + Some(&TopLevelPage::Trips), )) } @@ -618,7 +662,7 @@ async fn trip_row( }) })?; - let item_row = components::trip::TripItemListRow::build( + let item_row = view::trip::TripItemListRow::build( trip_id, &item, models::Item::get_category_max_weight(&state.database_pool, item.item.category_id).await?, @@ -633,8 +677,7 @@ async fn trip_row( })?; // TODO biggest_category_weight? - let category_row = - components::trip::TripCategoryListRow::build(trip_id, &category, true, 0, true); + let category_row = view::trip::TripCategoryListRow::build(trip_id, &category, true, 0, true); Ok(html::concat(item_row, category_row)) } @@ -797,7 +840,7 @@ async fn trip_total_weight_htmx( ) -> Result { let total_weight = models::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?; - Ok(components::trip::TripInfoTotalWeightRow::build( + Ok(view::trip::TripInfoTotalWeightRow::build( trip_id, total_weight, )) @@ -838,7 +881,7 @@ async fn trip_state_set( } if is_htmx(&headers) { - Ok(components::trip::TripInfoStateRow::build(&new_state).into_response()) + Ok(view::trip::TripInfoStateRow::build(&new_state).into_response()) } else { Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id)).into_response()) } @@ -864,9 +907,9 @@ async fn trips_types( let trip_types: Vec = models::TripsType::all(&state.database_pool).await?; - Ok(components::Root::build( - &components::trip::types::TypeList::build(&state.client_state, trip_types), - &components::TopLevelPage::Trips, + Ok(view::Root::build( + &view::trip::types::TypeList::build(&state.client_state, trip_types), + Some(&TopLevelPage::Trips), )) } @@ -931,9 +974,9 @@ async fn inventory_item( message: format!("inventory item with id {id} not found"), }))?; - Ok(components::Root::build( - &components::inventory::InventoryItem::build(&state.client_state, &item), - &components::TopLevelPage::Inventory, + Ok(view::Root::build( + &view::inventory::InventoryItem::build(&state.client_state, &item), + Some(&TopLevelPage::Inventory), )) } @@ -965,7 +1008,7 @@ async fn trip_category_select( Ok(( headers, - components::trip::TripItems::build(Some(active_category), &trip), + view::trip::TripItems::build(Some(active_category), &trip), )) } @@ -995,7 +1038,7 @@ async fn inventory_category_select( Ok(( headers, - components::inventory::Inventory::build( + view::inventory::Inventory::build( active_category, &inventory.categories, state.client_state.edit_item, @@ -1015,9 +1058,9 @@ async fn trip_packagelist( trip.load_categories(&state.database_pool).await?; - Ok(components::Root::build( - &components::trip::packagelist::TripPackageList::build(&trip), - &components::TopLevelPage::Trips, + Ok(view::Root::build( + &view::trip::packagelist::TripPackageList::build(&trip), + Some(&TopLevelPage::Trips), )) } @@ -1040,7 +1083,7 @@ async fn trip_item_packagelist_set_pack_htmx( message: format!("an item with id {item_id} does not exist"), }))?; - Ok(components::trip::packagelist::TripPackageListRow::build( + Ok(view::trip::packagelist::TripPackageListRow::build( trip_id, &item, )) } @@ -1066,7 +1109,7 @@ async fn trip_item_packagelist_set_unpack_htmx( message: format!("an item with id {item_id} does not exist"), }))?; - Ok(components::trip::packagelist::TripPackageListRow::build( + Ok(view::trip::packagelist::TripPackageListRow::build( trip_id, &item, )) } diff --git a/rust/src/components/home.rs b/rust/src/view/home.rs similarity index 100% rename from rust/src/components/home.rs rename to rust/src/view/home.rs diff --git a/rust/src/components/inventory.rs b/rust/src/view/inventory.rs similarity index 100% rename from rust/src/components/inventory.rs rename to rust/src/view/inventory.rs diff --git a/rust/src/components/mod.rs b/rust/src/view/mod.rs similarity index 56% rename from rust/src/components/mod.rs rename to rust/src/view/mod.rs index 067f0b5..1030996 100644 --- a/rust/src/components/mod.rs +++ b/rust/src/view/mod.rs @@ -6,15 +6,34 @@ pub mod trip; pub struct Root; -#[derive(PartialEq, Eq)] -pub enum TopLevelPage { - Inventory, - Trips, - None, -} +use crate::TopLevelPage; impl Root { - pub fn build(body: &Markup, active_page: &TopLevelPage) -> Markup { + pub fn build(body: &Markup, active_page: Option<&TopLevelPage>) -> Markup { + let menu_item = |item: TopLevelPage, active_page: Option<&TopLevelPage>| { + let active = active_page.map(|page| *page == item).unwrap_or(false); + html!( + a + href=(item.path()) + hx-boost="true" + #{"header-link-" (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" { (item.name()) }} + ) + }; + html!( (DOCTYPE) html { @@ -66,42 +85,8 @@ impl Root { ."gap-x-10" ."items-stretch" { - a - href="/inventory/" - hx-boost="true" - #header-link-inventory - ."px-5" - ."flex" - ."h-full" - ."text-lg" - ."hover:bg-gray-300" - - // invisible top border to fix alignment - ."border-t-gray-200"[active_page == &TopLevelPage::Inventory] - ."hover:border-t-gray-300"[active_page == &TopLevelPage::Inventory] - - ."border-b-gray-500"[active_page == &TopLevelPage::Inventory] - ."border-y-4"[active_page == &TopLevelPage::Inventory] - ."font-bold"[active_page == &TopLevelPage::Inventory] - { span ."m-auto" ."font-semibold" { "Inventory" }} - a - href="/trips/" - hx-boost="true" - #header-link-trips - ."px-5" - ."flex" - ."h-full" - ."text-lg" - ."hover:bg-gray-300" - - // invisible top border to fix alignment - ."border-t-gray-200"[active_page == &TopLevelPage::Trips] - ."hover:border-t-gray-300"[active_page == &TopLevelPage::Trips] - - ."border-gray-500"[active_page == &TopLevelPage::Trips] - ."border-y-4"[active_page == &TopLevelPage::Trips] - ."font-bold"[active_page == &TopLevelPage::Trips] - { span ."m-auto" ."font-semibold" { "Trips" }} + (menu_item(TopLevelPage::Inventory, active_page)) + (menu_item(TopLevelPage::Trips, active_page)) } } (body) diff --git a/rust/src/components/trip/mod.rs b/rust/src/view/trip/mod.rs similarity index 100% rename from rust/src/components/trip/mod.rs rename to rust/src/view/trip/mod.rs diff --git a/rust/src/components/trip/packagelist.rs b/rust/src/view/trip/packagelist.rs similarity index 100% rename from rust/src/components/trip/packagelist.rs rename to rust/src/view/trip/packagelist.rs diff --git a/rust/src/components/trip/types.rs b/rust/src/view/trip/types.rs similarity index 100% rename from rust/src/components/trip/types.rs rename to rust/src/view/trip/types.rs