Calendar (WIP)

This commit is contained in:
Greg Burri 2025-01-29 14:37:25 +01:00
parent 9d3f9e9c60
commit 79a0aeb1b8
24 changed files with 613 additions and 231 deletions

159
Cargo.lock generated
View file

@ -26,7 +26,7 @@ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
"version_check", "version_check",
"zerocopy", "zerocopy 0.7.35",
] ]
[[package]] [[package]]
@ -315,9 +315,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@ -356,6 +356,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@ -420,6 +421,7 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
name = "common" name = "common"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"ron", "ron",
"serde", "serde",
] ]
@ -477,9 +479,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -832,10 +834,22 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "getrandom"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
"wasi 0.13.3+wasi-0.2.2",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.31.1"
@ -912,7 +926,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
"gloo-events", "gloo-events",
"gloo-utils", "gloo-utils",
"serde", "serde",
@ -1156,9 +1170,9 @@ checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.9.5" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]] [[package]]
name = "httpdate" name = "httpdate"
@ -1177,9 +1191,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.5.2" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -1573,7 +1587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -1609,7 +1623,7 @@ dependencies = [
"num-integer", "num-integer",
"num-iter", "num-iter",
"num-traits", "num-traits",
"rand", "rand 0.8.5",
"smallvec", "smallvec",
"zeroize", "zeroize",
] ]
@ -1707,7 +1721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [ dependencies = [
"base64ct", "base64ct",
"rand_core", "rand_core 0.6.4",
"subtle", "subtle",
] ]
@ -1808,7 +1822,7 @@ version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [ dependencies = [
"zerocopy", "zerocopy 0.7.35",
] ]
[[package]] [[package]]
@ -1861,8 +1875,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.0",
"zerocopy 0.8.14",
] ]
[[package]] [[package]]
@ -1872,7 +1897,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.0",
] ]
[[package]] [[package]]
@ -1881,7 +1916,17 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.15",
]
[[package]]
name = "rand_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.14",
] ]
[[package]] [[package]]
@ -1897,8 +1942,8 @@ dependencies = [
"derive_more", "derive_more",
"itertools", "itertools",
"lettre", "lettre",
"rand", "rand 0.9.0",
"rand_core", "rand_core 0.9.0",
"rinja", "rinja",
"ron", "ron",
"serde", "serde",
@ -1974,7 +2019,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom", "getrandom 0.2.15",
"libc", "libc",
"spin", "spin",
"untrusted", "untrusted",
@ -2047,7 +2092,7 @@ dependencies = [
"num-traits", "num-traits",
"pkcs1", "pkcs1",
"pkcs8", "pkcs8",
"rand_core", "rand_core 0.6.4",
"signature", "signature",
"spki", "spki",
"subtle", "subtle",
@ -2105,9 +2150,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.10.1" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
@ -2128,9 +2173,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
@ -2171,9 +2216,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.137" version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -2256,7 +2301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [ dependencies = [
"digest", "digest",
"rand_core", "rand_core 0.6.4",
] ]
[[package]] [[package]]
@ -2422,7 +2467,7 @@ dependencies = [
"memchr", "memchr",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"rand", "rand 0.8.5",
"rsa", "rsa",
"serde", "serde",
"sha1", "sha1",
@ -2461,7 +2506,7 @@ dependencies = [
"md-5", "md-5",
"memchr", "memchr",
"once_cell", "once_cell",
"rand", "rand 0.8.5",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -2588,13 +2633,13 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.15.0" version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"getrandom", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@ -2921,9 +2966,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
@ -3011,6 +3056,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "wasite" name = "wasite"
version = "0.1.0" version = "0.1.0"
@ -3315,6 +3369,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "write16" name = "write16"
version = "1.0.0" version = "1.0.0"
@ -3358,7 +3421,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"zerocopy-derive", "zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
dependencies = [
"zerocopy-derive 0.8.14",
] ]
[[package]] [[package]]
@ -3372,6 +3444,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "zerocopy-derive"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.5" version = "0.1.5"

View file

@ -1,9 +1,14 @@
* FIX: when the event blur is triggered when changing page, the async process doesn't finish all the time * FIX: when the event blur is triggered when changing page, the async process doesn't finish all the time
* User can change default_servings in profile
* Can choose servings number in recipe view
* Default number is the user setting user.default_servings
* A symbol show the native recipe servings number
* Check position of message error in profile/sign in/sign up with flex grid layout * Check position of message error in profile/sign in/sign up with flex grid layout
* Define the UI (mockups). * Define the UI (mockups).
* Two CSS: one for desktop and one for mobile * Two CSS: one for desktop and one for mobile
* Use CSS flex/grid to define a good design/layout * Use CSS flex/grid to define a good design/layout
* CSS for toast and modal dialog * CSS for toast and modal dialog
* Calendar: Choose the first day of the week
* Make a search page * Make a search page
Use FTS5: Use FTS5:
https://sqlite.org/fts5.html https://sqlite.org/fts5.html
@ -13,6 +18,10 @@
* Make the home page: Define what to display to the user * Make the home page: Define what to display to the user
* Show existing tags when editing a recipe * Show existing tags when editing a recipe
[ok] Add a table for website global settings with two column: name + value
* Add a boolean settings to enable/disable new inscription
[ok] Add a [is_admin] flag to [User] table
[ok] Test when there is an SQL error (syntax error for sample)
[ok] Drag and drop of steps and groups to define their order [ok] Drag and drop of steps and groups to define their order
[ok] Force tags in lowercase [ok] Force tags in lowercase
[ok] Remove the given language to recipe_edit and replace it by tr (like the header) [ok] Remove the given language to recipe_edit and replace it by tr (like the header)

View file

@ -16,7 +16,7 @@ tower-http = { version = "0.6", features = ["fs", "trace"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
chrono = "0.4" chrono = { version = "0.4", features = ["serde"] }
# Rust object notation, to load configuration files. # Rust object notation, to load configuration files.
ron = "0.8" ron = "0.8"
@ -30,8 +30,8 @@ sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
rinja = { version = "0.3" } rinja = { version = "0.3" }
argon2 = { version = "0.5", features = ["default", "std"] } argon2 = { version = "0.5", features = ["default", "std"] }
rand_core = { version = "0.6", features = ["std"] } rand_core = { version = "0.9", features = ["std"] }
rand = "0.8" rand = "0.9"
strum = "0.26" strum = "0.26"
strum_macros = "0.26" strum_macros = "0.26"

View file

@ -41,6 +41,10 @@
width: 14%; width: 14%;
text-align: center; text-align: center;
margin: 0; margin: 0;
&.current-month {
background-color: blue;
}
} }
} }
} }

View file

@ -172,6 +172,8 @@ CREATE TABLE [RecipeScheduled] (
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
); );
CREATE INDEX [RecipeScheduled_date_index] ON [RecipeScheduled]([date]);
CREATE TABLE [ShoppingEntry] ( CREATE TABLE [ShoppingEntry] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[user_id] INTEGER NOT NULL, [user_id] INTEGER NOT NULL,

View file

@ -1,10 +1,9 @@
use chrono::prelude::*; use chrono::{prelude::*, Days};
use common::ron_api::Difficulty;
use itertools::Itertools; use itertools::Itertools;
use super::{Connection, DBError, Result}; use super::{Connection, DBError, Result};
use crate::data::model; use crate::{data::model, user_authentication};
use common::ron_api::Difficulty;
impl Connection { impl Connection {
/// Returns all the recipe titles where recipe is written in the given language. /// Returns all the recipe titles where recipe is written in the given language.
@ -106,11 +105,10 @@ SELECT COUNT(*)
FROM [Recipe] FROM [Recipe]
INNER JOIN [User] ON [User].[id] = [Recipe].[user_id] INNER JOIN [User] ON [User].[id] = [Recipe].[user_id]
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id] INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
WHERE [Group].[id] IN ({}) AND ([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2)) WHERE [Group].[id] IN ({}) AND ([user_id] = $1 OR (SELECT [is_admin] FROM [User] WHERE [id] = $1))
"#, "#,
params params
); );
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id); let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
for id in group_ids { for id in group_ids {
query = query.bind(id); query = query.bind(id);
@ -147,11 +145,10 @@ FROM [Recipe]
INNER JOIN [User] ON [User].[id] = [Recipe].[user_id] INNER JOIN [User] ON [User].[id] = [Recipe].[user_id]
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id] INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id] INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
WHERE [Step].[id] IN ({}) AND ([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2)) WHERE [Step].[id] IN ({}) AND ([user_id] = $1 OR (SELECT [is_admin] FROM [User] WHERE [id] = $1))
"#, "#,
params params
); );
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id); let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
for id in steps_ids { for id in steps_ids {
query = query.bind(id); query = query.bind(id);
@ -199,11 +196,10 @@ INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id] INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
INNER JOIN [Ingredient] ON [Ingredient].[step_id] = [Step].[id] INNER JOIN [Ingredient] ON [Ingredient].[step_id] = [Step].[id]
WHERE [Ingredient].[id] IN ({}) AND WHERE [Ingredient].[id] IN ({}) AND
([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2)) ([user_id] = $1 OR (SELECT [is_admin] FROM [User] WHERE [id] = $1))
"#, "#,
params params
); );
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id); let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
for id in ingredients_ids { for id in ingredients_ids {
query = query.bind(id); query = query.bind(id);
@ -755,6 +751,73 @@ VALUES ($1, $2)
Ok(()) Ok(())
} }
pub async fn add_schedule_recipe(
&self,
user_id: i64,
recipe_id: i64,
date: NaiveDate,
servings: u32,
) -> Result<()> {
sqlx::query(
r#"
INSERT INTO [RecipeScheduled] (user_id, recipe_id, date, servings)
VALUES ($1, $2, $3, $4)
"#,
)
.bind(user_id)
.bind(recipe_id)
.bind(date)
.bind(servings)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn remove_scheduled_recipe(
&self,
user_id: i64,
recipe_id: i64,
date: NaiveDate,
) -> Result<()> {
sqlx::query(
r#"
DELETE FROM [RecipeScheduled]
WHERE [user_id] = $1 AND [recipe_id] = $2 AND [date] = $3
"#,
)
.bind(user_id)
.bind(recipe_id)
.bind(date)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn get_scheduled_recipes(
&self,
user_id: i64,
start_date: NaiveDate,
end_date: NaiveDate,
) -> Result<Vec<(NaiveDate, String, i64)>> {
sqlx::query_as(
r#"
SELECT [date], [Recipe].[title], [Recipe].[id], [RecipeScheduled].[date]
FROM [RecipeScheduled]
INNER JOIN [Recipe] ON [Recipe].[id] = [RecipeScheduled].[recipe_id]
WHERE [RecipeScheduled].[user_id] = $1 AND [date] >= $2 AND [date] <= $3
ORDER BY [date]
"#,
)
.bind(user_id)
.bind(start_date)
.bind(end_date)
.fetch_all(&self.pool)
.await
.map_err(DBError::from)
}
} }
#[cfg(test)] #[cfg(test)]
@ -884,4 +947,83 @@ VALUES
Ok(()) Ok(())
} }
#[tokio::test]
async fn schedule_recipe() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let user_id = create_a_user(&connection).await?;
let recipe_id_1 = connection.create_recipe(user_id).await?;
connection.set_recipe_title(recipe_id_1, "recipe 1").await?;
let recipe_id_2 = connection.create_recipe(user_id).await?;
connection.set_recipe_title(recipe_id_2, "recipe 2").await?;
let today = NaiveDate::from_ymd_opt(2025, 1, 23).unwrap();
let yesterday = today - Days::new(1);
let tomorrow = today + Days::new(1);
connection
.add_schedule_recipe(user_id, recipe_id_1, today, 4)
.await?;
connection
.add_schedule_recipe(user_id, recipe_id_2, yesterday, 4)
.await?;
connection
.add_schedule_recipe(user_id, recipe_id_1, tomorrow, 4)
.await?;
assert_eq!(
connection
.get_scheduled_recipes(user_id, today, today)
.await?,
vec![(
NaiveDate::from_ymd_opt(2025, 1, 23).unwrap(),
"recipe 1".to_string(),
1
)]
);
assert_eq!(
connection
.get_scheduled_recipes(user_id, yesterday, tomorrow)
.await?,
vec![
(
NaiveDate::from_ymd_opt(2025, 1, 22).unwrap(),
"recipe 2".to_string(),
2
),
(
NaiveDate::from_ymd_opt(2025, 1, 23).unwrap(),
"recipe 1".to_string(),
1
),
(
NaiveDate::from_ymd_opt(2025, 1, 24).unwrap(),
"recipe 1".to_string(),
1
)
]
);
connection
.remove_scheduled_recipe(user_id, recipe_id_1, today)
.await?;
connection
.remove_scheduled_recipe(user_id, recipe_id_2, yesterday)
.await?;
connection
.remove_scheduled_recipe(user_id, recipe_id_1, tomorrow)
.await?;
assert_eq!(
connection
.get_scheduled_recipes(user_id, yesterday, tomorrow)
.await?,
vec![]
);
Ok(())
}
} }

View file

@ -1,5 +1,5 @@
use chrono::{prelude::*, Duration}; use chrono::{prelude::*, Duration};
use rand::distributions::{Alphanumeric, DistString}; use rand::distr::{Alphanumeric, SampleString};
use sqlx::Sqlite; use sqlx::Sqlite;
use super::{Connection, DBError, Result}; use super::{Connection, DBError, Result};
@ -57,7 +57,7 @@ pub enum ResetPasswordResult {
} }
fn generate_token() -> String { fn generate_token() -> String {
Alphanumeric.sample_string(&mut rand::thread_rng(), consts::TOKEN_SIZE) Alphanumeric.sample_string(&mut rand::rng(), consts::TOKEN_SIZE)
} }
impl Connection { impl Connection {

View file

@ -177,6 +177,10 @@ async fn main() {
"/recipe/set_ingredients_order", "/recipe/set_ingredients_order",
put(services::ron::set_ingredients_order), put(services::ron::set_ingredients_order),
) )
.route(
"/calendar/get_scheduled_recipes",
get(services::ron::get_scheduled_recipes),
)
.fallback(services::ron::not_found); .fallback(services::ron::not_found);
let fragments_routes = Router::new().route( let fragments_routes = Router::new().route(

View file

@ -5,6 +5,7 @@ use axum::{
response::{ErrorResponse, IntoResponse, Result}, response::{ErrorResponse, IntoResponse, Result},
}; };
use axum_extra::extract::cookie::{Cookie, CookieJar}; use axum_extra::extract::cookie::{Cookie, CookieJar};
use chrono::NaiveDate;
use serde::Deserialize; use serde::Deserialize;
// use tracing::{event, Level}; // use tracing::{event, Level};
@ -183,11 +184,11 @@ async fn check_user_rights_recipe_ingredient(
async fn check_user_rights_recipe_ingredients( async fn check_user_rights_recipe_ingredients(
connection: &db::Connection, connection: &db::Connection,
user: &Option<model::User>, user: &Option<model::User>,
step_ids: &[i64], ingredient_ids: &[i64],
) -> Result<()> { ) -> Result<()> {
if user.is_none() if user.is_none()
|| !connection || !connection
.can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, step_ids) .can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, ingredient_ids)
.await? .await?
{ {
Err(ErrorResponse::from(ron_error( Err(ErrorResponse::from(ron_error(
@ -599,7 +600,39 @@ pub async fn set_ingredients_order(
Ok(StatusCode::OK) Ok(StatusCode::OK)
} }
///// 404 ///// /// Calendar ///
#[derive(Deserialize)]
pub struct DateRange {
start_date: NaiveDate,
end_date: NaiveDate,
}
#[debug_handler]
pub async fn get_scheduled_recipes(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
date_range: Query<DateRange>,
) -> Result<impl IntoResponse> {
if let Some(user) = user {
Ok(ron_response(
StatusCode::OK,
common::ron_api::ScheduledRecipes {
recipes: connection
.get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date)
.await?,
},
))
} else {
Err(ErrorResponse::from(ron_error(
StatusCode::UNAUTHORIZED,
NOT_AUTHORIZED_MESSAGE,
)))
}
}
/// 404 ///
#[debug_handler] #[debug_handler]
pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse { pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
ron_error(StatusCode::NOT_FOUND, "Not found") ron_error(StatusCode::NOT_FOUND, "Not found")

View file

@ -1,12 +1,13 @@
use std::{borrow::Borrow, fs::File, sync::LazyLock}; use std::{borrow::Borrow, fs::File, sync::LazyLock};
use common::utils;
use ron::de::from_reader; use ron::de::from_reader;
use serde::Deserialize; use serde::Deserialize;
use strum::EnumCount; use strum::EnumCount;
use strum_macros::EnumCount; use strum_macros::EnumCount;
use tracing::{event, Level}; use tracing::{event, Level};
use crate::{consts, utils}; use crate::consts;
#[derive(Debug, Clone, EnumCount, Deserialize)] #[derive(Debug, Clone, EnumCount, Deserialize)]
pub enum Sentence { pub enum Sentence {
@ -109,6 +110,10 @@ pub enum Sentence {
RecipeIngredientQuantity, RecipeIngredientQuantity,
RecipeIngredientUnit, RecipeIngredientUnit,
RecipeIngredientComment, RecipeIngredientComment,
RecipeDeleteConfirmation,
RecipeGroupDeleteConfirmation,
RecipeStepDeleteConfirmation,
RecipeIngredientDeleteConfirmation,
// View Recipe. // View Recipe.
RecipeOneServing, RecipeOneServing,

View file

@ -39,44 +39,3 @@ pub fn get_url_from_host(host: &str) -> String {
host host
) )
} }
pub fn substitute(str: &str, pattern: &str, replacements: &[&str]) -> String {
let mut result = String::with_capacity(
(str.len() + replacements.iter().map(|s| s.len()).sum::<usize>())
.saturating_sub(pattern.len() * replacements.len()),
);
let mut i = 0;
for s in str.split(pattern) {
result.push_str(s);
if i < replacements.len() {
result.push_str(replacements[i]);
}
i += 1;
}
if i == 1 {
return str.to_string();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_substitute() {
assert_eq!(substitute("", "", &[]), "");
assert_eq!(substitute("", "", &[""]), "");
assert_eq!(substitute("", "{}", &["a"]), "");
assert_eq!(substitute("a", "{}", &["b"]), "a");
assert_eq!(substitute("a{}", "{}", &["b"]), "ab");
assert_eq!(substitute("{}c", "{}", &["b"]), "bc");
assert_eq!(substitute("a{}c", "{}", &["b"]), "abc");
assert_eq!(substitute("{}b{}", "{}", &["a", "c"]), "abc");
assert_eq!(substitute("{}{}{}", "{}", &["a", "bc", "def"]), "abcdef");
assert_eq!(substitute("{}{}{}", "{}", &["a"]), "a");
}
}

View file

@ -38,7 +38,7 @@
<ul class="days"> <ul class="days">
{% for i in 0..7 %} {% for i in 0..7 %}
{% for j in 0..5 %} {% for j in 0..5 %}
<li id="day-{{i}}{{j}}"></li> <li id="day-{{i}}{{j}}"><div class="number"></div><div class="scheduled-recipes"></div></li>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -134,6 +134,11 @@
</div> </div>
<div class="dropzone"></div> <div class="dropzone"></div>
<span class="recipe-delete-confirmation">{{ tr.t(Sentence::RecipeDeleteConfirmation) }}</span>
<span class="recipe-group-delete-confirmation">{{ tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span>
<span class="recipe-step-delete-confirmation">{{ tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span>
<span class="recipe-ingredient-delete-confirmation">{{ tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span>
</div> </div>
</div> </div>

View file

@ -95,6 +95,10 @@
(RecipeIngredientQuantity, "Quantity"), (RecipeIngredientQuantity, "Quantity"),
(RecipeIngredientUnit, "Unit"), (RecipeIngredientUnit, "Unit"),
(RecipeIngredientComment, "Comment"), (RecipeIngredientComment, "Comment"),
(RecipeDeleteConfirmation, "Are you sure to delete the recipe: '{}'?"),
(RecipeGroupDeleteConfirmation, "Are you sure to delete the group: '{}'?"),
(RecipeStepDeleteConfirmation, "Are you sure to delete the step: '{}'?"),
(RecipeIngredientDeleteConfirmation, "Are you sure to delete the ingredient: '{}'?"),
(RecipeOneServing, "1 serving"), (RecipeOneServing, "1 serving"),
(RecipeSomeServings, "{} servings"), (RecipeSomeServings, "{} servings"),
@ -217,6 +221,10 @@
(RecipeIngredientQuantity, "Quantité"), (RecipeIngredientQuantity, "Quantité"),
(RecipeIngredientUnit, "Unité"), (RecipeIngredientUnit, "Unité"),
(RecipeIngredientComment, "Commentaire"), (RecipeIngredientComment, "Commentaire"),
(RecipeDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer la recette : '{}' ?"),
(RecipeGroupDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer le groupe : '{}' ?"),
(RecipeStepDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer l'étape : '{}' ?"),
(RecipeIngredientDeleteConfirmation, "Êtes-vous sûr de vouloir supprimer 'ingrédient : '{}' ?"),
(RecipeOneServing, "pour 1 personne"), (RecipeOneServing, "pour 1 personne"),
(RecipeSomeServings, "pour {} personnes"), (RecipeSomeServings, "pour {} personnes"),

View file

@ -7,3 +7,4 @@ edition = "2021"
[dependencies] [dependencies]
ron = "0.8" ron = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }

View file

@ -1,3 +1,4 @@
use chrono::NaiveDate;
use ron::ser::{to_string_pretty, PrettyConfig}; use ron::ser::{to_string_pretty, PrettyConfig};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -16,7 +17,7 @@ pub struct Id {
pub id: i64, pub id: i64,
} }
///// RECIPE ///// /// RECIPE ///
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct SetRecipeTitle { pub struct SetRecipeTitle {
@ -158,7 +159,7 @@ pub struct Ingredient {
pub quantity_unit: String, pub quantity_unit: String,
} }
///// PROFILE ///// /// PROFILE ///
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct UpdateProfile { pub struct UpdateProfile {
@ -174,3 +175,11 @@ where
// TODO: handle'unwrap'. // TODO: handle'unwrap'.
to_string_pretty(&ron, PrettyConfig::new()).unwrap() to_string_pretty(&ron, PrettyConfig::new()).unwrap()
} }
/// Calendar ///
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ScheduledRecipes {
// (Scheduled date, recipe title, recipe id).
pub recipes: Vec<(NaiveDate, String, i64)>,
}

View file

@ -12,3 +12,44 @@ pub fn validate_password(password: &str) -> PasswordValidation {
PasswordValidation::Ok PasswordValidation::Ok
} }
} }
pub fn substitute(str: &str, pattern: &str, replacements: &[&str]) -> String {
let mut result = String::with_capacity(
(str.len() + replacements.iter().map(|s| s.len()).sum::<usize>())
.saturating_sub(pattern.len() * replacements.len()),
);
let mut i = 0;
for s in str.split(pattern) {
result.push_str(s);
if i < replacements.len() {
result.push_str(replacements[i]);
}
i += 1;
}
if i == 1 {
return str.to_string();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_substitute() {
assert_eq!(substitute("", "", &[]), "");
assert_eq!(substitute("", "", &[""]), "");
assert_eq!(substitute("", "{}", &["a"]), "");
assert_eq!(substitute("a", "{}", &["b"]), "a");
assert_eq!(substitute("a{}", "{}", &["b"]), "ab");
assert_eq!(substitute("{}c", "{}", &["b"]), "bc");
assert_eq!(substitute("a{}c", "{}", &["b"]), "abc");
assert_eq!(substitute("{}b{}", "{}", &["a", "c"]), "abc");
assert_eq!(substitute("{}{}{}", "{}", &["a", "bc", "def"]), "abcdef");
assert_eq!(substitute("{}{}{}", "{}", &["a"]), "a");
}
}

View file

@ -13,7 +13,7 @@ default = ["console_error_panic_hook"]
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
chrono = "0.4" chrono = { version = "0.4", features = ["serde"] }
ron = "0.8" ron = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -1,100 +1,144 @@
use std::{ use std::sync::{
ops::{AddAssign, SubAssign},
sync::{
atomic::{AtomicI32, AtomicU32, Ordering}, atomic::{AtomicI32, AtomicU32, Ordering},
Arc, Arc, Mutex,
},
}; };
use chrono::{offset::Local, Datelike, Days, NaiveDate, Weekday}; use chrono::{offset::Local, DateTime, Datelike, Days, Months, NaiveDate, Weekday};
use common::ron_api;
use gloo::{console::log, events::EventListener}; use gloo::{console::log, events::EventListener};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
use web_sys::Element; use web_sys::Element;
use crate::utils::{by_id, SelectorExt}; use crate::{
request,
utils::{by_id, selector, SelectorExt},
};
pub fn setup(calendar: &Element) { struct CalendarStateInternal {
current_date: DateTime<Local>,
selected_date: DateTime<Local>,
}
#[derive(Clone)]
struct CalendarState {
internal_state: Arc<Mutex<CalendarStateInternal>>,
}
impl CalendarState {
pub fn new() -> Self {
let current_date = Local::now();
Self {
internal_state: Arc::new(Mutex::new(CalendarStateInternal {
current_date,
selected_date: current_date,
})),
}
}
pub fn to_next_month(&self) -> DateTime<Local> {
let mut locker = self.internal_state.lock().unwrap();
let new_date = locker
.current_date
.checked_add_months(Months::new(1))
.unwrap();
locker.current_date = new_date;
new_date
}
pub fn to_previous_month(&self) -> DateTime<Local> {
let mut locker = self.internal_state.lock().unwrap();
let new_date = locker
.current_date
.checked_sub_months(Months::new(1))
.unwrap();
locker.current_date = new_date;
new_date
}
pub fn get_current_date(&self) -> DateTime<Local> {
self.internal_state.lock().unwrap().current_date
}
pub fn get_selected_date(&self) -> DateTime<Local> {
self.internal_state.lock().unwrap().selected_date
}
}
pub fn setup(calendar: Element) {
let prev: Element = calendar.selector(".prev"); let prev: Element = calendar.selector(".prev");
let next: Element = calendar.selector(".next"); let next: Element = calendar.selector(".next");
let current_month = Arc::new(AtomicU32::new(Local::now().month())); let state = CalendarState::new();
let current_year = Arc::new(AtomicI32::new(Local::now().year()));
display_month(calendar, Local::now().year(), Local::now().month()); display_month(&calendar, state.get_current_date());
let calendar_clone = calendar.clone(); let calendar_clone = calendar.clone();
let current_month_clone = current_month.clone(); let state_clone = state.clone();
let current_year_clone = current_year.clone();
EventListener::new(&prev, "click", move |_event| { EventListener::new(&prev, "click", move |_event| {
let mut m = current_month_clone.load(Ordering::Relaxed) - 1; let m = state_clone.to_previous_month();
if m == 0 { display_month(&calendar_clone, m);
current_year_clone.fetch_sub(1, Ordering::Relaxed);
m = 12
}
current_month_clone.store(m, Ordering::Relaxed);
display_month(
&calendar_clone,
current_year_clone.load(Ordering::Relaxed),
m,
);
}) })
.forget(); .forget();
let calendar_clone = calendar.clone(); let calendar_clone = calendar.clone();
let current_month_clone = current_month.clone(); let state_clone = state.clone();
let current_year_clone = current_year.clone();
EventListener::new(&next, "click", move |_event| { EventListener::new(&next, "click", move |_event| {
let mut m = current_month_clone.load(Ordering::Relaxed) + 1; let m = state_clone.to_next_month();
if m == 13 { display_month(&calendar_clone, m);
current_year_clone.fetch_add(1, Ordering::Relaxed);
m = 1
}
current_month_clone.store(m, Ordering::Relaxed);
display_month(
&calendar_clone,
current_year_clone.load(Ordering::Relaxed),
m,
);
}) })
.forget(); .forget();
// now.weekday() // let days: Element = calendar.selector(".days");
// let state_clone = state.clone();
// EventListener::new(&days, "click", move |event| {
// log!(event);
// let target: Element = event.target().unwrap().dyn_into().unwrap();
// if
// })
// .forget();
// console!(now.to_string()); // let calendar_clone = calendar.clone();
// let current_month_clone = current_month.clone();
// let current_year_clone = current_year.clone();
// EventListener::new(&next, "click", move |_event| {
// let mut m = current_month_clone.load(Ordering::Relaxed) + 1;
// if m == 13 {
// current_year_clone.fetch_add(1, Ordering::Relaxed);
// m = 1
// }
// current_month_clone.store(m, Ordering::Relaxed);
// display_month(
// &calendar_clone,
// current_year_clone.load(Ordering::Relaxed),
// m,
// );
// })
// .forget();
} }
// fn translate_month(month: u32) -> &'static str { const NB_CALENDAR_ROW: u64 = 5;
// match
// }
fn display_month(calendar: &Element, year: i32, month: u32) {
log!(year, month);
fn display_month(calendar: &Element, date: DateTime<Local>) {
calendar calendar
.selector::<Element>(".year") .selector::<Element>(".year")
.set_inner_html(&year.to_string()); .set_inner_html(&date.year().to_string());
for (i, m) in calendar for (i, m) in calendar
.selector_all::<Element>(".month") .selector_all::<Element>(".month")
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
if i as u32 + 1 == month { if i as u32 + 1 == date.month() {
m.set_class_name("month current"); m.set_class_name("month current");
} else { } else {
m.set_class_name("month"); m.set_class_name("month");
} }
} }
// calendar let mut current = date;
// .selector::<Element>(".month")
// .set_inner_html(&month.to_string());
let mut current = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); while (current - Days::new(1)).month() == date.month() {
// let mut day = Local:: ;
while (current - Days::new(1)).month() == month {
current = current - Days::new(1); current = current - Days::new(1);
} }
@ -102,20 +146,46 @@ fn display_month(calendar: &Element, year: i32, month: u32) {
current = current - Days::new(1); current = current - Days::new(1);
} }
for i in 0..7 { let first_day = current;
for j in 0..5 {
let li: Element = by_id(&format!("day-{}{}", i, j));
li.set_inner_html(&current.day().to_string());
if current == Local::now().date_naive() { for i in 0..7 {
li.set_class_name("current-month today"); for j in 0..NB_CALENDAR_ROW {
} else if current.month() == month { let day_element: Element = by_id(&format!("day-{}{}", i, j));
li.set_class_name("current-month"); let day_content: Element = day_element.selector(".number");
day_content.set_inner_html(&current.day().to_string());
if current == Local::now() {
day_element.set_class_name("current-month today");
} else if current.month() == date.month() {
day_element.set_class_name("current-month");
} else { } else {
li.set_class_name(""); day_element.set_class_name("");
} }
current = current + Days::new(1); current = current + Days::new(1);
} }
} }
spawn_local(async move {
let scheduled_recipes: ron_api::ScheduledRecipes = request::get(
"calendar/get_scheduled_recipes",
[
("start_date", first_day.date_naive().to_string()),
(
"end_date",
(first_day + Days::new(NB_CALENDAR_ROW * 7))
.date_naive()
.to_string(),
),
],
)
.await
.unwrap();
for recipe in scheduled_recipes.recipes {
log!(recipe.1);
}
// create_tag_elements(recipe_id, &tags.tags);
});
} }

View file

@ -1,3 +1,10 @@
use common::ron_api;
use gloo::{console::log, events::EventListener, utils::window};
use utils::by_id;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlSelectElement;
mod calendar; mod calendar;
mod modal_dialog; mod modal_dialog;
mod on_click; mod on_click;
@ -7,14 +14,6 @@ mod request;
mod toast; mod toast;
mod utils; mod utils;
use gloo::{console::log, events::EventListener, utils::window};
use utils::by_id;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::HtmlSelectElement;
use common::ron_api;
#[wasm_bindgen(start)] #[wasm_bindgen(start)]
pub fn main() -> Result<(), JsValue> { pub fn main() -> Result<(), JsValue> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();

View file

@ -6,17 +6,13 @@ use crate::{
utils::{by_id, selector_and_clone, SelectorExt}, utils::{by_id, selector_and_clone, SelectorExt},
}; };
pub enum DialogContent<'a, T> pub async fn show(element_selector: &str) -> bool {
where show_and_initialize(element_selector, async |_| {}).await
T: Fn(&Element),
{
Text(&'a str),
CloneFromElement(&'a str, T),
} }
pub async fn show<T>(content: DialogContent<'_, T>) -> bool pub async fn show_and_initialize<T>(element_selector: &str, initializer: T) -> bool
where where
T: Fn(&Element), T: AsyncFn(Element),
{ {
let dialog: HtmlDialogElement = by_id("modal-dialog"); let dialog: HtmlDialogElement = by_id("modal-dialog");
@ -25,15 +21,10 @@ where
let content_element = dialog.selector::<Element>(".content"); let content_element = dialog.selector::<Element>(".content");
match content {
DialogContent::Text(message) => content_element.set_inner_html(message),
DialogContent::CloneFromElement(element_selector, initilizer) => {
let element: Element = selector_and_clone(element_selector); let element: Element = selector_and_clone(element_selector);
content_element.set_inner_html(""); content_element.set_inner_html("");
content_element.append_child(&element).unwrap(); content_element.append_child(&element).unwrap();
initilizer(&element); initializer(element).await;
}
}
dialog.show_modal().unwrap(); dialog.show_modal().unwrap();

View file

@ -1,5 +1,6 @@
use std::{cell::RefCell, rc, sync::Mutex}; use std::{cell::RefCell, rc, sync::Mutex};
use common::{ron_api, utils::substitute};
use gloo::{ use gloo::{
events::{EventListener, EventListenerOptions}, events::{EventListener, EventListenerOptions},
net::http::Request, net::http::Request,
@ -12,14 +13,17 @@ use web_sys::{
KeyboardEvent, KeyboardEvent,
}; };
use common::ron_api;
use crate::{ use crate::{
modal_dialog, request, modal_dialog, request,
toast::{self, Level}, toast::{self, Level},
utils::{by_id, selector, selector_and_clone, SelectorExt}, utils::{by_id, selector, selector_and_clone, SelectorExt},
}; };
use futures::{
future::{FutureExt, Ready},
pin_mut, select, Future,
};
pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> { pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
// Title. // Title.
{ {
@ -248,12 +252,18 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
// Delete recipe button. // Delete recipe button.
let delete_button: HtmlInputElement = by_id("input-delete"); let delete_button: HtmlInputElement = by_id("input-delete");
EventListener::new(&delete_button, "click", move |_event| { EventListener::new(&delete_button, "click", move |_event| {
let title: HtmlInputElement = by_id("input-title");
spawn_local(async move { spawn_local(async move {
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!( if modal_dialog::show_and_initialize(
"Are you sure to delete the recipe '{}'", "#hidden-templates .recipe-delete-confirmation",
title.value() async |element| {
))) let title: HtmlInputElement = by_id("input-title");
element.set_inner_html(&substitute(
&element.inner_html(),
"{}",
&[&title.value()],
));
},
)
.await .await
{ {
let body = ron_api::Id { id: recipe_id }; let body = ron_api::Id { id: recipe_id };
@ -377,14 +387,18 @@ fn create_group_element(group: &ron_api::Group) -> Element {
let group_element_cloned = group_element.clone(); let group_element_cloned = group_element.clone();
let delete_button: HtmlInputElement = group_element.selector(".input-group-delete"); let delete_button: HtmlInputElement = group_element.selector(".input-group-delete");
EventListener::new(&delete_button, "click", move |_event| { EventListener::new(&delete_button, "click", move |_event| {
// FIXME: How to avoid cloning twice?
let group_element_cloned = group_element_cloned.clone();
spawn_local(async move {
if modal_dialog::show_and_initialize(
"#hidden-templates .recipe-group-delete-confirmation",
async move |element| {
let name = group_element_cloned let name = group_element_cloned
.selector::<HtmlInputElement>(".input-group-name") .selector::<HtmlInputElement>(".input-group-name")
.value(); .value();
spawn_local(async move { element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&name]));
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!( },
"Are you sure to delete the group '{}'", )
name
)))
.await .await
{ {
let body = ron_api::Id { id: group_id }; let body = ron_api::Id { id: group_id };
@ -515,14 +529,18 @@ fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element
let step_element_cloned = step_element.clone(); let step_element_cloned = step_element.clone();
let delete_button: HtmlInputElement = step_element.selector(".input-step-delete"); let delete_button: HtmlInputElement = step_element.selector(".input-step-delete");
EventListener::new(&delete_button, "click", move |_event| { EventListener::new(&delete_button, "click", move |_event| {
// FIXME: How to avoid cloning twice?
let step_element_cloned = step_element_cloned.clone();
spawn_local(async move {
if modal_dialog::show_and_initialize(
"#hidden-templates .recipe-step-delete-confirmation",
async move |element| {
let action = step_element_cloned let action = step_element_cloned
.selector::<HtmlTextAreaElement>(".text-area-step-action") .selector::<HtmlTextAreaElement>(".text-area-step-action")
.value(); .value();
spawn_local(async move { element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&action]));
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!( },
"Are you sure to delete the step '{}'", )
action
)))
.await .await
{ {
let body = ron_api::Id { id: step_id }; let body = ron_api::Id { id: step_id };
@ -665,14 +683,18 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
let ingredient_element_cloned = ingredient_element.clone(); let ingredient_element_cloned = ingredient_element.clone();
let delete_button: HtmlInputElement = ingredient_element.selector(".input-ingredient-delete"); let delete_button: HtmlInputElement = ingredient_element.selector(".input-ingredient-delete");
EventListener::new(&delete_button, "click", move |_event| { EventListener::new(&delete_button, "click", move |_event| {
// FIXME: How to avoid cloning twice?
let ingredient_element_cloned = ingredient_element_cloned.clone();
spawn_local(async move {
if modal_dialog::show_and_initialize(
"#hidden-templates .recipe-ingredient-delete-confirmation",
async move |element| {
let name = ingredient_element_cloned let name = ingredient_element_cloned
.selector::<HtmlInputElement>(".input-ingredient-name") .selector::<HtmlInputElement>(".input-ingredient-name")
.value(); .value();
spawn_local(async move { element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&name]));
if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!( },
"Are you sure to delete the ingredient '{}'", )
name
)))
.await .await
{ {
let body = ron_api::Id { id: ingredient_id }; let body = ron_api::Id { id: ingredient_id };

View file

@ -1,3 +1,6 @@
use std::future::Future;
use common::ron_api;
use gloo::{ use gloo::{
console::console, console::console,
events::EventListener, events::EventListener,
@ -11,8 +14,6 @@ use web_sys::{
KeyboardEvent, KeyboardEvent,
}; };
use common::ron_api;
use crate::{ use crate::{
calendar, modal_dialog, request, calendar, modal_dialog, request,
toast::{self, Level}, toast::{self, Level},
@ -22,15 +23,10 @@ use crate::{
pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> { pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
let add_to_planner: Element = selector("#recipe-view .add-to-planner"); let add_to_planner: Element = selector("#recipe-view .add-to-planner");
EventListener::new(&add_to_planner, "click", move |_event| { EventListener::new(&add_to_planner, "click", move |_event| {
// console!("CLICK".to_string());
spawn_local(async move { spawn_local(async move {
modal_dialog::show(modal_dialog::DialogContent::CloneFromElement( modal_dialog::show_and_initialize("#hidden-templates .calendar", async |element| {
"#hidden-templates .calendar",
|element| {
// console!("SETUP...".to_string());
calendar::setup(element); calendar::setup(element);
}, })
))
.await; .await;
}); });
}) })

View file

@ -1,9 +1,8 @@
use common::ron_api;
use gloo::net::http::{Request, RequestBuilder}; use gloo::net::http::{Request, RequestBuilder};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error; use thiserror::Error;
use common::ron_api;
use crate::toast::{self, Level}; use crate::toast::{self, Level};
#[derive(Error, Debug)] #[derive(Error, Debug)]