This commit is contained in:
2023-08-29 21:34:01 +02:00
parent 45a25a49cc
commit d42793ee38
7 changed files with 453 additions and 165 deletions

View File

@@ -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"
}

View File

@@ -406,7 +406,7 @@ impl TripItem {
}, },
pool, pool,
"UPDATE trips_items "UPDATE trips_items
SET " => "pick" => "= ? SET pick = ?
WHERE trip_id = ? WHERE trip_id = ?
AND item_id = ? AND item_id = ?
AND user_id = ?", AND user_id = ?",

View File

@@ -5,6 +5,8 @@ use axum::{
Form, Form,
}; };
use crate::view::Component;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
@@ -98,13 +100,27 @@ pub struct TripTypeUpdate {
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn root(Extension(current_user): Extension<models::user::User>) -> impl IntoResponse { pub async fn root(
Extension(current_user): Extension<models::user::User>,
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( view::Root::build(
&Context::build(current_user), &Context::build(current_user),
&view::home::Home::build(), &view::home::Home::build(),
None, None,
) )
} }
}
#[tracing::instrument] #[tracing::instrument]
pub async fn icon() -> impl IntoResponse { pub async fn icon() -> impl IntoResponse {
@@ -166,13 +182,28 @@ pub async fn inventory_inactive(
Extension(current_user): Extension<models::user::User>, Extension(current_user): Extension<models::user::User>,
State(mut state): State<AppState>, State(mut state): State<AppState>,
Query(inventory_query): Query<InventoryQuery>, Query(inventory_query): Query<InventoryQuery>,
headers: HeaderMap,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
state.client_state.edit_item = inventory_query.edit_item; state.client_state.edit_item = inventory_query.edit_item;
state.client_state.active_category_id = None; state.client_state.active_category_id = None;
let inventory = models::inventory::Inventory::load(&ctx, &state.database_pool).await?; let inventory = models::inventory::Inventory::load(&ctx, &state.database_pool).await?;
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( Ok(view::Root::build(
&ctx, &ctx,
&view::inventory::Inventory::build( &view::inventory::Inventory::build(
@@ -183,6 +214,7 @@ pub async fn inventory_inactive(
Some(&TopLevelPage::Inventory), Some(&TopLevelPage::Inventory),
)) ))
} }
}
#[tracing::instrument] #[tracing::instrument]
pub async fn inventory_item_validate_name( pub async fn inventory_item_validate_name(
@@ -345,16 +377,28 @@ pub async fn trip_create(
pub async fn trips( pub async fn trips(
Extension(current_user): Extension<models::user::User>, Extension(current_user): Extension<models::user::User>,
State(state): State<AppState>, State(state): State<AppState>,
headers: HeaderMap,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
let trips = models::trips::Trip::all(&ctx, &state.database_pool).await?; let trips = models::trips::Trip::all(&ctx, &state.database_pool).await?;
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( Ok(view::Root::build(
&ctx, &ctx,
&view::trip::TripManager::build(trips), &view::trip::TripManager::build(trips),
Some(&TopLevelPage::Trips), Some(&TopLevelPage::Trips),
)) ))
} }
}
#[tracing::instrument] #[tracing::instrument]
pub async fn trip( pub async fn trip(

View File

@@ -261,31 +261,31 @@ macro_rules! execute {
} }
}; };
( $class:expr, $pool:expr, $( $query:expr )=>+, $( $args:tt )*) => { // ( $class:expr, $pool:expr, $( $query:expr )=>+, $( $args:tt )*) => {
{ // {
use tracing::Instrument as _; // use tracing::Instrument as _;
async { // async {
// $crate::sqlite::sqlx_query($class, $( $query )+ , &[]); // // $crate::sqlite::sqlx_query($class, $( $query )+ , &[]);
// println!("haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay: {}", $crate::strip_plus!($(+ $query )+)); // // println!("haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay: {}", $crate::strip_plus!($(+ $query )+));
let result: Result<sqlx::sqlite::SqliteQueryResult, Error> = sqlx::query!( // let result: Result<sqlx::sqlite::SqliteQueryResult, Error> = sqlx::query!(
// "x" + "y", // // "x" + "y",
$crate::strip_plus!($(+ $query )+), // $crate::strip_plus!($(+ $query )+),
// "UPDATE trips_items // // "UPDATE trips_items
// SET " + "pick" + // // SET " + "pick" +
// "= ? // // "= ?
// WHERE trip_id = ? // // WHERE trip_id = ?
// AND item_id = ? // // AND item_id = ?
// AND user_id = ?", // // AND user_id = ?",
$( $args )* // $( $args )*
) // )
.execute($pool) // .execute($pool)
.await // .await
.map_err(|e| e.into()); // .map_err(|e| e.into());
result // result
}.instrument(tracing::info_span!("packager::sql::query", "query")) // }.instrument(tracing::info_span!("packager::sql::query", "query"))
} // }
}; // };
} }
#[macro_export] #[macro_export]

21
rust/src/view/error.rs Normal file
View File

@@ -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) }
}
}
)
}
}

View File

@@ -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 home;
pub mod inventory; pub mod inventory;
pub mod root;
pub mod trip; 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] #[tracing::instrument]
pub fn build(context: &Context, body: &Markup, active_page: Option<&TopLevelPage>) -> Markup { // fn new() -> Self {
let menu_item = |item: TopLevelPage, active_page: Option<&TopLevelPage>| { // NOTE: this could also use a static AtomicUsize incrementing integer, which might be faster
let active = active_page.map_or(false, |page| *page == item); // Self(random::<u32>())
html!( // }
a #[tracing::instrument]
href=(item.path()) fn html_id(&self) -> String {
#{"header-link-" (item.id())} let id = {
."px-5" let mut hasher = Sha256::new();
."flex" hasher.update(self.0.as_bytes());
."h-full" hasher.finalize()
."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!( // 9 bytes is enough to be unique
(DOCTYPE) // If this is divisible by 3, it means that we can base64-encode it without
html { // any "=" padding
head { //
title { "Packager" } // cannot panic, as the output for sha256 will always be bit
script src="https://unpkg.com/htmx.org@1.9.4" {} let id = &id[..9];
script src="https://unpkg.com/alpinejs@3.12.3" defer {}
script src="https://cdn.tailwindcss.com/3.3.3" {} // URL_SAFE because we cannot have slashes in the output
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.2.96/css/materialdesignicons.min.css" {} let id = base64::engine::general_purpose::URL_SAFE.encode(id);
link rel="shortcut icon" type="image/svg+xml" href="/favicon.svg" {}
script { (PreEscaped(include_str!(concat!(env!("CARGO_MANIFEST_DIR"),"/js/app.js")))) } id
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)
}
}
)
} }
} }
pub struct ErrorPage; impl fmt::Display for ComponentId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.html_id())
}
}
impl ErrorPage { #[derive(Debug)]
#[tracing::instrument] pub enum HtmxTarget {
pub fn build(message: &str) -> Markup { Myself,
html!( Component(ComponentId),
(DOCTYPE)
html {
head {
title { "Packager" }
} }
body {
h1 { "Error" } #[derive(Debug)]
p { (message) } 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<Parent> for ComponentId {
fn from(value: Parent) -> Self {
match value {
Parent::Root => ComponentId("/".into()),
Parent::Component(c) => c,
} }
} }
}
impl From<ComponentId> 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;
}

216
rust/src/view/root.rs Normal file
View File

@@ -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))
}
)
}
}