mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
parent
3ebf61e77e
commit
a5e8575ef9
125
Cargo.lock
generated
125
Cargo.lock
generated
@ -38,6 +38,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansi_term"
|
name = "ansi_term"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@ -441,15 +450,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.19"
|
version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"time 0.1.44",
|
"time 0.1.44",
|
||||||
|
"wasm-bindgen",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -505,6 +516,16 @@ dependencies = [
|
|||||||
"os_str_bytes",
|
"os_str_bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codespan-reporting"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||||
|
dependencies = [
|
||||||
|
"termcolor",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@ -540,6 +561,12 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -646,6 +673,50 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cxxbridge-flags",
|
||||||
|
"cxxbridge-macro",
|
||||||
|
"link-cplusplus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-build"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"codespan-reporting",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"scratch",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-flags"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-macro"
|
||||||
|
version = "1.0.81"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@ -1112,6 +1183,30 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.53"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||||
|
dependencies = [
|
||||||
|
"cxx",
|
||||||
|
"cxx-build",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@ -1182,6 +1277,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-auth",
|
"axum-auth",
|
||||||
|
"chrono",
|
||||||
"ciborium",
|
"ciborium",
|
||||||
"clap",
|
"clap",
|
||||||
"cookie",
|
"cookie",
|
||||||
@ -1205,6 +1301,7 @@ version = "0.2.9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64",
|
||||||
|
"chrono",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"recipes",
|
"recipes",
|
||||||
"reqwasm",
|
"reqwasm",
|
||||||
@ -1249,6 +1346,15 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "link-cplusplus"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@ -1723,6 +1829,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scratch"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sct"
|
name = "sct"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -1909,6 +2021,7 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
@ -2402,6 +2515,12 @@ version = "1.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
3
Makefile
3
Makefile
@ -43,5 +43,8 @@ clean:
|
|||||||
sqlx-migrate:
|
sqlx-migrate:
|
||||||
cd kitchen; cargo sqlx migrate run --database-url $(sqlite_url)
|
cd kitchen; cargo sqlx migrate run --database-url $(sqlite_url)
|
||||||
|
|
||||||
|
sqlx-revert:
|
||||||
|
cd kitchen; cargo sqlx migrate revert --database-url $(sqlite_url)
|
||||||
|
|
||||||
sqlx-prepare:
|
sqlx-prepare:
|
||||||
cd kitchen; cargo sqlx prepare --database-url $(sqlite_url)
|
cd kitchen; cargo sqlx prepare --database-url $(sqlite_url)
|
||||||
|
@ -19,6 +19,7 @@ ciborium = "0.2.0"
|
|||||||
tower = "0.4.13"
|
tower = "0.4.13"
|
||||||
serde = "1.0.144"
|
serde = "1.0.144"
|
||||||
cookie = "0.16.0"
|
cookie = "0.16.0"
|
||||||
|
chrono = "0.4.22"
|
||||||
|
|
||||||
[dependencies.argon2]
|
[dependencies.argon2]
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -53,4 +54,4 @@ features = ["tokio1"]
|
|||||||
|
|
||||||
[dependencies.sqlx]
|
[dependencies.sqlx]
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
features = ["sqlite", "runtime-async-std-rustls", "offline"]
|
features = ["sqlite", "runtime-async-std-rustls", "offline", "chrono"]
|
@ -1,3 +1,2 @@
|
|||||||
-- Add down migration script here
|
-- Add down migration script here
|
||||||
drop table plans;
|
|
||||||
drop table plan_recipes;
|
drop table plan_recipes;
|
@ -1,3 +1,2 @@
|
|||||||
-- Add up migration script here
|
-- Add up migration script here
|
||||||
CREATE TABLE plans(id NUMBER, user_id TEXT, date TEXT);
|
CREATE table plan_recipes(user_id TEXT NOT NULL, plan_date DATE NOT NULL, recipe_id TEXT NOT NULL, count integer NOT NULL);
|
||||||
CREATE table plan_recipes(plan_id NUMBER, recipe_id TEXT);
|
|
@ -42,6 +42,36 @@
|
|||||||
},
|
},
|
||||||
"query": "select recipe_id, recipe_text from recipes where user_id = ? and recipe_id = ?"
|
"query": "select recipe_id, recipe_text from recipes where user_id = ? and recipe_id = ?"
|
||||||
},
|
},
|
||||||
|
"19832e3582c05ed49c676fde33cde64274379a83a8dd130f6eec96c1d7250909": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "plan_date: NaiveDate",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "recipe_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "count",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Int64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date"
|
||||||
|
},
|
||||||
"3fd4017569dca4fe73e97e0e2bd612027a8c1b17b0b10faabd6459f56ca1c0bb": {
|
"3fd4017569dca4fe73e97e0e2bd612027a8c1b17b0b10faabd6459f56ca1c0bb": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -134,6 +164,46 @@
|
|||||||
},
|
},
|
||||||
"query": "insert into sessions (id, session_value) values (?, ?)"
|
"query": "insert into sessions (id, session_value) values (?, ?)"
|
||||||
},
|
},
|
||||||
|
"ad3408cd773dd8f9308255ec2800171638a1aeda9817c57fb8360f97115f8e97": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "plan_date: NaiveDate",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "recipe_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "count",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Int64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "with max_date as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes group by user_id\n)\n\nselect plan_recipes.plan_date as \"plan_date: NaiveDate\", plan_recipes.recipe_id, plan_recipes.count\n from plan_recipes\n inner join max_date on plan_recipes.user_id = max_date.user_id\nwhere\n plan_recipes.user_id = ?\n and plan_recipes.plan_date = max_date.plan_date"
|
||||||
|
},
|
||||||
|
"c889e3621cb2977204b847c03930cde394cc16eaa63741f8ca07484a41f1aa87": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "insert into plan_recipes (user_id, plan_date, recipe_id, count) values (?, ?, ?, ?)"
|
||||||
|
},
|
||||||
"c988364f9f83f4fa8bd0e594bab432ee7c9ec47ca40f4d16e5e2a8763653f377": {
|
"c988364f9f83f4fa8bd0e594bab432ee7c9ec47ca40f4d16e5e2a8763653f377": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
@ -181,13 +181,80 @@ async fn api_save_recipes(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||||
if let FoundUserId(UserId(id)) = session {
|
if let FoundUserId(UserId(id)) = session {
|
||||||
if let Err(e) = app_store
|
let result = app_store
|
||||||
.store_recipes_for_user(id.as_str(), &recipes)
|
.store_recipes_for_user(id.as_str(), &recipes)
|
||||||
|
.await;
|
||||||
|
match result.map_err(|e| format!("Error: {:?}", e)) {
|
||||||
|
Ok(val) => Ok(axum::Json::from(val)),
|
||||||
|
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"You must be authorized to use this API call".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_plan(
|
||||||
|
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||||
|
session: storage::UserIdFromSession,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||||
|
if let FoundUserId(UserId(id)) = session {
|
||||||
|
match app_store
|
||||||
|
.fetch_latest_meal_plan(&id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Error: {:?}", e))
|
||||||
|
{
|
||||||
|
Ok(val) => Ok(axum::Json::from(val)),
|
||||||
|
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"You must be authorized to use this API call".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_plan_since(
|
||||||
|
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||||
|
session: storage::UserIdFromSession,
|
||||||
|
Path(date): Path<chrono::NaiveDate>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||||
|
if let FoundUserId(UserId(id)) = session {
|
||||||
|
match app_store
|
||||||
|
.fetch_meal_plans_since(&id, date)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Error: {:?}", e))
|
||||||
|
{
|
||||||
|
Ok(val) => Ok(axum::Json::from(val)),
|
||||||
|
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"You must be authorized to use this API call".to_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn api_save_plan(
|
||||||
|
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||||
|
session: storage::UserIdFromSession,
|
||||||
|
Json(meal_plan): Json<Vec<(String, i32)>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||||
|
if let FoundUserId(UserId(id)) = session {
|
||||||
|
if let Err(e) = app_store
|
||||||
|
.save_meal_plan(id.as_str(), &meal_plan, chrono::Local::now().date_naive())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e));
|
return (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e));
|
||||||
}
|
}
|
||||||
(StatusCode::OK, "Successfully saved categories".to_owned())
|
(StatusCode::OK, "Successfully saved mealPlan".to_owned())
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
@ -214,6 +281,9 @@ pub async fn ui_main(recipe_dir_path: PathBuf, store_path: PathBuf, listen_socke
|
|||||||
.route("/api/v1/recipes", get(api_recipes).post(api_save_recipes))
|
.route("/api/v1/recipes", get(api_recipes).post(api_save_recipes))
|
||||||
// recipe entry api path route
|
// recipe entry api path route
|
||||||
.route("/api/v1/recipe/:recipe_id", get(api_recipe_entry))
|
.route("/api/v1/recipe/:recipe_id", get(api_recipe_entry))
|
||||||
|
// mealplan api path routes
|
||||||
|
.route("/api/v1/plan", get(api_plan).post(api_save_plan))
|
||||||
|
.route("/api/v1/plan/:date", get(api_plan_since))
|
||||||
// categories api path route
|
// categories api path route
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/categories",
|
"/api/v1/categories",
|
||||||
|
10
kitchen/src/web/storage/fetch_latest_meal_plan.sql
Normal file
10
kitchen/src/web/storage/fetch_latest_meal_plan.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
with max_date as (
|
||||||
|
select user_id, max(date(plan_date)) as plan_date from plan_recipes group by user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
select plan_recipes.plan_date as "plan_date: NaiveDate", plan_recipes.recipe_id, plan_recipes.count
|
||||||
|
from plan_recipes
|
||||||
|
inner join max_date on plan_recipes.user_id = max_date.user_id
|
||||||
|
where
|
||||||
|
plan_recipes.user_id = ?
|
||||||
|
and plan_recipes.plan_date = max_date.plan_date
|
6
kitchen/src/web/storage/fetch_meal_plans_since.sql
Normal file
6
kitchen/src/web/storage/fetch_meal_plans_since.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
select plan_date as "plan_date: NaiveDate", recipe_id, count
|
||||||
|
from plan_recipes
|
||||||
|
where
|
||||||
|
user_id = ?
|
||||||
|
and date(plan_date) > ?
|
||||||
|
order by user_id, plan_date
|
@ -12,8 +12,8 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use async_std::sync::Arc;
|
use async_std::sync::Arc;
|
||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::{collections::BTreeMap, path::Path};
|
||||||
|
|
||||||
use argon2::{
|
use argon2::{
|
||||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||||
@ -26,6 +26,7 @@ use axum::{
|
|||||||
headers::Cookie,
|
headers::Cookie,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
};
|
};
|
||||||
|
use chrono::NaiveDate;
|
||||||
use ciborium;
|
use ciborium;
|
||||||
use recipes::RecipeEntry;
|
use recipes::RecipeEntry;
|
||||||
use secrecy::{ExposeSecret, Secret};
|
use secrecy::{ExposeSecret, Secret};
|
||||||
@ -100,6 +101,24 @@ pub trait APIStore {
|
|||||||
user_id: S,
|
user_id: S,
|
||||||
id: S,
|
id: S,
|
||||||
) -> Result<Option<RecipeEntry>>;
|
) -> Result<Option<RecipeEntry>>;
|
||||||
|
|
||||||
|
async fn fetch_latest_meal_plan<S: AsRef<str> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: S,
|
||||||
|
) -> Result<Option<Vec<(String, i32)>>>;
|
||||||
|
|
||||||
|
async fn fetch_meal_plans_since<S: AsRef<str> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: S,
|
||||||
|
date: NaiveDate,
|
||||||
|
) -> Result<Option<BTreeMap<NaiveDate, Vec<(String, i32)>>>>;
|
||||||
|
|
||||||
|
async fn save_meal_plan<S: AsRef<str> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: S,
|
||||||
|
recipe_counts: &Vec<(String, i32)>,
|
||||||
|
date: NaiveDate,
|
||||||
|
) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -371,4 +390,93 @@ impl APIStore for SqliteStore {
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn save_meal_plan<S: AsRef<str> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: S,
|
||||||
|
recipe_counts: &Vec<(String, i32)>,
|
||||||
|
date: NaiveDate,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_id = user_id.as_ref();
|
||||||
|
let mut transaction = self.pool.as_ref().begin().await?;
|
||||||
|
for (id, count) in recipe_counts {
|
||||||
|
sqlx::query_file!(
|
||||||
|
"src/web/storage/save_meal_plan.sql",
|
||||||
|
user_id,
|
||||||
|
date,
|
||||||
|
id,
|
||||||
|
count
|
||||||
|
)
|
||||||
|
.execute(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
transaction.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_meal_plans_since<S: AsRef<str> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: S,
|
||||||
|
date: NaiveDate,
|
||||||
|
) -> Result<Option<BTreeMap<NaiveDate, Vec<(String, i32)>>>> {
|
||||||
|
let user_id = user_id.as_ref();
|
||||||
|
struct Row {
|
||||||
|
pub plan_date: NaiveDate,
|
||||||
|
pub recipe_id: String,
|
||||||
|
pub count: i64,
|
||||||
|
}
|
||||||
|
// NOTE(jwall): It feels like I shouldn't have to use an override here
|
||||||
|
// but I do because of the way sqlite does types and how that interacts
|
||||||
|
// with sqlx's type inference machinery.
|
||||||
|
let rows = sqlx::query_file_as!(
|
||||||
|
Row,
|
||||||
|
r#"src/web/storage/fetch_meal_plans_since.sql"#,
|
||||||
|
user_id,
|
||||||
|
date
|
||||||
|
)
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await?;
|
||||||
|
if rows.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut result = BTreeMap::new();
|
||||||
|
for row in rows {
|
||||||
|
let (date, recipe_id, count): (NaiveDate, String, i64) =
|
||||||
|
(row.plan_date, row.recipe_id, row.count);
|
||||||
|
result
|
||||||
|
.entry(date.clone())
|
||||||
|
.or_insert_with(|| Vec::new())
|
||||||
|
.push((recipe_id, count as i32));
|
||||||
|
}
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_latest_meal_plan<S: AsRef<str> + Send>(
|
||||||
|
&self,
|
||||||
|
user_id: S,
|
||||||
|
) -> Result<Option<Vec<(String, i32)>>> {
|
||||||
|
let user_id = user_id.as_ref();
|
||||||
|
struct Row {
|
||||||
|
pub plan_date: NaiveDate,
|
||||||
|
pub recipe_id: String,
|
||||||
|
pub count: i64,
|
||||||
|
}
|
||||||
|
// NOTE(jwall): It feels like I shouldn't have to use an override here
|
||||||
|
// but I do because of the way sqlite does types and how that interacts
|
||||||
|
// with sqlx's type inference machinery.
|
||||||
|
let rows =
|
||||||
|
sqlx::query_file_as!(Row, "src/web/storage/fetch_latest_meal_plan.sql", user_id,)
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await?;
|
||||||
|
if rows.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
let (_, recipe_id, count): (NaiveDate, String, i64) =
|
||||||
|
(row.plan_date, row.recipe_id, row.count);
|
||||||
|
result.push((recipe_id, count as i32));
|
||||||
|
}
|
||||||
|
Ok(Some(result))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1
kitchen/src/web/storage/save_meal_plan.sql
Normal file
1
kitchen/src/web/storage/save_meal_plan.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
insert into plan_recipes (user_id, plan_date, recipe_id, count) values (?, ?, ?, ?)
|
@ -23,6 +23,10 @@ async-trait = "0.1.57"
|
|||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
sycamore-router = "0.8"
|
sycamore-router = "0.8"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "0.4.22"
|
||||||
|
features = ["serde"]
|
||||||
|
|
||||||
[dependencies.reqwasm]
|
[dependencies.reqwasm]
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
||||||
|
@ -62,10 +62,19 @@ pub async fn init_page_state(store: &HttpStore, state: &app_state::State) -> Res
|
|||||||
state.recipes.set(recipes);
|
state.recipes.set(recipes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(rs) = recipes {
|
|
||||||
for r in rs {
|
if let Ok(Some(plan)) = store.get_plan().await {
|
||||||
if !state.recipe_counts.get().contains_key(r.recipe_id()) {
|
// set the counts.
|
||||||
state.set_recipe_count_by_index(&r.recipe_id().to_owned(), 0);
|
for (id, count) in plan {
|
||||||
|
state.set_recipe_count_by_index(&id, count as usize);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Initialize things to zero
|
||||||
|
if let Some(rs) = recipes {
|
||||||
|
for r in rs {
|
||||||
|
if !state.recipe_counts.get().contains_key(r.recipe_id()) {
|
||||||
|
state.set_recipe_count_by_index(&r.recipe_id().to_owned(), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,4 +316,59 @@ impl HttpStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn save_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> {
|
||||||
|
let mut path = self.root.clone();
|
||||||
|
path.push_str("/plan");
|
||||||
|
let storage = js_lib::get_storage();
|
||||||
|
let serialized_plan = to_string(&plan).expect("Unable to encode plan as json");
|
||||||
|
storage.set("plan", &serialized_plan)?;
|
||||||
|
let resp = reqwasm::http::Request::post(&path)
|
||||||
|
.body(to_string(&plan).expect("Unable to encode plan as json"))
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if resp.status() != 200 {
|
||||||
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
|
} else {
|
||||||
|
debug!("We got a valid response back!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_plan(&self) -> Result<Option<Vec<(String, i32)>>, Error> {
|
||||||
|
let mut path = self.root.clone();
|
||||||
|
path.push_str("/plan");
|
||||||
|
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||||
|
let storage = js_lib::get_storage();
|
||||||
|
if resp.status() != 200 {
|
||||||
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
|
} else {
|
||||||
|
debug!("We got a valid response back");
|
||||||
|
let plan: Option<Vec<(String, i32)>> =
|
||||||
|
resp.json().await.map_err(|e| format!("{}", e))?;
|
||||||
|
if let Some(ref entry) = plan {
|
||||||
|
let serialized: String = to_string(entry).map_err(|e| format!("{}", e))?;
|
||||||
|
storage.set("plan", &serialized)?
|
||||||
|
}
|
||||||
|
Ok(plan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_plans_since(
|
||||||
|
&self,
|
||||||
|
date: chrono::NaiveDate,
|
||||||
|
) -> Result<BTreeMap<chrono::NaiveDate, Vec<(String, i32)>>, Error> {
|
||||||
|
let mut path = self.root.clone();
|
||||||
|
path.push_str("/plan");
|
||||||
|
path.push_str(&format!("/{}", date));
|
||||||
|
// TODO(jwall): How does this play with the cache?
|
||||||
|
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||||
|
if resp.status() != 200 {
|
||||||
|
Err(format!("Status: {}", resp.status()).into())
|
||||||
|
} else {
|
||||||
|
debug!("We got a valid response back");
|
||||||
|
Ok(resp.json().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ use tracing::{debug, instrument, warn};
|
|||||||
use recipes::{Ingredient, IngredientAccumulator, Recipe};
|
use recipes::{Ingredient, IngredientAccumulator, Recipe};
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub recipe_counts: RcSignal<BTreeMap<String, usize>>,
|
pub recipe_counts: RcSignal<BTreeMap<String, RcSignal<usize>>>,
|
||||||
pub extras: RcSignal<Vec<(usize, (RcSignal<String>, RcSignal<String>))>>,
|
pub extras: RcSignal<Vec<(usize, (RcSignal<String>, RcSignal<String>))>>,
|
||||||
pub staples: RcSignal<Option<Recipe>>,
|
pub staples: RcSignal<Option<Recipe>>,
|
||||||
pub recipes: RcSignal<BTreeMap<String, Recipe>>,
|
pub recipes: RcSignal<BTreeMap<String, Recipe>>,
|
||||||
@ -45,12 +45,12 @@ impl State {
|
|||||||
use_context::<std::rc::Rc<Self>>(cx).clone()
|
use_context::<std::rc::Rc<Self>>(cx).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_menu_list(&self) -> Vec<(String, usize)> {
|
pub fn get_menu_list(&self) -> Vec<(String, RcSignal<usize>)> {
|
||||||
self.recipe_counts
|
self.recipe_counts
|
||||||
.get()
|
.get()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| (k.clone(), *v))
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
.filter(|(_, v)| *v != 0)
|
.filter(|(_, v)| *(v.get_untracked()) != 0)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ impl State {
|
|||||||
let mut acc = IngredientAccumulator::new();
|
let mut acc = IngredientAccumulator::new();
|
||||||
let recipe_counts = self.get_menu_list();
|
let recipe_counts = self.get_menu_list();
|
||||||
for (idx, count) in recipe_counts.iter() {
|
for (idx, count) in recipe_counts.iter() {
|
||||||
for _ in 0..*count {
|
for _ in 0..*count.get_untracked() {
|
||||||
acc.accumulate_from(
|
acc.accumulate_from(
|
||||||
self.recipes
|
self.recipes
|
||||||
.get()
|
.get()
|
||||||
@ -96,14 +96,27 @@ impl State {
|
|||||||
groups
|
groups
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_recipe_count_by_index(&self, key: &String) -> Option<usize> {
|
/// Retrieves the count for a recipe without triggering subscribers to the entire
|
||||||
self.recipe_counts.get().get(key).cloned()
|
/// recipe count set.
|
||||||
|
pub fn get_recipe_count_by_index(&self, key: &String) -> Option<RcSignal<usize>> {
|
||||||
|
self.recipe_counts.get_untracked().get(key).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_recipe_count_by_index(&self, key: &String, count: usize) -> usize {
|
pub fn reset_recipe_counts(&self) {
|
||||||
let mut counts = self.recipe_counts.get().as_ref().clone();
|
for (key, count) in self.recipe_counts.get_untracked().iter() {
|
||||||
counts.insert(key.clone(), count);
|
count.set(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the recipe_count by index. Does not trigger subscribers to the entire set of recipe_counts.
|
||||||
|
/// This does trigger subscribers of the specific recipe you are updating though.
|
||||||
|
pub fn set_recipe_count_by_index(&self, key: &String, count: usize) -> RcSignal<usize> {
|
||||||
|
let mut counts = self.recipe_counts.get_untracked().as_ref().clone();
|
||||||
|
counts
|
||||||
|
.entry(key.clone())
|
||||||
|
.and_modify(|e| e.set(count))
|
||||||
|
.or_insert_with(|| create_rc_signal(count));
|
||||||
self.recipe_counts.set(counts);
|
self.recipe_counts.set(counts);
|
||||||
count
|
self.recipe_counts.get_untracked().get(key).unwrap().clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@ pub mod categories;
|
|||||||
pub mod header;
|
pub mod header;
|
||||||
pub mod recipe;
|
pub mod recipe;
|
||||||
pub mod recipe_list;
|
pub mod recipe_list;
|
||||||
|
pub mod recipe_plan;
|
||||||
pub mod recipe_selection;
|
pub mod recipe_selection;
|
||||||
pub mod recipe_selector;
|
|
||||||
pub mod shopping_list;
|
pub mod shopping_list;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ pub use categories::*;
|
|||||||
pub use header::*;
|
pub use header::*;
|
||||||
pub use recipe::*;
|
pub use recipe::*;
|
||||||
pub use recipe_list::*;
|
pub use recipe_list::*;
|
||||||
|
pub use recipe_plan::*;
|
||||||
pub use recipe_selection::*;
|
pub use recipe_selection::*;
|
||||||
pub use recipe_selector::*;
|
|
||||||
pub use shopping_list::*;
|
pub use shopping_list::*;
|
||||||
pub use tabs::*;
|
pub use tabs::*;
|
||||||
|
@ -20,7 +20,7 @@ use crate::{api::*, app_state};
|
|||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub fn RecipeSelector<G: Html>(cx: Scope) -> View<G> {
|
pub fn RecipePlan<G: Html>(cx: Scope) -> View<G> {
|
||||||
let rows = create_memo(cx, move || {
|
let rows = create_memo(cx, move || {
|
||||||
let state = app_state::State::get_from_context(cx);
|
let state = app_state::State::get_from_context(cx);
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
@ -37,9 +37,10 @@ pub fn RecipeSelector<G: Html>(cx: Scope) -> View<G> {
|
|||||||
}
|
}
|
||||||
rows
|
rows
|
||||||
});
|
});
|
||||||
let clicked = create_signal(cx, false);
|
let refresh_click = create_signal(cx, false);
|
||||||
|
let save_click = create_signal(cx, false);
|
||||||
create_effect(cx, move || {
|
create_effect(cx, move || {
|
||||||
clicked.track();
|
refresh_click.track();
|
||||||
let store = HttpStore::get_from_context(cx);
|
let store = HttpStore::get_from_context(cx);
|
||||||
let state = app_state::State::get_from_context(cx);
|
let state = app_state::State::get_from_context(cx);
|
||||||
spawn_local_scoped(cx, {
|
spawn_local_scoped(cx, {
|
||||||
@ -47,9 +48,24 @@ pub fn RecipeSelector<G: Html>(cx: Scope) -> View<G> {
|
|||||||
if let Err(err) = init_page_state(store.as_ref(), state.as_ref()).await {
|
if let Err(err) = init_page_state(store.as_ref(), state.as_ref()).await {
|
||||||
error!(?err);
|
error!(?err);
|
||||||
};
|
};
|
||||||
|
state.reset_recipe_counts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
create_effect(cx, move || {
|
||||||
|
save_click.track();
|
||||||
|
let store = HttpStore::get_from_context(cx);
|
||||||
|
let state = app_state::State::get_from_context(cx);
|
||||||
|
spawn_local_scoped(cx, {
|
||||||
|
let mut plan = Vec::new();
|
||||||
|
for (key, count) in state.recipe_counts.get_untracked().iter() {
|
||||||
|
plan.push((key.clone(), *count.get_untracked() as i32));
|
||||||
|
}
|
||||||
|
async move {
|
||||||
|
store.save_plan(plan).await.expect("Failed to save plan");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
view! {cx,
|
view! {cx,
|
||||||
table(class="recipe_selector no-print") {
|
table(class="recipe_selector no-print") {
|
||||||
(View::new_fragment(
|
(View::new_fragment(
|
||||||
@ -69,10 +85,15 @@ pub fn RecipeSelector<G: Html>(cx: Scope) -> View<G> {
|
|||||||
}).collect()
|
}).collect()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
input(type="button", value="Refresh Recipes", on:click=move |_| {
|
input(type="button", value="Reset Recipes", on:click=move |_| {
|
||||||
// Poor man's click event signaling.
|
// Poor man's click event signaling.
|
||||||
let toggle = !*clicked.get();
|
let toggle = !*refresh_click.get();
|
||||||
clicked.set(toggle);
|
refresh_click.set(toggle);
|
||||||
|
})
|
||||||
|
input(type="button", value="Save Plan", on:click=move |_| {
|
||||||
|
// Poor man's click event signaling.
|
||||||
|
let toggle = !*save_click.get();
|
||||||
|
save_click.set(toggle);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -43,6 +43,15 @@ pub fn RecipeSelection<G: Html>(cx: Scope, props: RecipeCheckBoxProps) -> View<G
|
|||||||
.unwrap_or_else(|| state.set_recipe_count_by_index(id.as_ref(), 0))
|
.unwrap_or_else(|| state.set_recipe_count_by_index(id.as_ref(), 0))
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
create_effect(cx, {
|
||||||
|
let id = id.clone();
|
||||||
|
let state = app_state::State::get_from_context(cx);
|
||||||
|
move || {
|
||||||
|
if let Some(usize_count) = state.get_recipe_count_by_index(id.as_ref()) {
|
||||||
|
count.set(format!("{}", *usize_count.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
let title = props.title.get().clone();
|
let title = props.title.get().clone();
|
||||||
let for_id = id.clone();
|
let for_id = id.clone();
|
||||||
let href = format!("/ui/recipe/view/{}", id);
|
let href = format!("/ui/recipe/view/{}", id);
|
||||||
@ -51,6 +60,7 @@ pub fn RecipeSelection<G: Html>(cx: Scope, props: RecipeCheckBoxProps) -> View<G
|
|||||||
div() {
|
div() {
|
||||||
label(for=for_id) { a(href=href) { (*title) } }
|
label(for=for_id) { a(href=href) { (*title) } }
|
||||||
input(type="number", class="item-count-sel", min="0", bind:value=count, name=name, on:change=move |_| {
|
input(type="number", class="item-count-sel", min="0", bind:value=count, name=name, on:change=move |_| {
|
||||||
|
let state = app_state::State::get_from_context(cx);
|
||||||
debug!(idx=%id, count=%(*count.get()), "setting recipe count");
|
debug!(idx=%id, count=%(*count.get()), "setting recipe count");
|
||||||
state.set_recipe_count_by_index(id.as_ref(), count.get().parse().expect("recipe count isn't a valid usize number"));
|
state.set_recipe_count_by_index(id.as_ref(), count.get().parse().expect("recipe count isn't a valid usize number"));
|
||||||
})
|
})
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
use super::PlanningPage;
|
use super::PlanningPage;
|
||||||
use crate::components::recipe_selector::*;
|
use crate::components::recipe_plan::*;
|
||||||
|
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
|
|
||||||
@ -21,6 +21,6 @@ pub fn PlanPage<G: Html>(cx: Scope) -> View<G> {
|
|||||||
view! {cx,
|
view! {cx,
|
||||||
PlanningPage(
|
PlanningPage(
|
||||||
selected=Some("Plan".to_owned()),
|
selected=Some("Plan".to_owned()),
|
||||||
) { RecipeSelector() }
|
) { RecipePlan() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user