regalade/gui/src/sidebar.rs

373 lines
11 KiB
Rust
Raw Normal View History

2023-05-29 22:28:42 +02:00
use api::RenameHouseholdRequest;
use dioxus::prelude::*;
2023-08-05 12:54:49 +02:00
use dioxus_router::prelude::*;
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,
2023-08-05 12:54:49 +02:00
use_error, use_full_context, use_trimmed_context, ErrorView, HouseholdInfo, Route,
};
#[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",
}
}
}
2023-08-05 12:54:49 +02:00
impl From<Route> for Option<Page> {
fn from(value: Route) -> Self {
match value {
Route::Index => Some(Page::Home),
Route::Login => None,
Route::OidcRedirect { .. } => None,
Route::HouseholdSelection => None,
Route::Ingredients => Some(Page::Ingredients),
Route::RecipeCreator => Some(Page::RecipeCreator),
Route::RecipeList => Some(Page::RecipeList),
Route::RecipeView { .. } => Some(Page::RecipeList),
}
}
}
#[derive(PartialEq)]
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
}
}
})
}
2023-08-05 12:54:49 +02:00
#[derive(Props, PartialEq)]
struct SidebarProps {
entries: Vec<MenuEntry>,
current: Page,
}
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);
2023-08-05 12:54:49 +02:00
let navigator = use_navigator(cx);
let leave = move || {
let token = ctx.read().login.token.clone();
let household = ctx.read().household.id;
2023-08-05 12:54:49 +02:00
to_owned![navigator];
cx.spawn(async move {
match do_leave(token, household).await {
Err(e) => {
log::error!("Could not leave household: {e:?}");
}
Ok(_) => {
2023-08-05 12:54:49 +02:00
navigator.push(Route::HouseholdSelection);
}
}
});
};
let logout = || {
LocalStorage::delete("token");
LocalStorage::delete("household");
2023-08-05 12:54:49 +02:00
navigator.push(Route::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" }
}
}
}
})
}
2023-08-05 12:54:49 +02:00
fn Sidebar(cx: Scope<SidebarProps>) -> 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 {}
}
}
2023-08-05 12:54:49 +02:00
div { class: "col py-3 overflow-scroll vh-100", Outlet::<Route> {} }
}
}
})
2023-05-28 19:48:58 +02:00
}
2023-08-05 12:54:49 +02:00
pub fn RegaladeSidebar(cx: Scope) -> Element {
let current: Route = use_route(cx).unwrap();
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! {
2023-08-05 12:54:49 +02:00
FullContextRedirect {
Sidebar { current: Option::from(current).unwrap(), entries: entries }
}
})
2023-05-28 19:48:58 +02:00
}