use api::{ CreateHouseholdRequest, CreateHouseholdResponse, Household, LoginRequest, LoginResponse, }; use gloo_storage::{errors::StorageError, LocalStorage, Storage}; use log::Level; use serde::{Deserialize, Serialize}; use uuid::Uuid; use wasm_bindgen::{prelude::*, JsCast}; use web_sys::HtmlInputElement; use yew::{prelude::*, suspense::use_future}; use itertools::Itertools; use yew_router::prelude::*; use crate::sidebar::RegaladeSidebar; mod sidebar; const API_ROUTE: &str = match option_env!("REGALADE_API_SERVER_BASE") { None => "http://localhost:8085", Some(v) => v, }; macro_rules! api { ($($arg:tt)*) => { &format!("{API_ROUTE}/api/{}", format_args!($($arg)*)) }; } #[derive(Routable, Debug, Clone, Copy, PartialEq, Eq)] enum Route { #[at("/")] Index, #[at("/login")] Login, #[at("/ingredients")] Ingredients, #[at("/household_select")] HouseholdSelect, #[at("/404")] #[not_found] NotFound, } #[function_component] fn App() -> Html { html! { render={switch} /> } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct HouseholdInfo { id: Uuid, name: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct LoginInfo { token: String, name: String, } #[derive(Debug, Clone)] struct RegaladeGlobalState { token: LoginInfo, household: HouseholdInfo, } impl RegaladeGlobalState { pub fn get_or_navigate(navigator: Navigator) -> Option { let token = match LocalStorage::get::("token") { Ok(v) => v, Err(StorageError::KeyNotFound(_)) => { navigator.push(&Route::Login); return None; } Err(e) => unreachable!("Could not get token: {e:?}"), }; let household = match LocalStorage::get::("household") { Ok(v) => v, Err(StorageError::KeyNotFound(_)) => { navigator.push(&Route::HouseholdSelect); return None; } Err(e) => unreachable!("Could not get household: {e:?}"), }; Some(Self { token, household }) } pub fn get() -> Self { let token = match LocalStorage::get::("token") { Ok(v) => v, Err(e) => unreachable!("Could not get token: {e:?}"), }; let household = match LocalStorage::get::("household") { Ok(v) => v, Err(e) => unreachable!("Could not get household: {e:?}"), }; Self { token, household } } } #[derive(Debug, PartialEq, Properties)] struct GlobalStateRedirectorProps { children: Children, route: Route, } #[function_component] fn GlobalStateRedirector(props: &GlobalStateRedirectorProps) -> Html { let navigator = use_navigator().unwrap(); let s = RegaladeGlobalState::get_or_navigate(navigator); match s { Some(_) => { html! { { for props.children.iter() } } } None => html! {}, } } async fn do_new_household(token: String, name: String) -> anyhow::Result { let rsp = gloo_net::http::Request::post(api!("household")) .json(&CreateHouseholdRequest { name })? .header("Authorization", &format!("Bearer {token}")) .send() .await?; if !rsp.ok() { anyhow::bail!("Request failed: {rsp:?}") } let rsp: CreateHouseholdResponse = rsp.json().await?; Ok(rsp.id) } #[wasm_bindgen(js_namespace = bootstrap)] extern "C" { type Modal; #[wasm_bindgen(static_method_of = Modal, js_name = "getInstance")] fn get_instance(selector: &str) -> Modal; #[wasm_bindgen(method)] fn hide(this: &Modal); } async fn fetch_households(token: String) -> anyhow::Result { let rsp = gloo_net::http::Request::get(api!("household")) .header("Authorization", &format!("Bearer {token}")) .send() .await?; if !rsp.ok() { anyhow::bail!("Request failed: {rsp:?}") } let rsp: api::Households = rsp.json().await?; Ok(rsp) } #[function_component] fn HouseholdListSelect() -> HtmlResult { let token = use_state(|| match LocalStorage::get::("token") { Ok(v) => v, Err(e) => unreachable!("Need to be logged in to list households: {e:?}"), }); let households = use_future(move || fetch_households(token.token.to_string()))?; let navigator = use_navigator().unwrap(); let mk_household = |(id, info): (_, Household)| { let name = info.name.clone(); let nav = navigator.clone(); let onclick = Callback::from(move |_| { log::info!("Clicked {id}"); if let Err(e) = LocalStorage::set( "household", HouseholdInfo { id, name: name.clone(), }, ) { log::error!("Could not select household: {e:?}"); return; } nav.push(&Route::Index); }); html! {<>
} }; Ok(match &*households { Ok(households) => html! { { for households .households .clone() .into_iter() .sorted_by_key(|(_,i)| i.name.clone()) .map(mk_household) } }, Err(e) => { log::error!("Could not fetch households: {e:?}"); html! { {""} } } }) } #[function_component] fn HouseholdSelection() -> Html { let token = use_state(|| match LocalStorage::get::("token") { Ok(v) => Some(v), Err(StorageError::KeyNotFound(_)) => None, Err(e) => unreachable!("Could not get household: {e:?}"), }); let error = use_state(|| None::); let navigator = use_navigator().unwrap(); let err = error.clone(); let tok = token.clone(); let onsubmit = Callback::from(move |e: SubmitEvent| { e.prevent_default(); let document = gloo_utils::document(); let token = tok.as_ref().unwrap().to_owned(); let name: HtmlInputElement = document .get_element_by_id("newHsName") .unwrap() .dyn_into() .expect("newHsName is not an input element"); let name = name.value(); let err = err.clone(); let navigator = navigator.clone(); wasm_bindgen_futures::spawn_local(async move { match do_new_household(token.token, name.clone()).await { Ok(id) => { let household_info = HouseholdInfo { name, id }; if let Err(e) = LocalStorage::set("household", household_info) { log::error!("Could not switch to new household: {e:?}") } let modal = Modal::get_instance("#newHsModal"); modal.hide(); navigator.push(&Route::Index); err.set(None); } Err(e) => { err.set(Some(format!("Could not create: {e:?}"))); } } }); }); let fallback = html! { {"Loading..."} }; match &*token { None => html! { to={Route::Login} /> }, Some(_) => html! {<>

{"Available"}



}, } } fn switch(route: Route) -> Html { match route { Route::Index => html! { {"Index"} }, Route::Login => html! { }, Route::Ingredients => html! { {"Ingredients"} }, Route::HouseholdSelect => html! { }, Route::NotFound => html! { "Page not found" }, } } async fn do_login(username: String, password: String) -> anyhow::Result<()> { let rsp = gloo_net::http::Request::post(api!("login")) .json(&LoginRequest { username: username.clone(), password, })? .send() .await?; if rsp.status() == 404 { anyhow::bail!("Account not foud") } else if !rsp.ok() { anyhow::bail!("Request failed: {rsp:?}") } let rsp: LoginResponse = rsp.json().await?; LocalStorage::set( "token", LoginInfo { token: rsp.token, name: username, }, )?; Ok(()) } #[function_component] fn Login() -> Html { let error = use_state(|| None); let navigator = use_navigator().unwrap(); let err = error.clone(); let onsubmit = Callback::from(move |e: SubmitEvent| { e.prevent_default(); let document = gloo_utils::document(); let username: HtmlInputElement = document .get_element_by_id("floatingUser") .unwrap() .dyn_into() .expect("floatingUser is not an input element"); let username = username.value(); let password: HtmlInputElement = document .get_element_by_id("floatingPass") .unwrap() .dyn_into() .expect("floatingUser is not an input element"); let password = password.value(); let err = err.clone(); let navigator = navigator.clone(); wasm_bindgen_futures::spawn_local(async move { match do_login(username, password).await { Ok(_) => { navigator.push(&Route::Index); err.set(None); } Err(e) => { err.set(Some(format!("Could not log in: {e:?}"))); } } }); }); html! {<>

{"Please log in"}

if let Some(err) = &*error { }
} } fn main() { console_log::init_with_level(Level::Debug).unwrap(); yew::Renderer::::with_root( gloo_utils::document() .body() .expect("no body") .get_elements_by_tag_name("main") .item(0) .expect("no main"), ) .render(); }