diff --git a/Cargo.lock b/Cargo.lock index ca4e2cf..97bfce6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,6 +1210,7 @@ dependencies = [ "reqwasm", "serde_json", "sycamore", + "sycamore-router", "tracing", "tracing-browser-subscriber", "wasm-bindgen", @@ -1998,7 +1999,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "sycamore" version = "0.8.2" -source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" dependencies = [ "ahash", "futures", @@ -2018,7 +2019,7 @@ dependencies = [ [[package]] name = "sycamore-core" version = "0.8.2" -source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" dependencies = [ "ahash", "sycamore-reactive", @@ -2027,7 +2028,7 @@ dependencies = [ [[package]] name = "sycamore-futures" version = "0.8.0" -source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" dependencies = [ "futures", "sycamore-reactive", @@ -2038,7 +2039,7 @@ dependencies = [ [[package]] name = "sycamore-macro" version = "0.8.2" -source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" dependencies = [ "once_cell", "proc-macro2", @@ -2049,7 +2050,7 @@ dependencies = [ [[package]] name = "sycamore-reactive" version = "0.8.1" -source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" dependencies = [ "ahash", "bumpalo", @@ -2059,10 +2060,33 @@ dependencies = [ "smallvec", ] +[[package]] +name = "sycamore-router" +version = "0.8.0" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" +dependencies = [ + "sycamore", + "sycamore-router-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "sycamore-router-macro" +version = "0.8.0" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" +dependencies = [ + "nom", + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "sycamore-web" version = "0.8.2" -source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297" +source = "git+https://github.com/sycamore-rs/sycamore/?rev=5d49777b4a66fb5730c40898fd2ee8cde15bcdc3#5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" dependencies = [ "html-escape", "indexmap", @@ -2378,6 +2402,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index add1489..34751a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "recipes", "kitchen", "web" ] [patch.crates-io] # TODO(jwall): When the fix for RcSignal Binding is released we can drop this patch. -sycamore = { git = "https://github.com/sycamore-rs/sycamore/", rev = "20b6069c470a51d2ba6197bb322036e8324ff297" } +sycamore = { git = "https://github.com/sycamore-rs/sycamore/", rev = "5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" } +sycamore-router = { git = "https://github.com/sycamore-rs/sycamore/", rev = "5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" } # NOTE(jwall): We are maintaining a patch to remove the unstable async_std_feature. It breaks in our project on # Rust v1.64 sqlx = { git = "https://github.com/zaphar/sqlx", branch = "remove_unstable_async_std_feature" } \ No newline at end of file diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index 60feb5c..3afb23b 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -53,10 +53,18 @@ impl Mealplan { pub struct RecipeEntry(pub String, pub String); impl RecipeEntry { + pub fn set_recipe_id>(&mut self, id: S) { + self.0 = id.into(); + } + pub fn recipe_id(&self) -> &str { self.0.as_str() } + pub fn set_recipe_text>(&mut self, text: S) { + self.1 = text.into(); + } + pub fn recipe_text(&self) -> &str { self.1.as_str() } diff --git a/web/Cargo.toml b/web/Cargo.toml index 8636866..0051bf7 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -21,6 +21,7 @@ tracing = "0.1.35" tracing-browser-subscriber = "0.1.0" async-trait = "0.1.57" base64 = "0.13.0" +sycamore-router = "0.8" [dependencies.reqwasm] version = "0.5.0" diff --git a/web/src/api.rs b/web/src/api.rs index a73ed47..d50b637 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -113,6 +113,12 @@ impl From for Error { } } +impl From<&'static str> for Error { + fn from(item: &'static str) -> Self { + Error(item.to_owned()) + } +} + impl From for Error { fn from(item: std::string::FromUtf8Error) -> Self { Error(format!("{:?}", item)) @@ -258,6 +264,9 @@ impl HttpStore { path.push_str("/recipes"); let storage = js_lib::get_storage(); for r in recipes.iter() { + if r.recipe_id().is_empty() { + return Err("Recipe Ids can not be empty".into()); + } storage.set( &recipe_key(r.recipe_id()), &to_string(&r).expect("Unable to serialize recipe entries"), diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 2468c19..0f2b4c6 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -18,23 +18,6 @@ use tracing::{debug, instrument, warn}; use recipes::{Ingredient, IngredientAccumulator, Recipe}; -#[derive(Debug, Clone)] -pub enum Routes { - Plan, - Inventory, - Cook, - Recipe(String), - Categories, - Login, - NotFound, -} - -impl Default for Routes { - fn default() -> Self { - Self::Plan - } -} - pub struct State { pub recipe_counts: RcSignal>, pub extras: RcSignal, RcSignal))>>, diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index d42e6ca..8d974f6 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -40,21 +40,25 @@ fn check_recipe_parses(text: &str, error_text: &Signal) -> bool { } #[component] -fn Editor(cx: Scope, recipe: RecipeEntry) -> View { +pub fn Editor(cx: Scope, recipe: RecipeEntry) -> View { let id = create_signal(cx, recipe.recipe_id().to_owned()); let text = create_signal(cx, recipe.recipe_text().to_owned()); let error_text = create_signal(cx, String::new()); let save_signal = create_signal(cx, ()); let dirty = create_signal(cx, false); + debug!("Creating effect"); create_effect(cx, move || { save_signal.track(); - if !*dirty.get() { + if !*dirty.get_untracked() { + debug!("Recipe text is unchanged"); return; } + debug!("Recipe text is changed"); spawn_local_scoped(cx, { let store = crate::api::HttpStore::get_from_context(cx); async move { + debug!("Attempting to save recipe"); if let Err(e) = store .save_recipes(vec![RecipeEntry( id.get_untracked().as_ref().clone(), @@ -71,6 +75,7 @@ fn Editor(cx: Scope, recipe: RecipeEntry) -> View { }); }); + debug!("creating dialog_view"); let dialog_view = view! {cx, dialog(id="error-dialog") { article{ @@ -88,6 +93,7 @@ fn Editor(cx: Scope, recipe: RecipeEntry) -> View { } }; + debug!("creating editor view"); view! {cx, (dialog_view) textarea(bind:value=text, rows=20, on:change=move |_| { diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index b8bed49..f68887a 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -18,7 +18,7 @@ use tracing::{debug, instrument}; use crate::app_state; -#[derive(Prop)] +#[derive(Props)] pub struct RecipeCheckBoxProps<'ctx> { pub i: String, pub title: &'ctx ReadSignal, diff --git a/web/src/components/tabs.rs b/web/src/components/tabs.rs index 906defe..a33abcc 100644 --- a/web/src/components/tabs.rs +++ b/web/src/components/tabs.rs @@ -14,7 +14,7 @@ use sycamore::prelude::*; use tracing::debug; -#[derive(Prop)] +#[derive(Props)] pub struct TabState<'a, G: Html> { pub children: Children<'a, G>, pub selected: Option, @@ -28,25 +28,28 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View selected, tablist, } = state; - let tablist = create_signal(cx, tablist.clone()); let children = children.call(cx); + let menu = View::new_fragment( + tablist + .iter() + .map(|&(href, show)| { + debug!(?selected, show, "identifying tab"); + let class = if selected.as_ref().map_or(false, |selected| selected == show) { + "no-print selected" + } else { + "no-print" + }; + view! {cx, + li(class=class) { a(href=href) { (show) } } + } + // TODO + }) + .collect(), + ); view! {cx, nav { ul(class="tabs") { - Indexed( - iterable=tablist, - view=move |cx, (href, show)| { - debug!(?selected, show, "identifying tab"); - let class = if selected.as_ref().map_or(false, |selected| selected == show) { - "no-print selected" - } else { - "no-print" - }; - view! {cx, - li(class=class) { a(href=href) { (show) } } - } - } - ) + (menu) } } main(class=".conatiner-fluid") { diff --git a/web/src/js_lib.rs b/web/src/js_lib.rs index bc7c16c..c4f2b73 100644 --- a/web/src/js_lib.rs +++ b/web/src/js_lib.rs @@ -11,9 +11,16 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, JsValue}; use web_sys::{window, Element, Storage}; +pub fn navigate_to_path(path: &str) -> Result<(), JsValue> { + window() + .expect("No window present") + .location() + .set_pathname(path) +} + pub fn get_element_by_id(id: &str) -> Result, Element> where E: JsCast, diff --git a/web/src/lib.rs b/web/src/lib.rs index f86cd7b..fc03a44 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -16,7 +16,7 @@ mod app_state; mod components; mod js_lib; mod pages; -mod router_integration; +mod routing; mod web; use sycamore::prelude::*; diff --git a/web/src/pages/manage/add_recipe.rs b/web/src/pages/manage/add_recipe.rs new file mode 100644 index 0000000..3ede650 --- /dev/null +++ b/web/src/pages/manage/add_recipe.rs @@ -0,0 +1,78 @@ +// Copyright 2022 Jeremy Wall (Jeremy@marzhilsltudios.com) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use sycamore::{futures::spawn_local_scoped, prelude::*}; + +use recipes::RecipeEntry; + +const STARTER_RECIPE: &'static str = "title: Title Here + +Description here. + +step: + +1 ingredient + +Instructions here +"; + +#[component] +pub fn AddRecipePage(cx: Scope) -> View { + let entry = create_signal(cx, RecipeEntry(String::new(), String::from(STARTER_RECIPE))); + let recipe_id = create_signal(cx, String::new()); + let create_recipe_signal = create_signal(cx, ()); + let dirty = create_signal(cx, false); + + create_effect(cx, || { + let mut entry_for_edit = entry.get_untracked().as_ref().clone(); + // TODO(jwall): This can probably be done more efficiently. + let id = recipe_id + .get() + .as_ref() + .replace(" ", "_") + .replace("\n", "") + .replace("\r", ""); + entry_for_edit.set_recipe_id(id); + entry.set(entry_for_edit); + }); + + create_effect(cx, move || { + create_recipe_signal.track(); + if !*dirty.get_untracked() { + return; + } + spawn_local_scoped(cx, { + let store = crate::api::HttpStore::get_from_context(cx); + async move { + let entry = entry.get_untracked(); + // TODO(jwall): Better error reporting here. + // TODO(jwall): Ensure that this id doesn't already exist. + store + .save_recipes(vec![entry.as_ref().clone()]) + .await + .expect("Unable to save New Recipe"); + crate::js_lib::navigate_to_path(&format!("/ui/recipe/{}", entry.recipe_id())) + .expect("Unable to navigate to recipe"); + } + }); + }); + view! {cx, + label(for="recipe_id") { "Recipe Id" } + input(bind:value=recipe_id, type="text", name="recipe_id", id="recipe_id", on:change=move |_| { + dirty.set(true); + }) + button(on:click=move |_| { + create_recipe_signal.trigger_subscribers(); + }) { "Create" } + } +} diff --git a/web/src/pages/manage/mod.rs b/web/src/pages/manage/mod.rs index 262d051..d3aa03d 100644 --- a/web/src/pages/manage/mod.rs +++ b/web/src/pages/manage/mod.rs @@ -14,9 +14,10 @@ use crate::components::tabs::*; use sycamore::prelude::*; +pub mod add_recipe; pub mod categories; -#[derive(Prop)] +#[derive(Props)] pub struct PageState<'a, G: Html> { pub children: Children<'a, G>, pub selected: Option, @@ -26,7 +27,10 @@ pub struct PageState<'a, G: Html> { pub fn ManagePage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View { let PageState { children, selected } = state; let children = children.call(cx); - let manage_tabs: Vec<(&'static str, &'static str)> = vec![("/ui/categories", "Categories")]; + let manage_tabs: Vec<(&'static str, &'static str)> = vec![ + ("/ui/categories", "Categories"), + ("/ui/new_recipe", "New Recipe"), + ]; view! {cx, TabbedView( diff --git a/web/src/pages/mod.rs b/web/src/pages/mod.rs index ab72881..0f7f45c 100644 --- a/web/src/pages/mod.rs +++ b/web/src/pages/mod.rs @@ -17,6 +17,7 @@ mod planning; mod recipe; pub use login::*; +pub use manage::add_recipe::*; pub use manage::categories::*; pub use planning::cook::*; pub use planning::inventory::*; diff --git a/web/src/pages/planning/mod.rs b/web/src/pages/planning/mod.rs index dbabf0c..3bd9f15 100644 --- a/web/src/pages/planning/mod.rs +++ b/web/src/pages/planning/mod.rs @@ -18,7 +18,7 @@ pub mod cook; pub mod inventory; pub mod plan; -#[derive(Prop)] +#[derive(Props)] pub struct PageState<'a, G: Html> { pub children: Children<'a, G>, pub selected: Option, diff --git a/web/src/pages/recipe.rs b/web/src/pages/recipe.rs index 83cc7ab..6a60459 100644 --- a/web/src/pages/recipe.rs +++ b/web/src/pages/recipe.rs @@ -16,7 +16,7 @@ use crate::components::recipe::Recipe; use sycamore::prelude::*; use tracing::instrument; -#[derive(Debug, Prop)] +#[derive(Debug, Props)] pub struct RecipePageProps { pub recipe: String, } diff --git a/web/src/router_integration.rs b/web/src/router_integration.rs deleted file mode 100644 index 1d82af9..0000000 --- a/web/src/router_integration.rs +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2022 zaphar -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -use std::fmt::Debug; -use std::rc::Rc; - -use sycamore::prelude::*; -use tracing::{debug, error, info, instrument}; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; -use web_sys::Event; -use web_sys::{Element, HtmlAnchorElement}; - -use crate::app_state::Routes; - -#[derive(Clone, Debug)] -pub struct BrowserIntegration(RcSignal<(String, String, String)>); - -impl BrowserIntegration { - pub fn new() -> Self { - let location = web_sys::window().unwrap_throw().location(); - Self(create_rc_signal(( - location.origin().unwrap_or(String::new()), - location.pathname().unwrap_or(String::new()), - location.hash().unwrap_or(String::new()), - ))) - } - - #[instrument(skip(self, f))] - pub fn register_post_state_handler(&self, f: Box) { - let closure = Closure::wrap(f); - web_sys::window() - .unwrap_throw() - .add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref()) - .unwrap_throw(); - closure.forget(); - } - - #[instrument(skip(self))] - pub fn click_handler(&self) -> Box { - let route_signal = self.0.clone(); - Box::new(move |ev| { - if let Some(tgt) = ev - .target() - .unwrap_throw() - .unchecked_into::() - .closest("a[href]") - .unwrap_throw() - .map(|e| e.unchecked_into::()) - { - debug!("handling navigation event."); - let location = web_sys::window().unwrap_throw().location(); - - if tgt.rel() == "external" { - debug!("External Link so ignoring."); - return; - } - - let origin = tgt.origin(); - let tgt_pathname = tgt.pathname(); - let hash = tgt.hash(); - match (location.origin().as_ref() == Ok(&origin), location.pathname().as_ref() == Ok(&tgt_pathname), location.hash().as_ref() == Ok(&hash)) { - (true, true, true) // Same location - | (false, _, _) /* different origin */ => { - // Do nothing this is the same location as we are already at. - } - (true, _, false) // different hash - | (true, false, _) /* different path */ => { - debug!("different path or hash"); - ev.prevent_default(); - // Signal the pathname change - let path = format!("{}{}{}", &origin, &tgt_pathname, &hash); - debug!("new route: ({}, {}, {})", origin, tgt_pathname, hash); - debug!("new path: ({})", &path); - route_signal.set((origin, tgt_pathname, hash)); - // Update History API. - let window = web_sys::window().unwrap_throw(); - let history = window.history().unwrap_throw(); - history - .push_state_with_url(&JsValue::UNDEFINED, "", Some(&path)) - .unwrap_throw(); - window.scroll_to_with_x_and_y(0.0, 0.0); - } - } - } - }) - } -} - -#[derive(Debug)] -pub struct RouterProps -where - G: GenericNode, - R: DeriveRoute + NotFound + Clone + Default + Debug + 'static, - F: Fn(Scope, &ReadSignal) -> View + 'static, -{ - pub route: R, - pub route_select: F, - pub browser_integration: BrowserIntegration, -} - -#[instrument(fields(?props.route, - origin=props.browser_integration.0.get().0, - pathn=props.browser_integration.0.get().1, - hash=props.browser_integration.0.get().2), - skip(props))] -#[component] -pub fn Router<'ctx, G, R, F>(cx: Scope, props: RouterProps) -> View -where - G: Html, - R: DeriveRoute + NotFound + Clone + Default + Debug + 'static, - F: Fn(Scope, &ReadSignal) -> View + 'static, -{ - debug!("Setting up router"); - let integration = Rc::new(props.browser_integration); - let route_select = Rc::new(props.route_select); - - let view_signal = create_signal(cx, View::empty()); - create_effect(cx, { - let integration = integration.clone(); - move || { - let path_signal = integration.0.clone(); - debug!(origin=%path_signal.get().0, path=%path_signal.get().1, hash=%path_signal.get().2, "new path"); - let path = path_signal.clone(); - let route = R::from(path.get().as_ref()); - debug!(?route, "new route"); - // TODO(jwall): this is an unnecessary use of signal. - let view = route_select.as_ref()(cx, &*create_signal(cx, route)); - register_click_handler(cx, &view, integration.clone()); - view_signal.set(view); - } - }); - - let path_signal = integration.0.clone(); - integration.register_post_state_handler(Box::new(move || { - let location = web_sys::window().unwrap_throw().location(); - path_signal.set(( - location.origin().unwrap_throw(), - location.pathname().unwrap_throw(), - location.hash().unwrap_throw(), - )); - })); - - // NOTE(jwall): This needs to be a dynamic node so Sycamore knows to rerender it - // based on the results of the effect above. - view! {cx, - (view_signal.get().as_ref()) - } -} - -#[instrument(skip_all)] -fn register_click_handler(cx: Scope, view: &View, integration: Rc) -where - G: GenericNode, -{ - debug!("Registring click handler on node(s)"); - if let Some(node) = view.as_node() { - node.event(cx, "click", integration.click_handler()); - } else if let Some(frag) = view.as_fragment() { - debug!(fragment=?frag); - for n in frag { - register_click_handler(cx, n, integration.clone()); - } - } else if let Some(dyn_node) = view.as_dyn() { - debug!(dynamic_node=?dyn_node); - } else { - debug!(node=?view, "Unknown node"); - } -} - -pub trait NotFound { - fn not_found() -> Self; -} - -impl NotFound for Routes { - fn not_found() -> Self { - Routes::NotFound - } -} - -pub trait DeriveRoute { - fn from(input: &(String, String, String)) -> Self; -} - -impl DeriveRoute for Routes { - #[instrument] - fn from(input: &(String, String, String)) -> Routes { - debug!(origin=%input.0, path=%input.1, hash=%input.2, "routing"); - let (_origin, path, _hash) = input; - let route = match path.as_str() { - "" | "/" | "/ui/" => Routes::default(), - "/ui/login" => Routes::Login, - "/ui/plan" => Routes::Plan, - "/ui/cook" => Routes::Cook, - "/ui/inventory" => Routes::Inventory, - "/ui/categories" => Routes::Categories, - h => { - if h.starts_with("/ui/recipe/") { - let parts: Vec<&str> = h.split("/").collect(); - debug!(?parts, "found recipe path"); - if let Some(&"recipe") = parts.get(2) { - if let Some(&idx) = parts.get(3) { - return Routes::Recipe(idx.to_owned()); - } - } - } - error!(origin=%input.0, path=%input.1, hash=%input.2, "Path not found"); - Routes::NotFound - } - }; - info!(route=?route, "Route identified"); - route - } -} diff --git a/web/src/routing/mod.rs b/web/src/routing/mod.rs new file mode 100644 index 0000000..28a8371 --- /dev/null +++ b/web/src/routing/mod.rs @@ -0,0 +1,89 @@ +// Copyright 2022 zaphar +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use sycamore::prelude::*; +//use sycamore_router::{HistoryIntegration, Route, Router}; +use sycamore_router::{HistoryIntegration, Route, Router}; +use tracing::instrument; + +use crate::pages::*; + +//mod router; +//use router::{HistoryIntegration, Router}; + +#[instrument] +fn route_switch<'a, G: Html>(cx: Scope<'a>, route: &'a ReadSignal) -> View { + // NOTE(jwall): This needs to not be a dynamic node. The rules around + // this are somewhat unclear and underdocumented for Sycamore. But basically + // avoid conditionals in the `view!` macro calls here. + view! {cx, + (match route.get().as_ref() { + Routes::Plan => view! {cx, + PlanPage() + }, + Routes::Inventory => view! {cx, + InventoryPage() + }, + Routes::Login => view! {cx, + LoginPage() + }, + Routes::Cook => view! {cx, + CookPage() + }, + Routes::Recipe(idx) => view! {cx, + RecipePage(recipe=idx.clone()) + }, + Routes::Categories => view! {cx, + CategoryPage() + }, + Routes::NewRecipe => view! {cx, + AddRecipePage() + }, + Routes::NotFound => view! {cx, + // TODO(Create a real one) + PlanPage() + }, + }) + } +} + +#[derive(Route, Debug)] +pub enum Routes { + #[to("/ui/plan")] + Plan, + #[to("/ui/inventory")] + Inventory, + #[to("/ui/cook")] + Cook, + #[to("/ui/recipe/")] + Recipe(String), + #[to("/ui/add_recipe")] + NewRecipe, + #[to("/ui/categories")] + Categories, + #[to("/ui/login")] + Login, + #[not_found] + NotFound, +} + +#[component] +pub fn Handler(cx: Scope) -> View { + view! {cx, + Router( + integration=HistoryIntegration::new(), + view=route_switch, + ) + } +} diff --git a/web/src/web.rs b/web/src/web.rs index a564a7d..9063ba8 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -15,39 +15,7 @@ use sycamore::{futures::spawn_local_scoped, prelude::*}; use tracing::{error, info, instrument}; use crate::components::Header; -use crate::pages::*; -use crate::{api, app_state::*, router_integration::*}; - -#[instrument] -fn route_switch(cx: Scope, route: &ReadSignal) -> View { - // NOTE(jwall): This needs to not be a dynamic node. The rules around - // this are somewhat unclear and underdocumented for Sycamore. But basically - // avoid conditionals in the `view!` macro calls here. - match route.get().as_ref() { - Routes::Plan => view! {cx, - PlanPage() - }, - Routes::Inventory => view! {cx, - InventoryPage() - }, - Routes::Login => view! {cx, - LoginPage() - }, - Routes::Cook => view! {cx, - CookPage() - }, - Routes::Recipe(idx) => view! {cx, - RecipePage(recipe=idx.clone()) - }, - Routes::Categories => view! {cx, - CategoryPage() - }, - Routes::NotFound => view! {cx, - // TODO(Create a real one) - PlanPage() - }, - } -} +use crate::{api, routing::Handler as RouteHandler}; #[instrument] #[component] @@ -66,14 +34,11 @@ pub fn UI(cx: Scope) -> View { if let Err(err) = api::init_page_state(store.as_ref(), state.as_ref()).await { error!(?err); }; + // TODO(jwall): This needs to be moved into the RouteHandler view.set(view! { cx, div(class="app") { Header { } - Router(RouterProps { - route: Routes::Plan, - route_select: route_switch, - browser_integration: BrowserIntegration::new(), - }) + RouteHandler() } }); }