2023-05-29 22:28:42 +02:00
|
|
|
use api::RenameHouseholdRequest;
|
2023-05-29 17:24:34 +02:00
|
|
|
use gloo_storage::{LocalStorage, Storage};
|
|
|
|
|
use uuid::Uuid;
|
2023-05-29 19:30:47 +02:00
|
|
|
use wasm_bindgen::JsCast;
|
|
|
|
|
use web_sys::HtmlInputElement;
|
2023-05-28 19:48:58 +02:00
|
|
|
use yew::prelude::*;
|
2023-05-29 15:35:30 +02:00
|
|
|
use yew_router::prelude::*;
|
2023-05-28 19:48:58 +02:00
|
|
|
|
2023-05-29 19:30:47 +02:00
|
|
|
use crate::{
|
|
|
|
|
api,
|
|
|
|
|
bootstrap::{bs, ConfirmDangerModal, FormModal},
|
2023-05-29 22:28:42 +02:00
|
|
|
do_add_user_to_household, do_resolve_user, HouseholdInfo, RegaladeGlobalState, Route,
|
2023-06-25 14:12:09 +02:00
|
|
|
RouteKind,
|
2023-05-29 19:30:47 +02:00
|
|
|
};
|
2023-05-29 17:24:34 +02:00
|
|
|
|
2023-05-28 19:48:58 +02:00
|
|
|
#[derive(PartialEq)]
|
|
|
|
|
struct MenuEntry {
|
|
|
|
|
icon: &'static str,
|
|
|
|
|
label: &'static str,
|
2023-06-25 14:12:09 +02:00
|
|
|
page: RouteKind,
|
2023-05-28 19:48:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
|
|
|
struct SidebarProps {
|
|
|
|
|
entries: Vec<MenuEntry>,
|
|
|
|
|
current: Route,
|
|
|
|
|
children: Children,
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 17:24:34 +02:00
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 19:30:47 +02:00
|
|
|
#[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>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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() {
|
|
|
|
|
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>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-28 19:48:58 +02:00
|
|
|
#[function_component]
|
|
|
|
|
fn Sidebar(props: &SidebarProps) -> Html {
|
2023-05-29 15:35:30 +02:00
|
|
|
let global_state = use_state(RegaladeGlobalState::get);
|
2023-05-29 17:24:34 +02:00
|
|
|
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);
|
|
|
|
|
});
|
2023-05-29 15:35:30 +02:00
|
|
|
|
2023-05-29 22:28:42 +02:00
|
|
|
let gs = global_state.clone();
|
|
|
|
|
let on_rename = Callback::from(move |_| {
|
|
|
|
|
gs.set(RegaladeGlobalState::get());
|
|
|
|
|
});
|
|
|
|
|
|
2023-05-28 19:48:58 +02:00
|
|
|
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",
|
2023-06-17 21:52:45 +02:00
|
|
|
"px-sm-3",
|
|
|
|
|
"px-1",
|
2023-05-28 19:48:58 +02:00
|
|
|
"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| {
|
2023-06-25 14:12:09 +02:00
|
|
|
let active = if Some(e.page) == props.current.kind() {
|
2023-05-28 19:48:58 +02:00
|
|
|
Some("active")
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
html! {
|
|
|
|
|
<li class="nav-item w-100">
|
2023-05-29 22:35:44 +02:00
|
|
|
<Link<Route>
|
|
|
|
|
classes={classes!(
|
|
|
|
|
"nav-link",
|
|
|
|
|
"text-white",
|
|
|
|
|
active,
|
|
|
|
|
)}
|
2023-06-25 14:12:09 +02:00
|
|
|
to={e.page.redirect_to()}
|
2023-05-29 22:35:44 +02:00
|
|
|
>
|
2023-05-28 19:48:58 +02:00
|
|
|
<i class={classes!("fs-4", e.icon)}></i>
|
|
|
|
|
<span class={classes!("ms-2", "d-none", "d-sm-inline")}>
|
|
|
|
|
{e.label}
|
|
|
|
|
</span>
|
2023-05-29 22:35:44 +02:00
|
|
|
</Link<Route>>
|
2023-05-28 19:48:58 +02:00
|
|
|
</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")}>
|
2023-05-29 15:35:30 +02:00
|
|
|
{format!("{} ({})", global_state.household.name, global_state.token.name)}
|
2023-05-28 19:48:58 +02:00
|
|
|
</strong>
|
|
|
|
|
</a>
|
2023-05-29 17:24:34 +02:00
|
|
|
<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>
|
2023-05-29 19:30:47 +02:00
|
|
|
<AddMemberModal
|
|
|
|
|
token={global_state.token.token.clone()}
|
|
|
|
|
household={global_state.household.id}
|
|
|
|
|
/>
|
2023-05-29 22:28:42 +02:00
|
|
|
<RenameHouseholdModal
|
|
|
|
|
token={global_state.token.token.clone()}
|
|
|
|
|
household={global_state.household.id}
|
|
|
|
|
name={global_state.household.name.clone()}
|
|
|
|
|
{on_rename}
|
|
|
|
|
/>
|
2023-05-28 19:48:58 +02:00
|
|
|
<ul class="dropdown-menu">
|
2023-05-29 17:24:34 +02:00
|
|
|
<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>
|
2023-05-29 19:30:47 +02:00
|
|
|
<li>
|
|
|
|
|
<a
|
|
|
|
|
class="dropdown-item"
|
|
|
|
|
href="#"
|
|
|
|
|
data-bs-toggle="modal"
|
|
|
|
|
data-bs-target="#addMember"
|
|
|
|
|
>
|
|
|
|
|
{"Add Member"}
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2023-05-29 22:28:42 +02:00
|
|
|
<li>
|
|
|
|
|
<a
|
|
|
|
|
class="dropdown-item"
|
|
|
|
|
href="#"
|
|
|
|
|
data-bs-toggle="modal"
|
|
|
|
|
data-bs-target="#renameHousehold"
|
|
|
|
|
>
|
|
|
|
|
{"Rename household"}
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
2023-05-28 19:48:58 +02:00
|
|
|
<hr />
|
2023-05-29 15:35:30 +02:00
|
|
|
<li>
|
|
|
|
|
<Link<Route>
|
|
|
|
|
classes={classes!("dropdown-item")}
|
|
|
|
|
to={Route::HouseholdSelect}
|
|
|
|
|
>
|
|
|
|
|
{"Change household"}
|
|
|
|
|
</Link<Route>>
|
|
|
|
|
</li>
|
2023-05-28 19:48:58 +02:00
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2023-06-22 22:33:00 +02:00
|
|
|
<div class={classes!("col", "py-3", "overflow-scroll", "vh-100")}>
|
2023-05-28 19:48:58 +02:00
|
|
|
{ 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",
|
2023-06-25 14:12:09 +02:00
|
|
|
page: RouteKind::Index,
|
|
|
|
|
},
|
|
|
|
|
MenuEntry {
|
|
|
|
|
label: "Recipes",
|
|
|
|
|
icon: "bi-book",
|
|
|
|
|
page: RouteKind::Recipe,
|
2023-05-28 19:48:58 +02:00
|
|
|
},
|
|
|
|
|
MenuEntry {
|
|
|
|
|
label: "Ingredients",
|
|
|
|
|
icon: "bi-egg-fill",
|
2023-06-25 14:12:09 +02:00
|
|
|
page: RouteKind::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",
|
2023-06-25 14:12:09 +02:00
|
|
|
page: RouteKind::NewRecipe,
|
2023-06-22 22:33:38 +02:00
|
|
|
},
|
2023-05-28 19:48:58 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
html! {
|
2023-05-29 15:35:30 +02:00
|
|
|
<Sidebar {entries} current={props.current}>
|
2023-05-28 19:48:58 +02:00
|
|
|
{ for props.children.iter() }
|
|
|
|
|
</Sidebar>
|
|
|
|
|
}
|
|
|
|
|
}
|