Make the GUI a library to support multiple platforms
This commit is contained in:
parent
d3f89a8757
commit
0d900024cb
24 changed files with 76 additions and 39 deletions
372
gui/src/sidebar.rs
Normal file
372
gui/src/sidebar.rs
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
use api::RenameHouseholdRequest;
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router::prelude::*;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use uuid::Uuid;
|
||||
|
||||
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, Route,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum Page {
|
||||
Home,
|
||||
Ingredients,
|
||||
RecipeCreator,
|
||||
RecipeList,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn to(&self) -> &'static str {
|
||||
match self {
|
||||
Page::Home => "/",
|
||||
Page::Ingredients => "/ingredients",
|
||||
Page::RecipeCreator => "/recipe_creator",
|
||||
Page::RecipeList => "/recipe",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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?);
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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();
|
||||
|
||||
cx.spawn(async move {
|
||||
match do_rename_household(token, household, name.to_string()).await {
|
||||
Ok(_) => {
|
||||
error.set(None);
|
||||
refresh.refresh();
|
||||
|
||||
let modal = bs::Modal::get_instance("#renameHousehold");
|
||||
modal.hide();
|
||||
}
|
||||
Err(e) => {
|
||||
error.set(Some(format!("Could not rename household: {e:?}")));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[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()),
|
||||
}
|
||||
}
|
||||
|
||||
LocalStorage::delete("household");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn SidebarDropdown(cx: Scope) -> Element {
|
||||
let ctx = use_full_context(cx);
|
||||
let navigator = use_navigator(cx);
|
||||
|
||||
let leave = move || {
|
||||
let token = ctx.read().login.token.clone();
|
||||
let household = ctx.read().household.id;
|
||||
to_owned![navigator];
|
||||
|
||||
cx.spawn(async move {
|
||||
match do_leave(token, household).await {
|
||||
Err(e) => {
|
||||
log::error!("Could not leave household: {e:?}");
|
||||
}
|
||||
Ok(_) => {
|
||||
navigator.push(Route::HouseholdSelection);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let logout = || {
|
||||
LocalStorage::delete("token");
|
||||
LocalStorage::delete("household");
|
||||
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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", Outlet::<Route> {} }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn RegaladeSidebar(cx: Scope) -> Element {
|
||||
let current: Route = use_route(cx).unwrap();
|
||||
let entries = vec![
|
||||
MenuEntry {
|
||||
label: "Home",
|
||||
icon: "bi-house",
|
||||
page: Page::Home,
|
||||
},
|
||||
MenuEntry {
|
||||
label: "Recipes",
|
||||
icon: "bi-book",
|
||||
page: Page::RecipeList,
|
||||
},
|
||||
MenuEntry {
|
||||
label: "Ingredients",
|
||||
icon: "bi-egg-fill",
|
||||
page: Page::Ingredients,
|
||||
},
|
||||
MenuEntry {
|
||||
label: "New Recipe",
|
||||
icon: "bi-clipboard2-plus-fill",
|
||||
page: Page::RecipeCreator,
|
||||
},
|
||||
];
|
||||
|
||||
cx.render(rsx! {
|
||||
FullContextRedirect {
|
||||
Sidebar { current: Option::from(current).unwrap(), entries: entries }
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue