diff --git a/Cargo.lock b/Cargo.lock index 19eeba8..a442793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,7 +224,7 @@ checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -383,7 +383,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 1.0.107", ] [[package]] @@ -750,7 +750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -777,7 +777,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.107", ] [[package]] @@ -794,7 +794,7 @@ checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -983,7 +983,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1056,14 +1056,15 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gloo-net" -version = "0.1.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2899cb1a13be9020b010967adc6b2a8a343b6f1428b90238c9d53ca24decc6db" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" dependencies = [ "futures-channel", "futures-core", "futures-sink", "gloo-utils", + "http", "js-sys", "pin-project", "serde", @@ -1088,9 +1089,9 @@ dependencies = [ [[package]] name = "gloo-utils" -version = "0.1.6" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" dependencies = [ "js-sys", "serde", @@ -1233,9 +1234,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1448,10 +1449,10 @@ dependencies = [ "base64 0.21.0", "chrono", "console_error_panic_hook", + "gloo-net", "js-sys", "maud", "recipes", - "reqwasm", "serde", "serde_json", "sycamore", @@ -1602,7 +1603,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1658,7 +1659,7 @@ checksum = "731f8ecebd9f3a4aa847dfe75455e4757a45da40a7793d2f0b1f9b6ed18b23f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1923,7 +1924,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1989,7 +1990,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "version_check", ] @@ -2006,9 +2007,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -2044,9 +2045,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2133,15 +2134,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "reqwasm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b89870d729c501fa7a68c43bf4d938bbb3a8c156d333d90faa0e8b3e3212fb" -dependencies = [ - "gloo-net", -] - [[package]] name = "ring" version = "0.16.20" @@ -2186,7 +2178,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 1.0.107", "walkdir", ] @@ -2311,7 +2303,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2525,7 +2517,7 @@ dependencies = [ "sha2 0.10.6", "sqlx-core", "sqlx-rt", - "syn", + "syn 1.0.107", "url", ] @@ -2618,7 +2610,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2653,7 +2645,7 @@ dependencies = [ "nom", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "unicode-xid", ] @@ -2694,6 +2686,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "0.1.1" @@ -2732,7 +2735,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2823,7 +2826,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2938,7 +2941,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -3158,28 +3161,26 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if 1.0.0", - "serde", - "serde_json", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -3197,9 +3198,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3207,22 +3208,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasm-bindgen-test" @@ -3269,7 +3270,7 @@ dependencies = [ "proc-macro2", "quote", "str_inflector", - "syn", + "syn 1.0.107", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 33fc471..4ba40ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "recipes", "kitchen", "web", "api" ] +resolver = "2" [patch.crates-io] # TODO(jwall): When the fix for RcSignal Binding is released we can drop this patch. diff --git a/Makefile b/Makefile index cdddd93..41ef01d 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,11 @@ mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) mkfile_dir := $(dir $(mkfile_path)) sqlite_url := sqlite://$(mkfile_dir)/.session_store/store.db +out := dist +project := kitchen + +export out +export kitchen kitchen: wasm kitchen/src/*.rs cd kitchen; cargo build @@ -27,15 +32,18 @@ static-prep: web/index.html web/favicon.ico web/static/*.css cp -r web/favicon.ico web/dist/ cp -r web/static web/dist/ -wasmrelease: wasmrelease-dist static-prep +wasmrelease: wasm-opt static-prep + +wasm-opt: wasmrelease-dist + cd web; sh ../scripts/wasm-opt.sh release wasmrelease-dist: web/src/*.rs web/src/components/*.rs - cd web; wasm-pack build --mode no-install --release --target web --no-typescript --out-name kitchen_wasm --out-dir dist/ + cd web; sh ../scripts/wasm-build.sh release wasm: wasm-dist static-prep wasm-dist: web/src/*.rs web/src/components/*.rs - cd web; wasm-pack build --mode no-install --target web --no-typescript --out-dir dist/ --features debug_logs + cd web; sh ../scripts/wasm-build.sh debug clean: rm -rf web/dist/* @@ -50,5 +58,5 @@ sqlx-add-%: sqlx-revert: cd kitchen; cargo sqlx migrate revert --database-url $(sqlite_url) -sqlx-prepare: +sqlx-prepare: kitchen cd kitchen; cargo sqlx prepare --database-url $(sqlite_url) diff --git a/flake.lock b/flake.lock index a36dfa2..9a80f3f 100644 --- a/flake.lock +++ b/flake.lock @@ -99,11 +99,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1679174867, - "narHash": "sha256-fFxb8wN3bjOMvHPr63Iyzo3cuHhQzWW03UkckfTeBWU=", + "lastModified": 1719152388, + "narHash": "sha256-pHg0nzAa2ZM+zFamfsY7ZvVaB19pMr5wl4G5nO0J7eU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f5ec87b82832736f1624874fd34eb60c0b68bdd6", + "rev": "be54c7d931a68ba6a79f097ce979288e90a74288", "type": "github" }, "original": { @@ -124,17 +124,16 @@ }, "rust-overlay": { "inputs": { - "flake-utils": "flake-utils_2", "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1702001829, - "narHash": "sha256-6gEVidNVqzTb06zIy2Gxhz9m6/jXyAgViRxfgEpZkQ8=", + "lastModified": 1718681902, + "narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c2a1dd067a928624c1aab36f976758c0722c79bd", + "rev": "16c8ad83297c278eebe740dea5491c1708960dd1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f01beae..8884f75 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,7 @@ let overlays = [ rust-overlay.overlays.default ]; pkgs = import nixpkgs { inherit system overlays; }; - rust-wasm = pkgs.rust-bin.stable."1.74.1".default.override { + rust-wasm = pkgs.rust-bin.stable."1.77.0".default.override { extensions = [ "rust-src" ]; # Add wasm32 as an extra target besides the native target. targets = [ "wasm32-unknown-unknown" ]; @@ -45,6 +45,14 @@ wasm-bindgen = pkgs.callPackage wasm-bindgenGen { inherit pkgs; }; kitchenWasm = kitchenWasmGen { inherit pkgs rust-wasm wasm-bindgen version; + lockFile = ./Cargo.lock; + outputHashes = { + # I'm maintaining some patches for these so the lockfile hashes are a little + # incorrect. We override those here. + "wasm-web-component-0.2.0" = "sha256-quuPgzGb2F96blHmD3BAUjsWQYbSyJGZl27PVrwL92k="; + "sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ="; + "sqlx-0.6.2" = "sha256-X/LFvtzRfiOIEZJiVzmFvvULPpjhqvI99pSwH7a//GM="; + }; }; kitchen = (kitchenGen { inherit pkgs version naersk-lib kitchenWasm rust-wasm; diff --git a/kitchen/build.rs b/kitchen/build.rs index 7609593..d506869 100644 --- a/kitchen/build.rs +++ b/kitchen/build.rs @@ -2,4 +2,4 @@ fn main() { // trigger recompilation when a new migration is added println!("cargo:rerun-if-changed=migrations"); -} \ No newline at end of file +} diff --git a/kitchen/migrations/20240701002811_recipe-servings.down.sql b/kitchen/migrations/20240701002811_recipe-servings.down.sql new file mode 100644 index 0000000..eea316a --- /dev/null +++ b/kitchen/migrations/20240701002811_recipe-servings.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER TABLE recipes DROP COLUMN serving_count; diff --git a/kitchen/migrations/20240701002811_recipe-servings.up.sql b/kitchen/migrations/20240701002811_recipe-servings.up.sql new file mode 100644 index 0000000..e1e3920 --- /dev/null +++ b/kitchen/migrations/20240701002811_recipe-servings.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE recipes ADD column serving_count number; diff --git a/kitchen/src/web/mod.rs b/kitchen/src/web/mod.rs index 09d1b03..c0ec8d0 100644 --- a/kitchen/src/web/mod.rs +++ b/kitchen/src/web/mod.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; // Copyright 2022 Jeremy Wall // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +11,7 @@ use std::collections::BTreeMap; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::BTreeMap; use std::path::PathBuf; use std::sync::Arc; use std::{collections::BTreeSet, net::SocketAddr}; diff --git a/models/browser_state.als b/models/browser_state.als new file mode 100644 index 0000000..2adf814 --- /dev/null +++ b/models/browser_state.als @@ -0,0 +1,51 @@ +sig Id {} +sig Text {} + +sig Recipe { + , id: one Id + , text: one Text +} + +fact { + no r1, r2: Recipe | (r1.id = r2.id) and (r1.text != r2.text) + no r1, r2: Recipe | (r1 != r2) and (r1.id = r2.id) +} + +sig Ingredient {} +sig Modifier {} +sig Amt {} + +sig ModifiedInventory { + , ingredient: one Ingredient + , modifier: lone Modifier + , amt: one Amt +} + +fact { + no mi1, mi2: ModifiedInventory | mi1 != mi2 && (mi1.ingredient = mi2.ingredient) and (mi1.modifier = mi2.modifier) +} + +sig DeletedInventory { + , ingredient: one Ingredient + , modifier: lone Modifier +} + +fact { + no mi1, mi2: DeletedInventory | mi1 != mi2 && (mi1.ingredient = mi2.ingredient) and (mi1.modifier = mi2.modifier) +} + +sig ExtraItems { + , ingredient: one Ingredient + , amt: one Amt +} + +sig State { + , recipes: some Recipe + , modified: set ModifiedInventory + , deleted: set DeletedInventory + , extras: set ExtraItems +} { + no rs: Recipe | rs not in recipes +} + +run { } for 3 but exactly 2 State, 2 Modifier, exactly 3 ModifiedInventory, exactly 9 Ingredient diff --git a/models/planning.d2 b/models/planning.d2 new file mode 100644 index 0000000..04dafdf --- /dev/null +++ b/models/planning.d2 @@ -0,0 +1,17 @@ +Meal Planning: { + shape: sequence_diagram + user: Cook; client: Kitchen frontend; kitchen: Kitchen backend + + user -> client: Start new meal Plan + client -> kitchen: new plan created + user -> client: Add recipe to meal plan + client -> kitchen: Update meal plan with recipe + client -> client: cache updated meal plan + user -> client: Do inventory + client -> kitchen: Store inventory mutations + client -> client: cache inventory mutations + user -> client: Undo mutation + client -> kitchen: Store inventory mutations + client -> client: cache inventory mutations + user -> user: Cook recipes +} diff --git a/models/planning.svg b/models/planning.svg new file mode 100644 index 0000000..b9984ec --- /dev/null +++ b/models/planning.svg @@ -0,0 +1,125 @@ + + + + + + + + +Meal PlanningCookKitchen frontendKitchen backend Start new meal Plan new plan created Add recipe to meal plan Update meal plan with recipe cache updated meal plan Do inventory Store inventory mutations cache inventory mutations Undo mutation Store inventory mutations cache inventory mutations Cook recipes + + + + + + + + + + + + + + + + + + diff --git a/nix/kitchenWasm/default.nix b/nix/kitchenWasm/default.nix index 500712b..7d7c00b 100644 --- a/nix/kitchenWasm/default.nix +++ b/nix/kitchenWasm/default.nix @@ -3,23 +3,18 @@ features ? "", rust-wasm, wasm-bindgen, + lockFile, + outputHashes, }: with pkgs; let pname = "kitchen-wasm"; src = ./../..; - lockFile = ./../../Cargo.lock; # NOTE(jwall): Because we use wasm-pack directly below we need # the cargo dependencies to already be installed. - cargoDeps = (pkgs.rustPlatform.importCargoLock { inherit lockFile; outputHashes = { - # I'm maintaining some patches for these so the lockfile hashes are a little - # incorrect. We override those here. - "wasm-web-component-0.2.0" = "sha256-quuPgzGb2F96blHmD3BAUjsWQYbSyJGZl27PVrwL92k="; - "sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ="; - "sqlx-0.6.2" = "sha256-X/LFvtzRfiOIEZJiVzmFvvULPpjhqvI99pSwH7a//GM="; - }; - }); + cargoDeps = (pkgs.rustPlatform.importCargoLock { inherit lockFile outputHashes; }); in +# TODO(zaphar): I should actually be leveraging naersklib.buildPackage with a postInstall for the optimization and bindgen stdenv.mkDerivation { inherit src pname; version = version; @@ -34,12 +29,19 @@ stdenv.mkDerivation { ''; # TODO(jwall): Build this from the root rather than the src. buildPhase = '' - echo building with wasm-pack mkdir -p $out cd web cp -r static $out - RUST_LOG=info wasm-pack build --mode no-install --release --target web --out-dir $out ${features}; + sh ../scripts/wasm-build.sh release + #cargo build --lib --release --target wasm32-unknown-unknown --target-dir $out ${features} --offline + #wasm-bindgen $out/wasm32-unknown-unknown/release/kitchen_wasm.wasm --out-dir $out --typescript --target web + #sh ../scripts/wasm-opt.sh release + wasm-opt $out/kitchen_wasm_bg.wasm -o $out/kitchen_wasm_bg-opt.wasm -O + rm -f $out/kitchen_wasm_bg.wasm + mv $out/kitchen_wasm_bg-opt.wasm $out/kitchen_wasm_bg.wasm cp -r index.html $out cp -r favicon.ico $out + rm -rf $out/release + rm -rf $out/wasm32-unknown-unknown ''; } diff --git a/nix/wasm-bindgen/default.nix b/nix/wasm-bindgen/default.nix index f9f8d19..14b4d18 100644 --- a/nix/wasm-bindgen/default.nix +++ b/nix/wasm-bindgen/default.nix @@ -8,9 +8,7 @@ in , nodejs , pkg-config , openssl -, stdenv , curl -, runCommand }: # This package is special so we don't use the naersk infrastructure to build it. @@ -20,14 +18,14 @@ rustPlatform.buildRustPackage rec { pname = "wasm-bindgen-cli"; # NOTE(jwall): This must exactly match the version of the wasm-bindgen crate # we are using. - version = "0.2.84"; + version = "0.2.89"; src = fetchCrate { inherit pname version; - sha256 = "sha256-0rK+Yx4/Jy44Fw5VwJ3tG243ZsyOIBBehYU54XP/JGk="; + sha256 = "sha256-IPxP68xtNSpwJjV2yNMeepAS0anzGl02hYlSTvPocz8="; }; - cargoSha256 = "sha256-vcpxcRlW1OKoD64owFF6mkxSqmNrvY+y3Ckn5UwEQ50="; + cargoSha256 = "sha256-pBeQaG6i65uJrJptZQLuIaCb/WCQMhba1Z1OhYqA8Zc="; nativeBuildInputs = [ pkg-config ]; @@ -36,5 +34,5 @@ rustPlatform.buildRustPackage rec { nativeCheckInputs = [ nodejs ]; # other tests require it to be ran in the wasm-bindgen monorepo - cargoTestFlags = [ "--test=interface-types" ]; -} \ No newline at end of file + cargoTestFlags = [ "--test=reference" ]; +} diff --git a/readme.md b/readme.md index f4d92c9..8edd299 100644 --- a/readme.md +++ b/readme.md @@ -6,10 +6,6 @@ A web assembly experiment in Meal Planning and Shopping List management. Ensure you have rust installed with support for the web assembly target. You can see instructions here: [Rust wasm book](https://rustwasm.github.io/docs/book/game-of-life/setup.html). -You will also want to have trunk installed. You can see instructions for that here: [trunk](https://trunkrs.dev/) - -Then obtain the source. We do not at this time publish kitchen on [crates.io](https://crates.io/). - ```sh git clone https://github.com/zaphar/kitchen cd kitchen @@ -23,7 +19,7 @@ make release # Hacking on kitchen -If you want to hack on kitchen, then you may find it useful to use trunk in dev mode. The run script will run build the app and run trunk with it watching for changes and reloading on demand in your browser. +The run script will run build the app and run it for you. ```sh ./run.sh @@ -37,4 +33,4 @@ If all of the above looks like too much work, and you already use the nix packag ```sh nix run github:zaphar/kitchen -``` \ No newline at end of file +``` diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index 416b5dd..c1e9aed 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -156,16 +156,28 @@ impl IngredientAccumulator { set.insert(recipe_title.clone()); self.inner.insert(key, (i.clone(), set)); } else { - let amt = match (self.inner[&key].0.amt, i.amt) { - (Volume(rvm), Volume(lvm)) => Volume(lvm + rvm), - (Count(lqty), Count(rqty)) => Count(lqty + rqty), - (Weight(lqty), Weight(rqty)) => Weight(lqty + rqty), + let amts = match (&self.inner[&key].0.amt, &i.amt) { + (Volume(rvm), Volume(lvm)) => vec![Volume(lvm + rvm)], + (Count(lqty), Count(rqty)) => vec![Count(lqty + rqty)], + (Weight(lqty), Weight(rqty)) => vec![Weight(lqty + rqty)], + (Package(lnm, lqty), Package(rnm, rqty)) => { + if lnm == rnm { + vec![Package(lnm.clone(), lqty + rqty)] + } else { + vec![ + Package(lnm.clone(), lqty.clone()), + Package(rnm.clone(), rqty.clone()), + ] + } + } _ => unreachable!(), }; - self.inner.get_mut(&key).map(|(i, set)| { - i.amt = amt; - set.insert(recipe_title.clone()); - }); + for amt in amts { + self.inner.get_mut(&key).map(|(i, set)| { + i.amt = amt; + set.insert(recipe_title.clone()); + }); + } } } } diff --git a/recipes/src/parse.rs b/recipes/src/parse.rs index bf2e30e..9ee2714 100644 --- a/recipes/src/parse.rs +++ b/recipes/src/parse.rs @@ -334,7 +334,14 @@ make_fn!(unit, text_token!("kg"), text_token!("grams"), text_token!("gram"), - text_token!("g")), + text_token!("g"), + text_token!("pkg"), + text_token!("package"), + text_token!("bottle"), + text_token!("bot"), + text_token!("bag"), + text_token!("can") + ), _ => ws, (u.to_lowercase().to_singular()) ) @@ -393,6 +400,7 @@ pub fn measure(i: StrIter) -> abortable_parser::Result { "oz" => Weight(Oz(qty)), "kg" | "kilogram" => Weight(Kilogram(qty)), "g" | "gram" => Weight(Gram(qty)), + "pkg" | "package" | "can" | "bag" | "bottle" | "bot" => Measure::pkg(s, qty), _u => { eprintln!("Invalid unit: {}", _u); unreachable!() @@ -418,9 +426,8 @@ pub fn normalize_name(name: &str) -> String { // NOTE(jwall): The below unwrap is safe because of the length // check above. let last = parts.last().unwrap(); - let normalized = last.to_singular(); prefix.push(' '); - prefix.push_str(&normalized); + prefix.push_str(&last.to_string()); return prefix; } return name.trim().to_lowercase().to_owned(); diff --git a/recipes/src/test.rs b/recipes/src/test.rs index ff5ebd6..8d89421 100644 --- a/recipes/src/test.rs +++ b/recipes/src/test.rs @@ -235,32 +235,30 @@ fn test_ingredient_name_parse() { #[test] fn test_ingredient_parse() { for (i, expected) in vec![ - //( - // "1 cup flour ", - // Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), - //), - //( - // "\t1 cup flour ", - // Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), - //), - //( - // "1 cup apple (chopped)", - // Ingredient::new( - // "apple", - // Some("chopped".to_owned()), - // Volume(Cup(Quantity::Whole(1))), - // "", - // ), - //), - //( - // "1 cup apple (chopped) ", - // Ingredient::new( - // "apple", - // Some("chopped".to_owned()), - // Volume(Cup(Quantity::Whole(1))), - // "", - // ), - //), + ( + "1 cup flour ", + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))), + ), + ( + "\t1 cup flour ", + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))), + ), + ( + "1 cup apple (chopped)", + Ingredient::new( + "apple", + Some("chopped".to_owned()), + Volume(Cup(Quantity::Whole(1))), + ), + ), + ( + "1 cup apple (chopped) ", + Ingredient::new( + "apple", + Some("chopped".to_owned()), + Volume(Cup(Quantity::Whole(1))), + ), + ), ( "1 green bell pepper (chopped) ", Ingredient::new( @@ -269,6 +267,46 @@ fn test_ingredient_parse() { Count(Quantity::Whole(1)), ), ), + ( + "1 pkg green onion", + Ingredient::new( + "green onion", + None, + Package("pkg".into(), Quantity::Whole(1)), + ), + ), + ( + "1 bottle green onion", + Ingredient::new( + "green onion", + None, + Package("bottle".into(), Quantity::Whole(1)), + ), + ), + ( + "1 bot green onion", + Ingredient::new( + "green onion", + None, + Package("bot".into(), Quantity::Whole(1)), + ), + ), + ( + "1 bag green onion", + Ingredient::new( + "green onion", + None, + Package("bag".into(), Quantity::Whole(1)), + ), + ), + ( + "1 can baked beans", + Ingredient::new( + "baked beans", + None, + Package("can".into(), Quantity::Whole(1)), + ), + ), ] { match parse::ingredient(StrIter::new(i)) { ParseResult::Complete(_, ing) => assert_eq!(ing, expected), diff --git a/recipes/src/unit.rs b/recipes/src/unit.rs index f31333a..ea60379 100644 --- a/recipes/src/unit.rs +++ b/recipes/src/unit.rs @@ -22,6 +22,7 @@ use std::{ convert::TryFrom, fmt::Display, ops::{Add, Div, Mul, Sub}, + rc::Rc, }; use num_rational::Ratio; @@ -179,6 +180,20 @@ impl VolumeMeasure { macro_rules! volume_op { ($trait:ident, $method:ident) => { + impl $trait for &VolumeMeasure { + type Output = VolumeMeasure; + + fn $method(self, lhs: Self) -> Self::Output { + let (l, r) = (self.get_ml(), lhs.get_ml()); + let result = ML($trait::$method(l, r)); + if self.metric() { + result.normalize() + } else { + result.into_tsp().normalize() + } + } + } + impl $trait for VolumeMeasure { type Output = Self; @@ -293,6 +308,20 @@ impl WeightMeasure { macro_rules! weight_op { ($trait:ident, $method:ident) => { + impl $trait for &WeightMeasure { + type Output = WeightMeasure; + + fn $method(self, lhs: Self) -> Self::Output { + let (l, r) = (self.get_grams(), lhs.get_grams()); + let result = WeightMeasure::Gram($trait::$method(l, r)); + if self.metric() { + result.normalize() + } else { + result.into_oz().normalize() + } + } + } + impl $trait for WeightMeasure { type Output = Self; @@ -335,18 +364,19 @@ impl Display for WeightMeasure { use WeightMeasure::{Gram, Kilogram, Oz, Pound}; -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] +#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] /// Measurements in a Recipe with associated units for them. pub enum Measure { /// Volume measurements as meter cubed base unit Volume(VolumeMeasure), /// Simple count of items Count(Quantity), + Package(Rc, Quantity), /// Weight measure as Grams base unit Weight(WeightMeasure), } -use Measure::{Count, Volume, Weight}; +use Measure::{Count, Package, Volume, Weight}; impl Measure { pub fn tsp(qty: Quantity) -> Self { @@ -407,11 +437,16 @@ impl Measure { Weight(Oz(qty)) } + pub fn pkg>>(name: S, qty: Quantity) -> Self { + Package(name.into(), qty) + } + pub fn measure_type(&self) -> String { match self { Volume(_) => "Volume", Count(_) => "Count", Weight(_) => "Weight", + Package(_, _) => "Package", } .to_owned() } @@ -421,6 +456,7 @@ impl Measure { Volume(vm) => vm.plural(), Count(qty) => qty.plural(), Weight(wm) => wm.plural(), + Package(_, qty) => qty.plural(), } } @@ -429,6 +465,7 @@ impl Measure { Volume(vm) => Volume(vm.normalize()), Count(qty) => Count(qty.clone()), Weight(wm) => Weight(wm.normalize()), + Package(nm, qty) => Package(nm.clone(), qty.clone()), } } } @@ -439,6 +476,7 @@ impl Display for Measure { Volume(vm) => write!(w, "{}", vm), Count(qty) => write!(w, "{}", qty), Weight(wm) => write!(w, "{}", wm), + Package(nm, qty) => write!(w, "{} {}", qty, nm), } } } @@ -533,6 +571,26 @@ impl TryFrom for Quantity { macro_rules! quantity_op { ($trait:ident, $method:ident) => { + impl $trait for &Quantity { + type Output = Quantity; + + fn $method(self, lhs: Self) -> Self::Output { + match (self, lhs) { + (Whole(rhs), Whole(lhs)) => Frac($trait::$method( + Ratio::from_integer(*rhs), + Ratio::from_integer(*lhs), + )), + (Frac(rhs), Frac(lhs)) => Frac($trait::$method(rhs, lhs)), + (Whole(rhs), Frac(lhs)) => { + Frac($trait::$method(Ratio::from_integer(*rhs), lhs)) + } + (Frac(rhs), Whole(lhs)) => { + Frac($trait::$method(rhs, Ratio::from_integer(*lhs))) + } + } + } + } + impl $trait for Quantity { type Output = Self; diff --git a/scripts/wasm-build.sh b/scripts/wasm-build.sh new file mode 100644 index 0000000..d92053d --- /dev/null +++ b/scripts/wasm-build.sh @@ -0,0 +1,9 @@ +set -x +buildtype=$1; + +if [ ${buildtype} = "release" ]; then + buildtype_flag="--release" +fi + +cargo build --lib ${buildtype_flag} --target wasm32-unknown-unknown --target-dir $out --features debug_logs +wasm-bindgen $out/wasm32-unknown-unknown/${buildtype}/kitchen_wasm.wasm --out-dir $out --typescript --target web diff --git a/scripts/wasm-opt.sh b/scripts/wasm-opt.sh new file mode 100644 index 0000000..f983110 --- /dev/null +++ b/scripts/wasm-opt.sh @@ -0,0 +1,6 @@ +set -x +buildtype=$1; + +wasm-opt $out/wasm32-unknown-unkown/${buildtype}/${project}_wasm.wasm --out-dir dist/ -O +rm -f $out/${project}_wasm_bg.wasm +mv $out/${project}_wasm_bg-opt.wasm dist/${project}_wasm_bg.wasm diff --git a/web/Cargo.toml b/web/Cargo.toml index 45bfb4a..6244a10 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -43,12 +43,12 @@ features = ["fmt", "time"] version = "0.4.22" features = ["serde"] -[dependencies.reqwasm] -version = "0.5.0" +[dependencies.gloo-net] +version = "0.4.0" [dependencies.wasm-bindgen] # we need wasm-bindgen v0.2.84 exactly -version = "= 0.2.84" +version = "= 0.2.89" [dependencies.web-sys] version = "0.3" @@ -56,6 +56,7 @@ features = [ "Event", "InputEvent", "CustomEvent", + "CustomEventInit", "EventTarget", "History", "HtmlAnchorElement", diff --git a/web/index.html b/web/index.html index 19de9be..30e81d5 100644 --- a/web/index.html +++ b/web/index.html @@ -19,7 +19,7 @@ - + @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/web/src/api.rs b/web/src/api.rs index a4a90db..dc6fe36 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -15,7 +15,7 @@ use std::collections::{BTreeMap, BTreeSet}; use base64::{self, Engine}; use chrono::NaiveDate; -use reqwasm; +use gloo_net; use serde_json::{from_str, to_string}; use sycamore::prelude::*; use tracing::{debug, error, instrument}; @@ -25,7 +25,10 @@ use recipes::{IngredientKey, RecipeEntry}; use wasm_bindgen::JsValue; use web_sys::Storage; -use crate::{app_state::{AppState, parse_recipes}, js_lib}; +use crate::{ + app_state::{parse_recipes, AppState}, + js_lib, +}; #[derive(Debug)] pub struct Error(String); @@ -66,8 +69,8 @@ impl From for Error { } } -impl From for Error { - fn from(item: reqwasm::Error) -> Self { +impl From for Error { + fn from(item: gloo_net::Error) -> Self { Error(format!("{:?}", item)) } } @@ -94,8 +97,15 @@ impl LocalStore { pub fn store_app_state(&self, state: &AppState) { self.migrate_local_store(); + let state = match to_string(state) { + Ok(state) => state, + Err(err) => { + error!(?err, ?state, "Error deserializing app_state"); + return; + } + }; self.store - .set("app_state", &to_string(state).unwrap()) + .set("app_state", &state) .expect("Failed to set our app state"); } @@ -104,7 +114,8 @@ impl LocalStore { self.store.get("app_state").map_or(None, |val| { val.map(|s| { debug!("Found an app_state object"); - let mut app_state: AppState = from_str(&s).expect("Failed to deserialize app state"); + let mut app_state: AppState = + from_str(&s).expect("Failed to deserialize app state"); let recipes = parse_recipes(&self.get_recipes()).expect("Failed to parse recipes"); if let Some(recipes) = recipes { debug!("Populating recipes"); @@ -153,12 +164,12 @@ impl LocalStore { } fn migrate_local_store(&self) { - for k in self.get_storage_keys() - .into_iter() - .filter(|k| k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples") { - // Deleting old local store key - debug!("Deleting old local store key {}", k); - self.store.delete(&k).expect("Failed to delete storage key"); + for k in self.get_storage_keys().into_iter().filter(|k| { + k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples" + }) { + // Deleting old local store key + debug!("Deleting old local store key {}", k); + self.store.delete(&k).expect("Failed to delete storage key"); } } @@ -266,13 +277,17 @@ impl HttpStore { debug!("attempting login request against api."); let mut path = self.v2_path(); path.push_str("/auth"); - let result = reqwasm::http::Request::get(&path) + let request = gloo_net::http::Request::get(&path) .header( - "Authorization", + "authorization", format!("Basic {}", token68(user, pass)).as_str(), ) - .send() - .await; + .mode(web_sys::RequestMode::SameOrigin) + .credentials(web_sys::RequestCredentials::SameOrigin) + .build() + .expect("Failed to build request"); + debug!(?request, "Sending auth request"); + let result = request.send().await; if let Ok(resp) = &result { if resp.status() == 200 { let user_data = resp @@ -294,7 +309,7 @@ impl HttpStore { debug!("Retrieving User Account data"); let mut path = self.v2_path(); path.push_str("/account"); - let result = reqwasm::http::Request::get(&path).send().await; + let result = gloo_net::http::Request::get(&path).send().await; if let Ok(resp) = &result { if resp.status() == 200 { let user_data = resp @@ -315,9 +330,9 @@ impl HttpStore { pub async fn fetch_categories(&self) -> Result>, Error> { let mut path = self.v2_path(); path.push_str("/category_map"); - let resp = match reqwasm::http::Request::get(&path).send().await { + let resp = match gloo_net::http::Request::get(&path).send().await { Ok(resp) => resp, - Err(reqwasm::Error::JsError(err)) => { + Err(gloo_net::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); return Ok(None); } @@ -345,9 +360,9 @@ impl HttpStore { pub async fn fetch_recipes(&self) -> Result>, Error> { let mut path = self.v2_path(); path.push_str("/recipes"); - let resp = match reqwasm::http::Request::get(&path).send().await { + let resp = match gloo_net::http::Request::get(&path).send().await { Ok(resp) => resp, - Err(reqwasm::Error::JsError(err)) => { + Err(gloo_net::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); return Ok(self.local_store.get_recipes()); } @@ -375,9 +390,9 @@ impl HttpStore { let mut path = self.v2_path(); path.push_str("/recipe/"); path.push_str(id.as_ref()); - let resp = match reqwasm::http::Request::get(&path).send().await { + let resp = match gloo_net::http::Request::get(&path).send().await { Ok(resp) => resp, - Err(reqwasm::Error::JsError(err)) => { + Err(gloo_net::Error::JsError(err)) => { error!(path, ?err, "Error hitting api"); return Ok(self.local_store.get_recipe_entry(id.as_ref())); } @@ -413,7 +428,7 @@ impl HttpStore { let mut path = self.v2_path(); path.push_str("/recipe"); path.push_str(&format!("/{}", recipe.as_ref())); - let resp = reqwasm::http::Request::delete(&path).send().await?; + let resp = gloo_net::http::Request::delete(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -431,10 +446,9 @@ impl HttpStore { return Err("Recipe Ids can not be empty".into()); } } - let serialized = to_string(&recipes).expect("Unable to serialize recipe entries"); - let resp = reqwasm::http::Request::post(&path) - .body(&serialized) - .header("content-type", "application/json") + let resp = gloo_net::http::Request::post(&path) + .json(&recipes) + .expect("Failed to set body") .send() .await?; if resp.status() != 200 { @@ -449,9 +463,9 @@ impl HttpStore { pub async fn store_categories(&self, categories: &Vec<(String, String)>) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/category_map"); - let resp = reqwasm::http::Request::post(&path) - .body(to_string(&categories).expect("Unable to encode categories as json")) - .header("content-type", "application/json") + let resp = gloo_net::http::Request::post(&path) + .json(&categories) + .expect("Failed to set body") .send() .await?; if resp.status() != 200 { @@ -503,9 +517,9 @@ impl HttpStore { pub async fn store_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/plan"); - let resp = reqwasm::http::Request::post(&path) - .body(to_string(&plan).expect("Unable to encode plan as json")) - .header("content-type", "application/json") + let resp = gloo_net::http::Request::post(&path) + .json(&plan) + .expect("Failed to set body") .send() .await?; if resp.status() != 200 { @@ -525,9 +539,9 @@ impl HttpStore { path.push_str("/plan"); path.push_str("/at"); path.push_str(&format!("/{}", date)); - let resp = reqwasm::http::Request::post(&path) - .body(to_string(&plan).expect("Unable to encode plan as json")) - .header("content-type", "application/json") + let resp = gloo_net::http::Request::post(&path) + .json(&plan) + .expect("Failed to set body") .send() .await?; if resp.status() != 200 { @@ -542,7 +556,7 @@ impl HttpStore { let mut path = self.v2_path(); path.push_str("/plan"); path.push_str("/all"); - let resp = reqwasm::http::Request::get(&path).send().await?; + let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -561,7 +575,7 @@ impl HttpStore { path.push_str("/plan"); path.push_str("/at"); path.push_str(&format!("/{}", date)); - let resp = reqwasm::http::Request::delete(&path).send().await?; + let resp = gloo_net::http::Request::delete(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -577,7 +591,7 @@ impl HttpStore { path.push_str("/plan"); path.push_str("/at"); path.push_str(&format!("/{}", date)); - let resp = reqwasm::http::Request::get(&path).send().await?; + let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -594,7 +608,7 @@ impl HttpStore { //pub async fn fetch_plan(&self) -> Result>, Error> { // let mut path = self.v2_path(); // path.push_str("/plan"); - // let resp = reqwasm::http::Request::get(&path).send().await?; + // let resp = gloo_net::http::Request::get(&path).send().await?; // if resp.status() != 200 { // Err(format!("Status: {}", resp.status()).into()) // } else { @@ -623,7 +637,7 @@ impl HttpStore { path.push_str("/inventory"); path.push_str("/at"); path.push_str(&format!("/{}", date)); - let resp = reqwasm::http::Request::get(&path).send().await?; + let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -658,7 +672,7 @@ impl HttpStore { > { let mut path = self.v2_path(); path.push_str("/inventory"); - let resp = reqwasm::http::Request::get(&path).send().await?; + let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { Err(format!("Status: {}", resp.status()).into()) } else { @@ -695,13 +709,10 @@ impl HttpStore { path.push_str(&format!("/{}", date)); let filtered_ingredients: Vec = filtered_ingredients.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); - debug!("Storing inventory data in cache"); - let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items)) - .expect("Unable to encode plan as json"); debug!("Storing inventory data via API"); - let resp = reqwasm::http::Request::post(&path) - .body(&serialized_inventory) - .header("content-type", "application/json") + let resp = gloo_net::http::Request::post(&path) + .json(&(filtered_ingredients, modified_amts, extra_items)) + .expect("Failed to set body") .send() .await?; if resp.status() != 200 { @@ -724,13 +735,10 @@ impl HttpStore { path.push_str("/inventory"); let filtered_ingredients: Vec = filtered_ingredients.into_iter().collect(); let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect(); - debug!("Storing inventory data in cache"); - let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items)) - .expect("Unable to encode plan as json"); debug!("Storing inventory data via API"); - let resp = reqwasm::http::Request::post(&path) - .body(&serialized_inventory) - .header("content-type", "application/json") + let resp = gloo_net::http::Request::post(&path) + .json(&(filtered_ingredients, modified_amts, extra_items)) + .expect("Failed to set body") .send() .await?; if resp.status() != 200 { @@ -745,7 +753,7 @@ impl HttpStore { pub async fn fetch_staples(&self) -> Result, Error> { let mut path = self.v2_path(); path.push_str("/staples"); - let resp = reqwasm::http::Request::get(&path).send().await?; + let resp = gloo_net::http::Request::get(&path).send().await?; if resp.status() != 200 { debug!("Invalid response back"); Err(format!("Status: {}", resp.status()).into()) @@ -759,15 +767,15 @@ impl HttpStore { } } - pub async fn store_staples>(&self, content: S) -> Result<(), Error> { + pub async fn store_staples + serde::Serialize>( + &self, + content: S, + ) -> Result<(), Error> { let mut path = self.v2_path(); path.push_str("/staples"); - let serialized_staples: String = - to_string(content.as_ref()).expect("Failed to serialize staples to json"); - - let resp = reqwasm::http::Request::post(&path) - .body(&serialized_staples) - .header("content-type", "application/json") + let resp = gloo_net::http::Request::post(&path) + .json(&content) + .expect("Failed to set body") .send() .await?; if resp.status() != 200 { diff --git a/web/src/app_state.rs b/web/src/app_state.rs index 88c0244..8770049 100644 --- a/web/src/app_state.rs +++ b/web/src/app_state.rs @@ -174,8 +174,9 @@ impl StateMachine { local_store: &LocalStore, original: &Signal, ) -> Result<(), crate::api::Error> { - // TODO(jwall): We use a linear Signal in here to ensure that we only - // call set on the signal once. + // NOTE(jwall): We use a linear Signal in here to ensure that we only + // call set on the signal once. When the LinearSignal get's dropped it + // will call set on the contained Signal. let mut original: LinearSignal = original.into(); if let Some(state) = local_store.fetch_app_state() { original = original.update(state); diff --git a/web/src/components/categories.rs b/web/src/components/categories.rs index f792067..06aa8db 100644 --- a/web/src/components/categories.rs +++ b/web/src/components/categories.rs @@ -49,7 +49,7 @@ fn CategoryRow<'ctx, G: Html>(cx: Scope<'ctx>, props: CategoryRowProps<'ctx>) -> }); view! {cx, tr() { - td() { + td(class="margin-bot-1 border-bottom") { (ingredient_clone) br() Indexed( iterable=recipes, diff --git a/web/src/components/footer.rs b/web/src/components/footer.rs index 7d522ac..f89a15d 100644 --- a/web/src/components/footer.rs +++ b/web/src/components/footer.rs @@ -17,8 +17,8 @@ use sycamore::prelude::*; #[component] pub fn Footer(cx: Scope) -> View { view! {cx, - nav(class="no-print") { - ul { + nav(class="no-print menu-font") { + ul(class="no-list") { li { a(href="https://github.com/zaphar/kitchen") { "On Github" } } } } diff --git a/web/src/components/header.rs b/web/src/components/header.rs index bda1fc4..3513721 100644 --- a/web/src/components/header.rs +++ b/web/src/components/header.rs @@ -23,9 +23,9 @@ pub fn Header<'ctx, G: Html>(cx: Scope<'ctx>, h: StateHandler<'ctx>) -> View None => "Login".to_owned(), }); view! {cx, - nav(class="no-print") { + nav(class="no-print row-flex align-center header-bg heavy-bottom-border menu-font") { h1(class="title") { "Kitchen" } - ul { + ul(class="row-flex align-center no-list") { li { a(href="/ui/planning/select") { "MealPlan" } } li { a(href="/ui/manage/ingredients") { "Manage" } } li { a(href="/ui/login") { (login.get()) } } diff --git a/web/src/components/number_field.rs b/web/src/components/number_field.rs index e0fd487..15446dc 100644 --- a/web/src/components/number_field.rs +++ b/web/src/components/number_field.rs @@ -14,9 +14,9 @@ use maud::html; use sycamore::prelude::*; use tracing::{debug, error}; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, JsValue}; use wasm_web_component::{web_component, WebComponentBinding}; -use web_sys::{CustomEvent, Event, HtmlElement, InputEvent, ShadowRoot, window}; +use web_sys::{window, CustomEvent, CustomEventInit, Event, HtmlElement, InputEvent, ShadowRoot}; use crate::js_lib::LogFailures; @@ -135,11 +135,10 @@ impl WebComponentBinding for NumberSpinner { return; } }; + let mut eventDict = CustomEventInit::new(); + eventDict.detail(&JsValue::from_f64(self.value as f64)); element - .set_attribute("val", &format!("{}", self.value)) - .swallow_and_log(); - element - .dispatch_event(&CustomEvent::new("updated").unwrap()) + .dispatch_event(&CustomEvent::new_with_event_init_dict("updated", &eventDict).unwrap()) .unwrap(); debug!("Dispatched updated event"); } @@ -147,13 +146,18 @@ impl WebComponentBinding for NumberSpinner { fn attribute_changed_mut( &mut self, _element: &web_sys::HtmlElement, - name: wasm_bindgen::JsValue, - old_value: wasm_bindgen::JsValue, - new_value: wasm_bindgen::JsValue, + name: JsValue, + old_value: JsValue, + new_value: JsValue, ) { let nval_el = self.get_input_el(); let name = name.as_string().unwrap(); - debug!(?name, ?old_value, ?new_value, "COUNTS: handling attribute change"); + debug!( + ?name, + ?old_value, + ?new_value, + "COUNTS: handling attribute change" + ); match name.as_str() { "val" => { debug!("COUNTS: got an updated value"); @@ -208,9 +212,10 @@ impl WebComponentBinding for NumberSpinner { #[derive(Props)] pub struct NumberProps<'ctx, F> where - F: Fn(Event), + F: Fn(CustomEvent), { name: String, + class: String, on_change: Option, min: f64, counter: &'ctx Signal, @@ -219,10 +224,11 @@ where #[component] pub fn NumberField<'ctx, F, G: Html>(cx: Scope<'ctx>, props: NumberProps<'ctx, F>) -> View where - F: Fn(web_sys::Event) + 'ctx, + F: Fn(CustomEvent) + 'ctx, { let NumberProps { name, + class, on_change, min, counter, @@ -231,21 +237,13 @@ where // TODO(jwall): I'm pretty sure this triggers: https://github.com/sycamore-rs/sycamore/issues/602 // Which means I probably have to wait till v0.9.0 drops or switch to leptos. let id = name.clone(); - create_effect(cx, move || { - let new_count = *counter.get(); - debug!(new_count, "COUNTS: Updating spinner with new value"); - if let Some(el) = window().unwrap().document().unwrap().get_element_by_id(id.as_str()) { - debug!("COUNTS: found element"); - el.set_attribute("val", new_count.to_string().as_str()).unwrap(); - } - }); - let id = name.clone(); + let initial_count = *counter.get(); view! {cx, - number-spinner(id=id, val=*counter.get(), min=min, on:updated=move |evt: Event| { - let target: HtmlElement = evt.target().unwrap().dyn_into().unwrap(); - let val: f64 = target.get_attribute("val").unwrap().parse().unwrap(); + number-spinner(id=id, class=(class), val=(initial_count), min=min, on:updated=move |evt: Event| { + let event = evt.unchecked_into::(); + let val: f64 = event.detail().as_f64().unwrap(); counter.set(val); - on_change.as_ref().map(|f| f(evt)); + on_change.as_ref().map(|f| f(event)); debug!(counter=%(counter.get_untracked()), "set counter to new value"); }) } diff --git a/web/src/components/plan_list.rs b/web/src/components/plan_list.rs index c7e8fb6..0a6060c 100644 --- a/web/src/components/plan_list.rs +++ b/web/src/components/plan_list.rs @@ -1,4 +1,3 @@ -use chrono::NaiveDate; // Copyright 2023 Jeremy Wall (Jeremy@marzhilsltudios.com) // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +11,7 @@ use chrono::NaiveDate; // 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 chrono::NaiveDate; use sycamore::prelude::*; use crate::app_state::{Message, StateHandler}; @@ -23,30 +23,25 @@ pub struct PlanListProps<'ctx> { list: &'ctx ReadSignal>, } -// TODO(jwall): We also need a "new plan button" #[instrument(skip_all, fields(dates=?props.list))] #[component] pub fn PlanList<'ctx, G: Html>(cx: Scope<'ctx>, props: PlanListProps<'ctx>) -> View { let PlanListProps { sh, list } = props; view! {cx, div() { - table() { + div(class="column-flex") { Indexed( iterable=list, view=move |cx, date| { let date_display = format!("{}", date); view!{cx, - tr() { - td() { - span(role="button", class="outline", on:click=move |_| { - sh.dispatch(cx, Message::SelectPlanDate(date, None)) - }) { (date_display) } - } - td() { - span(role="button", class="destructive", on:click=move |_| { - sh.dispatch(cx, Message::DeletePlan(date, None)) - }) { "Delete Plan" } - } + div(class="row-flex margin-bot-half") { + button(class="outline margin-right-1", on:click=move |_| { + sh.dispatch(cx, Message::SelectPlanDate(date, None)) + }) { (date_display) } + button(class="destructive", on:click=move |_| { + sh.dispatch(cx, Message::DeletePlan(date, None)) + }) { "Delete Plan" } } } }, diff --git a/web/src/components/recipe.rs b/web/src/components/recipe.rs index 2e9ca50..9bd75d7 100644 --- a/web/src/components/recipe.rs +++ b/web/src/components/recipe.rs @@ -79,12 +79,14 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) debug!("creating editor view"); view! {cx, - label(for="recipe_category") { "Category" } - input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true)) - div(class="grid") { - div { - label(for="recipe_text") { "Recipe" } - textarea(name="recipe_text", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { + div { + label(for="recipe_category") { "Category" } + input(name="recipe_category", bind:value=category, on:change=move |_| dirty.set(true)) + } + div { + div(class="row-flex") { + label(for="recipe_text", class="block align-stretch expand-height") { "Recipe: " } + textarea(class="width-third", name="recipe_text", bind:value=text, aria-invalid=aria_hint.get(), cols="50", rows=20, on:change=move |_| { dirty.set(true); check_recipe_parses(text.get_untracked().as_str(), error_text, aria_hint); }, on:input=move |_| { @@ -97,34 +99,36 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>) } div(class="parse") { (error_text.get()) } } - span(role="button", on:click=move |_| { - let unparsed = text.get_untracked(); - if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) { - debug!("triggering a save"); - if !*dirty.get_untracked() { - debug!("Recipe text is unchanged"); - return; + div { + button(on:click=move |_| { + let unparsed = text.get_untracked(); + if check_recipe_parses(unparsed.as_str(), error_text, aria_hint) { + debug!("triggering a save"); + if !*dirty.get_untracked() { + debug!("Recipe text is unchanged"); + return; + } + debug!("Recipe text is changed"); + let category = category.get_untracked(); + let category = if category.is_empty() { + None + } else { + Some(category.as_ref().clone()) + }; + let recipe_entry = RecipeEntry( + id.get_untracked().as_ref().clone(), + text.get_untracked().as_ref().clone(), + category, + ); + sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None)); + dirty.set(false); } - debug!("Recipe text is changed"); - let category = category.get_untracked(); - let category = if category.is_empty() { - None - } else { - Some(category.as_ref().clone()) - }; - let recipe_entry = RecipeEntry( - id.get_untracked().as_ref().clone(), - text.get_untracked().as_ref().clone(), - category, - ); - sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None)); - dirty.set(false); - } - // TODO(jwall): Show error message if trying to save when recipe doesn't parse. - }) { "Save" } " " - span(role="button", on:click=move |_| { - sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned(), Some(Box::new(|| sycamore_router::navigate("/ui/planning/plan"))))); - }) { "delete" } " " + // TODO(jwall): Show error message if trying to save when recipe doesn't parse. + }) { "Save" } " " + button(on:click=move |_| { + sh.dispatch(cx, Message::RemoveRecipe(id.get_untracked().as_ref().to_owned(), Some(Box::new(|| sycamore_router::navigate("/ui/planning/plan"))))); + }) { "delete" } " " + } } } @@ -142,7 +146,7 @@ fn Steps(cx: Scope, steps: Vec) -> View { view! {cx, div { h3 { "Step " (idx + 1) } - ul(class="ingredients") { + ul(class="ingredients no-list") { (ingredient_fragments) } div(class="instructions") { diff --git a/web/src/components/recipe_plan.rs b/web/src/components/recipe_plan.rs index 31c6968..83b2081 100644 --- a/web/src/components/recipe_plan.rs +++ b/web/src/components/recipe_plan.rs @@ -52,7 +52,7 @@ pub fn CategoryGroup<'ctx, G: Html>( }); view! {cx, h2 { (category) } - div(class="recipe_selector no-print") { + div(class="no-print row-flex flex-wrap-start align-stretch") { (View::new_fragment( rows.get().iter().cloned().map(|r| { view ! {cx, @@ -61,7 +61,7 @@ pub fn CategoryGroup<'ctx, G: Html>( view=move |cx, sig| { let title = create_memo(cx, move || sig.get().1.title.clone()); view! {cx, - div(class="cell") { RecipeSelection(i=sig.get().0.to_owned(), title=title, sh=sh) } + div(class="cell column-flex justify-end align-stretch") { RecipeSelection(i=sig.get().0.to_owned(), title=title, sh=sh) } } }, key=|sig| sig.get().0.to_owned(), @@ -108,13 +108,13 @@ pub fn RecipePlan<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie }, key=|(ref cat, _)| cat.clone(), ) - span(role="button", on:click=move |_| { + button(on:click=move |_| { sh.dispatch(cx, Message::LoadState(None)); }) { "Reset" } " " - span(role="button", on:click=move |_| { + button(on:click=move |_| { sh.dispatch(cx, Message::ResetRecipeCounts); }) { "Clear All" } " " - span(role="button", on:click=move |_| { + button(on:click=move |_| { // Poor man's click event signaling. sh.dispatch(cx, Message::SaveState(None)); }) { "Save Plan" } " " diff --git a/web/src/components/recipe_selection.rs b/web/src/components/recipe_selection.rs index 4fb66c7..3e6777b 100644 --- a/web/src/components/recipe_selection.rs +++ b/web/src/components/recipe_selection.rs @@ -65,8 +65,8 @@ pub fn RecipeSelection<'ctx, G: Html>( let name = format!("recipe_id:{}", id); let for_id = name.clone(); view! {cx, - label(for=for_id) { a(href=href) { (*title) } } - NumberField(name=name, counter=count, min=0.0, on_change=Some(move |_| { + label(for=for_id, class="flex-item-grow") { a(href=href) { (*title) } } + NumberField(name=name, class="flex-item-shrink".to_string(), counter=count, min=0.0, on_change=Some(move |_| { debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count"); sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as usize)); })) diff --git a/web/src/components/shopping_list.rs b/web/src/components/shopping_list.rs index 772923b..4248a80 100644 --- a/web/src/components/shopping_list.rs +++ b/web/src/components/shopping_list.rs @@ -109,12 +109,12 @@ fn make_ingredients_rows<'ctx, G: Html>( view! {cx, tr { td { - input(bind:value=amt_signal, type="text", on:change=move |_| { + input(bind:value=amt_signal, class="width-5", type="text", on:change=move |_| { sh.dispatch(cx, Message::UpdateAmt(k_clone.clone(), amt_signal.get_untracked().as_ref().clone())); }) } td { - input(type="button", class="no-print destructive", value="X", on:click={ + input(type="button", class="fit-content no-print destructive", value="X", on:click={ move |_| { sh.dispatch(cx, Message::AddFilteredIngredient(k.clone())); }}) @@ -143,14 +143,14 @@ fn make_extras_rows<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V view! {cx, tr { td { - input(bind:value=amt_signal, type="text", on:change=move |_| { + input(bind:value=amt_signal, class="width-5", type="text", on:change=move |_| { sh.dispatch(cx, Message::UpdateExtra(idx, amt_signal.get_untracked().as_ref().clone(), name_signal.get_untracked().as_ref().clone())); }) } td { - input(type="button", class="no-print destructive", value="X", on:click=move |_| { + input(type="button", class="fit-content no-print destructive", value="X", on:click=move |_| { sh.dispatch(cx, Message::RemoveExtra(idx)); }) } @@ -194,9 +194,7 @@ fn make_shopping_table<'ctx, G: Html>( #[instrument(skip_all)] #[component] pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { - let show_staples = sh.get_selector(cx, |state| { - state.get().use_staples - }); + let show_staples = sh.get_selector(cx, |state| state.get().use_staples); view! {cx, h1 { "Shopping List " } label(for="show_staples_cb") { "Show staples" } @@ -205,15 +203,15 @@ pub fn ShoppingList<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> V sh.dispatch(cx, Message::UpdateUseStaples(value)); }) (make_shopping_table(cx, sh, show_staples)) - span(role="button", class="no-print", on:click=move |_| { + button(class="no-print", on:click=move |_| { info!("Registering add item request for inventory"); sh.dispatch(cx, Message::AddExtra(String::new(), String::new())); }) { "Add Item" } " " - span(role="button", class="no-print", on:click=move |_| { + button(class="no-print", on:click=move |_| { info!("Registering reset request for inventory"); sh.dispatch(cx, Message::ResetInventory); }) { "Reset" } " " - span(role="button", class="no-print", on:click=move |_| { + button(class="no-print", on:click=move |_| { info!("Registering save request for inventory"); sh.dispatch(cx, Message::SaveState(None)); }) { "Save" } " " diff --git a/web/src/components/staples.rs b/web/src/components/staples.rs index 3cb420f..98894e2 100644 --- a/web/src/components/staples.rs +++ b/web/src/components/staples.rs @@ -72,8 +72,8 @@ pub fn IngredientsEditor<'ctx, G: Html>( debug!("creating editor view"); view! {cx, - div(class="grid") { - textarea(bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { + div { + textarea(class="width-third", bind:value=text, aria-invalid=aria_hint.get(), rows=20, on:change=move |_| { dirty.set(true); }, on:input=move |_| { let current_ts = js_lib::get_ms_timestamp(); @@ -84,7 +84,7 @@ pub fn IngredientsEditor<'ctx, G: Html>( }) div(class="parse") { (error_text.get()) } } - span(role="button", on:click=move |_| { + button(on:click=move |_| { let unparsed = text.get(); if !*dirty.get_untracked() { debug!("Staples text is unchanged"); diff --git a/web/src/components/tabs.rs b/web/src/components/tabs.rs index 2e876ef..6b38811 100644 --- a/web/src/components/tabs.rs +++ b/web/src/components/tabs.rs @@ -47,12 +47,12 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View .collect(), ); view! {cx, - nav { - ul(class="tabs") { + nav(class="menu-bg menu-font-2 flex-item-shrink") { + ul(class="tabs pad-left no-list row-flex align-center") { (menu) } } - main(class=".conatiner-fluid") { + main(class="flex-item-grow content-font") { (children) } } diff --git a/web/src/js_lib.rs b/web/src/js_lib.rs index 6acf37a..82596f5 100644 --- a/web/src/js_lib.rs +++ b/web/src/js_lib.rs @@ -16,7 +16,8 @@ use tracing::error; use web_sys::{window, Storage, Window}; pub fn get_storage() -> Storage { - get_window().local_storage() + get_window() + .local_storage() .expect("Failed to get storage") .expect("No storage available") } @@ -26,8 +27,7 @@ pub fn get_ms_timestamp() -> u32 { } pub fn get_window() -> Window { - window() - .expect("No window present") + window().expect("No window present") } pub trait LogFailures { diff --git a/web/src/lib.rs b/web/src/lib.rs index af53b8d..188d5c5 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -15,10 +15,10 @@ mod api; mod app_state; mod components; mod js_lib; +mod linear; mod pages; mod routing; mod web; -mod linear; use sycamore::prelude::*; use wasm_bindgen::prelude::wasm_bindgen; diff --git a/web/src/linear.rs b/web/src/linear.rs index 80c16d9..355910b 100644 --- a/web/src/linear.rs +++ b/web/src/linear.rs @@ -24,7 +24,10 @@ pub struct LinearSignal<'ctx, Payload> { impl<'ctx, Payload> Into> for &'ctx Signal { fn into(self) -> LinearSignal<'ctx, Payload> { - LinearSignal { signal: self, nv: None } + LinearSignal { + signal: self, + nv: None, + } } } diff --git a/web/src/pages/login.rs b/web/src/pages/login.rs index 09d3a0d..9c2510c 100644 --- a/web/src/pages/login.rs +++ b/web/src/pages/login.rs @@ -27,9 +27,13 @@ pub fn LoginForm<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View input(type="text", id="username", bind:value=username) label(for="password") { "Password" } input(type="password", bind:value=password) - span(role="button", on:click=move |_| { + button(on:click=move |evt: web_sys::Event| { info!("Attempting login request"); let (username, password) = ((*username.get_untracked()).clone(), (*password.get_untracked()).clone()); + // NOTE(jwall): This is required if we want to keep the below auth request from + // failing to send with blocked by browser. This is because it's on a click and + // the form tries to do a submit event and aborts our network request. + evt.prevent_default(); if username != "" && password != "" { spawn_local_scoped(cx, async move { let store = crate::api::HttpStore::get_from_context(cx); diff --git a/web/src/pages/planning/cook.rs b/web/src/pages/planning/cook.rs index 2ae3cf9..e213f5b 100644 --- a/web/src/pages/planning/cook.rs +++ b/web/src/pages/planning/cook.rs @@ -18,9 +18,13 @@ use crate::{app_state::StateHandler, components::recipe_list::*}; #[component] pub fn CookPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let current_plan = sh.get_selector(cx, |state| { + state.get().selected_plan_date + }); view! {cx, PlanningPage( selected=Some("Cook".to_owned()), + plan_date = current_plan, ) { RecipeList(sh) } } } diff --git a/web/src/pages/planning/inventory.rs b/web/src/pages/planning/inventory.rs index 39a84ea..67bb43e 100644 --- a/web/src/pages/planning/inventory.rs +++ b/web/src/pages/planning/inventory.rs @@ -18,9 +18,13 @@ use crate::{app_state::StateHandler, components::shopping_list::*}; #[component] pub fn InventoryPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let current_plan = sh.get_selector(cx, |state| { + state.get().selected_plan_date + }); view! {cx, PlanningPage( selected=Some("Inventory".to_owned()), + plan_date = current_plan, ) { ShoppingList(sh) } } } diff --git a/web/src/pages/planning/mod.rs b/web/src/pages/planning/mod.rs index 940e35b..27fe3a1 100644 --- a/web/src/pages/planning/mod.rs +++ b/web/src/pages/planning/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::components::tabs::*; +use chrono::NaiveDate; use sycamore::prelude::*; pub mod cook; @@ -25,14 +26,19 @@ pub use plan::*; pub use select::*; #[derive(Props)] -pub struct PageState<'a, G: Html> { - pub children: Children<'a, G>, +pub struct PageState<'ctx, G: Html> { + pub children: Children<'ctx, G>, pub selected: Option, + pub plan_date: &'ctx ReadSignal>, } #[component] -pub fn PlanningPage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View { - let PageState { children, selected } = state; +pub fn PlanningPage<'ctx, G: Html>(cx: Scope<'ctx>, state: PageState<'ctx, G>) -> View { + let PageState { + children, + selected, + plan_date, + } = state; let children = children.call(cx); let planning_tabs: Vec<(String, &'static str)> = vec![ ("/ui/planning/select".to_owned(), "Select"), @@ -45,6 +51,10 @@ pub fn PlanningPage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View TabbedView( selected=selected, tablist=planning_tabs, - ) { (children) } + ) { div { + "Plan Date: " (plan_date.get().map_or(String::from("Unknown"), |d| format!("{}", d))) + } + (children) + } } } diff --git a/web/src/pages/planning/plan.rs b/web/src/pages/planning/plan.rs index f3f4975..a8e3bba 100644 --- a/web/src/pages/planning/plan.rs +++ b/web/src/pages/planning/plan.rs @@ -18,9 +18,13 @@ use sycamore::prelude::*; #[component] pub fn PlanPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View { + let current_plan = sh.get_selector(cx, |state| { + state.get().selected_plan_date + }); view! {cx, PlanningPage( selected=Some("Plan".to_owned()), + plan_date = current_plan, ) { RecipePlan(sh) } } } diff --git a/web/src/pages/planning/select.rs b/web/src/pages/planning/select.rs index b361879..2057926 100644 --- a/web/src/pages/planning/select.rs +++ b/web/src/pages/planning/select.rs @@ -32,12 +32,16 @@ pub fn SelectPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> Vie plans.sort_unstable_by(|d1, d2| d2.cmp(d1)); plans }); + let current_plan = sh.get_selector(cx, |state| { + state.get().selected_plan_date + }); view! {cx, PlanningPage( selected=Some("Select".to_owned()), + plan_date = current_plan.clone(), ) { PlanList(sh=sh, list=plan_dates) - span(role="button", on:click=move |_| { + button(on:click=move |_| { sh.dispatch(cx, Message::SelectPlanDate(chrono::offset::Local::now().naive_local().date(), Some(Box::new(|| { sycamore_router::navigate("/ui/planning/plan"); })))) diff --git a/web/src/routing/mod.rs b/web/src/routing/mod.rs index 5108c27..cef073d 100644 --- a/web/src/routing/mod.rs +++ b/web/src/routing/mod.rs @@ -136,11 +136,10 @@ pub fn Handler<'ctx, G: Html>(cx: Scope<'ctx>, props: HandlerProps<'ctx>) -> Vie integration=HistoryIntegration::new(), view=move |cx: Scope, route: &ReadSignal| { view!{cx, - div(class="app") { - Header(sh) - (route_switch(route.get().as_ref(), cx, sh)) - Footer { } - } + div(class="column-flex") { + Header(sh) + (route_switch(route.get().as_ref(), cx, sh)) + } } }, ) diff --git a/web/src/web.rs b/web/src/web.rs index c2b6883..bd40dc7 100644 --- a/web/src/web.rs +++ b/web/src/web.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use sycamore::{futures::spawn_local_scoped, prelude::*}; -use tracing::{info, debug, instrument}; +use tracing::{debug, info, instrument}; use crate::app_state::Message; use crate::{api, routing::Handler as RouteHandler}; diff --git a/web/static/app.css b/web/static/app.css index 70a6006..f31b032 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -1,5 +1,5 @@ /** - * Copyright 2022 Jeremy Wall + * Copyright 2023 Jeremy Wall * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,34 @@ --unicode-button-size: 2em; --toast-anim-duration: 3s; --notification-font-size: calc(var(--font-size) / 2); - --error-message-color: rgba(255, 98, 0, 0.797); + --error-message-color: #CD5C08; --error-message-bg: grey; - --border-width: 2px; + --border-width: 3px; --cell-margin: 1em; + --nav-margin: 2em; + --main-color: #A9907E; + --light-accent: #F3DEBA; + --dark-accent: #ABC4AA; + --heavy-accent: #675D50; + --text-color: black; + --menu-bg: var(--main-color); + --header-bg: var(--light-accent); + --font-size: 1.5rem; + --menu-font-size: 2em; + --cell-target: 30%; } +/** TODO(jwall): Dark color scheme? +@media (prefers-color-scheme: dark) { + :root { + --text-color: white; + --menu-bg: var(--main-color); + --header-bg: var(--dark-accent); + } +} +**/ + +/** TODO(jwall): Seperate these out into composable classes **/ @media print { .no-print, @@ -39,28 +61,138 @@ } } -@media (min-width: 768px) { - :root { - --font-size: 35px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --tab-border-color: lightgrey; - } -} - +/** Resets **/ body { - padding: 10px; - margin: 10px; + margin: 0px; + padding: 0px; + background-color: var(--header-bg); + font-size: var(--font-size) } -nav>ul.tabs>li { - border-style: none; +body * { + color: black; + font-size: inherit; } -nav>ul.tabs>li.selected { +a { + text-decoration: none; +} + +/** layout classes **/ + +.column-flex { + display: flex; + flex-direction: column; +} + +.row-flex { + display: flex; + flex-direction: row; +} + +.flex-item-grow { + flex: 1 0 auto; +} + +.flex-item-shrink { + flex: 0 1 auto; +} + +.flex-wrap-start { + flex-wrap: wrap; + justify-content: flex-start; +} + +.expand-height { + height: 100%; + min-height: fit-content; +} + +.align-center { + align-items: center; +} + +.align-stretch { + align-items: stretch; +} + +.width-third { + min-width: fit-content; + width: 33%; +} + +.inline-block { + display: inline-block; +} + +.block { + display: block; +} + +.no-list { + list-style-type: none; +} + +.fit-content { + width: fit-content; +} + +.width-10 { + width: 10em; +} + +.width-5 { + width: 5em; +} + +.border-bottom { + border-bottom-style: solid; + border-bottom-color: var(--tab-border-color); + border-bottom-width: var(--tab-border-width); +} + +.margin-bot-1 { + margin-bottom: 1em; +} + +.margin-bot-half { + margin-bottom: .5em; +} + +.margin-right-1 { + margin-right: 1em; +} + +/** Typography classes **/ + +.menu-font { + font-size: var(--menu-font-size); +} + +.menu-font-2 { + font-size: calc(var(--menu-font-size) / 1.5); +} + +.content-font { + font-size: 1.5em; +} + +/** element specific styling **/ +nav li { + margin-right: var(--nav-margin); +} + +/** color and borders **/ +.header-bg { + background-color: var(--header-bg); +} + +.heavy-bottom-border { + border-bottom: var(--border-width) solid var(--heavy-accent) +} + +/** Situational **/ +.selected { border-style: none; border-bottom-style: var(--tab-border-style); border-bottom-color: var(--tab-border-color); @@ -74,10 +206,40 @@ nav>h1 { display: inline; vertical-align: middle; text-align: left; + color: black; +} + +main { + border-bottom-left-radius: 1em; + padding: 1em; + width: 100%; + overflow-block: scroll; +} + +.cell { + margin: 1em; + width: var(--cell-target); +} + +.justify-end { + justify-content: flex-end; +} + +.menu-bg { + background-color: var(--menu-bg); +} + +.pad-left { + padding-left: .5em; +} + +.app nav li { + margin-bottom: var(--nav-margin); } .destructive { - background-color: firebrick !important; + background-color: #CD5C08 !important; + font-weight: bold; } .item-count-inc-dec { @@ -129,24 +291,3 @@ nav>h1 { opacity: 0 } } - -.recipe_selector { - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - align-items: stretch; - align-content: stretch; -} - -.recipe_selector .cell { - margin: 1em; - width: calc(100% / 5); -} - -.cell { - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: stretch; - align-content: stretch; -} diff --git a/web/static/normalize.css b/web/static/normalize.css new file mode 100644 index 0000000..192eb9c --- /dev/null +++ b/web/static/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +}