Router Integration and entrypoint sycamore 0.8 conversion

This commit is contained in:
Jeremy Wall 2022-09-25 17:04:46 -04:00
parent 481e44911f
commit 8c5093d77f
22 changed files with 696 additions and 629 deletions

219
Cargo.lock generated
View File

@ -748,10 +748,25 @@ dependencies = [
] ]
[[package]] [[package]]
name = "futures-channel" name = "futures"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -759,15 +774,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@ -787,9 +802,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
@ -806,6 +821,17 @@ dependencies = [
"waker-fn", "waker-fn",
] ]
[[package]]
name = "futures-macro"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures-rustls" name = "futures-rustls"
version = "0.22.2" version = "0.22.2"
@ -819,25 +845,29 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.21" version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@ -1009,6 +1039,15 @@ dependencies = [
"digest 0.9.0", "digest 0.9.0",
] ]
[[package]]
name = "html-escape"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c"
dependencies = [
"utf8-width",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.8" version = "0.2.8"
@ -1194,79 +1233,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexical"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6"
dependencies = [
"lexical-core",
]
[[package]]
name = "lexical-core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46"
dependencies = [
"lexical-parse-float",
"lexical-parse-integer",
"lexical-util",
"lexical-write-float",
"lexical-write-integer",
]
[[package]]
name = "lexical-parse-float"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
dependencies = [
"lexical-parse-integer",
"lexical-util",
"static_assertions",
]
[[package]]
name = "lexical-parse-integer"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
dependencies = [
"lexical-util",
"static_assertions",
]
[[package]]
name = "lexical-util"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
dependencies = [
"static_assertions",
]
[[package]]
name = "lexical-write-float"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862"
dependencies = [
"lexical-util",
"lexical-write-integer",
"static_assertions",
]
[[package]]
name = "lexical-write-integer"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446"
dependencies = [
"lexical-util",
"static_assertions",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.126" version = "0.2.126"
@ -1886,6 +1852,15 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "slotmap"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.9.0" version = "1.9.0"
@ -2008,12 +1983,6 @@ dependencies = [
"futures-rustls", "futures-rustls",
] ]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.2" version = "0.1.2"
@ -2038,28 +2007,48 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "sycamore" name = "sycamore"
version = "0.7.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
checksum = "f5cea65876897bb946a623e16bf3df2de4997a6872d95b99dfaed5dd8e14e264"
dependencies = [ dependencies = [
"ahash", "ahash",
"futures",
"indexmap", "indexmap",
"js-sys", "js-sys",
"lexical",
"paste", "paste",
"smallvec", "sycamore-core",
"sycamore-futures",
"sycamore-macro", "sycamore-macro",
"sycamore-reactive", "sycamore-reactive",
"sycamore-web",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
] ]
[[package]]
name = "sycamore-core"
version = "0.8.2"
source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
dependencies = [
"ahash",
"sycamore-reactive",
]
[[package]]
name = "sycamore-futures"
version = "0.8.0"
source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
dependencies = [
"futures",
"sycamore-reactive",
"tokio",
"wasm-bindgen-futures",
]
[[package]] [[package]]
name = "sycamore-macro" name = "sycamore-macro"
version = "0.7.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
checksum = "1d6911dba86d928ed3c898ee182c1a8a9f00299aa78875bc9308e7fd389e5bb4"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
@ -2069,14 +2058,28 @@ dependencies = [
[[package]] [[package]]
name = "sycamore-reactive" name = "sycamore-reactive"
version = "0.7.1" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
checksum = "20809429d0f9c2ffcbb3f192957a5d0c505519138e41c5d38808c5b42b3c53ab"
dependencies = [ dependencies = [
"ahash", "ahash",
"bumpalo",
"indexmap", "indexmap",
"serde", "serde",
"slotmap",
"smallvec", "smallvec",
]
[[package]]
name = "sycamore-web"
version = "0.8.2"
source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
dependencies = [
"html-escape",
"indexmap",
"js-sys",
"once_cell",
"sycamore-core",
"sycamore-reactive",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
] ]
@ -2409,6 +2412,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "utf8-width"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.1.2" version = "1.1.2"

View File

@ -1,2 +1,6 @@
[workspace] [workspace]
members = [ "recipes", "kitchen", "web", "recipe-store"] members = [ "recipes", "kitchen", "web", "recipe-store"]
[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" }

View File

@ -181,10 +181,10 @@ make_fn!(
( (
Duration::from_secs( Duration::from_secs(
match u { match u {
"ms" => (cnt / 1000), "ms" => cnt / 1000,
"s" | "sec" => cnt.into(), "s" | "sec" => cnt.into(),
"m" | "min" => (dbg!(cnt) * 60), "m" | "min" => dbg!(cnt) * 60,
"h" | "hr" | "hrs" => (cnt * 60 * 60), "h" | "hr" | "hrs" => cnt * 60 * 60,
_ => unreachable!(), _ => unreachable!(),
}.into() }.into()
) )

View File

@ -48,8 +48,8 @@ features = [
] ]
[dependencies.sycamore] [dependencies.sycamore]
version = "0.7.1" version = "0.8.2"
features = ["futures", "serde", "default"] features = ["suspense", "serde", "default", ]
[profile.release] [profile.release]
lto = true lto = true

View File

@ -11,14 +11,14 @@
// 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 serde_json::{from_str, to_string}; use serde_json::from_str;
use sycamore::{futures::spawn_local_in_scope, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{debug, error, instrument}; use tracing::{debug, error, instrument};
use web_sys::HtmlDialogElement; use web_sys::HtmlDialogElement;
use recipes::parse; use recipes::parse;
use crate::{js_lib::get_element_by_id, service::get_appservice_from_context}; use crate::{js_lib::get_element_by_id, service::AppService};
fn get_error_dialog() -> HtmlDialogElement { fn get_error_dialog() -> HtmlDialogElement {
get_element_by_id::<HtmlDialogElement>("error-dialog") get_element_by_id::<HtmlDialogElement>("error-dialog")
@ -26,7 +26,7 @@ fn get_error_dialog() -> HtmlDialogElement {
.unwrap() .unwrap()
} }
fn check_category_text_parses(unparsed: &str, error_text: Signal<String>) -> bool { fn check_category_text_parses(unparsed: &str, error_text: &Signal<String>) -> bool {
let el = get_error_dialog(); let el = get_error_dialog();
if let Err(e) = parse::as_categories(unparsed) { if let Err(e) = parse::as_categories(unparsed) {
error!(?e, "Error parsing categories"); error!(?e, "Error parsing categories");
@ -40,12 +40,13 @@ fn check_category_text_parses(unparsed: &str, error_text: Signal<String>) -> boo
} }
#[instrument] #[instrument]
#[component(Categories<G>)] #[component]
pub fn categories() -> View<G> { pub fn Categories<G: Html>(cx: Scope) -> View<G> {
let app_service = get_appservice_from_context(); let app_service = use_context::<AppService>(cx);
let save_signal = Signal::new(()); let save_signal = create_signal(cx, ());
let error_text = Signal::new(String::new()); let error_text = create_signal(cx, String::new());
let category_text = Signal::new( let category_text = create_signal(
cx,
match app_service match app_service
.get_category_text() .get_category_text()
.expect("Failed to get categories.") .expect("Failed to get categories.")
@ -54,26 +55,27 @@ pub fn categories() -> View<G> {
.map_err(|e| format!("{}", e)) .map_err(|e| format!("{}", e))
.expect("Failed to parse categories as json"), .expect("Failed to parse categories as json"),
None => String::new(), None => String::new(),
}, //.unwrap_or_else(|| String::new()), },
); );
create_effect( create_effect(cx, move || {
cloned!((app_service, category_text, save_signal, error_text) => move || {
// TODO(jwall): This is triggering on load which is not desired. // TODO(jwall): This is triggering on load which is not desired.
save_signal.get(); save_signal.track();
spawn_local_in_scope({ spawn_local_scoped(cx, {
cloned!((app_service, category_text, error_text) => async move { async move {
// TODO(jwall): Save the categories. // TODO(jwall): Save the categories.
if let Err(e) = app_service.save_categories(category_text.get_untracked().as_ref().clone()).await { if let Err(e) = app_service
.save_categories(category_text.get_untracked().as_ref().clone())
.await
{
error!(?e, "Failed to save categories"); error!(?e, "Failed to save categories");
error_text.set(format!("{:?}", e)); error_text.set(format!("{:?}", e));
} }
}) }
});
}); });
}),
);
let dialog_view = cloned!((error_text) => view! { let dialog_view = view! {cx,
dialog(id="error-dialog") { dialog(id="error-dialog") {
article{ article{
header { header {
@ -88,20 +90,20 @@ pub fn categories() -> View<G> {
} }
} }
} }
}); };
cloned!((category_text, error_text) => view! { view! {cx,
(dialog_view) (dialog_view)
textarea(bind:value=category_text.clone(), rows=20) textarea(bind:value=category_text, rows=20)
a(role="button", href="#", on:click=cloned!((category_text, error_text) => move |_| { a(role="button", href="#", on:click=move |_| {
check_category_text_parses(category_text.get().as_str(), error_text.clone()); check_category_text_parses(category_text.get().as_str(), error_text);
})) { "Check" } " " }) { "Check" } " "
a(role="button", href="#", on:click=cloned!((category_text, error_text) => move |_| { a(role="button", href="#", on:click=move |_| {
// TODO(jwall): check and then save the categories. // TODO(jwall): check and then save the categories.
if check_category_text_parses(category_text.get().as_str(), error_text.clone()) { if check_category_text_parses(category_text.get().as_str(), error_text) {
debug!("triggering category save"); debug!("triggering category save");
save_signal.trigger_subscribers(); save_signal.trigger_subscribers();
} }
})) { "Save" } }) { "Save" }
}) }
} }

View File

@ -14,10 +14,10 @@
use sycamore::prelude::*; use sycamore::prelude::*;
#[component(Header<G>)] #[component]
pub fn header() -> View<G> { pub fn Header<G: Html>(cx: Scope) -> View<G> {
view! { view! {cx,
div(class="menu") { div(class="menu no-print") {
h1 { "Meal Plan" } h1 { "Meal Plan" }
} }
} }

View File

@ -11,11 +11,11 @@
// 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 sycamore::{futures::spawn_local_in_scope, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{debug, error}; use tracing::{debug, error};
use web_sys::HtmlDialogElement; use web_sys::HtmlDialogElement;
use crate::{js_lib::get_element_by_id, service::get_appservice_from_context}; use crate::{js_lib::get_element_by_id, service::AppService};
use recipe_store::RecipeEntry; use recipe_store::RecipeEntry;
use recipes; use recipes;
@ -25,7 +25,7 @@ fn get_error_dialog() -> HtmlDialogElement {
.unwrap() .unwrap()
} }
fn check_recipe_parses(text: &str, error_text: Signal<String>) -> bool { fn check_recipe_parses(text: &str, error_text: &Signal<String>) -> bool {
if let Err(e) = recipes::parse::as_recipe(text) { if let Err(e) = recipes::parse::as_recipe(text) {
error!(?e, "Error parsing recipe"); error!(?e, "Error parsing recipe");
error_text.set(e); error_text.set(e);
@ -40,32 +40,34 @@ fn check_recipe_parses(text: &str, error_text: Signal<String>) -> bool {
} }
} }
#[component(Editor<G>)] #[component]
fn editor(recipe: RecipeEntry) -> View<G> { fn Editor<G: Html>(cx: Scope, recipe: &RecipeEntry) -> View<G> {
let id = Signal::new(recipe.recipe_id().to_owned()); let id = create_signal(cx, recipe.recipe_id().to_owned());
let text = Signal::new(recipe.recipe_text().to_owned()); let text = create_signal(cx, recipe.recipe_text().to_owned());
let error_text = Signal::new(String::new()); let error_text = create_signal(cx, String::new());
let app_service = get_appservice_from_context(); let app_service = use_context::<AppService>(cx);
let save_signal = Signal::new(()); let save_signal = create_signal(cx, ());
create_effect( create_effect(cx, move || {
cloned!((id, app_service, text, save_signal, error_text) => move || {
// TODO(jwall): This is triggering on load which is not desired. // TODO(jwall): This is triggering on load which is not desired.
save_signal.get(); save_signal.track();
spawn_local_in_scope({ spawn_local_scoped(cx, {
cloned!((id, app_service, text, error_text) => async move { async move {
if let Err(e) = app_service if let Err(e) = app_service
.save_recipes(vec![RecipeEntry(id.get_untracked().as_ref().clone(), text.get_untracked().as_ref().clone())]) .save_recipes(vec![RecipeEntry(
.await { id.get_untracked().as_ref().clone(),
text.get_untracked().as_ref().clone(),
)])
.await
{
error!(?e, "Failed to save recipe"); error!(?e, "Failed to save recipe");
error_text.set(format!("{:?}", e)); error_text.set(format!("{:?}", e));
}; };
}) }
});
}); });
}),
);
let dialog_view = cloned!((error_text) => view! { let dialog_view = view! {cx,
dialog(id="error-dialog") { dialog(id="error-dialog") {
article{ article{
header { header {
@ -80,73 +82,84 @@ fn editor(recipe: RecipeEntry) -> View<G> {
} }
} }
} }
}); };
cloned!((text, error_text) => view! { view! {cx,
(dialog_view) (dialog_view)
textarea(bind:value=text.clone(), rows=20) textarea(bind:value=text, rows=20)
a(role="button" , href="#", on:click=cloned!((text, error_text) => move |_| { a(role="button" , href="#", on:click=move |_| {
let unparsed = text.get(); let unparsed = text.get();
check_recipe_parses(unparsed.as_str(), error_text.clone()); check_recipe_parses(unparsed.as_str(), error_text.clone());
})) { "Check" } " " }) { "Check" } " "
a(role="button", href="#", on:click=cloned!((text, error_text) => move |_| { a(role="button", href="#", on:click=move |_| {
let unparsed = text.get(); let unparsed = text.get();
if check_recipe_parses(unparsed.as_str(), error_text.clone()) { if check_recipe_parses(unparsed.as_str(), error_text.clone()) {
debug!("triggering a save"); debug!("triggering a save");
save_signal.trigger_subscribers(); save_signal.trigger_subscribers();
}; };
})) { "Save" } }) { "Save" }
}) }
} }
#[component(Steps<G>)] #[component]
fn steps(steps: ReadSignal<Vec<recipes::Step>>) -> View<G> { fn Steps<'ctx, G: Html>(cx: Scope<'ctx>, steps: &'ctx ReadSignal<Vec<recipes::Step>>) -> View<G> {
view! { view! {cx,
h2 { "Steps: " } h2 { "Steps: " }
div(class="recipe_steps") { div(class="recipe_steps") {
Indexed(IndexedProps{ Indexed(
iterable: steps, iterable=steps,
template: |step: recipes::Step| { view! { view = |cx, step: recipes::Step| { view! {cx,
div { div {
h3 { "Instructions" } h3 { "Instructions" }
ul(class="ingredients") { ul(class="ingredients") {
Indexed(IndexedProps{ Indexed(
iterable: Signal::new(step.ingredients).handle(), iterable = create_signal(cx, step.ingredients),
template: |i| { view! { view = |cx, i| { view! {cx,
li { li {
(i.amt) " " (i.name) " " (i.form.as_ref().map(|f| format!("({})", f)).unwrap_or(String::new())) (i.amt) " " (i.name) " " (i.form.as_ref().map(|f| format!("({})", f)).unwrap_or(String::new()))
} }
}} }}
}) )
} }
div(class="instructions") { div(class="instructions") {
(step.instructions) (step.instructions)
} }
}} }}
} }
}) )
} }
} }
} }
#[component(Recipe<G>)] #[component]
pub fn recipe(idx: ReadSignal<String>) -> View<G> { pub fn Recipe<'ctx, G: Html>(cx: Scope<'ctx>, recipe_id: String) -> View<G> {
let app_service = get_appservice_from_context(); let app_service = use_context::<AppService>(cx).clone();
let view = Signal::new(View::empty()); let view = create_signal(cx, View::empty());
let show_edit = Signal::new(false); let show_edit = create_signal(cx, false);
create_effect(cloned!((idx, app_service, view, show_edit) => move || { // FIXME(jwall): This has too many unwrap() calls
if let Some(recipe) = app_service
.fetch_recipes_from_storage()
.unwrap()
.1
.unwrap()
.get(&recipe_id)
{
let recipe = create_signal(cx, recipe.clone());
let title = create_memo(cx, move || recipe.get().title.clone());
let desc = create_memo(cx, move || {
recipe
.clone()
.get()
.desc
.clone()
.unwrap_or_else(|| String::new())
});
let steps = create_memo(cx, move || recipe.get().steps.clone());
create_effect(cx, move || {
if *show_edit.get() { if *show_edit.get() {
return; return;
} }
let recipe_id: String = idx.get().as_ref().to_owned(); view.set(view! {cx,
if let Some(recipe) = app_service.get_recipes().get().get(&recipe_id) {
let recipe = recipe.clone();
let title = create_memo(cloned!((recipe) => move || recipe.get().title.clone()));
let desc = create_memo(
cloned!((recipe) => move || recipe.clone().get().desc.clone().unwrap_or_else(|| String::new())),
);
let steps = create_memo(cloned!((recipe) => move || recipe.get().steps.clone()));
view.set(view! {
div(class="recipe") { div(class="recipe") {
h1(class="recipe_title") { (title.get()) } h1(class="recipe_title") { (title.get()) }
div(class="recipe_description") { div(class="recipe_description") {
@ -155,22 +168,25 @@ pub fn recipe(idx: ReadSignal<String>) -> View<G> {
Steps(steps) Steps(steps)
} }
}); });
} });
})); if let Some(entry) = app_service
create_effect(cloned!((idx, app_service, view, show_edit) => move || { .fetch_recipe_text(recipe_id.as_str())
let recipe_id: String = idx.get().as_ref().to_owned(); .expect("No such recipe")
{
let entry_ref = create_ref(cx, entry);
create_effect(cx, move || {
if !(*show_edit.get()) { if !(*show_edit.get()) {
return; return;
} }
if let Some(entry) = app_service.fetch_recipe_text(recipe_id.as_str()).expect("No such recipe") { view.set(view! {cx,
view.set(view! { Editor(entry_ref)
Editor(entry) });
}); });
} }
})); }
view! { view! {cx,
a(role="button", href="#", on:click=cloned!((show_edit) => move |_| { show_edit.set(true); })) { "Edit" } " " a(role="button", href="#", on:click=move |_| { show_edit.set(true); }) { "Edit" } " "
a(role="button", href="#", on:click=cloned!((show_edit) => move |_| { show_edit.set(false); })) { "View" } a(role="button", href="#", on:click=move |_| { show_edit.set(false); }) { "View" }
(view.get().as_ref()) (view.get().as_ref())
} }
} }

View File

@ -11,32 +11,29 @@
// 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 crate::components::Recipe; use crate::{components::Recipe, service::AppService};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, instrument}; use tracing::{debug, instrument};
use crate::service::get_appservice_from_context;
#[instrument] #[instrument]
#[component(RecipeList<G>)] #[component]
pub fn recipe_list() -> View<G> { pub fn RecipeList<G: Html>(cx: Scope) -> View<G> {
let app_service = get_appservice_from_context(); let app_service = use_context::<AppService>(cx);
let menu_list = create_memo(move || app_service.get_menu_list()); let menu_list = create_memo(cx, || app_service.get_menu_list());
view! { view! {cx,
h1 { "Recipe List" } h1 { "Recipe List" }
div() { div() {
Indexed(IndexedProps{ Indexed(
iterable: menu_list, iterable=menu_list,
template: |(idx, _count)| { view= |cx, (idx, _count)| {
debug!(idx=%idx, "Rendering recipe"); debug!(idx=%idx, "Rendering recipe");
let idx = Signal::new(idx); view ! {cx,
view ! { Recipe(idx)
Recipe(idx.handle())
hr() hr()
} }
} }
}) )
} }
} }
} }

View File

@ -18,37 +18,43 @@ use tracing::{debug, instrument};
use crate::service::get_appservice_from_context; use crate::service::get_appservice_from_context;
pub struct RecipeCheckBoxProps { #[derive(Prop)]
pub struct RecipeCheckBoxProps<'ctx> {
pub i: String, pub i: String,
pub title: ReadSignal<String>, pub title: &'ctx ReadSignal<String>,
} }
#[instrument(skip(props), fields( #[instrument(skip(props, cx), fields(
idx=%props.i, idx=%props.i,
title=%props.title.get() title=%props.title.get()
))] ))]
#[component(RecipeSelection<G>)] #[component]
pub fn recipe_selection(props: RecipeCheckBoxProps) -> View<G> { pub fn RecipeSelection<G: Html>(cx: Scope, props: RecipeCheckBoxProps) -> View<G> {
let app_service = get_appservice_from_context(); let mut app_service = get_appservice_from_context(cx).clone();
// This is total hack but it works around the borrow issues with // This is total hack but it works around the borrow issues with
// the `view!` macro. // the `view!` macro.
let id = Rc::new(props.i); let id = Rc::new(props.i);
let count = Signal::new(format!( let count = create_signal(
cx,
format!(
"{}", "{}",
app_service.get_recipe_count_by_index(id.as_ref()) app_service
)); .get_recipe_count_by_index(id.as_ref())
.unwrap_or_else(|| app_service.set_recipe_count_by_index(id.as_ref(), 0))
),
);
let title = props.title.get().clone();
let for_id = id.clone(); let for_id = id.clone();
let href = format!("/ui/recipe/{}", id); let href = format!("/ui/recipe/{}", id);
let name = format!("recipe_id:{}", id); let name = format!("recipe_id:{}", id);
let value = id.clone(); view! {cx,
view! {
div() { div() {
label(for=for_id) { a(href=href) { (props.title.get()) } } label(for=for_id) { a(href=href) { (*title) } }
input(type="number", class="item-count-sel", min="0", bind:value=count.clone(), name=name, value=value, on:change=cloned!((id) => move |_| { input(type="number", class="item-count-sel", min="0", bind:value=count, name=name, on:change=move |_| {
let mut app_service = app_service.clone(); let mut app_service = app_service.clone();
debug!(idx=%id, count=%(*count.get()), "setting recipe count"); debug!(idx=%id, count=%(*count.get()), "setting recipe count");
app_service.set_recipe_count_by_index(id.as_ref().to_owned(), count.get().parse().unwrap()); app_service.set_recipe_count_by_index(id.as_ref(), count.get().parse().unwrap());
})) })
} }
} }
} }

View File

@ -12,48 +12,57 @@
// 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 recipes::Recipe; use recipes::Recipe;
use sycamore::{futures::spawn_local_in_scope, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{error, instrument}; use tracing::{error, instrument};
use crate::components::recipe_selection::*; use crate::components::recipe_selection::*;
use crate::service::AppService; use crate::service::*;
#[instrument] #[instrument]
#[component(RecipeSelector<G>)] pub fn RecipeSelector<G: Html>(cx: Scope) -> View<G> {
pub fn recipe_selector(app_service: AppService) -> View<G> { let app_service = get_appservice_from_context(cx).clone();
let rows = create_memo(cloned!(app_service => move || { let rows = create_memo(cx, move || {
let mut rows = Vec::new(); let mut rows = Vec::new();
for row in app_service.get_recipes().get().iter().map(|(k, v)| (k.clone(), v.clone())).collect::<Vec<(String, Signal<Recipe>)>>().chunks(4) { if let (_, Some(bt)) = app_service.fetch_recipes_from_storage().unwrap() {
rows.push(Signal::new(Vec::from(row))); for row in bt
.iter()
.map(|(k, v)| create_signal(cx, (k.clone(), v.clone())))
.collect::<Vec<&Signal<(String, Recipe)>>>()
.chunks(4)
{
rows.push(create_signal(cx, Vec::from(row)));
}
} }
rows rows
})); });
let clicked = Signal::new(false); let app_service = get_appservice_from_context(cx).clone();
create_effect(cloned!((clicked, app_service) => move || { let clicked = create_signal(cx, false);
clicked.get(); create_effect(cx, move || {
spawn_local_in_scope(cloned!((app_service) => { clicked.track();
spawn_local_scoped(cx, {
let mut app_service = app_service.clone(); let mut app_service = app_service.clone();
async move { async move {
if let Err(err) = app_service.refresh().await { if let Err(err) = app_service.synchronize().await {
error!(?err); error!(?err);
}; };
} }
})); });
})); });
view! { view! {cx,
table(class="recipe_selector no-print") { table(class="recipe_selector no-print") {
(View::new_fragment( (View::new_fragment(
rows.get().iter().cloned().map(|r| { rows.get().iter().cloned().map(|r| {
view ! { view ! {cx,
tr { Keyed(KeyedProps{ tr { Keyed(
iterable: r.handle(), iterable=r,
template: |(i, recipe)| { view=|cx, sig| {
view! { let title = create_memo(cx, move || sig.get().1.title.clone());
td { RecipeSelection(RecipeCheckBoxProps{i: i, title: create_memo(move || recipe.get().title.clone())}) } view! {cx,
td { RecipeSelection(i=sig.get().0.to_owned(), title=title) }
} }
}, },
key: |r| r.0.clone(), key=|sig| sig.get().0.to_owned(),
})} )}
} }
}).collect() }).collect()
)) ))

View File

@ -13,27 +13,146 @@
// limitations under the License. // limitations under the License.
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use recipes::{Ingredient, IngredientKey};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::{debug, instrument}; use tracing::{debug, instrument};
use crate::service::get_appservice_from_context; use crate::service::get_appservice_from_context;
fn make_ingredients_rows<'ctx, G: Html>(
cx: Scope<'ctx>,
ingredients: &'ctx ReadSignal<Vec<(IngredientKey, (Ingredient, BTreeSet<String>))>>,
modified_amts: &'ctx Signal<BTreeMap<IngredientKey, RcSignal<String>>>,
filtered_keys: RcSignal<BTreeSet<IngredientKey>>,
) -> View<G> {
view!(
cx,
Indexed(
iterable = ingredients,
view = move |cx, (k, (i, rs))| {
let mut modified_amt_set = modified_amts.get().as_ref().clone();
let amt = modified_amt_set
.entry(k.clone())
.or_insert(create_rc_signal(format!("{}", i.amt.normalize())))
.clone();
modified_amts.set(modified_amt_set);
let name = i.name;
let category = if i.category == "" {
"other".to_owned()
} else {
i.category
};
let form = i.form.map(|form| format!("({})", form)).unwrap_or_default();
let recipes = rs
.iter()
.fold(String::new(), |acc, s| format!("{}{},", acc, s))
.trim_end_matches(",")
.to_owned();
view! {cx,
tr {
td {
input(bind:value=amt, type="text")
}
td {
input(type="button", class="no-print destructive", value="X", on:click={
let filtered_keys = filtered_keys.clone();
move |_| {
let mut keyset = filtered_keys.get().as_ref().clone();
keyset.insert(k.clone());
filtered_keys.set(keyset);
}})
}
td { (name) " " (form) "" br {} "" (category) "" }
td { (recipes) }
}
}
}
)
)
}
fn make_extras_rows<'ctx, G: Html>(
cx: Scope<'ctx>,
extras: &'ctx Signal<Vec<(usize, (&'ctx Signal<String>, &'ctx Signal<String>))>>,
) -> View<G> {
view! {cx,
Indexed(
iterable=extras,
view= move |cx, (idx, (amt, name))| {
view! {cx,
tr {
td {
input(bind:value=amt, type="text")
}
td {
input(type="button", class="no-print destructive", value="X", on:click=move |_| {
extras.set(extras.get().iter()
.filter(|(i, _)| *i != idx)
.map(|(_, v)| v.clone())
.enumerate()
.collect())
})
}
td {
input(bind:value=name, type="text")
}
td { "Misc" }
}
}
}
)
}
}
fn make_shopping_table<'ctx, G: Html>(
cx: Scope<'ctx>,
ingredients: &'ctx ReadSignal<Vec<(IngredientKey, (Ingredient, BTreeSet<String>))>>,
modified_amts: &'ctx Signal<BTreeMap<IngredientKey, RcSignal<String>>>,
extras: &'ctx Signal<Vec<(usize, (&'ctx Signal<String>, &'ctx Signal<String>))>>,
filtered_keys: RcSignal<BTreeSet<IngredientKey>>,
) -> View<G> {
let extra_rows_view = make_extras_rows(cx, extras);
let ingredient_rows =
make_ingredients_rows(cx, ingredients, modified_amts, filtered_keys.clone());
view! {cx,
table(class="pad-top shopping-list page-breaker container-fluid", role="grid") {
tr {
th { " Quantity " }
th { " Delete " }
th { " Ingredient " }
th { " Recipes " }
}
tbody {
(ingredient_rows)
(extra_rows_view)
}
}
}
}
#[instrument] #[instrument]
#[component(ShoppingList<G>)] #[component]
pub fn shopping_list() -> View<G> { pub fn ShoppingList<G: Html>(cx: Scope) -> View<G> {
let app_service = get_appservice_from_context(); let app_service = get_appservice_from_context(cx);
let filtered_keys = Signal::new(BTreeSet::new()); let filtered_keys: RcSignal<BTreeSet<IngredientKey>> = create_rc_signal(BTreeSet::new());
let ingredients_map = Signal::new(BTreeMap::new()); let ingredients_map = create_rc_signal(BTreeMap::new());
let extras = Signal::new(Vec::<(usize, (Signal<String>, Signal<String>))>::new()); let extras = create_signal(
let modified_amts = Signal::new(BTreeMap::new()); cx,
let show_staples = Signal::new(true); Vec::<(usize, (&Signal<String>, &Signal<String>))>::new(),
create_effect(
cloned!((app_service, ingredients_map, show_staples) => move || {
ingredients_map.set(app_service.get_shopping_list(*show_staples.get()));
}),
); );
let modified_amts = create_signal(cx, BTreeMap::new());
let show_staples = create_signal(cx, true);
create_effect(cx, {
let ingredients_map = ingredients_map.clone();
move || {
ingredients_map.set(app_service.get_shopping_list(*show_staples.get()));
}
});
debug!(ingredients_map=?ingredients_map.get_untracked()); debug!(ingredients_map=?ingredients_map.get_untracked());
let ingredients = create_memo(cloned!((ingredients_map, filtered_keys) => move || { let ingredients = create_memo(cx, {
let filtered_keys = filtered_keys.clone();
let ingredients_map = ingredients_map.clone();
move || {
let mut ingredients = Vec::new(); let mut ingredients = Vec::new();
// This has the effect of sorting the ingredients by category // This has the effect of sorting the ingredients by category
for (_, ingredients_list) in ingredients_map.get().iter() { for (_, ingredients_list) in ingredients_map.get().iter() {
@ -44,100 +163,42 @@ pub fn shopping_list() -> View<G> {
} }
} }
ingredients ingredients
})); }
debug!(ingredients = ?ingredients.get_untracked()); });
let table_view = Signal::new(View::empty()); let table_view = create_signal(cx, View::empty());
create_effect( create_effect(cx, {
cloned!((table_view, ingredients, filtered_keys, modified_amts, extras) => move || { let filtered_keys = filtered_keys.clone();
move || {
if (ingredients.get().len() > 0) || (extras.get().len() > 0) { if (ingredients.get().len() > 0) || (extras.get().len() > 0) {
let t = view ! { table_view.set(make_shopping_table(
table(class="pad-top shopping-list page-breaker container-fluid", role="grid") { cx,
tr { ingredients,
th { " Quantity " } modified_amts.clone(),
th { " Delete " } extras.clone(),
th { " Ingredient " } filtered_keys.clone(),
th { " Recipes " } ));
}
tbody {
Indexed(IndexedProps{
iterable: ingredients.clone(),
template: cloned!((filtered_keys, modified_amts) => move |(k, (i, rs))| {
let mut modified_amt_set = (*modified_amts.get()).clone();
let amt = modified_amt_set.entry(k.clone()).or_insert(Signal::new(format!("{}", i.amt.normalize()))).clone();
modified_amts.set(modified_amt_set);
let name = i.name;
let category = if i.category == "" { "other".to_owned() } else { i.category };
let form = i.form.map(|form| format!("({})", form)).unwrap_or_default();
let recipes = rs.iter().fold(String::new(), |acc, s| format!("{}{},", acc, s)).trim_end_matches(",").to_owned();
view! {
tr {
td {
input(bind:value=amt.clone(), type="text")
}
td {
input(type="button", class="no-print destructive", value="X", on:click=cloned!((filtered_keys) => move |_| {
let mut keyset = (*filtered_keys.get()).clone();
keyset.insert(k.clone());
filtered_keys.set(keyset);
}))
}
td { (name) " " (form) "" br {} "" (category) "" }
td { (recipes) }
}
}
}),
})
Indexed(IndexedProps{
iterable: extras.handle(),
template: cloned!((extras) => move |(idx, (amt, name))| {
view! {
tr {
td {
input(bind:value=amt.clone(), type="text")
}
td {
input(type="button", class="no-print destructive", value="X", on:click=cloned!((extras) => move |_| {
extras.set(extras.get().iter()
.filter(|(i, _)| *i != idx)
.map(|(_, v)| v.clone())
.enumerate()
.collect())
}))
}
td {
input(bind:value=name.clone(), type="text")
}
td { "Misc" }
}
}
})
})
}
}
};
table_view.set(t);
} else { } else {
table_view.set(View::empty()); table_view.set(View::empty());
} }
}), }
); });
view! { view! {cx,
h1 { "Shopping List " } h1 { "Shopping List " }
label(for="show_staples_cb") { "Show staples" } label(for="show_staples_cb") { "Show staples" }
input(id="show_staples_cb", type="checkbox", bind:checked=show_staples.clone()) input(id="show_staples_cb", type="checkbox", bind:checked=show_staples)
(table_view.get().as_ref().clone()) (table_view.get().as_ref().clone())
input(type="button", value="Add Item", class="no-print", on:click=cloned!((extras) => move |_| { input(type="button", value="Add Item", class="no-print", on:click=move |_| {
let mut cloned_extras: Vec<(Signal<String>, Signal<String>)> = (*extras.get()).iter().map(|(_, v)| v.clone()).collect(); let mut cloned_extras: Vec<(&Signal<String>, &Signal<String>)> = (*extras.get()).iter().map(|(_, tpl)| *tpl).collect();
cloned_extras.push((Signal::new("".to_owned()), Signal::new("".to_owned()))); cloned_extras.push((create_signal(cx, "".to_owned()), create_signal(cx, "".to_owned())));
extras.set(cloned_extras.drain(0..).enumerate().collect()); extras.set(cloned_extras.drain(0..).enumerate().collect());
})) })
input(type="button", value="Reset", class="no-print", on:click=cloned!((ingredients_map, filtered_keys, app_service, modified_amts, extras, show_staples) => move |_| { input(type="button", value="Reset", class="no-print", on:click=move |_| {
// TODO(jwall): We should actually pop up a modal here or use a different set of items. // TODO(jwall): We should actually pop up a modal here or use a different set of items.
ingredients_map.set(app_service.get_shopping_list(*show_staples.get())); ingredients_map.set(app_service.get_shopping_list(*show_staples.get()));
// clear the filter_signal // clear the filter_signal
filtered_keys.set(BTreeSet::new()); filtered_keys.set(BTreeSet::new());
modified_amts.set(BTreeMap::new()); modified_amts.set(BTreeMap::new());
extras.set(Vec::new()); extras.set(Vec::new());
})) })
} }
} }

View File

@ -13,15 +13,17 @@
// limitations under the License. // limitations under the License.
use sycamore::prelude::*; use sycamore::prelude::*;
#[derive(Clone)] use super::Header;
#[derive(Clone, Prop)]
pub struct TabState<G: GenericNode> { pub struct TabState<G: GenericNode> {
pub inner: View<G>, pub inner: View<G>,
} }
#[component(TabbedView<G>)] #[component]
pub fn tabbed_view(state: TabState<G>) -> View<G> { pub fn TabbedView<G: Html>(cx: Scope, state: TabState<G>) -> View<G> {
cloned!((state) => view! { view! {cx,
header(class="no-print") { Header { }
nav { nav {
ul { ul {
li { a(href="/ui/plan", class="no-print") { "Plan" } " > " li { a(href="/ui/plan", class="no-print") { "Plan" } " > "
@ -38,9 +40,8 @@ pub fn tabbed_view(state: TabState<G>) -> View<G> {
li { a(href="https://github.com/zaphar/kitchen") { "Github" } } li { a(href="https://github.com/zaphar/kitchen") { "Github" } }
} }
} }
}
main(class=".conatiner-fluid") { main(class=".conatiner-fluid") {
(state.inner) (state.inner)
} }
}) }
} }

View File

@ -33,5 +33,5 @@ pub fn main() {
// TODO(jwall): use the tracing_subscriber_browser default setup function when it exists. // TODO(jwall): use the tracing_subscriber_browser default setup function when it exists.
tracing_browser_subscriber::configure_as_global_default(); tracing_browser_subscriber::configure_as_global_default();
} }
sycamore::render(|| view! { UI() }); sycamore::render(|cx| view! { cx, UI() });
} }

View File

@ -18,12 +18,12 @@ use sycamore::prelude::*;
use tracing::instrument; use tracing::instrument;
#[instrument] #[instrument]
#[component(CategoryPage<G>)] #[component()]
pub fn category_page() -> View<G> { pub fn CategoryPage<G: Html>(cx: Scope) -> View<G> {
view! { view! {cx,
TabbedView(TabState { TabbedView(TabState {
inner: view! { inner: view! {cx,
Categories() Categories { }
} }
}) })
} }

View File

@ -15,12 +15,12 @@ use crate::components::{recipe_list::*, tabs::*};
use sycamore::prelude::*; use sycamore::prelude::*;
#[component(CookPage<G>)] #[component]
pub fn cook_page() -> View<G> { pub fn CookPage<G: Html>(cx: Scope) -> View<G> {
view! { view! {cx,
TabbedView(TabState { TabbedView(TabState {
inner: view! { inner: view! {cx,
RecipeList() RecipeList { }
}, },
}) })
} }

View File

@ -15,12 +15,12 @@ use crate::components::{shopping_list::*, tabs::*};
use sycamore::prelude::*; use sycamore::prelude::*;
#[component(InventoryPage<G>)] #[component]
pub fn inventory_page() -> View<G> { pub fn InventoryPage<G: Html>(cx: Scope) -> View<G> {
view! { view! {cx,
TabbedView(TabState { TabbedView(TabState {
inner: view! { inner: view! {cx,
ShoppingList() ShoppingList {}
}, },
}) })
} }

View File

@ -15,7 +15,7 @@ use crate::components::tabs::*;
use base64; use base64;
use reqwasm::http; use reqwasm::http;
use sycamore::{futures::spawn_local_in_scope, prelude::*}; use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
fn token68(user: String, pass: String) -> String { fn token68(user: String, pass: String) -> String {
@ -46,42 +46,42 @@ async fn authenticate(user: String, pass: String) -> bool {
return false; return false;
} }
#[component(LoginForm<G>)] #[component]
pub fn login_form() -> View<G> { pub fn LoginForm<G: Html>(cx: Scope) -> View<G> {
let username = Signal::new("".to_owned()); let username = create_signal(cx, "".to_owned());
let password = Signal::new("".to_owned()); let password = create_signal(cx, "".to_owned());
let clicked = Signal::new(("".to_owned(), "".to_owned())); let clicked = create_signal(cx, ("".to_owned(), "".to_owned()));
create_effect(cloned!((clicked) => move || { create_effect(cx, move || {
let (username, password) = (*clicked.get()).clone(); let (username, password) = (*clicked.get()).clone();
if username != "" && password != "" { if username != "" && password != "" {
spawn_local_in_scope(async move { spawn_local_scoped(cx, async move {
debug!("authenticating against ui"); debug!("authenticating against ui");
// TODO(jwall): Navigate to plan if the below is successful. // TODO(jwall): Navigate to plan if the below is successful.
authenticate(username, password).await; authenticate(username, password).await;
}); });
} }
})); });
view! { view! {cx,
form() { form() {
label(for="username") { "Username" } label(for="username") { "Username" }
input(type="text", id="username", bind:value=username.clone()) input(type="text", id="username", bind:value=username)
label(for="password") { "Password" } label(for="password") { "Password" }
input(type="password", bind:value=password.clone()) input(type="password", bind:value=password)
input(type="button", value="Login", on:click=cloned!((clicked) => move |_| { input(type="button", value="Login", on:click=move |_| {
info!("Attempting login request"); info!("Attempting login request");
clicked.set(((*username.get_untracked()).clone(), (*password.get_untracked()).clone())); clicked.set(((*username.get_untracked()).clone(), (*password.get_untracked()).clone()));
debug!("triggering login click subscribers"); debug!("triggering login click subscribers");
clicked.trigger_subscribers(); clicked.trigger_subscribers();
})) { } }) { }
} }
} }
} }
#[component(LoginPage<G>)] #[component]
pub fn login_page() -> View<G> { pub fn LoginPage<G: Html>(cx: Scope) -> View<G> {
view! { view! {cx,
TabbedView(TabState { TabbedView(TabState {
inner: view! { LoginForm() } inner: view! {cx, LoginForm { } }
}) })
} }
} }

View File

@ -15,14 +15,12 @@ use crate::components::{recipe_selector::*, tabs::*};
use sycamore::prelude::*; use sycamore::prelude::*;
use super::PageProps; #[component]
pub fn PlanPage<G: Html>(cx: Scope) -> View<G> {
#[component(PlanPage<G>)] view! {cx,
pub fn plan_page(props: PageProps) -> View<G> {
view! {
TabbedView(TabState { TabbedView(TabState {
inner: view! { inner: view! {cx,
RecipeSelector(props.service.clone()) RecipeSelector()
}, },
}) })
} }

View File

@ -16,18 +16,18 @@ use crate::components::{recipe::Recipe, tabs::*};
use sycamore::prelude::*; use sycamore::prelude::*;
use tracing::instrument; use tracing::instrument;
#[derive(Debug)] #[derive(Debug, Prop)]
pub struct RecipePageProps { pub struct RecipePageProps {
pub recipe: Signal<String>, pub recipe: String,
} }
#[instrument] #[instrument]
#[component(RecipePage<G>)] #[component()]
pub fn recipe_page(props: RecipePageProps) -> View<G> { pub fn RecipePage<G: Html>(cx: Scope, props: RecipePageProps) -> View<G> {
view! { view! {cx,
TabbedView(TabState { TabbedView(TabState {
inner: view! { inner: view! {cx,
Recipe(props.recipe.handle()) Recipe(props.recipe)
} }
}) })
} }

View File

@ -24,12 +24,12 @@ use web_sys::{Element, HtmlAnchorElement};
use crate::app_state::AppRoutes; use crate::app_state::AppRoutes;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct BrowserIntegration(Signal<(String, String, String)>); pub struct BrowserIntegration(RcSignal<(String, String, String)>);
impl BrowserIntegration { impl BrowserIntegration {
pub fn new() -> Self { pub fn new() -> Self {
let location = web_sys::window().unwrap_throw().location(); let location = web_sys::window().unwrap_throw().location();
Self(Signal::new(( Self(create_rc_signal((
location.origin().unwrap_or(String::new()), location.origin().unwrap_or(String::new()),
location.pathname().unwrap_or(String::new()), location.pathname().unwrap_or(String::new()),
location.hash().unwrap_or(String::new()), location.hash().unwrap_or(String::new()),
@ -102,7 +102,7 @@ pub struct RouterProps<R, F, G>
where where
G: GenericNode, G: GenericNode,
R: DeriveRoute + NotFound + Clone + Default + Debug + 'static, R: DeriveRoute + NotFound + Clone + Default + Debug + 'static,
F: Fn(ReadSignal<R>) -> View<G> + 'static, F: Fn(Scope, &ReadSignal<R>) -> View<G> + 'static,
{ {
pub route: R, pub route: R,
pub route_select: F, pub route_select: F,
@ -114,56 +114,62 @@ where
pathn=props.browser_integration.0.get().1, pathn=props.browser_integration.0.get().1,
hash=props.browser_integration.0.get().2), hash=props.browser_integration.0.get().2),
skip(props))] skip(props))]
#[component(Router<G>)] #[component]
pub fn router<R, F>(props: RouterProps<R, F, G>) -> View<G> pub fn Router<'ctx, G, R, F>(cx: Scope, props: RouterProps<R, F, G>) -> View<G>
where where
G: Html,
R: DeriveRoute + NotFound + Clone + Default + Debug + 'static, R: DeriveRoute + NotFound + Clone + Default + Debug + 'static,
F: Fn(ReadSignal<R>) -> View<G> + 'static, F: Fn(Scope, &ReadSignal<R>) -> View<G> + 'static,
{ {
debug!("Setting up router"); debug!("Setting up router");
let integration = Rc::new(props.browser_integration); let integration = Rc::new(props.browser_integration);
let route_select = Rc::new(props.route_select); let route_select = Rc::new(props.route_select);
let view_signal = Signal::new(View::empty()); let view_signal = create_signal(cx, View::empty());
create_effect( create_effect(cx, {
cloned!((view_signal, integration, route_select) => move || { let integration = integration.clone();
move || {
let path_signal = integration.0.clone(); let path_signal = integration.0.clone();
debug!(origin=%path_signal.get().0, path=%path_signal.get().1, hash=%path_signal.get().2, "new path"); debug!(origin=%path_signal.get().0, path=%path_signal.get().1, hash=%path_signal.get().2, "new path");
let path = path_signal.clone(); let path = path_signal.clone();
let route = R::from(path.get().as_ref()); let route = R::from(path.get().as_ref());
debug!(?route, "new route"); debug!(?route, "new route");
// TODO(jwall): this is an unnecessary use of signal. // TODO(jwall): this is an unnecessary use of signal.
let view = route_select.as_ref()(Signal::new(route).handle()); let view = route_select.as_ref()(cx, &*create_signal(cx, route));
register_click_handler(&view, integration.clone()); register_click_handler(cx, &view, integration.clone());
view_signal.set(view); view_signal.set(view);
}), }
); });
let path_signal = integration.0.clone(); let path_signal = integration.0.clone();
integration.register_post_state_handler(Box::new(cloned!((path_signal) => move || { integration.register_post_state_handler(Box::new(move || {
let location = web_sys::window().unwrap_throw().location(); let location = web_sys::window().unwrap_throw().location();
path_signal.set((location.origin().unwrap_throw(), location.pathname().unwrap_throw(), location.hash().unwrap_throw())); 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 // NOTE(jwall): This needs to be a dynamic node so Sycamore knows to rerender it
// based on the results of the effect above. // based on the results of the effect above.
view! { view! {cx,
(view_signal.get().as_ref()) (view_signal.get().as_ref())
} }
} }
#[instrument(skip_all)] #[instrument(skip_all)]
fn register_click_handler<G>(view: &View<G>, integration: Rc<BrowserIntegration>) fn register_click_handler<G>(cx: Scope, view: &View<G>, integration: Rc<BrowserIntegration>)
where where
G: GenericNode<EventType = Event>, G: GenericNode<EventType = Event>,
{ {
debug!("Registring click handler on node(s)"); debug!("Registring click handler on node(s)");
if let Some(node) = view.as_node() { if let Some(node) = view.as_node() {
node.event("click", integration.click_handler()); node.event(cx, "click", integration.click_handler());
} else if let Some(frag) = view.as_fragment() { } else if let Some(frag) = view.as_fragment() {
debug!(fragment=?frag); debug!(fragment=?frag);
for n in frag { for n in frag {
register_click_handler(n, integration.clone()); register_click_handler(cx, n, integration.clone());
} }
} else if let Some(dyn_node) = view.as_dyn() { } else if let Some(dyn_node) = view.as_dyn() {
debug!(dynamic_node=?dyn_node); debug!(dynamic_node=?dyn_node);

View File

@ -16,7 +16,7 @@ use std::collections::{BTreeMap, BTreeSet};
use reqwasm; use reqwasm;
//use serde::{Deserialize, Serialize}; //use serde::{Deserialize, Serialize};
use serde_json::{from_str, to_string}; use serde_json::{from_str, to_string};
use sycamore::{context::use_context, prelude::*}; use sycamore::prelude::*;
use tracing::{debug, error, info, instrument, warn}; use tracing::{debug, error, info, instrument, warn};
use web_sys::Storage; use web_sys::Storage;
@ -25,26 +25,27 @@ use recipes::{parse, Ingredient, IngredientAccumulator, Recipe};
use crate::js_lib; use crate::js_lib;
pub fn get_appservice_from_context() -> AppService { pub fn get_appservice_from_context(cx: Scope) -> &AppService {
use_context::<AppService>() use_context::<AppService>(cx)
} }
// TODO(jwall): We should not be cloning this.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AppService { pub struct AppService {
recipes: Signal<BTreeMap<String, Signal<Recipe>>>, recipe_counts: RcSignal<BTreeMap<String, usize>>,
staples: Signal<Option<Recipe>>, staples: RcSignal<Option<Recipe>>,
category_map: Signal<BTreeMap<String, String>>, recipes: RcSignal<BTreeMap<String, Recipe>>,
menu_list: Signal<BTreeMap<String, usize>>, category_map: RcSignal<BTreeMap<String, String>>,
store: HttpStore, store: HttpStore,
} }
impl AppService { impl AppService {
pub fn new(store: HttpStore) -> Self { pub fn new(store: HttpStore) -> Self {
Self { Self {
recipes: Signal::new(BTreeMap::new()), recipe_counts: create_rc_signal(BTreeMap::new()),
staples: Signal::new(None), staples: create_rc_signal(None),
category_map: Signal::new(BTreeMap::new()), recipes: create_rc_signal(BTreeMap::new()),
menu_list: Signal::new(BTreeMap::new()), category_map: create_rc_signal(BTreeMap::new()),
store: store, store: store,
} }
} }
@ -53,9 +54,18 @@ impl AppService {
js_lib::get_storage().map_err(|e| format!("{:?}", e)) js_lib::get_storage().map_err(|e| format!("{:?}", e))
} }
pub fn get_menu_list(&self) -> Vec<(String, usize)> {
self.recipe_counts
.get()
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect()
}
#[instrument(skip(self))] #[instrument(skip(self))]
async fn synchronize(&self) -> Result<(), String> { pub async fn synchronize(&mut self) -> Result<(), String> {
info!("Synchronizing Recipes"); info!("Synchronizing Recipes");
// TODO(jwall): Make our caching logic using storage more robust.
let storage = self.get_storage()?.unwrap(); let storage = self.get_storage()?.unwrap();
let recipes = self let recipes = self
.store .store
@ -68,6 +78,19 @@ impl AppService {
&(to_string(&recipes).map_err(|e| format!("{:?}", e))?), &(to_string(&recipes).map_err(|e| format!("{:?}", e))?),
) )
.map_err(|e| format!("{:?}", e))?; .map_err(|e| format!("{:?}", e))?;
if let Ok((staples, recipes)) = self.fetch_recipes_from_storage() {
self.staples.set(staples);
if let Some(recipes) = recipes {
self.recipes.set(recipes);
}
}
if let Some(rs) = recipes {
for r in rs {
if !self.recipe_counts.get().contains_key(r.recipe_id()) {
self.set_recipe_count_by_index(&r.recipe_id().to_owned(), 0);
}
}
}
info!("Synchronizing categories"); info!("Synchronizing categories");
match self.store.get_categories().await { match self.store.get_categories().await {
Ok(Some(categories_content)) => { Ok(Some(categories_content)) => {
@ -86,6 +109,54 @@ impl AppService {
Ok(()) Ok(())
} }
pub fn get_recipe_count_by_index(&self, key: &String) -> Option<usize> {
self.recipe_counts.get().get(key).cloned()
}
pub fn set_recipe_count_by_index(&mut self, key: &String, count: usize) -> usize {
let mut counts = self.recipe_counts.get().as_ref().clone();
counts.insert(key.clone(), count);
self.recipe_counts.set(counts);
count
}
#[instrument(skip(self))]
pub fn get_shopping_list(
&self,
show_staples: bool,
) -> BTreeMap<String, Vec<(Ingredient, BTreeSet<String>)>> {
let mut acc = IngredientAccumulator::new();
let recipe_counts = self.get_menu_list();
for (idx, count) in recipe_counts.iter() {
for _ in 0..*count {
acc.accumulate_from(self.recipes.get().get(idx).unwrap());
}
}
if show_staples {
if let Some(staples) = self.staples.get().as_ref() {
acc.accumulate_from(staples);
}
}
let mut ingredients = acc.ingredients();
let mut groups = BTreeMap::new();
let cat_map = self.category_map.get().clone();
for (_, (i, recipes)) in ingredients.iter_mut() {
let category = if let Some(cat) = cat_map.get(&i.name) {
cat.clone()
} else {
"other".to_owned()
};
i.category = category.clone();
groups
.entry(category)
.or_insert(vec![])
.push((i.clone(), recipes.clone()));
}
debug!(?self.category_map);
// FIXME(jwall): Sort by categories and names.
groups
}
pub fn get_category_text(&self) -> Result<Option<String>, String> { pub fn get_category_text(&self) -> Result<Option<String>, String> {
let storage = self.get_storage()?.unwrap(); let storage = self.get_storage()?.unwrap();
storage storage
@ -175,86 +246,6 @@ impl AppService {
Ok(self.fetch_categories_from_storage()?) Ok(self.fetch_categories_from_storage()?)
} }
#[instrument(skip(self))]
pub async fn refresh(&mut self) -> Result<(), String> {
self.synchronize().await?;
debug!("refreshing recipes");
if let (staples, Some(r)) = self.fetch_recipes().await? {
self.set_recipes(r);
self.staples.set(staples);
}
debug!("refreshing categories");
if let Some(categories) = self.fetch_categories().await? {
self.set_categories(categories);
}
Ok(())
}
pub fn get_recipe_by_index(&self, idx: &str) -> Option<Signal<Recipe>> {
self.recipes.get().get(idx).map(|r| r.clone())
}
#[instrument(skip(self))]
pub fn get_shopping_list(
&self,
show_staples: bool,
) -> BTreeMap<String, Vec<(Ingredient, BTreeSet<String>)>> {
let mut acc = IngredientAccumulator::new();
let recipe_counts = self.menu_list.get();
for (idx, count) in recipe_counts.iter() {
for _ in 0..*count {
acc.accumulate_from(self.get_recipe_by_index(idx).unwrap().get().as_ref());
}
}
if show_staples {
if let Some(staples) = self.staples.get().as_ref() {
acc.accumulate_from(staples);
}
}
let mut ingredients = acc.ingredients();
let mut groups = BTreeMap::new();
let cat_map = self.category_map.get().clone();
for (_, (i, recipes)) in ingredients.iter_mut() {
let category = if let Some(cat) = cat_map.get(&i.name) {
cat.clone()
} else {
"other".to_owned()
};
i.category = category.clone();
groups
.entry(category)
.or_insert(vec![])
.push((i.clone(), recipes.clone()));
}
debug!(?self.category_map);
// FIXME(jwall): Sort by categories and names.
groups
}
pub fn set_recipe_count_by_index(&mut self, i: String, count: usize) {
let mut v = (*self.menu_list.get()).clone();
v.insert(i, count);
self.menu_list.set(v);
}
pub fn get_recipe_count_by_index(&self, i: &str) -> usize {
self.menu_list.get().get(i).map(|i| *i).unwrap_or_default()
}
pub fn get_recipes(&self) -> Signal<BTreeMap<String, Signal<Recipe>>> {
self.recipes.clone()
}
pub fn get_menu_list(&self) -> Vec<(String, usize)> {
self.menu_list
.get()
.iter()
// We exclude recipes in the menu_list with count 0
.filter(|&(_, count)| *count != 0)
.map(|(idx, count)| (idx.clone(), *count))
.collect()
}
pub async fn save_recipes(&self, recipes: Vec<RecipeEntry>) -> Result<(), String> { pub async fn save_recipes(&self, recipes: Vec<RecipeEntry>) -> Result<(), String> {
self.store.save_recipes(recipes).await?; self.store.save_recipes(recipes).await?;
Ok(()) Ok(())
@ -264,19 +255,6 @@ impl AppService {
self.store.save_categories(categories).await?; self.store.save_categories(categories).await?;
Ok(()) Ok(())
} }
pub fn set_recipes(&mut self, recipes: BTreeMap<String, Recipe>) {
self.recipes.set(
recipes
.iter()
.map(|(i, r)| (i.clone(), Signal::new(r.clone())))
.collect(),
);
}
pub fn set_categories(&mut self, categories: BTreeMap<String, String>) {
self.category_map.set(categories);
}
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -18,85 +18,64 @@ use crate::{
router_integration::*, router_integration::*,
service::{self, AppService}, service::{self, AppService},
}; };
use tracing::{debug, error, info, instrument}; use tracing::{error, info, instrument};
use sycamore::{ use sycamore::{futures::spawn_local_scoped, prelude::*};
context::{ContextProvider, ContextProviderProps},
futures::spawn_local_in_scope,
prelude::*,
};
#[instrument] #[instrument]
fn route_switch<G: Html>(route: ReadSignal<AppRoutes>) -> View<G> { fn route_switch<G: Html>(cx: Scope, route: &ReadSignal<AppRoutes>) -> View<G> {
// NOTE(jwall): This needs to not be a dynamic node. The rules around // NOTE(jwall): This needs to not be a dynamic node. The rules around
// this are somewhat unclear and underdocumented for Sycamore. But basically // this are somewhat unclear and underdocumented for Sycamore. But basically
// avoid conditionals in the `view!` macro calls here. // avoid conditionals in the `view!` macro calls here.
cloned!((route) => match route.get().as_ref() { match route.get().as_ref() {
AppRoutes::Plan => view! { AppRoutes::Plan => view! {cx,
PlanPage() PlanPage()
}, },
AppRoutes::Inventory => view! { AppRoutes::Inventory => view! {cx,
InventoryPage() InventoryPage()
}, },
AppRoutes::Login => view! { AppRoutes::Login => view! {cx,
LoginPage() LoginPage()
}, },
AppRoutes::Cook => view! { AppRoutes::Cook => view! {cx,
CookPage() CookPage()
}, },
AppRoutes::Recipe(idx) => view! { AppRoutes::Recipe(idx) => view! {cx,
RecipePage(RecipePageProps { recipe: Signal::new(idx.clone()) }) RecipePage(recipe=idx.clone())
}, },
AppRoutes::Categories => view ! { AppRoutes::Categories => view! {cx,
CategoryPage() CategoryPage()
}, },
AppRoutes::NotFound => view! { AppRoutes::NotFound => view! {cx,
// TODO(Create a real one) // TODO(Create a real one)
PlanPage() PlanPage()
}, },
AppRoutes::Error(ref e) => { AppRoutes::Error(ref e) => {
let e = e.clone(); let e = e.clone();
view! { view! {cx,
"Error: " (e) "Error: " (e)
} }
} }
}) }
}
fn get_appservice() -> AppService {
AppService::new(service::HttpStore::new("/api/v1".to_owned()))
} }
#[instrument] #[instrument]
#[component(UI<G>)] #[component]
pub fn ui() -> View<G> { pub fn UI<G: Html>(cx: Scope) -> View<G> {
let app_service = get_appservice(); let app_service = AppService::new(service::HttpStore::new("/api/v1".to_owned()));
provide_context(cx, app_service.clone());
info!("Starting UI"); info!("Starting UI");
view! {
// NOTE(jwall): Set the app_service in our toplevel scope. Children will be able
// to find the service as long as they are a child of this scope.
ContextProvider(ContextProviderProps {
value: app_service.clone(),
children: || {
create_effect(move || {
spawn_local_in_scope({
let mut app_service = app_service.clone();
async move {
debug!("fetching recipes");
match app_service.fetch_recipes_from_storage() {
Ok((_, Some(recipes))) => {
app_service.set_recipes(recipes);
}
Ok((_, None)) => {
error!("No recipes to find");
}
Err(msg) => error!("Failed to get recipes {}", msg),
}
}
});
});
view! { let view = create_signal(cx, View::empty());
// FIXME(jwall): We need a way to trigger refreshes when required. Turn this
// into a create_effect with a refresh signal stored as a context.
spawn_local_scoped(cx, {
let mut app_service = crate::service::get_appservice_from_context(cx).clone();
async move {
if let Err(err) = app_service.synchronize().await {
error!(?err);
};
view.set(view! { cx,
div(class="app") { div(class="app") {
Header() Header()
Router(RouterProps { Router(RouterProps {
@ -105,8 +84,9 @@ pub fn ui() -> View<G> {
browser_integration: BrowserIntegration::new(), browser_integration: BrowserIntegration::new(),
}) })
} }
});
} }
} });
})
} view! { cx, (view.get().as_ref()) }
} }