From d42793ee38aad5fb38f9be3fe6b78c9f5c013b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Tue, 29 Aug 2023 21:34:01 +0200 Subject: [PATCH] html --- ...2a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d.json | 12 + rust/src/models/trips.rs | 2 +- rust/src/routing/routes.rs | 86 +++++-- rust/src/sqlite.rs | 48 ++-- rust/src/view/error.rs | 21 ++ rust/src/view/mod.rs | 233 +++++++++--------- rust/src/view/root.rs | 216 ++++++++++++++++ 7 files changed, 453 insertions(+), 165 deletions(-) create mode 100644 rust/.sqlx/query-bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d.json create mode 100644 rust/src/view/error.rs create mode 100644 rust/src/view/root.rs diff --git a/rust/.sqlx/query-bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d.json b/rust/.sqlx/query-bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d.json new file mode 100644 index 0000000..7b7b706 --- /dev/null +++ b/rust/.sqlx/query-bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE trips_items\n SET pick = ?\n WHERE trip_id = ?\n AND item_id = ?\n AND user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "bc01ec2128b21bbdfaab9fc42782a4b30ea2c88d120b4bf2c56cdf0f8d4c5d9d" +} diff --git a/rust/src/models/trips.rs b/rust/src/models/trips.rs index 7be9c0d..be42251 100644 --- a/rust/src/models/trips.rs +++ b/rust/src/models/trips.rs @@ -406,7 +406,7 @@ impl TripItem { }, pool, "UPDATE trips_items - SET " => "pick" => "= ? + SET pick = ? WHERE trip_id = ? AND item_id = ? AND user_id = ?", diff --git a/rust/src/routing/routes.rs b/rust/src/routing/routes.rs index 19d668c..8aea033 100644 --- a/rust/src/routing/routes.rs +++ b/rust/src/routing/routes.rs @@ -5,6 +5,8 @@ use axum::{ Form, }; +use crate::view::Component; + use serde::Deserialize; use uuid::Uuid; @@ -98,12 +100,26 @@ pub struct TripTypeUpdate { } #[tracing::instrument] -pub async fn root(Extension(current_user): Extension) -> impl IntoResponse { - view::Root::build( - &Context::build(current_user), - &view::home::Home::build(), - None, - ) +pub async fn root( + Extension(current_user): Extension, + headers: HeaderMap, +) -> impl IntoResponse { + if htmx::is_htmx(&headers) { + view::root::Body::init( + view::Parent::Root, + view::root::BodyArgs { + body: &view::home::Home::build(), + active_page: None, + }, + ) + .build(&Context::build(current_user)) + } else { + view::Root::build( + &Context::build(current_user), + &view::home::Home::build(), + None, + ) + } } #[tracing::instrument] @@ -166,22 +182,38 @@ pub async fn inventory_inactive( Extension(current_user): Extension, State(mut state): State, Query(inventory_query): Query, + headers: HeaderMap, ) -> Result { let ctx = Context::build(current_user); state.client_state.edit_item = inventory_query.edit_item; state.client_state.active_category_id = None; - let inventory = models::inventory::Inventory::load(&ctx, &state.database_pool).await?; - Ok(view::Root::build( - &ctx, - &view::inventory::Inventory::build( - None, - &inventory.categories, - state.client_state.edit_item, - ), - Some(&TopLevelPage::Inventory), - )) + if htmx::is_htmx(&headers) { + Ok(view::root::Body::init( + view::Parent::Root, + view::root::BodyArgs { + body: &view::inventory::Inventory::build( + None, + &inventory.categories, + state.client_state.edit_item, + ), + + active_page: Some(&TopLevelPage::Inventory), + }, + ) + .build(&ctx)) + } else { + Ok(view::Root::build( + &ctx, + &view::inventory::Inventory::build( + None, + &inventory.categories, + state.client_state.edit_item, + ), + Some(&TopLevelPage::Inventory), + )) + } } #[tracing::instrument] @@ -345,15 +377,27 @@ pub async fn trip_create( pub async fn trips( Extension(current_user): Extension, State(state): State, + headers: HeaderMap, ) -> Result { let ctx = Context::build(current_user); let trips = models::trips::Trip::all(&ctx, &state.database_pool).await?; - Ok(view::Root::build( - &ctx, - &view::trip::TripManager::build(trips), - Some(&TopLevelPage::Trips), - )) + if htmx::is_htmx(&headers) { + Ok(view::root::Body::init( + view::Parent::Root, + view::root::BodyArgs { + body: &view::trip::TripManager::build(trips), + active_page: Some(&TopLevelPage::Trips), + }, + ) + .build(&ctx)) + } else { + Ok(view::Root::build( + &ctx, + &view::trip::TripManager::build(trips), + Some(&TopLevelPage::Trips), + )) + } } #[tracing::instrument] diff --git a/rust/src/sqlite.rs b/rust/src/sqlite.rs index e7e80a6..3511a4f 100644 --- a/rust/src/sqlite.rs +++ b/rust/src/sqlite.rs @@ -261,31 +261,31 @@ macro_rules! execute { } }; - ( $class:expr, $pool:expr, $( $query:expr )=>+, $( $args:tt )*) => { - { - use tracing::Instrument as _; - async { - // $crate::sqlite::sqlx_query($class, $( $query )+ , &[]); - // println!("haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay: {}", $crate::strip_plus!($(+ $query )+)); - let result: Result = sqlx::query!( - // "x" + "y", - $crate::strip_plus!($(+ $query )+), - // "UPDATE trips_items - // SET " + "pick" + - // "= ? - // WHERE trip_id = ? - // AND item_id = ? - // AND user_id = ?", - $( $args )* - ) - .execute($pool) - .await - .map_err(|e| e.into()); + // ( $class:expr, $pool:expr, $( $query:expr )=>+, $( $args:tt )*) => { + // { + // use tracing::Instrument as _; + // async { + // // $crate::sqlite::sqlx_query($class, $( $query )+ , &[]); + // // println!("haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay: {}", $crate::strip_plus!($(+ $query )+)); + // let result: Result = sqlx::query!( + // // "x" + "y", + // $crate::strip_plus!($(+ $query )+), + // // "UPDATE trips_items + // // SET " + "pick" + + // // "= ? + // // WHERE trip_id = ? + // // AND item_id = ? + // // AND user_id = ?", + // $( $args )* + // ) + // .execute($pool) + // .await + // .map_err(|e| e.into()); - result - }.instrument(tracing::info_span!("packager::sql::query", "query")) - } - }; + // result + // }.instrument(tracing::info_span!("packager::sql::query", "query")) + // } + // }; } #[macro_export] diff --git a/rust/src/view/error.rs b/rust/src/view/error.rs new file mode 100644 index 0000000..373cedd --- /dev/null +++ b/rust/src/view/error.rs @@ -0,0 +1,21 @@ +use maud::{html, Markup, DOCTYPE}; + +pub struct ErrorPage; + +impl ErrorPage { + #[tracing::instrument] + pub fn build(message: &str) -> Markup { + html!( + (DOCTYPE) + html { + head { + title { "Packager" } + } + body { + h1 { "Error" } + p { (message) } + } + } + ) + } +} diff --git a/rust/src/view/mod.rs b/rust/src/view/mod.rs index edf3d64..f8c68af 100644 --- a/rust/src/view/mod.rs +++ b/rust/src/view/mod.rs @@ -1,135 +1,130 @@ -use super::Context; +use std::fmt; -use maud::{html, Markup, PreEscaped, DOCTYPE}; +use base64::Engine as _; +use sha2::{Digest, Sha256}; +use crate::Context; +use maud::Markup; + +pub mod error; pub mod home; pub mod inventory; +pub mod root; pub mod trip; -pub struct Root; +pub use error::ErrorPage; +pub use root::Root; -use crate::TopLevelPage; +#[derive(Debug)] +pub enum HtmxAction { + Get(String), +} -impl Root { +impl fmt::Display for HtmxAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Get(path) => write!(f, "{}", path), + } + } +} + +#[derive(Debug)] +pub enum FallbackAction { + Get(String), +} + +impl fmt::Display for FallbackAction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Get(path) => write!(f, "{}", path), + } + } +} + +#[derive(Debug, Clone)] +pub struct ComponentId(String); + +impl ComponentId { #[tracing::instrument] - pub fn build(context: &Context, body: &Markup, active_page: Option<&TopLevelPage>) -> Markup { - let menu_item = |item: TopLevelPage, active_page: Option<&TopLevelPage>| { - let active = active_page.map_or(false, |page| *page == item); - html!( - a - href=(item.path()) - #{"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()) }} - ) + // fn new() -> Self { + // NOTE: this could also use a static AtomicUsize incrementing integer, which might be faster + // Self(random::()) + // } + #[tracing::instrument] + fn html_id(&self) -> String { + let id = { + let mut hasher = Sha256::new(); + hasher.update(self.0.as_bytes()); + hasher.finalize() }; - html!( - (DOCTYPE) - html { - head { - title { "Packager" } - script src="https://unpkg.com/htmx.org@1.9.4" {} - script src="https://unpkg.com/alpinejs@3.12.3" defer {} - script src="https://cdn.tailwindcss.com/3.3.3" {} - link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" {} - link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {} - script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) } - meta name="htmx-config" content=r#"{"useTemplateFragments":true}"# {} - } - body - { - header - #header - ."h-16" - ."bg-gray-200" - ."flex" - ."flex-row" - ."flex-nowrap" - ."justify-between" - ."items-stretch" - { - a - #home - href="/" - ."flex" - ."flex-row" - ."items-center" - ."gap-3" - ."px-5" - ."hover:bg-gray-300" - { - img ."h-12" src="/assets/luggage.svg" {} - span - ."text-xl" - ."font-semibold" - { "Packager" } - } - nav - ."grow" - ."flex" - ."flex-row" - ."justify-center" - ."gap-x-10" - ."items-stretch" - { - (menu_item(TopLevelPage::Inventory, active_page)) - (menu_item(TopLevelPage::Trips, active_page)) - } - a - ."flex" - ."flex-row" - ."items-center" - ."gap-3" - ."px-5" - ."bg-gray-200" - ."hover:bg-gray-300" - href=(format!("/user/{}", context.user.id)) - { - span - ."m-auto" - ."mdi" - ."mdi-account" - ."text-3xl" - {} - p { (context.user.fullname)} - } - } - (body) - } - } - ) + // 9 bytes is enough to be unique + // If this is divisible by 3, it means that we can base64-encode it without + // any "=" padding + // + // cannot panic, as the output for sha256 will always be bit + let id = &id[..9]; + + // URL_SAFE because we cannot have slashes in the output + let id = base64::engine::general_purpose::URL_SAFE.encode(id); + + id } } -pub struct ErrorPage; - -impl ErrorPage { - #[tracing::instrument] - pub fn build(message: &str) -> Markup { - html!( - (DOCTYPE) - html { - head { - title { "Packager" } - } - body { - h1 { "Error" } - p { (message) } - } - } - ) +impl fmt::Display for ComponentId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.html_id()) } } + +#[derive(Debug)] +pub enum HtmxTarget { + Myself, + Component(ComponentId), +} + +#[derive(Debug)] +pub struct HtmxComponent { + id: ComponentId, + action: HtmxAction, + fallback_action: FallbackAction, + target: HtmxTarget, +} + +impl HtmxComponent { + fn target(&self) -> &ComponentId { + match self.target { + HtmxTarget::Myself => &self.id, + HtmxTarget::Component(ref id) => id, + } + } +} + +#[derive(Debug)] +pub enum Parent { + Root, + Component(ComponentId), +} + +impl From for ComponentId { + fn from(value: Parent) -> Self { + match value { + Parent::Root => ComponentId("/".into()), + Parent::Component(c) => c, + } + } +} + +impl From for Parent { + fn from(value: ComponentId) -> Self { + Self::Component(value) + } +} + +pub trait Component { + type Args; + + fn init(parent: Parent, args: Self::Args) -> Self; + fn build(self, context: &Context) -> Markup; +} diff --git a/rust/src/view/root.rs b/rust/src/view/root.rs new file mode 100644 index 0000000..6c55f65 --- /dev/null +++ b/rust/src/view/root.rs @@ -0,0 +1,216 @@ +use crate::{Context, TopLevelPage}; + +use maud::{html, Markup, PreEscaped, DOCTYPE}; + +use super::{ + Component, ComponentId, FallbackAction, HtmxAction, HtmxComponent, HtmxTarget, Parent, +}; + +pub struct Header; + +impl Header { + #[tracing::instrument] + pub fn build() -> Markup { + html!( + head { + title { "Packager" } + script src="https://unpkg.com/htmx.org@1.9.4" {} + script src="https://unpkg.com/alpinejs@3.12.3" defer {} + script src="https://cdn.tailwindcss.com/3.3.3" {} + link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" {} + link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {} + script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) } + meta name="htmx-config" content=r#"{"useTemplateFragments":true}"# {} + } + ) + } +} + +pub struct HeaderLink<'a> { + htmx: HtmxComponent, + args: HeaderLinkArgs<'a>, +} + +pub struct HeaderLinkArgs<'a> { + pub item: TopLevelPage, + pub active_page: Option<&'a TopLevelPage>, +} + +impl<'a> Component for HeaderLink<'a> { + type Args = HeaderLinkArgs<'a>; + + #[tracing::instrument(skip(args))] + fn init(parent: Parent, args: Self::Args) -> Self { + Self { + htmx: HtmxComponent { + id: ComponentId(format!("/header/component/{}", args.item.id())), + action: HtmxAction::Get(args.item.path().to_string()), + fallback_action: FallbackAction::Get(args.item.path().to_string()), + target: HtmxTarget::Component(parent.into()), + }, + args, + } + } + + #[tracing::instrument(skip(self))] + fn build(self, context: &Context) -> Markup { + let active = self + .args + .active_page + .map_or(false, |page| *page == self.args.item); + html!( + a + href=(self.args.item.path()) + hx-get=(self.args.item.path()) + hx-target={ "#" (self.htmx.target().html_id()) } + hx-swap="outerHtml" + #{"header-link-" (self.args.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" { (self.args.item.name()) }} + ) + } +} + +pub struct Body<'a> { + htmx: HtmxComponent, + args: BodyArgs<'a>, +} + +pub struct BodyArgs<'a> { + pub body: &'a Markup, + pub active_page: Option<&'a TopLevelPage>, +} + +impl<'a> Component for Body<'a> { + type Args = BodyArgs<'a>; + + #[tracing::instrument(skip(args))] + fn init(parent: Parent, args: Self::Args) -> Self { + Self { + htmx: HtmxComponent { + id: ComponentId("/body/".into()), + action: HtmxAction::Get("/".into()), + fallback_action: FallbackAction::Get("/".into()), + target: HtmxTarget::Myself, + }, + args, + } + } + + #[tracing::instrument(skip(self))] + fn build(self, context: &Context) -> Markup { + html!( + body #(self.htmx.id.html_id()) + { + header + #header + ."h-16" + ."bg-gray-200" + ."flex" + ."flex-row" + ."flex-nowrap" + ."justify-between" + ."items-stretch" + { + a + #home + href=(self.htmx.fallback_action) + hx-get=(self.htmx.action) + hx-target={ "#" (self.htmx.target()) } + hx-swap="outerHTML" + ."flex" + ."flex-row" + ."items-center" + ."gap-3" + ."px-5" + ."hover:bg-gray-300" + { + img ."h-12" src="/assets/luggage.svg" {} + span + ."text-xl" + ."font-semibold" + { "Packager" } + } + nav + ."grow" + ."flex" + ."flex-row" + ."justify-center" + ."gap-x-10" + ."items-stretch" + { + ( + // todo make clone() unnecessary + // make ComponentId take &str instead of owned string + HeaderLink::init( + self.htmx.id.clone().into(), + HeaderLinkArgs { + item: TopLevelPage::Inventory, + active_page: self.args.active_page + } + ).build(&context) + ) + ( + HeaderLink::init( + self.htmx.id.clone().into(), + HeaderLinkArgs { + item: TopLevelPage::Trips, + active_page: self.args.active_page + } + ).build(&context) + ) + } + a + ."flex" + ."flex-row" + ."items-center" + ."gap-3" + ."px-5" + ."bg-gray-200" + ."hover:bg-gray-300" + href=(format!("/user/{}", context.user.id)) + { + span + ."m-auto" + ."mdi" + ."mdi-account" + ."text-3xl" + {} + p { (context.user.fullname)} + } + } + (self.args.body) + } + ) + } +} + +pub struct Root; + +impl Root { + #[tracing::instrument] + pub fn build(context: &Context, body: &Markup, active_page: Option<&TopLevelPage>) -> Markup { + html!( + (DOCTYPE) + html { + (Header::build()) + (Body::init(Parent::Root, BodyArgs { + body, + active_page + }).build(&context)) + } + ) + } +}