ref
This commit is contained in:
@@ -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)),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
121
rust/src/main.rs
121
rust/src/main.rs
@@ -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,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user