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]]
name = "futures-channel"
version = "0.3.21"
name = "futures"
version = "0.3.24"
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 = [
"futures-core",
"futures-sink",
@ -759,15 +774,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.21"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
[[package]]
name = "futures-executor"
version = "0.3.21"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
dependencies = [
"futures-core",
"futures-task",
@ -787,9 +802,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.21"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
[[package]]
name = "futures-lite"
@ -806,6 +821,17 @@ dependencies = [
"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]]
name = "futures-rustls"
version = "0.22.2"
@ -819,25 +845,29 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.21"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
[[package]]
name = "futures-task"
version = "0.3.21"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
[[package]]
name = "futures-util"
version = "0.3.21"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@ -1009,6 +1039,15 @@ dependencies = [
"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]]
name = "http"
version = "0.2.8"
@ -1194,79 +1233,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "libc"
version = "0.2.126"
@ -1886,6 +1852,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slotmap"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.9.0"
@ -2008,12 +1983,6 @@ dependencies = [
"futures-rustls",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stringprep"
version = "0.1.2"
@ -2038,28 +2007,48 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "sycamore"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5cea65876897bb946a623e16bf3df2de4997a6872d95b99dfaed5dd8e14e264"
version = "0.8.2"
source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
dependencies = [
"ahash",
"futures",
"indexmap",
"js-sys",
"lexical",
"paste",
"smallvec",
"sycamore-core",
"sycamore-futures",
"sycamore-macro",
"sycamore-reactive",
"sycamore-web",
"wasm-bindgen",
"wasm-bindgen-futures",
"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]]
name = "sycamore-macro"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d6911dba86d928ed3c898ee182c1a8a9f00299aa78875bc9308e7fd389e5bb4"
version = "0.8.2"
source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
dependencies = [
"once_cell",
"proc-macro2",
@ -2069,14 +2058,28 @@ dependencies = [
[[package]]
name = "sycamore-reactive"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20809429d0f9c2ffcbb3f192957a5d0c505519138e41c5d38808c5b42b3c53ab"
version = "0.8.1"
source = "git+https://github.com/sycamore-rs/sycamore/?rev=20b6069c470a51d2ba6197bb322036e8324ff297#20b6069c470a51d2ba6197bb322036e8324ff297"
dependencies = [
"ahash",
"bumpalo",
"indexmap",
"serde",
"slotmap",
"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",
"web-sys",
]
@ -2409,6 +2412,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf8-width"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "uuid"
version = "1.1.2"

View File

@ -1,2 +1,6 @@
[workspace]
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(
match u {
"ms" => (cnt / 1000),
"ms" => cnt / 1000,
"s" | "sec" => cnt.into(),
"m" | "min" => (dbg!(cnt) * 60),
"h" | "hr" | "hrs" => (cnt * 60 * 60),
"m" | "min" => dbg!(cnt) * 60,
"h" | "hr" | "hrs" => cnt * 60 * 60,
_ => unreachable!(),
}.into()
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,37 +18,43 @@ use tracing::{debug, instrument};
use crate::service::get_appservice_from_context;
pub struct RecipeCheckBoxProps {
#[derive(Prop)]
pub struct RecipeCheckBoxProps<'ctx> {
pub i: String,
pub title: ReadSignal<String>,
pub title: &'ctx ReadSignal<String>,
}
#[instrument(skip(props), fields(
#[instrument(skip(props, cx), fields(
idx=%props.i,
title=%props.title.get()
))]
#[component(RecipeSelection<G>)]
pub fn recipe_selection(props: RecipeCheckBoxProps) -> View<G> {
let app_service = get_appservice_from_context();
#[component]
pub fn RecipeSelection<G: Html>(cx: Scope, props: RecipeCheckBoxProps) -> View<G> {
let mut app_service = get_appservice_from_context(cx).clone();
// This is total hack but it works around the borrow issues with
// the `view!` macro.
let id = Rc::new(props.i);
let count = Signal::new(format!(
"{}",
app_service.get_recipe_count_by_index(id.as_ref())
));
let count = create_signal(
cx,
format!(
"{}",
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 href = format!("/ui/recipe/{}", id);
let name = format!("recipe_id:{}", id);
let value = id.clone();
view! {
view! {cx,
div() {
label(for=for_id) { a(href=href) { (props.title.get()) } }
input(type="number", class="item-count-sel", min="0", bind:value=count.clone(), name=name, value=value, on:change=cloned!((id) => move |_| {
label(for=for_id) { a(href=href) { (*title) } }
input(type="number", class="item-count-sel", min="0", bind:value=count, name=name, on:change=move |_| {
let mut app_service = app_service.clone();
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
// limitations under the License.
use recipes::Recipe;
use sycamore::{futures::spawn_local_in_scope, prelude::*};
use sycamore::{futures::spawn_local_scoped, prelude::*};
use tracing::{error, instrument};
use crate::components::recipe_selection::*;
use crate::service::AppService;
use crate::service::*;
#[instrument]
#[component(RecipeSelector<G>)]
pub fn recipe_selector(app_service: AppService) -> View<G> {
let rows = create_memo(cloned!(app_service => move || {
pub fn RecipeSelector<G: Html>(cx: Scope) -> View<G> {
let app_service = get_appservice_from_context(cx).clone();
let rows = create_memo(cx, move || {
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) {
rows.push(Signal::new(Vec::from(row)));
if let (_, Some(bt)) = app_service.fetch_recipes_from_storage().unwrap() {
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
}));
let clicked = Signal::new(false);
create_effect(cloned!((clicked, app_service) => move || {
clicked.get();
spawn_local_in_scope(cloned!((app_service) => {
});
let app_service = get_appservice_from_context(cx).clone();
let clicked = create_signal(cx, false);
create_effect(cx, move || {
clicked.track();
spawn_local_scoped(cx, {
let mut app_service = app_service.clone();
async move {
if let Err(err) = app_service.refresh().await {
if let Err(err) = app_service.synchronize().await {
error!(?err);
};
}
}));
}));
view! {
});
});
view! {cx,
table(class="recipe_selector no-print") {
(View::new_fragment(
rows.get().iter().cloned().map(|r| {
view ! {
tr { Keyed(KeyedProps{
iterable: r.handle(),
template: |(i, recipe)| {
view! {
td { RecipeSelection(RecipeCheckBoxProps{i: i, title: create_memo(move || recipe.get().title.clone())}) }
view ! {cx,
tr { Keyed(
iterable=r,
view=|cx, sig| {
let title = create_memo(cx, move || sig.get().1.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()
))

View File

@ -13,131 +13,192 @@
// limitations under the License.
use std::collections::{BTreeMap, BTreeSet};
use recipes::{Ingredient, IngredientKey};
use sycamore::prelude::*;
use tracing::{debug, instrument};
use crate::service::get_appservice_from_context;
#[instrument]
#[component(ShoppingList<G>)]
pub fn shopping_list() -> View<G> {
let app_service = get_appservice_from_context();
let filtered_keys = Signal::new(BTreeSet::new());
let ingredients_map = Signal::new(BTreeMap::new());
let extras = Signal::new(Vec::<(usize, (Signal<String>, Signal<String>))>::new());
let modified_amts = Signal::new(BTreeMap::new());
let show_staples = Signal::new(true);
create_effect(
cloned!((app_service, ingredients_map, show_staples) => move || {
ingredients_map.set(app_service.get_shopping_list(*show_staples.get()));
}),
);
debug!(ingredients_map=?ingredients_map.get_untracked());
let ingredients = create_memo(cloned!((ingredients_map, filtered_keys) => move || {
let mut ingredients = Vec::new();
// This has the effect of sorting the ingredients by category
for (_, ingredients_list) in ingredients_map.get().iter() {
for (i, recipes) in ingredients_list.iter() {
if !filtered_keys.get().contains(&i.key()) {
ingredients.push((i.key(), (i.clone(), recipes.clone())));
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) }
}
}
}
}
ingredients
}));
debug!(ingredients = ?ingredients.get_untracked());
let table_view = Signal::new(View::empty());
create_effect(
cloned!((table_view, ingredients, filtered_keys, modified_amts, extras) => move || {
if (ingredients.get().len() > 0) || (extras.get().len() > 0) {
let t = view ! {
table(class="pad-top shopping-list page-breaker container-fluid", role="grid") {
tr {
th { " Quantity " }
th { " Delete " }
th { " Ingredient " }
th { " 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" }
}
}
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" }
}
}
})
})
}
)
}
}
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]
#[component]
pub fn ShoppingList<G: Html>(cx: Scope) -> View<G> {
let app_service = get_appservice_from_context(cx);
let filtered_keys: RcSignal<BTreeSet<IngredientKey>> = create_rc_signal(BTreeSet::new());
let ingredients_map = create_rc_signal(BTreeMap::new());
let extras = create_signal(
cx,
Vec::<(usize, (&Signal<String>, &Signal<String>))>::new(),
);
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());
let ingredients = create_memo(cx, {
let filtered_keys = filtered_keys.clone();
let ingredients_map = ingredients_map.clone();
move || {
let mut ingredients = Vec::new();
// This has the effect of sorting the ingredients by category
for (_, ingredients_list) in ingredients_map.get().iter() {
for (i, recipes) in ingredients_list.iter() {
if !filtered_keys.get().contains(&i.key()) {
ingredients.push((i.key(), (i.clone(), recipes.clone())));
}
};
table_view.set(t);
}
}
ingredients
}
});
let table_view = create_signal(cx, View::empty());
create_effect(cx, {
let filtered_keys = filtered_keys.clone();
move || {
if (ingredients.get().len() > 0) || (extras.get().len() > 0) {
table_view.set(make_shopping_table(
cx,
ingredients,
modified_amts.clone(),
extras.clone(),
filtered_keys.clone(),
));
} else {
table_view.set(View::empty());
}
}),
);
view! {
}
});
view! {cx,
h1 { "Shopping List " }
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())
input(type="button", value="Add Item", class="no-print", on:click=cloned!((extras) => move |_| {
let mut cloned_extras: Vec<(Signal<String>, Signal<String>)> = (*extras.get()).iter().map(|(_, v)| v.clone()).collect();
cloned_extras.push((Signal::new("".to_owned()), Signal::new("".to_owned())));
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(|(_, tpl)| *tpl).collect();
cloned_extras.push((create_signal(cx, "".to_owned()), create_signal(cx, "".to_owned())));
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.
ingredients_map.set(app_service.get_shopping_list(*show_staples.get()));
// clear the filter_signal
filtered_keys.set(BTreeSet::new());
modified_amts.set(BTreeMap::new());
extras.set(Vec::new());
}))
})
}
}

View File

@ -13,15 +13,17 @@
// limitations under the License.
use sycamore::prelude::*;
#[derive(Clone)]
use super::Header;
#[derive(Clone, Prop)]
pub struct TabState<G: GenericNode> {
pub inner: View<G>,
}
#[component(TabbedView<G>)]
pub fn tabbed_view(state: TabState<G>) -> View<G> {
cloned!((state) => view! {
header(class="no-print") {
#[component]
pub fn TabbedView<G: Html>(cx: Scope, state: TabState<G>) -> View<G> {
view! {cx,
Header { }
nav {
ul {
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" } }
}
}
}
main(class=".conatiner-fluid") {
(state.inner)
}
})
}
}

View File

@ -33,5 +33,5 @@ pub fn main() {
// TODO(jwall): use the tracing_subscriber_browser default setup function when it exists.
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;
#[instrument]
#[component(CategoryPage<G>)]
pub fn category_page() -> View<G> {
view! {
#[component()]
pub fn CategoryPage<G: Html>(cx: Scope) -> View<G> {
view! {cx,
TabbedView(TabState {
inner: view! {
Categories()
inner: view! {cx,
Categories { }
}
})
}

View File

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

View File

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

View File

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

View File

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

View File

@ -24,12 +24,12 @@ use web_sys::{Element, HtmlAnchorElement};
use crate::app_state::AppRoutes;
#[derive(Clone, Debug)]
pub struct BrowserIntegration(Signal<(String, String, String)>);
pub struct BrowserIntegration(RcSignal<(String, String, String)>);
impl BrowserIntegration {
pub fn new() -> Self {
let location = web_sys::window().unwrap_throw().location();
Self(Signal::new((
Self(create_rc_signal((
location.origin().unwrap_or(String::new()),
location.pathname().unwrap_or(String::new()),
location.hash().unwrap_or(String::new()),
@ -102,7 +102,7 @@ pub struct RouterProps<R, F, G>
where
G: GenericNode,
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_select: F,
@ -114,56 +114,62 @@ where
pathn=props.browser_integration.0.get().1,
hash=props.browser_integration.0.get().2),
skip(props))]
#[component(Router<G>)]
pub fn router<R, F>(props: RouterProps<R, F, G>) -> View<G>
#[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(ReadSignal<R>) -> View<G> + '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 = Signal::new(View::empty());
create_effect(
cloned!((view_signal, integration, route_select) => move || {
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()(Signal::new(route).handle());
register_click_handler(&view, integration.clone());
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(cloned!((path_signal) => move || {
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()));
})));
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! {
view! {cx,
(view_signal.get().as_ref())
}
}
#[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
G: GenericNode<EventType = Event>,
{
debug!("Registring click handler on node(s)");
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() {
debug!(fragment=?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() {
debug!(dynamic_node=?dyn_node);

View File

@ -16,7 +16,7 @@ use std::collections::{BTreeMap, BTreeSet};
use reqwasm;
//use serde::{Deserialize, Serialize};
use serde_json::{from_str, to_string};
use sycamore::{context::use_context, prelude::*};
use sycamore::prelude::*;
use tracing::{debug, error, info, instrument, warn};
use web_sys::Storage;
@ -25,26 +25,27 @@ use recipes::{parse, Ingredient, IngredientAccumulator, Recipe};
use crate::js_lib;
pub fn get_appservice_from_context() -> AppService {
use_context::<AppService>()
pub fn get_appservice_from_context(cx: Scope) -> &AppService {
use_context::<AppService>(cx)
}
// TODO(jwall): We should not be cloning this.
#[derive(Clone, Debug)]
pub struct AppService {
recipes: Signal<BTreeMap<String, Signal<Recipe>>>,
staples: Signal<Option<Recipe>>,
category_map: Signal<BTreeMap<String, String>>,
menu_list: Signal<BTreeMap<String, usize>>,
recipe_counts: RcSignal<BTreeMap<String, usize>>,
staples: RcSignal<Option<Recipe>>,
recipes: RcSignal<BTreeMap<String, Recipe>>,
category_map: RcSignal<BTreeMap<String, String>>,
store: HttpStore,
}
impl AppService {
pub fn new(store: HttpStore) -> Self {
Self {
recipes: Signal::new(BTreeMap::new()),
staples: Signal::new(None),
category_map: Signal::new(BTreeMap::new()),
menu_list: Signal::new(BTreeMap::new()),
recipe_counts: create_rc_signal(BTreeMap::new()),
staples: create_rc_signal(None),
recipes: create_rc_signal(BTreeMap::new()),
category_map: create_rc_signal(BTreeMap::new()),
store: store,
}
}
@ -53,9 +54,18 @@ impl AppService {
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))]
async fn synchronize(&self) -> Result<(), String> {
pub async fn synchronize(&mut self) -> Result<(), String> {
info!("Synchronizing Recipes");
// TODO(jwall): Make our caching logic using storage more robust.
let storage = self.get_storage()?.unwrap();
let recipes = self
.store
@ -68,6 +78,19 @@ impl AppService {
&(to_string(&recipes).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");
match self.store.get_categories().await {
Ok(Some(categories_content)) => {
@ -86,6 +109,54 @@ impl AppService {
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> {
let storage = self.get_storage()?.unwrap();
storage
@ -175,86 +246,6 @@ impl AppService {
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> {
self.store.save_recipes(recipes).await?;
Ok(())
@ -264,19 +255,6 @@ impl AppService {
self.store.save_categories(categories).await?;
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)]

View File

@ -18,95 +18,75 @@ use crate::{
router_integration::*,
service::{self, AppService},
};
use tracing::{debug, error, info, instrument};
use tracing::{error, info, instrument};
use sycamore::{
context::{ContextProvider, ContextProviderProps},
futures::spawn_local_in_scope,
prelude::*,
};
use sycamore::{futures::spawn_local_scoped, prelude::*};
#[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
// this are somewhat unclear and underdocumented for Sycamore. But basically
// avoid conditionals in the `view!` macro calls here.
cloned!((route) => match route.get().as_ref() {
AppRoutes::Plan => view! {
match route.get().as_ref() {
AppRoutes::Plan => view! {cx,
PlanPage()
},
AppRoutes::Inventory => view! {
AppRoutes::Inventory => view! {cx,
InventoryPage()
},
AppRoutes::Login => view! {
AppRoutes::Login => view! {cx,
LoginPage()
},
AppRoutes::Cook => view! {
AppRoutes::Cook => view! {cx,
CookPage()
},
AppRoutes::Recipe(idx) => view! {
RecipePage(RecipePageProps { recipe: Signal::new(idx.clone()) })
AppRoutes::Recipe(idx) => view! {cx,
RecipePage(recipe=idx.clone())
},
AppRoutes::Categories => view ! {
AppRoutes::Categories => view! {cx,
CategoryPage()
},
AppRoutes::NotFound => view! {
AppRoutes::NotFound => view! {cx,
// TODO(Create a real one)
PlanPage()
},
AppRoutes::Error(ref e) => {
let e = e.clone();
view! {
view! {cx,
"Error: " (e)
}
}
})
}
fn get_appservice() -> AppService {
AppService::new(service::HttpStore::new("/api/v1".to_owned()))
}
}
#[instrument]
#[component(UI<G>)]
pub fn ui() -> View<G> {
let app_service = get_appservice();
#[component]
pub fn UI<G: Html>(cx: Scope) -> View<G> {
let app_service = AppService::new(service::HttpStore::new("/api/v1".to_owned()));
provide_context(cx, app_service.clone());
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! {
div(class="app") {
Header()
Router(RouterProps {
route: AppRoutes::Plan,
route_select: route_switch,
browser_integration: BrowserIntegration::new(),
})
}
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") {
Header()
Router(RouterProps {
route: AppRoutes::Plan,
route_select: route_switch,
browser_integration: BrowserIntegration::new(),
})
}
}
})
}
});
}
});
view! { cx, (view.get().as_ref()) }
}