mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-28 20:29:53 -04:00
Compare commits
21 Commits
4ffb481634
...
5be85e7b6b
Author | SHA1 | Date | |
---|---|---|---|
5be85e7b6b | |||
24f045e757 | |||
e048767773 | |||
b52fdedb58 | |||
4230eabcae | |||
40ff02bb46 | |||
8de0307e44 | |||
63fec9f6df | |||
bd058150ed | |||
579a726dd8 | |||
e393047448 | |||
c00865e633 | |||
d97913e676 | |||
4aaa2b1a06 | |||
388fbc9ee4 | |||
550d92179b | |||
3cc52a06a3 | |||
d282da4b76 | |||
40f0b5db66 | |||
149dc8961e | |||
4a5efd52de |
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]]
|
||||
|
28
flake.lock
generated
28
flake.lock
generated
@ -31,21 +31,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@ -96,11 +81,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": {
|
||||
@ -121,17 +106,16 @@
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1678397831,
|
||||
"narHash": "sha256-7xbxSoiht8G+Zgz55R0ILPsTdbnksILCDMIxeg8Buns=",
|
||||
"lastModified": 1718681902,
|
||||
"narHash": "sha256-E/T7Ge6ayEQe7FVKMJqDBoHyLhRhjc6u9CmU8MyYfy0=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "bdf08e2f43488283eeb25b4a7e7ecba9147a955c",
|
||||
"rev": "16c8ad83297c278eebe740dea5491c1708960dd1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -25,7 +25,7 @@
|
||||
let
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
rust-wasm = pkgs.rust-bin.stable."1.68.0".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" ];
|
||||
|
@ -2,4 +2,4 @@
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
||||
}
|
||||
|
@ -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 |
@ -34,12 +34,20 @@ stdenv.mkDerivation {
|
||||
'';
|
||||
# TODO(jwall): Build this from the root rather than the src.
|
||||
buildPhase = ''
|
||||
set -x
|
||||
echo building with wasm-pack
|
||||
wasm-pack --version
|
||||
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};
|
||||
cargo build --lib --release --target wasm32-unknown-unknown --target-dir $out --offline
|
||||
wasm-bindgen $out/wasm32-unknown-unknown/release/kitchen_wasm.wasm --out-dir $out --typescript --target web
|
||||
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
|
||||
'';
|
||||
}
|
||||
|
@ -20,14 +20,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 +36,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" ];
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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