Progress on rework

This commit is contained in:
traxys 2024-01-07 19:30:38 +01:00
parent e14c1f0918
commit 4d4fab60cb
4 changed files with 321 additions and 9 deletions

3
public/regalade.css Normal file
View file

@ -0,0 +1,3 @@
.inline {
display: inline;
}

View file

@ -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<AppState>,
Form(form): Form<HouseholdName>,
) -> Result<Redirect, RouteError> {
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<AppState>,
Form(form): Form<CreateHousehold>,
Form(form): Form<HouseholdName>,
) -> Result<Redirect, RouteError> {
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<AppState>,
session: Session,
Path(household): Path<Uuid>,
) -> Result<Redirect, RouteError> {
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<AppState>,
) -> Result<Redirect, RouteError> {
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::<Uuid>("household").await?;
Ok(Redirect::to("/household/select"))
}

View file

@ -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<crate::AppState>;
@ -36,6 +37,11 @@ pub fn base_page_with_head(body: Markup, head: Option<Markup>) -> 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<DbErr> for Box<RouteError> {
}
}
impl From<TransactionError<RouteError>> for RouteError {
fn from(value: TransactionError<RouteError>) -> 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<Redirect, RouteError> {
session.delete().await?;
Ok(Redirect::to("/login"))
}
// #[derive(Serialize, Deserialize, Debug)]
@ -308,8 +365,12 @@ pub(crate) fn router() -> Router<AppState> {
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()))
}

164
src/app/sidebar.rs Normal file
View file

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