regalade/app/src/sidebar.rs

363 lines
11 KiB
Rust
Raw Normal View History

2023-05-29 22:28:42 +02:00
use api::RenameHouseholdRequest;
use dioxus::prelude::*;
use dioxus_router::{use_router, Link};
use gloo_storage::{LocalStorage, Storage};
use uuid::Uuid;
2023-05-28 19:48:58 +02:00
use crate::{
api,
bootstrap::{bs, ConfirmDangerModal, FormModal},
do_add_user_to_household, do_resolve_user,
full_context::FullContextRedirect,
use_error, use_full_context, use_trimmed_context, ErrorView, HouseholdInfo,
};
#[derive(Clone, Copy, PartialEq)]
pub enum Page {
Home,
Ingredients,
RecipeCreator,
RecipeList,
2023-05-28 19:48:58 +02:00
}
impl Page {
pub fn to(&self) -> &'static str {
match self {
Page::Home => "/",
Page::Ingredients => "/ingredients",
Page::RecipeCreator => "/recipe_creator",
Page::RecipeList => "/recipe",
}
}
}
struct MenuEntry {
icon: &'static str,
label: &'static str,
page: Page,
}
fn AddMemberModal(cx: Scope) -> Element {
let error = use_error(cx);
let (token, household) = use_trimmed_context(cx);
let member = use_state(cx, String::new);
let add_member = move || {
to_owned![member, error, token, member];
cx.spawn(async move {
match do_resolve_user(token.clone(), member.to_string()).await {
Ok(Some(uid)) => match do_add_user_to_household(token, household, uid).await {
Err(e) => {
error.set(Some(format!("Could not add user: {e:?}")));
}
Ok(_) => {
error.set(None);
let modal = bs::Modal::get_instance("#addMember");
modal.hide();
}
},
Ok(None) => {
error.set(Some(format!("User {member} does not exist")));
}
Err(e) => {
error.set(Some(format!("Could not resolve user: {e:?}")));
}
}
})
};
cx.render(rsx! {
FormModal {
id: "addMember",
centered: true,
submit_label: "Add",
title: "Add a member",
on_submit: move |_| add_member(),
ErrorView { error: error }
div { class: "form-floating",
input {
class: "form-control",
id: "addMemberName",
placeholder: "Member name",
value: "{member}",
oninput: move |e| member.set(e.value.to_string())
}
label { "for": "addMemberName", "Member name" }
}
}
})
}
2023-05-29 22:28:42 +02:00
async fn do_rename_household(token: String, household: Uuid, name: String) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::patch(api!("household/{household}"))
.header("Authorization", &format!("Bearer {token}"))
.json(&RenameHouseholdRequest { name: name.clone() })?
.send()
.await?;
if !rsp.ok() {
anyhow::bail!("Could not leave: {}", rsp.text().await?);
2023-05-29 22:28:42 +02:00
}
LocalStorage::set(
"household",
HouseholdInfo {
id: household,
name,
},
)
.expect("Could not set household info");
Ok(())
}
#[inline_props]
fn RenameHousehold<'a>(cx: Scope<'a>, name: &'a str) -> Element {
let error = use_error(cx);
let ctx = use_full_context(cx);
let name = use_state(cx, || name.to_string());
2023-05-29 22:28:42 +02:00
let rename_hs = move |_| {
to_owned![name, error];
let (token, household) = {
let h = ctx.read();
(h.login.token.clone(), h.household.id)
};
let refresh = ctx.refresh_handle();
2023-05-29 22:28:42 +02:00
cx.spawn(async move {
match do_rename_household(token, household, name.to_string()).await {
Ok(_) => {
error.set(None);
refresh.refresh();
2023-05-29 22:28:42 +02:00
let modal = bs::Modal::get_instance("#renameHousehold");
modal.hide();
}
Err(e) => {
error.set(Some(format!("Could not rename household: {e:?}")));
2023-05-29 22:28:42 +02:00
}
}
});
};
cx.render(rsx! {
FormModal {
id: "renameHousehold",
centered: true,
submit_label: "Rename",
title: "Rename household",
on_submit: rename_hs,
ErrorView { error: error }
div { class: "form-floating",
input {
id: "householdRename",
class: "form-control",
placeholder: "New household name",
value: "{name}",
oninput: move |e| name.set(e.value.clone())
}
label { "for": "householdRename", "New household name" }
2023-05-29 22:28:42 +02:00
}
}
})
}
#[derive(Props)]
struct SidebarProps<'a> {
entries: Vec<MenuEntry>,
current: Page,
children: Element<'a>,
}
async fn do_leave(token: String, household: Uuid) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::delete(api!("household/{household}"))
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.body();
match body {
None => anyhow::bail!("Could not leave: {rsp:?}"),
Some(s) => anyhow::bail!("Could not leave: {}", s.to_string()),
}
2023-05-29 22:28:42 +02:00
}
LocalStorage::delete("household");
Ok(())
2023-05-29 22:28:42 +02:00
}
fn SidebarDropdown(cx: Scope) -> Element {
let ctx = use_full_context(cx);
let router = use_router(cx);
let leave = move || {
let token = ctx.read().login.token.clone();
let household = ctx.read().household.id;
to_owned![router];
cx.spawn(async move {
match do_leave(token, household).await {
Err(e) => {
log::error!("Could not leave household: {e:?}");
}
Ok(_) => {
router.navigate_to("/household_selection");
}
}
});
};
let logout = || {
LocalStorage::delete("token");
LocalStorage::delete("household");
router.navigate_to("/login");
};
cx.render(rsx! {
div { class: "dropdown",
a {
href: "#",
"data-bs-toggle": "dropdown",
"aria-expanded": "false",
class: "d-flex align-items-center text-white text-decoration-none dropdown-toggle",
i { class: "fs-4 bi-house-door-fill" }
strong { class: "ms-2 d-none d-sm-inline",
"{ctx.read().household.name} ({ctx.read().login.name})"
}
}
ConfirmDangerModal {
id: "leaveModal",
title: "Leave household",
centered: true,
on_confirm: move |_| leave(),
"Are you sure you want to leave the household '{ctx.read().household.name}' ?"
}
AddMemberModal {}
RenameHousehold { name: "{ctx.read().household.name}" }
ul { class: "dropdown-menu",
li { a { class: "dropdown-item", href: "#", onclick: move |_| logout(), "Logout" } }
hr {}
li {
a {
class: "dropdown-item",
href: "#",
"data-bs-toggle": "modal",
"data-bs-target": "#leaveModal",
"Leave household"
}
}
li {
a {
class: "dropdown-item",
href: "#",
"data-bs-toggle": "modal",
"data-bs-target": "#addMember",
"Add member"
}
}
li {
a {
class: "dropdown-item",
href: "#",
"data-bs-toggle": "modal",
"data-bs-target": "#renameHousehold",
"Rename household"
}
}
hr {}
li {
Link { to: "/household_selection", class: "dropdown-item", "Change household" }
}
}
}
})
}
fn Sidebar<'a>(cx: Scope<'a, SidebarProps<'a>>) -> Element {
let entries = cx.props.entries.iter().map(|e| {
let active = if e.page == cx.props.current {
"active"
} else {
""
};
rsx! {
li { class: "nav-item w-100",
Link { to: e.page.to(), class: "nav-link text-white {active}",
i { class: "fs-4 {e.icon}" }
span { class: "ms-2 d-none d-sm-inline", "{e.label}" }
}
}
}
2023-05-29 22:28:42 +02:00
});
cx.render(rsx! {
div { class: "container-fluid",
div { class: "row flex-nowrap",
div { class: "col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark-subtle",
div { class: "d-flex flex-column align-items-center align-items-sm-start px-sm-3 px-1 pt-2 text-white min-vh-100",
Link {
to: "/",
class: "d-flex align-items-center pb-3 mb-md-0 me-md-auto text-white text-decoration-none",
span { class: "fs-5 d-none d-sm-inline", "Menu" }
}
hr { class: "w-100 d-none d-sm-inline" }
ul {
id: "menu",
class: "nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start w-100",
entries
}
hr { class: "w-100" }
SidebarDropdown {}
}
}
div { class: "col py-3 overflow-scroll vh-100", &cx.props.children }
}
}
})
2023-05-28 19:48:58 +02:00
}
#[derive(Props)]
pub struct RegaladeSidebarProps<'a> {
current: Page,
children: Element<'a>,
2023-05-28 19:48:58 +02:00
}
pub fn RegaladeSidebar<'a>(cx: Scope<'a, RegaladeSidebarProps<'a>>) -> Element {
2023-05-28 19:48:58 +02:00
let entries = vec![
MenuEntry {
label: "Home",
icon: "bi-house",
page: Page::Home,
2023-06-25 14:12:09 +02:00
},
MenuEntry {
label: "Recipes",
icon: "bi-book",
page: Page::RecipeList,
2023-05-28 19:48:58 +02:00
},
MenuEntry {
label: "Ingredients",
icon: "bi-egg-fill",
page: Page::Ingredients,
2023-05-28 19:48:58 +02:00
},
2023-06-22 22:33:38 +02:00
MenuEntry {
label: "New Recipe",
icon: "bi-clipboard2-plus-fill",
page: Page::RecipeCreator,
2023-06-22 22:33:38 +02:00
},
2023-05-28 19:48:58 +02:00
];
cx.render(rsx! {
FullContextRedirect {
Sidebar { current: cx.props.current, entries: entries, &cx.props.children }
}
})
2023-05-28 19:48:58 +02:00
}