mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-21 19:29:49 -04:00
Merge branch 'look_and_feel'
This commit is contained in:
commit
6087d31aad
107
Cargo.lock
generated
107
Cargo.lock
generated
@ -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]]
|
||||
|
@ -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.
|
||||
|
16
Makefile
16
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)
|
||||
|
13
flake.lock
generated
13
flake.lock
generated
@ -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": {
|
||||
|
10
flake.nix
10
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;
|
||||
|
@ -2,4 +2,4 @@
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE recipes DROP COLUMN serving_count;
|
2
kitchen/migrations/20240701002811_recipe-servings.up.sql
Normal file
2
kitchen/migrations/20240701002811_recipe-servings.up.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE recipes ADD column serving_count number;
|
@ -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};
|
||||
|
51
models/browser_state.als
Normal file
51
models/browser_state.als
Normal file
@ -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
|
17
models/planning.d2
Normal file
17
models/planning.d2
Normal file
@ -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
|
||||
}
|
125
models/planning.svg
Normal file
125
models/planning.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 94 KiB |
@ -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
|
||||
'';
|
||||
}
|
||||
|
@ -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" ];
|
||||
}
|
||||
cargoTestFlags = [ "--test=reference" ];
|
||||
}
|
||||
|
@ -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
|
||||
```
|
||||
```
|
||||
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -334,7 +334,14 @@ make_fn!(unit<StrIter, String>,
|
||||
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<StrIter, Measure> {
|
||||
"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();
|
||||
|
@ -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),
|
||||
|
@ -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<str>, 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<S: Into<Rc<str>>>(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<f32> 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;
|
||||
|
||||
|
9
scripts/wasm-build.sh
Normal file
9
scripts/wasm-build.sh
Normal file
@ -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
|
6
scripts/wasm-opt.sh
Normal file
6
scripts/wasm-opt.sh
Normal file
@ -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
|
@ -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",
|
||||
|
@ -19,7 +19,7 @@
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" name="viewport"
|
||||
content="width=device-width, initial-scale=1.0" charset="UTF-8">
|
||||
<link rel="stylesheet" href="/ui/static/pico.min.css">
|
||||
<link rel="stylesheet" href="/ui/static/normalize.css">
|
||||
<link rel="stylesheet" href="/ui/static/app.css">
|
||||
</head>
|
||||
|
||||
@ -35,4 +35,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
134
web/src/api.rs
134
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<std::string::FromUtf8Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwasm::Error> for Error {
|
||||
fn from(item: reqwasm::Error) -> Self {
|
||||
impl From<gloo_net::Error> 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<Option<Vec<(String, String)>>, 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<Option<Vec<RecipeEntry>>, 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<Option<Vec<(String, i32)>>, 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<IngredientKey> = 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<IngredientKey> = 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<Option<String>, 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<S: AsRef<str>>(&self, content: S) -> Result<(), Error> {
|
||||
pub async fn store_staples<S: AsRef<str> + 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 {
|
||||
|
@ -174,8 +174,9 @@ impl StateMachine {
|
||||
local_store: &LocalStore,
|
||||
original: &Signal<AppState>,
|
||||
) -> 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<AppState> = original.into();
|
||||
if let Some(state) = local_store.fetch_app_state() {
|
||||
original = original.update(state);
|
||||
|
@ -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,
|
||||
|
@ -17,8 +17,8 @@ use sycamore::prelude::*;
|
||||
#[component]
|
||||
pub fn Footer<G: Html>(cx: Scope) -> View<G> {
|
||||
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" } }
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ pub fn Header<'ctx, G: Html>(cx: Scope<'ctx>, h: StateHandler<'ctx>) -> View<G>
|
||||
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()) } }
|
||||
|
@ -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<F>,
|
||||
min: f64,
|
||||
counter: &'ctx Signal<f64>,
|
||||
@ -219,10 +224,11 @@ where
|
||||
#[component]
|
||||
pub fn NumberField<'ctx, F, G: Html>(cx: Scope<'ctx>, props: NumberProps<'ctx, F>) -> View<G>
|
||||
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::<CustomEvent>();
|
||||
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");
|
||||
})
|
||||
}
|
||||
|
@ -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<Vec<NaiveDate>>,
|
||||
}
|
||||
|
||||
// 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<G> {
|
||||
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" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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<G: Html>(cx: Scope, steps: Vec<recipes::Step>) -> View<G> {
|
||||
view! {cx,
|
||||
div {
|
||||
h3 { "Step " (idx + 1) }
|
||||
ul(class="ingredients") {
|
||||
ul(class="ingredients no-list") {
|
||||
(ingredient_fragments)
|
||||
}
|
||||
div(class="instructions") {
|
||||
|
@ -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" } " "
|
||||
|
@ -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));
|
||||
}))
|
||||
|
@ -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<G> {
|
||||
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" } " "
|
||||
|
@ -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");
|
||||
|
@ -47,12 +47,12 @@ pub fn TabbedView<'a, G: Html>(cx: Scope<'a>, state: TabState<'a, G>) -> View<G>
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
@ -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<V, E> {
|
||||
|
@ -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;
|
||||
|
@ -24,7 +24,10 @@ pub struct LinearSignal<'ctx, Payload> {
|
||||
|
||||
impl<'ctx, Payload> Into<LinearSignal<'ctx, Payload>> for &'ctx Signal<Payload> {
|
||||
fn into(self) -> LinearSignal<'ctx, Payload> {
|
||||
LinearSignal { signal: self, nv: None }
|
||||
LinearSignal {
|
||||
signal: self,
|
||||
nv: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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<G> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
@ -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<G> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
@ -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<String>,
|
||||
pub plan_date: &'ctx ReadSignal<Option<NaiveDate>>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PlanningPage<'a, G: Html>(cx: Scope<'a>, state: PageState<'a, G>) -> View<G> {
|
||||
let PageState { children, selected } = state;
|
||||
pub fn PlanningPage<'ctx, G: Html>(cx: Scope<'ctx>, state: PageState<'ctx, G>) -> View<G> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,13 @@ use sycamore::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn PlanPage<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View<G> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}))))
|
||||
|
@ -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<Routes>| {
|
||||
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))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -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};
|
||||
|
@ -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;
|
||||
}
|
||||
|
349
web/static/normalize.css
vendored
Normal file
349
web/static/normalize.css
vendored
Normal file
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user