From 4d4fab60cbbd01455ec03b7c10c3d216874b0196 Mon Sep 17 00:00:00 2001 From: traxys Date: Sun, 7 Jan 2024 19:30:38 +0100 Subject: [PATCH] Progress on rework --- public/regalade.css | 3 + src/app/household.rs | 92 ++++++++++++++++++++++-- src/app/mod.rs | 71 +++++++++++++++++-- src/app/sidebar.rs | 164 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 public/regalade.css create mode 100644 src/app/sidebar.rs diff --git a/public/regalade.css b/public/regalade.css new file mode 100644 index 0000000..97304c8 --- /dev/null +++ b/public/regalade.css @@ -0,0 +1,3 @@ +.inline { + display: inline; +} diff --git a/src/app/household.rs b/src/app/household.rs index 68f03fa..95ae65a 100644 --- a/src/app/household.rs +++ b/src/app/household.rs @@ -1,12 +1,12 @@ use axum::{ async_trait, - extract::{FromRef, FromRequestParts, State}, + extract::{FromRef, FromRequestParts, Path, State}, http::request::Parts, response::Redirect, Form, }; use maud::{html, Markup}; -use sea_orm::{prelude::*, ActiveValue}; +use sea_orm::{prelude::*, ActiveValue, DatabaseTransaction, TransactionTrait}; use serde::{Deserialize, Serialize}; use tower_sessions::Session; use uuid::Uuid; @@ -132,14 +132,28 @@ pub(super) async fn household_selection( } #[derive(Serialize, Deserialize, Debug)] -pub(super) struct CreateHousehold { +pub(super) struct HouseholdName { name: String, } +pub(super) async fn rename( + household: CurrentHousehold, + state: State, + Form(form): Form, +) -> Result { + let mut household: household::ActiveModel = household.0.into(); + + household.name = ActiveValue::Set(form.name); + + household.update(&state.db).await?; + + Ok(Redirect::to("/")) +} + pub(super) async fn create_household( user: AuthenticatedUser, state: State, - Form(form): Form, + Form(form): Form, ) -> Result { let household = household::ActiveModel { name: ActiveValue::Set(form.name), @@ -157,3 +171,73 @@ pub(super) async fn create_household( Ok(Redirect::to("/household/select")) } + +pub(super) async fn select_household( + user: AuthenticatedUser, + state: State, + session: Session, + Path(household): Path, +) -> Result { + let hs_is_authorized = user + .model + .find_related(Household) + .filter(household::Column::Id.eq(household)) + .count(&state.db) + .await?; + + if hs_is_authorized == 0 { + return Err(RouteError::RessourceNotFound); + } + + session.insert("household", household).await?; + + Ok(Redirect::to("/")) +} + +async fn delete_household( + household: household::Model, + txn: &DatabaseTransaction, +) -> Result<(), RouteError> { + for ingredient in household.find_related(Ingredient).all(txn).await? { + ingredient.delete(txn).await?; + } + + household.delete(txn).await?; + + Ok(()) +} + +pub(super) async fn leave( + household: CurrentHousehold, + user: AuthenticatedUser, + session: Session, + state: State, +) -> Result { + state + .db + .transaction(|txn| { + Box::pin(async move { + HouseholdMembers::delete_by_id((household.0.id, user.model.id)) + .exec(txn) + .await?; + + let Some(household) = Household::find_by_id(household.0.id) + .one(txn) + .await? else { + return Err(RouteError::InvalidRequest("No such household".into())); + }; + + let member_count = household.find_related(User).count(txn).await?; + if member_count == 0 { + delete_household(household, txn).await?; + } + + Ok(()) + }) + }) + .await?; + + session.remove::("household").await?; + + Ok(Redirect::to("/household/select")) +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 205c088..7f96a0c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -18,9 +18,10 @@ use uuid::Uuid; use crate::entity::{prelude::*, user}; -use self::household::CurrentHousehold; +use self::{household::CurrentHousehold, sidebar::SidebarLocation}; mod household; +mod sidebar; type AppState = Arc; @@ -36,6 +37,11 @@ pub fn base_page_with_head(body: Markup, head: Option) -> Markup { rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous"; + link rel="stylesheet" href="/regalade.css"; + link rel="stylesheet" + href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" + integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" + crossorigin="anonymous"; @if let Some(head) = head { (head) } @@ -117,6 +123,15 @@ impl From for Box { } } +impl From> for RouteError { + fn from(value: TransactionError) -> Self { + match value { + TransactionError::Connection(e) => e.into(), + TransactionError::Transaction(e) => e, + } + } +} + impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { match self { @@ -267,10 +282,52 @@ fn not_found() -> (StatusCode, Markup) { error_page(StatusCode::NOT_FOUND, "Page not found") } -async fn index(_user: AuthenticatedUser, household: CurrentHousehold) -> Markup { - base_page(html! { - "Hello world in " (household.0.name) "!" - }) +fn confirm_danger_modal(id: &str, inner: &str, action: &str, title: &str) -> Markup { + html! { + .modal + #(id) + tabindex="-1" + aria-labelledby={(id) "Label"} + aria-hidden="true" { + .modal-dialog.modal-dialog-centered { + .modal-content { + .modal-header { + h1 .modal-title."fs-5" #{(id) "Label"} { (title) } + button + .btn-close + data-bs-dismiss="modal" + aria-label="Cancel" {} + } + .modal-body { + (inner) + } + .modal-footer { + button .btn.btn-secondary data-bs-dismiss="modal" { "Cancel" } + form action=(action) method="post" .inline { + button type="submit" .btn.btn-danger { "Confirm" } + } + } + } + } + } + } +} + +async fn index(user: AuthenticatedUser, household: CurrentHousehold) -> Markup { + sidebar::sidebar( + SidebarLocation::Home, + &household, + &user, + html! { + "Hello world in " (household.0.name) "!" + }, + ) +} + +async fn logout(session: Session) -> Result { + session.delete().await?; + + Ok(Redirect::to("/login")) } // #[derive(Serialize, Deserialize, Debug)] @@ -308,8 +365,12 @@ pub(crate) fn router() -> Router { router .route("/", get(index)) .route("/login", get(oidc_login)) + .route("/logout", get(logout)) .route("/household/select", get(household::household_selection)) + .route("/household/select/:id", get(household::select_household)) .route("/household/create", post(household::create_household)) + .route("/household/leave", post(household::leave)) + .route("/household/rename", post(household::rename)) .route("/login/redirect/:id", get(oidc_login_finish)) .fallback_service(ServeDir::new(public).fallback((|| async { not_found() }).into_service())) } diff --git a/src/app/sidebar.rs b/src/app/sidebar.rs new file mode 100644 index 0000000..705cd96 --- /dev/null +++ b/src/app/sidebar.rs @@ -0,0 +1,164 @@ +use maud::{html, Markup}; + +use super::{base_page, confirm_danger_modal, household::CurrentHousehold, AuthenticatedUser}; + +#[derive(Clone, Copy, PartialEq)] +pub(super) enum SidebarLocation { + Home, + RecipeList, + Ingredients, + RecipeCreator, +} + +impl SidebarLocation { + fn to(&self) -> &'static str { + match self { + SidebarLocation::Home => "/", + SidebarLocation::RecipeList => "/recipes", + SidebarLocation::Ingredients => "/ingredients", + SidebarLocation::RecipeCreator => "/recipes/create", + } + } +} + +struct MenuEntry { + icon: &'static str, + label: &'static str, + location: SidebarLocation, +} + +fn rename_hs_modal(current: &str) -> Markup { + html! { + .modal.fade + #renameHsModal + tabindex="-1" + aria-labelledby="renameHsModalLabel" + aria-hidden="true" { + .modal-dialog.modal-dialog-centered { + .modal-content { + .modal-header { + h1 .modal-title."fs-5" #renameHsModalLabel { "Rename Household" } + input + type="reset" + form="renameHsModalForm" + .btn-close + data-bs-dismiss="modal" + aria-label="Close" + value=""; + } + .modal-body { + form #renameHsModalForm method="post" action="/household/rename" { + .form-floating { + input + .form-control + #renameHsName + placeholder="Household name" + name="name" + value=(current); + label for="renameHsName" { "New household name" } + } + } + } + .modal-footer { + input + type="reset" + form="renameHsModalForm" + .btn.btn-danger + data-bs-dismiss="modal" + value="Cancel"; + input + type="submit" + form="renameHsModalForm" + .btn.btn-primary + data-bs-dismiss="modal" + value="Rename"; + } + } + } + } + } +} + +pub(super) fn sidebar( + current: SidebarLocation, + household: &CurrentHousehold, + user: &AuthenticatedUser, + inner: Markup, +) -> Markup { + let entries = &[ + MenuEntry { + label: "Home", + icon: "bi-house", + location: SidebarLocation::Home, + }, + MenuEntry { + label: "Recipes", + icon: "bi-book", + location: SidebarLocation::RecipeList, + }, + MenuEntry { + label: "Ingredients", + icon: "bi-egg-fill", + location: SidebarLocation::Ingredients, + }, + MenuEntry { + label: "New Recipe", + icon: "bi-clipboard2-plus-fill", + location: SidebarLocation::RecipeCreator, + }, + ]; + + base_page(html! { + .container-fluid { + .row.flex-nowrap { + .col-auto."col-md-3"."col-xl-2"."px-sm-2"."px-0".bg-dark-subtle { + .d-flex.flex-column.align-items-center.align-items-sm-start."px-sm-3"."px-1"."pt-2".text-white."min-vh-100" { + ul #menu .nav.nav-pills.flex-column.mb-sm-auto."mb-0".align-items-center.align-items-sm-start."w-100" { + @for entry in entries { + li .nav-item."w-100" { + a href=(entry.location.to()) + class={"nav-link text-white" ( + (entry.location == current).then_some(" active").unwrap_or("") + )} { + i class={"fs-4 " (entry.icon)} {} + span ."ms-2".d-none.d-sm-inline { (entry.label) } + } + } + } + } + hr ."w-100"; + (confirm_danger_modal( + "leaveModal", + &format!("Are you sure you want to leave the household {}", household.0.name), + "/household/leave", + "Leave Household", + )) + (rename_hs_modal(&household.0.name)) + .dropdown { + a href="#" "data-bs-toggle"="dropdown" "aria-expanded"="false" + .d-flex.align-items-center.text-white.text-decoration-none.dropdown-toggle { + i ."fs-4".bi-house-door-fill {} + strong ."ms-2".d-none.d-sm-inline { + (household.0.name) " (" (user.model.name) ")" + } + } + ul .dropdown-menu { + li { a .dropdown-item href="/logout" {"Logout"}} + hr; + li { a .dropdown-item href="#" data-bs-toggle="modal" data-bs-target="#leaveModal" { + "Leave household" + }} + li { a .dropdown-item href="#" data-bs-toggle="modal" data-bs-target="#renameHsModal" { + "Rename household" + }} + hr; + li { a .dropdown-item href="/household/select" {"Change household"}} + } + } + } + } + .col."py-3".overflow-scroll."vh-100" { (inner) } + } + } + }) +}