mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Add recipe page
This commit is contained in:
parent
7e3b94261a
commit
2926d9042a
42
Cargo.lock
generated
42
Cargo.lock
generated
@ -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"
|
||||
|
@ -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" }
|
@ -53,10 +53,18 @@ impl Mealplan {
|
||||
pub struct RecipeEntry(pub String, pub String);
|
||||
|
||||
impl RecipeEntry {
|
||||
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
||||
self.0 = id.into();
|
||||
}
|
||||
|
||||
pub fn recipe_id(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) {
|
||||
self.1 = text.into();
|
||||
}
|
||||
|
||||
pub fn recipe_text(&self) -> &str {
|
||||
self.1.as_str()
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -113,6 +113,12 @@ impl From<String> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(item: &'static str) -> Self {
|
||||
Error(item.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> 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"),
|
||||
|
@ -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<BTreeMap<String, usize>>,
|
||||
pub extras: RcSignal<Vec<(usize, (RcSignal<String>, RcSignal<String>))>>,
|
||||
|
@ -40,21 +40,25 @@ fn check_recipe_parses(text: &str, error_text: &Signal<String>) -> bool {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Editor<G: Html>(cx: Scope, recipe: RecipeEntry) -> View<G> {
|
||||
pub fn Editor<G: Html>(cx: Scope, recipe: RecipeEntry) -> View<G> {
|
||||
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<G: Html>(cx: Scope, recipe: RecipeEntry) -> View<G> {
|
||||
});
|
||||
});
|
||||
|
||||
debug!("creating dialog_view");
|
||||
let dialog_view = view! {cx,
|
||||
dialog(id="error-dialog") {
|
||||
article{
|
||||
@ -88,6 +93,7 @@ fn Editor<G: Html>(cx: Scope, recipe: RecipeEntry) -> View<G> {
|
||||
}
|
||||
};
|
||||
|
||||
debug!("creating editor view");
|
||||
view! {cx,
|
||||
(dialog_view)
|
||||
textarea(bind:value=text, rows=20, on:change=move |_| {
|
||||
|
@ -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<String>,
|
||||
|
@ -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<String>,
|
||||
@ -28,14 +28,11 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View<G>
|
||||
selected,
|
||||
tablist,
|
||||
} = state;
|
||||
let tablist = create_signal(cx, tablist.clone());
|
||||
let children = children.call(cx);
|
||||
view! {cx,
|
||||
nav {
|
||||
ul(class="tabs") {
|
||||
Indexed(
|
||||
iterable=tablist,
|
||||
view=move |cx, (href, show)| {
|
||||
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"
|
||||
@ -45,8 +42,14 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View<G>
|
||||
view! {cx,
|
||||
li(class=class) { a(href=href) { (show) } }
|
||||
}
|
||||
}
|
||||
)
|
||||
// TODO
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
view! {cx,
|
||||
nav {
|
||||
ul(class="tabs") {
|
||||
(menu)
|
||||
}
|
||||
}
|
||||
main(class=".conatiner-fluid") {
|
||||
|
@ -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<E>(id: &str) -> Result<Option<E>, Element>
|
||||
where
|
||||
E: JsCast,
|
||||
|
@ -16,7 +16,7 @@ mod app_state;
|
||||
mod components;
|
||||
mod js_lib;
|
||||
mod pages;
|
||||
mod router_integration;
|
||||
mod routing;
|
||||
mod web;
|
||||
|
||||
use sycamore::prelude::*;
|
||||
|
78
web/src/pages/manage/add_recipe.rs
Normal file
78
web/src/pages/manage/add_recipe.rs
Normal file
@ -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<G: Html>(cx: Scope) -> View<G> {
|
||||
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" }
|
||||
}
|
||||
}
|
@ -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<String>,
|
||||
@ -26,7 +27,10 @@ pub struct PageState<'a, G: Html> {
|
||||
pub fn ManagePage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View<G> {
|
||||
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(
|
||||
|
@ -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::*;
|
||||
|
@ -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<String>,
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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<dyn FnMut()>) {
|
||||
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<dyn Fn(web_sys::Event)> {
|
||||
let route_signal = self.0.clone();
|
||||
Box::new(move |ev| {
|
||||
if let Some(tgt) = ev
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<Element>()
|
||||
.closest("a[href]")
|
||||
.unwrap_throw()
|
||||
.map(|e| e.unchecked_into::<HtmlAnchorElement>())
|
||||
{
|
||||
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<R, F, G>
|
||||
where
|
||||
G: GenericNode,
|
||||
R: DeriveRoute + NotFound + Clone + Default + Debug + 'static,
|
||||
F: Fn(Scope, &ReadSignal<R>) -> View<G> + '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<R, F, G>) -> View<G>
|
||||
where
|
||||
G: Html,
|
||||
R: DeriveRoute + NotFound + Clone + Default + Debug + 'static,
|
||||
F: Fn(Scope, &ReadSignal<R>) -> View<G> + '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<G>(cx: Scope, view: &View<G>, integration: Rc<BrowserIntegration>)
|
||||
where
|
||||
G: GenericNode<EventType = Event>,
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
89
web/src/routing/mod.rs
Normal file
89
web/src/routing/mod.rs
Normal file
@ -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<Routes>) -> View<G> {
|
||||
// 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/<id>")]
|
||||
Recipe(String),
|
||||
#[to("/ui/add_recipe")]
|
||||
NewRecipe,
|
||||
#[to("/ui/categories")]
|
||||
Categories,
|
||||
#[to("/ui/login")]
|
||||
Login,
|
||||
#[not_found]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Handler<G: Html>(cx: Scope) -> View<G> {
|
||||
view! {cx,
|
||||
Router(
|
||||
integration=HistoryIntegration::new(),
|
||||
view=route_switch,
|
||||
)
|
||||
}
|
||||
}
|
@ -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<G: Html>(cx: Scope, route: &ReadSignal<Routes>) -> View<G> {
|
||||
// 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<G: Html>(cx: Scope) -> View<G> {
|
||||
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()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user