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",
|
"reqwasm",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sycamore",
|
"sycamore",
|
||||||
|
"sycamore-router",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-browser-subscriber",
|
"tracing-browser-subscriber",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@ -1998,7 +1999,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore"
|
name = "sycamore"
|
||||||
version = "0.8.2"
|
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 = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"futures",
|
"futures",
|
||||||
@ -2018,7 +2019,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-core"
|
name = "sycamore-core"
|
||||||
version = "0.8.2"
|
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 = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"sycamore-reactive",
|
"sycamore-reactive",
|
||||||
@ -2027,7 +2028,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-futures"
|
name = "sycamore-futures"
|
||||||
version = "0.8.0"
|
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 = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"sycamore-reactive",
|
"sycamore-reactive",
|
||||||
@ -2038,7 +2039,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-macro"
|
name = "sycamore-macro"
|
||||||
version = "0.8.2"
|
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 = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -2049,7 +2050,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "sycamore-reactive"
|
name = "sycamore-reactive"
|
||||||
version = "0.8.1"
|
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 = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
@ -2059,10 +2060,33 @@ dependencies = [
|
|||||||
"smallvec",
|
"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]]
|
[[package]]
|
||||||
name = "sycamore-web"
|
name = "sycamore-web"
|
||||||
version = "0.8.2"
|
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 = [
|
dependencies = [
|
||||||
"html-escape",
|
"html-escape",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@ -2378,6 +2402,12 @@ version = "1.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode_categories"
|
name = "unicode_categories"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
@ -3,7 +3,8 @@ members = [ "recipes", "kitchen", "web" ]
|
|||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# TODO(jwall): When the fix for RcSignal Binding is released we can drop this patch.
|
# 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
|
# NOTE(jwall): We are maintaining a patch to remove the unstable async_std_feature. It breaks in our project on
|
||||||
# Rust v1.64
|
# Rust v1.64
|
||||||
sqlx = { git = "https://github.com/zaphar/sqlx", branch = "remove_unstable_async_std_feature" }
|
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);
|
pub struct RecipeEntry(pub String, pub String);
|
||||||
|
|
||||||
impl RecipeEntry {
|
impl RecipeEntry {
|
||||||
|
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
||||||
|
self.0 = id.into();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn recipe_id(&self) -> &str {
|
pub fn recipe_id(&self) -> &str {
|
||||||
self.0.as_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 {
|
pub fn recipe_text(&self) -> &str {
|
||||||
self.1.as_str()
|
self.1.as_str()
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ tracing = "0.1.35"
|
|||||||
tracing-browser-subscriber = "0.1.0"
|
tracing-browser-subscriber = "0.1.0"
|
||||||
async-trait = "0.1.57"
|
async-trait = "0.1.57"
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
|
sycamore-router = "0.8"
|
||||||
|
|
||||||
[dependencies.reqwasm]
|
[dependencies.reqwasm]
|
||||||
version = "0.5.0"
|
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 {
|
impl From<std::string::FromUtf8Error> for Error {
|
||||||
fn from(item: std::string::FromUtf8Error) -> Self {
|
fn from(item: std::string::FromUtf8Error) -> Self {
|
||||||
Error(format!("{:?}", item))
|
Error(format!("{:?}", item))
|
||||||
@ -258,6 +264,9 @@ impl HttpStore {
|
|||||||
path.push_str("/recipes");
|
path.push_str("/recipes");
|
||||||
let storage = js_lib::get_storage();
|
let storage = js_lib::get_storage();
|
||||||
for r in recipes.iter() {
|
for r in recipes.iter() {
|
||||||
|
if r.recipe_id().is_empty() {
|
||||||
|
return Err("Recipe Ids can not be empty".into());
|
||||||
|
}
|
||||||
storage.set(
|
storage.set(
|
||||||
&recipe_key(r.recipe_id()),
|
&recipe_key(r.recipe_id()),
|
||||||
&to_string(&r).expect("Unable to serialize recipe entries"),
|
&to_string(&r).expect("Unable to serialize recipe entries"),
|
||||||
|
@ -18,23 +18,6 @@ use tracing::{debug, instrument, warn};
|
|||||||
|
|
||||||
use recipes::{Ingredient, IngredientAccumulator, Recipe};
|
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 struct State {
|
||||||
pub recipe_counts: RcSignal<BTreeMap<String, usize>>,
|
pub recipe_counts: RcSignal<BTreeMap<String, usize>>,
|
||||||
pub extras: RcSignal<Vec<(usize, (RcSignal<String>, RcSignal<String>))>>,
|
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]
|
#[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 id = create_signal(cx, recipe.recipe_id().to_owned());
|
||||||
let text = create_signal(cx, recipe.recipe_text().to_owned());
|
let text = create_signal(cx, recipe.recipe_text().to_owned());
|
||||||
let error_text = create_signal(cx, String::new());
|
let error_text = create_signal(cx, String::new());
|
||||||
let save_signal = create_signal(cx, ());
|
let save_signal = create_signal(cx, ());
|
||||||
let dirty = create_signal(cx, false);
|
let dirty = create_signal(cx, false);
|
||||||
|
|
||||||
|
debug!("Creating effect");
|
||||||
create_effect(cx, move || {
|
create_effect(cx, move || {
|
||||||
save_signal.track();
|
save_signal.track();
|
||||||
if !*dirty.get() {
|
if !*dirty.get_untracked() {
|
||||||
|
debug!("Recipe text is unchanged");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
debug!("Recipe text is changed");
|
||||||
spawn_local_scoped(cx, {
|
spawn_local_scoped(cx, {
|
||||||
let store = crate::api::HttpStore::get_from_context(cx);
|
let store = crate::api::HttpStore::get_from_context(cx);
|
||||||
async move {
|
async move {
|
||||||
|
debug!("Attempting to save recipe");
|
||||||
if let Err(e) = store
|
if let Err(e) = store
|
||||||
.save_recipes(vec![RecipeEntry(
|
.save_recipes(vec![RecipeEntry(
|
||||||
id.get_untracked().as_ref().clone(),
|
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,
|
let dialog_view = view! {cx,
|
||||||
dialog(id="error-dialog") {
|
dialog(id="error-dialog") {
|
||||||
article{
|
article{
|
||||||
@ -88,6 +93,7 @@ fn Editor<G: Html>(cx: Scope, recipe: RecipeEntry) -> View<G> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug!("creating editor view");
|
||||||
view! {cx,
|
view! {cx,
|
||||||
(dialog_view)
|
(dialog_view)
|
||||||
textarea(bind:value=text, rows=20, on:change=move |_| {
|
textarea(bind:value=text, rows=20, on:change=move |_| {
|
||||||
|
@ -18,7 +18,7 @@ use tracing::{debug, instrument};
|
|||||||
|
|
||||||
use crate::app_state;
|
use crate::app_state;
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Props)]
|
||||||
pub struct RecipeCheckBoxProps<'ctx> {
|
pub struct RecipeCheckBoxProps<'ctx> {
|
||||||
pub i: String,
|
pub i: String,
|
||||||
pub title: &'ctx ReadSignal<String>,
|
pub title: &'ctx ReadSignal<String>,
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Props)]
|
||||||
pub struct TabState<'a, G: Html> {
|
pub struct TabState<'a, G: Html> {
|
||||||
pub children: Children<'a, G>,
|
pub children: Children<'a, G>,
|
||||||
pub selected: Option<String>,
|
pub selected: Option<String>,
|
||||||
@ -28,25 +28,28 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View<G>
|
|||||||
selected,
|
selected,
|
||||||
tablist,
|
tablist,
|
||||||
} = state;
|
} = state;
|
||||||
let tablist = create_signal(cx, tablist.clone());
|
|
||||||
let children = children.call(cx);
|
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,
|
view! {cx,
|
||||||
nav {
|
nav {
|
||||||
ul(class="tabs") {
|
ul(class="tabs") {
|
||||||
Indexed(
|
(menu)
|
||||||
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) } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main(class=".conatiner-fluid") {
|
main(class=".conatiner-fluid") {
|
||||||
|
@ -11,9 +11,16 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
use web_sys::{window, Element, Storage};
|
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>
|
pub fn get_element_by_id<E>(id: &str) -> Result<Option<E>, Element>
|
||||||
where
|
where
|
||||||
E: JsCast,
|
E: JsCast,
|
||||||
|
@ -16,7 +16,7 @@ mod app_state;
|
|||||||
mod components;
|
mod components;
|
||||||
mod js_lib;
|
mod js_lib;
|
||||||
mod pages;
|
mod pages;
|
||||||
mod router_integration;
|
mod routing;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
use sycamore::prelude::*;
|
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 crate::components::tabs::*;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
|
pub mod add_recipe;
|
||||||
pub mod categories;
|
pub mod categories;
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Props)]
|
||||||
pub struct PageState<'a, G: Html> {
|
pub struct PageState<'a, G: Html> {
|
||||||
pub children: Children<'a, G>,
|
pub children: Children<'a, G>,
|
||||||
pub selected: Option<String>,
|
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> {
|
pub fn ManagePage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View<G> {
|
||||||
let PageState { children, selected } = state;
|
let PageState { children, selected } = state;
|
||||||
let children = children.call(cx);
|
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,
|
view! {cx,
|
||||||
TabbedView(
|
TabbedView(
|
||||||
|
@ -17,6 +17,7 @@ mod planning;
|
|||||||
mod recipe;
|
mod recipe;
|
||||||
|
|
||||||
pub use login::*;
|
pub use login::*;
|
||||||
|
pub use manage::add_recipe::*;
|
||||||
pub use manage::categories::*;
|
pub use manage::categories::*;
|
||||||
pub use planning::cook::*;
|
pub use planning::cook::*;
|
||||||
pub use planning::inventory::*;
|
pub use planning::inventory::*;
|
||||||
|
@ -18,7 +18,7 @@ pub mod cook;
|
|||||||
pub mod inventory;
|
pub mod inventory;
|
||||||
pub mod plan;
|
pub mod plan;
|
||||||
|
|
||||||
#[derive(Prop)]
|
#[derive(Props)]
|
||||||
pub struct PageState<'a, G: Html> {
|
pub struct PageState<'a, G: Html> {
|
||||||
pub children: Children<'a, G>,
|
pub children: Children<'a, G>,
|
||||||
pub selected: Option<String>,
|
pub selected: Option<String>,
|
||||||
|
@ -16,7 +16,7 @@ use crate::components::recipe::Recipe;
|
|||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
#[derive(Debug, Prop)]
|
#[derive(Debug, Props)]
|
||||||
pub struct RecipePageProps {
|
pub struct RecipePageProps {
|
||||||
pub recipe: String,
|
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 tracing::{error, info, instrument};
|
||||||
|
|
||||||
use crate::components::Header;
|
use crate::components::Header;
|
||||||
use crate::pages::*;
|
use crate::{api, routing::Handler as RouteHandler};
|
||||||
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()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
#[component]
|
#[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 {
|
if let Err(err) = api::init_page_state(store.as_ref(), state.as_ref()).await {
|
||||||
error!(?err);
|
error!(?err);
|
||||||
};
|
};
|
||||||
|
// TODO(jwall): This needs to be moved into the RouteHandler
|
||||||
view.set(view! { cx,
|
view.set(view! { cx,
|
||||||
div(class="app") {
|
div(class="app") {
|
||||||
Header { }
|
Header { }
|
||||||
Router(RouterProps {
|
RouteHandler()
|
||||||
route: Routes::Plan,
|
|
||||||
route_select: route_switch,
|
|
||||||
browser_integration: BrowserIntegration::new(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user