regalade/app/src/sidebar.rs
2023-05-29 22:28:42 +02:00

446 lines
16 KiB
Rust

use api::RenameHouseholdRequest;
use gloo_storage::{LocalStorage, Storage};
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_router::prelude::*;
use crate::{
api,
bootstrap::{bs, ConfirmDangerModal, FormModal},
do_add_user_to_household, do_resolve_user, HouseholdInfo, RegaladeGlobalState, Route,
};
#[derive(PartialEq)]
struct MenuEntry {
icon: &'static str,
label: &'static str,
page: Route,
}
#[derive(Properties, PartialEq)]
struct SidebarProps {
entries: Vec<MenuEntry>,
current: Route,
children: Children,
}
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()),
}
}
LocalStorage::delete("household");
Ok(())
}
#[derive(PartialEq, Properties)]
struct AddMemberProps {
token: String,
household: Uuid,
}
#[function_component]
fn AddMemberModal(props: &AddMemberProps) -> Html {
let error = use_state(|| None::<String>);
let err = error.clone();
let token = props.token.clone();
let household = props.household;
let add_member = Callback::from(move |_| {
let document = gloo_utils::document();
let name: HtmlInputElement = document
.get_element_by_id("addMemberName")
.unwrap()
.dyn_into()
.expect("addMemberName is not an input element");
let name = name.value();
let err = err.clone();
let token = token.clone();
wasm_bindgen_futures::spawn_local(async move {
match do_resolve_user(token.clone(), name.clone()).await {
Ok(Some(uid)) => match do_add_user_to_household(token, household, uid).await {
Err(e) => {
err.set(Some(format!("Could not add user: {e:?}")));
}
Ok(_) => {
err.set(None);
let modal = bs::Modal::get_instance("#addMember");
modal.hide();
}
},
Ok(None) => {
err.set(Some(format!("User '{name}' does not exist")));
}
Err(e) => {
err.set(Some(format!("Could not resolve user '{name}': {e:?}")));
}
}
})
});
html! {
<FormModal
id="addMember"
centered={true}
submit_label="Add"
title="Add a member"
on_submit={add_member}
>
if let Some(err) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{err}
</div>
}
<div class="form-floating">
<input
id="addMemberName"
class={classes!("form-control")}
placeholder="Member Name"
/>
<label for="addMemberName">{"Member name"}</label>
</div>
</FormModal>
}
}
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() {
let body = rsp.body();
match body {
None => anyhow::bail!("Could not leave: {rsp:?}"),
Some(s) => anyhow::bail!("Could not leave: {}", s.to_string()),
}
}
LocalStorage::set(
"household",
HouseholdInfo {
id: household,
name,
},
)
.expect("Could not set household info");
Ok(())
}
#[derive(Properties, PartialEq)]
struct RenameHouseholdProps {
token: String,
household: Uuid,
name: String,
on_rename: Callback<()>,
}
#[function_component]
fn RenameHouseholdModal(props: &RenameHouseholdProps) -> Html {
let error = use_state(|| None::<String>);
let err = error.clone();
let token = props.token.clone();
let household = props.household;
let on_rename = props.on_rename.clone();
let add_member = Callback::from(move |_| {
let document = gloo_utils::document();
let name: HtmlInputElement = document
.get_element_by_id("householdRename")
.unwrap()
.dyn_into()
.expect("householdRename is not an input element");
let name = name.value();
let err = err.clone();
let token = token.clone();
let on_rename = on_rename.clone();
wasm_bindgen_futures::spawn_local(async move {
match do_rename_household(token, household, name).await {
Ok(_) => {
err.set(None);
on_rename.emit(());
let modal = bs::Modal::get_instance("#renameHousehold");
modal.hide();
}
Err(e) => {
err.set(Some(format!("Could not rename household: {e:?}")));
}
}
})
});
html! {
<FormModal
id="renameHousehold"
centered={true}
submit_label="Rename"
title="Rename houshold"
on_submit={add_member}
>
if let Some(err) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{err}
</div>
}
<div class="form-floating">
<input
id="householdRename"
class={classes!("form-control")}
placeholder="New household name"
value={props.name.clone()}
/>
<label for="householdRename">{"New household name"}</label>
</div>
</FormModal>
}
}
#[function_component]
fn Sidebar(props: &SidebarProps) -> Html {
let global_state = use_state(RegaladeGlobalState::get);
let navigator = use_navigator().unwrap();
let token = global_state.token.token.clone();
let household = global_state.household.id;
let nav = navigator.clone();
let leave_household = Callback::from(move |()| {
let token = token.clone();
let nav = nav.clone();
wasm_bindgen_futures::spawn_local(async move {
match do_leave(token, household).await {
Err(e) => {
log::error!("Could not leave household: {e:?}");
}
Ok(_) => {
nav.push(&Route::HouseholdSelect);
}
}
})
});
let logout = Callback::from(move |_| {
LocalStorage::delete("token");
LocalStorage::delete("household");
navigator.push(&Route::HouseholdSelect);
});
let gs = global_state.clone();
let on_rename = Callback::from(move |_| {
gs.set(RegaladeGlobalState::get());
});
html! {
<div class="container-fluid">
<div class={classes!("row", "flex-nowrap")}>
<div class={classes!(
"col-auto",
"col-md-3",
"col-xl-2",
"px-sm-2",
"px-0",
"bg-dark-subtle"
)}>
<div class={classes!(
"d-flex",
"flex-column",
"align-items-center",
"align-items-sm-start",
"px-3",
"pt-2",
"text-white",
"min-vh-100"
)}>
<a href="/" class={classes!(
"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"}</span>
</a>
<hr class={classes!("w-100", "d-none", "d-sm-inline")} />
<ul id="menu" class={classes!(
"nav",
"nav-pills",
"flex-column",
"mb-sm-auto",
"mb-0",
"align-items-center",
"align-items-sm-start",
"w-100",
)}>
{
for props.entries.iter().map(|e| {
let active = if e.page == props.current {
Some("active")
} else {
None
};
html! {
<li class="nav-item w-100">
<a href="#" class={classes!(
"nav-link",
"text-white",
active,
)}>
<i class={classes!("fs-4", e.icon)}></i>
<span class={classes!("ms-2", "d-none", "d-sm-inline")}>
{e.label}
</span>
</a>
</li>
}
})
}
</ul>
<hr class="w-100" />
<div class={classes!("dropdown")}>
<a href="#"
data-bs-toggle="dropdown"
aria-expanded="false"
class={classes!(
"d-flex",
"align-items-center",
"text-white",
"text-decoration-none",
"dropdown-toggle",
)}
>
<i class={classes!("fs-4", "bi-house-door-fill")}></i>
<strong class={classes!("ms-2", "d-none", "d-sm-inline")}>
{format!("{} ({})", global_state.household.name, global_state.token.name)}
</strong>
</a>
<ConfirmDangerModal
id="leaveModal"
title="Leaving Household"
centered={true}
on_confirm={leave_household}
>
{format!("Are you sure you want to leave the household '{}' ?", global_state.household.name)}
</ConfirmDangerModal>
<AddMemberModal
token={global_state.token.token.clone()}
household={global_state.household.id}
/>
<RenameHouseholdModal
token={global_state.token.token.clone()}
household={global_state.household.id}
name={global_state.household.name.clone()}
{on_rename}
/>
<ul class="dropdown-menu">
<li>
<a
class="dropdown-item"
href="#"
onclick={logout}
>
{"Logout"}
</a>
</li>
<hr />
<li>
<a
class="dropdown-item"
href="#"
data-bs-toggle="modal"
data-bs-target="#leaveModal"
>
{"Leave Household"}
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
data-bs-toggle="modal"
data-bs-target="#addMember"
>
{"Add Member"}
</a>
</li>
<li>
<a
class="dropdown-item"
href="#"
data-bs-toggle="modal"
data-bs-target="#renameHousehold"
>
{"Rename household"}
</a>
</li>
<hr />
<li>
<Link<Route>
classes={classes!("dropdown-item")}
to={Route::HouseholdSelect}
>
{"Change household"}
</Link<Route>>
</li>
</ul>
</div>
</div>
</div>
<div class={classes!("col", "py-3")}>
{ for props.children.iter() }
</div>
</div>
</div>
}
}
#[derive(Properties, PartialEq)]
pub(crate) struct RegaladeSidebarProps {
pub(crate) current: Route,
pub(crate) children: Children,
}
#[function_component]
pub(crate) fn RegaladeSidebar(props: &RegaladeSidebarProps) -> Html {
let entries = vec![
MenuEntry {
label: "Home",
icon: "bi-house",
page: Route::Index,
},
MenuEntry {
label: "Ingredients",
icon: "bi-egg-fill",
page: Route::Ingredients,
},
];
html! {
<Sidebar {entries} current={props.current}>
{ for props.children.iter() }
</Sidebar>
}
}