2023-05-29 22:28:42 +02:00
|
|
|
use api::RenameHouseholdRequest;
|
2023-07-03 23:20:19 +02:00
|
|
|
use dioxus::prelude::*;
|
2023-08-05 12:54:49 +02:00
|
|
|
use dioxus_router::prelude::*;
|
2023-05-29 17:24:34 +02:00
|
|
|
use gloo_storage::{LocalStorage, Storage};
|
|
|
|
|
use uuid::Uuid;
|
2023-05-28 19:48:58 +02:00
|
|
|
|
2023-05-29 19:30:47 +02:00
|
|
|
use crate::{
|
|
|
|
|
api,
|
|
|
|
|
bootstrap::{bs, ConfirmDangerModal, FormModal},
|
2023-07-03 23:20:19 +02:00
|
|
|
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,
|
2023-05-29 19:30:47 +02:00
|
|
|
};
|
2023-05-29 17:24:34 +02:00
|
|
|
|
2023-07-03 23:20:19 +02:00
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
|
|
|
pub enum Page {
|
|
|
|
|
Home,
|
|
|
|
|
Ingredients,
|
|
|
|
|
RecipeCreator,
|
|
|
|
|
RecipeList,
|
2023-05-28 19:48:58 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-03 23:20:19 +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-05-29 17:24:34 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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)]
|
2023-07-03 23:20:19 +02:00
|
|
|
struct MenuEntry {
|
|
|
|
|
icon: &'static str,
|
|
|
|
|
label: &'static str,
|
|
|
|
|
page: Page,
|
2023-05-29 19:30:47 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-03 23:20:19 +02:00
|
|
|
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 {
|
2023-05-29 19:30:47 +02:00
|
|
|
Ok(Some(uid)) => match do_add_user_to_household(token, household, uid).await {
|
|
|
|
|
Err(e) => {
|
2023-07-03 23:20:19 +02:00
|
|
|
error.set(Some(format!("Could not add user: {e:?}")));
|
2023-05-29 19:30:47 +02:00
|
|
|
}
|
|
|
|
|
Ok(_) => {
|
2023-07-03 23:20:19 +02:00
|
|
|
error.set(None);
|
2023-05-29 19:30:47 +02:00
|
|
|
|
|
|
|
|
let modal = bs::Modal::get_instance("#addMember");
|
|
|
|
|
modal.hide();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Ok(None) => {
|
2023-07-03 23:20:19 +02:00
|
|
|
error.set(Some(format!("User {member} does not exist")));
|
2023-05-29 19:30:47 +02:00
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2023-07-03 23:20:19 +02:00
|
|
|
error.set(Some(format!("Could not resolve user: {e:?}")));
|
2023-05-29 19:30:47 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
2023-07-03 23:20:19 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 19:30:47 +02:00
|
|
|
}
|
2023-07-03 23:20:19 +02:00
|
|
|
}
|
|
|
|
|
})
|
2023-05-29 19:30:47 +02:00
|
|
|
}
|
|
|
|
|
|
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() {
|
2023-07-03 23:20:19 +02:00
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-03 23:20:19 +02:00
|
|
|
#[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
|
|
|
|
2023-07-03 23:20:19 +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
|
|
|
|
2023-07-03 23:20:19 +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) => {
|
2023-07-03 23:20:19 +02:00
|
|
|
error.set(Some(format!("Could not rename household: {e:?}")));
|
2023-05-29 22:28:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-07-03 23:20:19 +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-07-03 23:20:19 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-05 12:54:49 +02:00
|
|
|
#[derive(Props, PartialEq)]
|
|
|
|
|
struct SidebarProps {
|
2023-07-03 23:20:19 +02:00
|
|
|
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
|
|
|
}
|
2023-07-03 23:20:19 +02:00
|
|
|
|
|
|
|
|
LocalStorage::delete("household");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
2023-05-29 22:28:42 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-03 23:20:19 +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);
|
2023-07-03 23:20:19 +02:00
|
|
|
|
|
|
|
|
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];
|
2023-07-03 23:20:19 +02:00
|
|
|
|
|
|
|
|
cx.spawn(async move {
|
2023-05-29 17:24:34 +02:00
|
|
|
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);
|
2023-05-29 17:24:34 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-07-03 23:20:19 +02:00
|
|
|
});
|
|
|
|
|
};
|
2023-05-29 17:24:34 +02:00
|
|
|
|
2023-07-03 23:20:19 +02:00
|
|
|
let logout = || {
|
2023-05-29 17:24:34 +02:00
|
|
|
LocalStorage::delete("token");
|
|
|
|
|
LocalStorage::delete("household");
|
|
|
|
|
|
2023-08-05 12:54:49 +02:00
|
|
|
navigator.push(Route::Login);
|
2023-07-03 23:20:19 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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-05-29 15:35:30 +02:00
|
|
|
|
2023-08-05 12:54:49 +02:00
|
|
|
fn Sidebar(cx: Scope<SidebarProps>) -> Element {
|
2023-07-03 23:20:19 +02:00
|
|
|
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
|
|
|
});
|
|
|
|
|
|
2023-07-03 23:20:19 +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-07-03 23:20:19 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
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",
|
2023-07-03 23:20:19 +02:00
|
|
|
page: Page::Home,
|
2023-06-25 14:12:09 +02:00
|
|
|
},
|
|
|
|
|
MenuEntry {
|
|
|
|
|
label: "Recipes",
|
|
|
|
|
icon: "bi-book",
|
2023-07-03 23:20:19 +02:00
|
|
|
page: Page::RecipeList,
|
2023-05-28 19:48:58 +02:00
|
|
|
},
|
|
|
|
|
MenuEntry {
|
|
|
|
|
label: "Ingredients",
|
|
|
|
|
icon: "bi-egg-fill",
|
2023-07-03 23:20:19 +02:00
|
|
|
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",
|
2023-07-03 23:20:19 +02:00
|
|
|
page: Page::RecipeCreator,
|
2023-06-22 22:33:38 +02:00
|
|
|
},
|
2023-05-28 19:48:58 +02:00
|
|
|
];
|
|
|
|
|
|
2023-07-03 23:20:19 +02:00
|
|
|
cx.render(rsx! {
|
2023-08-05 12:54:49 +02:00
|
|
|
FullContextRedirect {
|
|
|
|
|
Sidebar { current: Option::from(current).unwrap(), entries: entries }
|
2023-07-03 23:20:19 +02:00
|
|
|
}
|
|
|
|
|
})
|
2023-05-28 19:48:58 +02:00
|
|
|
}
|