This commit is contained in:
2023-08-29 21:34:00 +02:00
parent 0f66ec80ac
commit 6fb11545d5
8 changed files with 120 additions and 96 deletions

View File

@@ -1,7 +1,7 @@
use std::fmt; use std::fmt;
use crate::components;
use crate::models; use crate::models;
use crate::view;
use axum::{ use axum::{
http::StatusCode, http::StatusCode,
@@ -74,38 +74,34 @@ impl IntoResponse for Error {
Self::Model(ref model_error) => match model_error { Self::Model(ref model_error) => match model_error {
models::Error::Database(_) => ( models::Error::Database(_) => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
components::ErrorPage::build(&format!("{}", self)), view::ErrorPage::build(&format!("{}", self)),
), ),
models::Error::Query(error) => match error { models::Error::Query(error) => match error {
models::QueryError::NotFound { description } => ( models::QueryError::NotFound { description } => {
StatusCode::NOT_FOUND, (StatusCode::NOT_FOUND, view::ErrorPage::build(&description))
components::ErrorPage::build(&description), }
),
_ => ( _ => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!("{}", error)), view::ErrorPage::build(&format!("{}", error)),
), ),
}, },
}, },
Self::Request(request_error) => match request_error { Self::Request(request_error) => match request_error {
RequestError::RefererNotFound => ( RequestError::RefererNotFound => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
components::ErrorPage::build("no referer header found"), view::ErrorPage::build("no referer header found"),
), ),
RequestError::RefererInvalid { message } => ( RequestError::RefererInvalid { message } => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
components::ErrorPage::build(&format!( view::ErrorPage::build(&format!("referer could not be converted: {}", message)),
"referer could not be converted: {}",
message
)),
), ),
RequestError::EmptyFormElement { name } => ( RequestError::EmptyFormElement { name } => (
StatusCode::UNPROCESSABLE_ENTITY, StatusCode::UNPROCESSABLE_ENTITY,
components::ErrorPage::build(&format!("empty form element: {}", name)), view::ErrorPage::build(&format!("empty form element: {}", name)),
), ),
RequestError::NotFound { message } => ( RequestError::NotFound { message } => (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
components::ErrorPage::build(&format!("not found: {}", message)), view::ErrorPage::build(&format!("not found: {}", message)),
), ),
}, },
} }

View File

@@ -10,13 +10,14 @@ use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use std::fmt;
use std::net::SocketAddr; use std::net::SocketAddr;
mod components;
mod error; mod error;
mod html; mod html;
mod models; mod models;
mod sqlite; mod sqlite;
mod view;
use error::{Error, RequestError, StartError}; 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 { enum HtmxEvents {
TripItemEdited, TripItemEdited,
} }
@@ -143,7 +190,7 @@ async fn main() -> Result<(), StartError> {
}), }),
) )
.nest( .nest(
"/trips/", (&TopLevelPage::Trips.path()).into(),
Router::new() Router::new()
.route("/", get(trips).post(trip_create)) .route("/", get(trips).post(trip_create))
.route("/types/", get(trips_types).post(trip_type_create)) .route("/types/", get(trips_types).post(trip_type_create))
@@ -183,7 +230,7 @@ async fn main() -> Result<(), StartError> {
), ),
) )
.nest( .nest(
"/inventory/", (&TopLevelPage::Inventory.path()).into(),
Router::new() Router::new()
.route("/", get(inventory_inactive)) .route("/", get(inventory_inactive))
.route("/categories/:id/select", post(inventory_category_select)) .route("/categories/:id/select", post(inventory_category_select))
@@ -216,10 +263,7 @@ async fn main() -> Result<(), StartError> {
} }
async fn root() -> impl IntoResponse { async fn root() -> impl IntoResponse {
components::Root::build( view::Root::build(&view::home::Home::build(), None)
&components::home::Home::build(),
&components::TopLevelPage::None,
)
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
@@ -251,13 +295,13 @@ async fn inventory_active(
}) })
.transpose()?; .transpose()?;
Ok(components::Root::build( Ok(view::Root::build(
&components::inventory::Inventory::build( &view::inventory::Inventory::build(
active_category, active_category,
&inventory.categories, &inventory.categories,
state.client_state.edit_item, 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?; let inventory = models::Inventory::load(&state.database_pool).await?;
Ok(components::Root::build( Ok(view::Root::build(
&components::inventory::Inventory::build( &view::inventory::Inventory::build(
None, None,
&inventory.categories, &inventory.categories,
state.client_state.edit_item, state.client_state.edit_item,
), ),
&components::TopLevelPage::Inventory, Some(&TopLevelPage::Inventory),
)) ))
} }
@@ -304,7 +348,7 @@ async fn inventory_item_validate_name(
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let exists = models::InventoryItem::name_exists(&state.database_pool, &new_item.name).await?; 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), Some(&new_item.name),
exists, exists,
)) ))
@@ -342,7 +386,7 @@ async fn inventory_item_create(
.unwrap(), .unwrap(),
); );
Ok(components::inventory::Inventory::build( Ok(view::inventory::Inventory::build(
active_category, active_category,
&inventory.categories, &inventory.categories,
state.client_state.edit_item, state.client_state.edit_item,
@@ -460,9 +504,9 @@ async fn trip_create(
async fn trips(State(state): State<AppState>) -> Result<impl IntoResponse, Error> { async fn trips(State(state): State<AppState>) -> Result<impl IntoResponse, Error> {
let trips = models::Trip::all(&state.database_pool).await?; let trips = models::Trip::all(&state.database_pool).await?;
Ok(components::Root::build( Ok(view::Root::build(
&components::trip::TripManager::build(trips), &view::trip::TripManager::build(trips),
&components::TopLevelPage::Trips, Some(&TopLevelPage::Trips),
)) ))
} }
@@ -507,13 +551,13 @@ async fn trip(
}) })
.transpose()?; .transpose()?;
Ok(components::Root::build( Ok(view::Root::build(
&components::trip::Trip::build( &view::trip::Trip::build(
&trip, &trip,
state.client_state.trip_edit_attribute, state.client_state.trip_edit_attribute,
active_category, 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, trip_id,
&item, &item,
models::Item::get_category_max_weight(&state.database_pool, item.item.category_id).await?, 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? // TODO biggest_category_weight?
let category_row = let category_row = view::trip::TripCategoryListRow::build(trip_id, &category, true, 0, true);
components::trip::TripCategoryListRow::build(trip_id, &category, true, 0, true);
Ok(html::concat(item_row, category_row)) Ok(html::concat(item_row, category_row))
} }
@@ -797,7 +840,7 @@ async fn trip_total_weight_htmx(
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let total_weight = let total_weight =
models::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?; models::Trip::find_total_picked_weight(&state.database_pool, trip_id).await?;
Ok(components::trip::TripInfoTotalWeightRow::build( Ok(view::trip::TripInfoTotalWeightRow::build(
trip_id, trip_id,
total_weight, total_weight,
)) ))
@@ -838,7 +881,7 @@ async fn trip_state_set(
} }
if is_htmx(&headers) { if is_htmx(&headers) {
Ok(components::trip::TripInfoStateRow::build(&new_state).into_response()) Ok(view::trip::TripInfoStateRow::build(&new_state).into_response())
} else { } else {
Ok(Redirect::to(&format!("/trips/{id}/", id = trip_id)).into_response()) 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> = models::TripsType::all(&state.database_pool).await?; let trip_types: Vec<models::TripsType> = models::TripsType::all(&state.database_pool).await?;
Ok(components::Root::build( Ok(view::Root::build(
&components::trip::types::TypeList::build(&state.client_state, trip_types), &view::trip::types::TypeList::build(&state.client_state, trip_types),
&components::TopLevelPage::Trips, Some(&TopLevelPage::Trips),
)) ))
} }
@@ -931,9 +974,9 @@ async fn inventory_item(
message: format!("inventory item with id {id} not found"), message: format!("inventory item with id {id} not found"),
}))?; }))?;
Ok(components::Root::build( Ok(view::Root::build(
&components::inventory::InventoryItem::build(&state.client_state, &item), &view::inventory::InventoryItem::build(&state.client_state, &item),
&components::TopLevelPage::Inventory, Some(&TopLevelPage::Inventory),
)) ))
} }
@@ -965,7 +1008,7 @@ async fn trip_category_select(
Ok(( Ok((
headers, 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(( Ok((
headers, headers,
components::inventory::Inventory::build( view::inventory::Inventory::build(
active_category, active_category,
&inventory.categories, &inventory.categories,
state.client_state.edit_item, state.client_state.edit_item,
@@ -1015,9 +1058,9 @@ async fn trip_packagelist(
trip.load_categories(&state.database_pool).await?; trip.load_categories(&state.database_pool).await?;
Ok(components::Root::build( Ok(view::Root::build(
&components::trip::packagelist::TripPackageList::build(&trip), &view::trip::packagelist::TripPackageList::build(&trip),
&components::TopLevelPage::Trips, 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"), 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, 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"), 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, trip_id, &item,
)) ))
} }

View File

@@ -6,15 +6,34 @@ pub mod trip;
pub struct Root; pub struct Root;
#[derive(PartialEq, Eq)] use crate::TopLevelPage;
pub enum TopLevelPage {
Inventory,
Trips,
None,
}
impl Root { 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!( html!(
(DOCTYPE) (DOCTYPE)
html { html {
@@ -66,42 +85,8 @@ impl Root {
."gap-x-10" ."gap-x-10"
."items-stretch" ."items-stretch"
{ {
a (menu_item(TopLevelPage::Inventory, active_page))
href="/inventory/" (menu_item(TopLevelPage::Trips, active_page))
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" }}
} }
} }
(body) (body)