mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Router Integration and entrypoint sycamore 0.8 conversion
This commit is contained in:
parent
481e44911f
commit
8c5093d77f
219
Cargo.lock
generated
219
Cargo.lock
generated
@ -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"
|
||||
|
@ -1,2 +1,6 @@
|
||||
[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" }
|
@ -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()
|
||||
)
|
||||
|
@ -48,9 +48,9 @@ features = [
|
||||
]
|
||||
|
||||
[dependencies.sycamore]
|
||||
version = "0.7.1"
|
||||
features = ["futures", "serde", "default"]
|
||||
version = "0.8.2"
|
||||
features = ["suspense", "serde", "default", ]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
opt-level = "s"
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
))
|
||||
|
@ -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());
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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() });
|
||||
}
|
||||
|
@ -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 { }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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 { }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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 {}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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 { } }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)]
|
||||
|
100
web/src/web.rs
100
web/src/web.rs
@ -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()) }
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user