diff --git a/src/app/catalog.rs b/src/app/catalog.rs new file mode 100644 index 0000000..e3864de --- /dev/null +++ b/src/app/catalog.rs @@ -0,0 +1,54 @@ +use askama::Template; +use serde::Deserialize; +use axum_htmx::HxRequest; +use axum::extract::State; +use sqlx::SqlitePool; +use askama_axum::{IntoResponse, Response}; +use crate::db::inventory_item::{inventory_item_get_all, inventory_item_get_search, DbInventoryItem}; +use crate::error::{AppError, QueryExtractor}; + +#[derive(Template)] +#[template(path = "catalog.html")] +struct CatalogTemplate { + items: Vec, + query: CatalogQueryArgs, +} + +#[derive(Template)] +#[template(path = "catalog_item_fragment.html")] +struct CatalogItemFragmentTemplate { + items: Vec +} + +/// Query string response for "authorized" endpoint +#[derive(Debug, Deserialize)] +pub struct CatalogQueryArgs { + #[serde(rename = "q")] + pub search: Option, + #[serde(alias = "p")] + pub page: Option, + #[serde(rename = "size")] + pub page_size: Option, +} + +#[axum::debug_handler] +pub async fn catalog( + QueryExtractor(query): QueryExtractor, + HxRequest(hx_request): HxRequest, + State(db): State +) -> Result { + let page = query.page.unwrap_or(0); + let page_size = query.page_size.unwrap_or(100); + + let items = match query.search.as_ref() { + Some(s) => inventory_item_get_search(&db, &s, page_size, page).await?, + None => inventory_item_get_all(&db, page_size, page).await?, + }; + + if hx_request { + Ok(CatalogItemFragmentTemplate { items }.into_response()) + } + else { + Ok(CatalogTemplate { items, query }.into_response()) + } +} \ No newline at end of file diff --git a/src/app/item.rs b/src/app/item.rs new file mode 100644 index 0000000..dee7346 --- /dev/null +++ b/src/app/item.rs @@ -0,0 +1,26 @@ +use askama::Template; +use askama_axum::IntoResponse; +use axum::extract::{Path, State}; +use sqlx::SqlitePool; +use axum::response::Response; +use crate::db::inventory_item::{inventory_item_get_by_id, sum_all_adjustments_for_item, DbInventoryItem}; +use crate::error::AppError; + +#[derive(Template)] +#[template(path = "item.html")] +struct ItemTemplate { + item: DbInventoryItem +} + +pub async fn item(State(db): State, Path(id): Path) -> Result { + let item = inventory_item_get_by_id(&db, id).await?; + + Ok(ItemTemplate { item }.into_response()) +} + + +pub async fn item_count(State(db): State, Path(id): Path) -> Result { + let count = sum_all_adjustments_for_item(&db, id).await?; + + Ok(count.to_string().into_response()) +} diff --git a/src/app/items.rs b/src/app/items.rs deleted file mode 100644 index d559a7f..0000000 --- a/src/app/items.rs +++ /dev/null @@ -1,74 +0,0 @@ -use askama::Template; -use askama_axum::IntoResponse; -use axum::extract::{Path, State}; -use sqlx::SqlitePool; -use axum::response::Response; -use axum_htmx::HxRequest; -use serde::Deserialize; -use crate::db::inventory_item::{inventory_item_get_all, inventory_item_get_by_id, inventory_item_get_search, sum_all_adjustments_for_item, DbInventoryItem}; -use crate::error::{AppError, QueryExtractor}; - -#[derive(Template)] -#[template(path = "item_list.html")] -struct ItemListTemplate { - items: Vec, - query: ItemsQueryArgs, -} - -#[derive(Template)] -#[template(path = "item_list_fragment.html")] -struct ItemListFragmentTemplate { - items: Vec -} - -/// Query string response for "authorized" endpoint -#[derive(Debug, Deserialize)] -pub struct ItemsQueryArgs { - #[serde(rename = "q")] - pub search: Option, - #[serde(alias = "p")] - pub page: Option, - #[serde(rename = "size")] - pub page_size: Option, -} - -#[axum::debug_handler] -pub async fn item_list( - QueryExtractor(query): QueryExtractor, - HxRequest(hx_request): HxRequest, - State(db): State -) -> Result { - let page = query.page.unwrap_or(0); - let page_size = query.page_size.unwrap_or(100); - - let items = match query.search.as_ref() { - Some(s) => inventory_item_get_search(&db, &s, page_size, page).await?, - None => inventory_item_get_all(&db, page_size, page).await?, - }; - - if hx_request { - Ok(ItemListFragmentTemplate { items }.into_response()) - } - else { - Ok(ItemListTemplate { items, query }.into_response()) - } -} - -#[derive(Template)] -#[template(path = "item.html")] -struct ItemTemplate { - item: DbInventoryItem -} - -pub async fn item(State(db): State, Path(id): Path) -> Result { - let item = inventory_item_get_by_id(&db, id).await?; - - Ok(ItemTemplate { item }.into_response()) -} - - -pub async fn item_count(State(db): State, Path(id): Path) -> Result { - let count = sum_all_adjustments_for_item(&db, id).await?; - - Ok(count.to_string().into_response()) -} diff --git a/src/app/mod.rs b/src/app/mod.rs index 5e608d2..feead54 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,23 +1,25 @@ use axum::middleware::from_extractor; use axum::Router; use axum::routing::{get, post}; -use axum_htmx::{AutoVaryLayer}; -use crate::app::state::AppState; +use axum_htmx::AutoVaryLayer; +use sqlx::SqlitePool; +use oauth2::basic::BasicClient; +use axum::extract::FromRef; use crate::session::SessionUser; -pub mod state; -pub mod items; +pub mod item; mod upload; +pub mod catalog; pub fn routes() -> Router { Router::new() - .route("/", get(items::item_list)) - .route("/index.html", get(items::item_list)) - .route("/items", get(items::item_list)) - .route("/items/", get(items::item_list)) - .route("/item/:item_id", get(items::item)) - .route("/item/:item_id/", get(items::item)) - .route("/item/:item_id/count", get(items::item_count)) + .route("/", get(catalog::catalog)) + .route("/index.html", get(catalog::catalog)) + .route("/items", get(catalog::catalog)) + .route("/items/", get(catalog::catalog)) + .route("/item/:item_id", get(item::item)) + .route("/item/:item_id/", get(item::item)) + .route("/item/:item_id/count", get(item::item_count)) .route("/catalog", post(upload::catalog)) // Ensure that all routes here require an authenticated user // whether explicitly asked or not @@ -25,3 +27,26 @@ pub fn routes() -> Router { .layer(AutoVaryLayer) } +// App state. Pretty basic stuff. Gets passed around by the server to the handlers and whatnot +// Use in a handler with the state enum: +// async fn handler(State(my_app_state): State) +#[derive(Clone)] +pub struct AppState { + pub db: SqlitePool, + pub oauth_client: BasicClient, +} + +// Axum extractors for app state. These allow the handler to just use +// pieces of the App state +// async fn handler(State(my_db): State) +impl FromRef for SqlitePool { + fn from_ref(input: &AppState) -> Self { + input.db.clone() + } +} + +impl FromRef for BasicClient { + fn from_ref(input: &AppState) -> Self { + input.oauth_client.clone() + } +} \ No newline at end of file diff --git a/src/app/state.rs b/src/app/state.rs deleted file mode 100644 index 6f39d1d..0000000 --- a/src/app/state.rs +++ /dev/null @@ -1,27 +0,0 @@ -use sqlx::SqlitePool; -use oauth2::basic::BasicClient; -use axum::extract::FromRef; - -// App state. Pretty basic stuff. Gets passed around by the server to the handlers and whatnot -// Use in a handler with the state enum: -// async fn handler(State(my_app_state): State) -#[derive(Clone)] -pub struct AppState { - pub db: SqlitePool, - pub oauth_client: BasicClient, -} - -// Axum extractors for app state. These allow the handler to just use -// pieces of the App state -// async fn handler(State(my_db): State) -impl FromRef for SqlitePool { - fn from_ref(input: &AppState) -> Self { - input.db.clone() - } -} - -impl FromRef for BasicClient { - fn from_ref(input: &AppState) -> Self { - input.oauth_client.clone() - } -} \ No newline at end of file diff --git a/src/auth.rs b/src/auth.rs index 6dd218e..cfbd041 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -13,7 +13,7 @@ use tower_sessions::Session; use crate::error::{AppError, AppForbiddenResponse}; use crate::error::QueryExtractor; -use crate::app::state::AppState; +use crate::app::AppState; use crate::session::{SessionUser, USER_SESSION}; use crate::db; // This module is all the stuff related to authentication and authorization diff --git a/src/error.rs b/src/error.rs index 87ee4e1..651fefa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use axum::response::{IntoResponse, Response}; use axum::Router; use axum::routing::get; use axum::extract::FromRequestParts; -use crate::app::state::AppState; +use crate::app::AppState; use crate::session::SessionUser; // This module is all the stuff related to handling error responses diff --git a/src/main.rs b/src/main.rs index c855dc3..05faf71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use app::state::AppState; +use app::AppState; use anyhow::{Context, Result}; use axum::Router; use tokio::signal; diff --git a/src/static_routes.rs b/src/static_routes.rs index f1d49fb..508014f 100644 --- a/src/static_routes.rs +++ b/src/static_routes.rs @@ -1,6 +1,6 @@ use axum::Router; use tower_http::services::ServeFile; -use crate::app::state::AppState; +use crate::app::AppState; pub fn routes() -> Router { Router::new() diff --git a/templates/item_list.html b/templates/catalog.html similarity index 80% rename from templates/item_list.html rename to templates/catalog.html index c28b53f..b7b866a 100644 --- a/templates/item_list.html +++ b/templates/catalog.html @@ -10,13 +10,14 @@ aria-label="Search" value='{{ query.search.as_deref().unwrap_or("") }}' hx-get="/items" - hx-trigger="search, keyup delay:200ms changed" + hx-trigger="search, keyup delay:500ms changed" hx-target="#items" + hx-push-url="true" />

- {% include "item_list_fragment.html" %} + {% include "catalog_item_fragment.html" %}
diff --git a/templates/item_list_fragment.html b/templates/catalog_item_fragment.html similarity index 74% rename from templates/item_list_fragment.html rename to templates/catalog_item_fragment.html index 5c55f9b..27d4094 100644 --- a/templates/item_list_fragment.html +++ b/templates/catalog_item_fragment.html @@ -2,7 +2,7 @@ {% for item in items %}
- +
Count: 0
Reorder Point: {{ item.reorder_point }}
diff --git a/templates/main.html b/templates/main.html index 0aa9f8f..ffd4be0 100644 --- a/templates/main.html +++ b/templates/main.html @@ -8,7 +8,7 @@ {% block title %}Title{% endblock %} - +