Calendar (WIP)
authorGreg Burri <greg.burri@gmail.com>
Wed, 29 Jan 2025 13:37:25 +0000 (14:37 +0100)
committerGreg Burri <greg.burri@gmail.com>
Wed, 29 Jan 2025 13:37:25 +0000 (14:37 +0100)
24 files changed:
Cargo.lock
TODO.md
backend/Cargo.toml
backend/scss/calendar.scss
backend/sql/version_1.sql
backend/src/data/db/recipe.rs
backend/src/data/db/user.rs
backend/src/main.rs
backend/src/services/ron.rs
backend/src/translation.rs
backend/src/utils.rs
backend/templates/calendar.html
backend/templates/recipe_edit.html
backend/translation.ron
common/Cargo.toml
common/src/ron_api.rs
common/src/utils.rs
frontend/Cargo.toml
frontend/src/calendar.rs
frontend/src/lib.rs
frontend/src/modal_dialog.rs
frontend/src/recipe_edit.rs
frontend/src/recipe_view.rs
frontend/src/request.rs

index a53d261..04b5c9f 100644 (file)
@@ -26,7 +26,7 @@ dependencies = [
  "cfg-if",
  "once_cell",
  "version_check",
- "zerocopy",
+ "zerocopy 0.7.35",
 ]
 
 [[package]]
@@ -315,9 +315,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.16.0"
+version = "3.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
 
 [[package]]
 name = "byteorder"
@@ -356,6 +356,7 @@ dependencies = [
  "iana-time-zone",
  "js-sys",
  "num-traits",
+ "serde",
  "wasm-bindgen",
  "windows-targets 0.52.6",
 ]
@@ -420,6 +421,7 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 name = "common"
 version = "0.1.0"
 dependencies = [
+ "chrono",
  "ron",
  "serde",
 ]
@@ -477,9 +479,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.16"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
 dependencies = [
  "libc",
 ]
@@ -832,10 +834,22 @@ dependencies = [
  "cfg-if",
  "js-sys",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "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]]
 name = "gimli"
 version = "0.31.1"
@@ -912,7 +926,7 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
  "gloo-events",
  "gloo-utils",
  "serde",
@@ -1156,9 +1170,9 @@ checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
 
 [[package]]
 name = "httparse"
-version = "1.9.5"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
+checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
 
 [[package]]
 name = "httpdate"
@@ -1177,9 +1191,9 @@ dependencies = [
 
 [[package]]
 name = "hyper"
-version = "1.5.2"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -1573,7 +1587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
 dependencies = [
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.52.0",
 ]
 
@@ -1609,7 +1623,7 @@ dependencies = [
  "num-integer",
  "num-iter",
  "num-traits",
- "rand",
+ "rand 0.8.5",
  "smallvec",
  "zeroize",
 ]
@@ -1707,7 +1721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
 dependencies = [
  "base64ct",
- "rand_core",
+ "rand_core 0.6.4",
  "subtle",
 ]
 
@@ -1808,7 +1822,7 @@ version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
 dependencies = [
- "zerocopy",
+ "zerocopy 0.7.35",
 ]
 
 [[package]]
@@ -1861,8 +1875,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "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]]
@@ -1872,7 +1897,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "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]]
@@ -1881,7 +1916,17 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 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]]
@@ -1897,8 +1942,8 @@ dependencies = [
  "derive_more",
  "itertools",
  "lettre",
- "rand",
- "rand_core",
+ "rand 0.9.0",
+ "rand_core 0.9.0",
  "rinja",
  "ron",
  "serde",
@@ -1974,7 +2019,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
  "cc",
  "cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
  "libc",
  "spin",
  "untrusted",
@@ -2047,7 +2092,7 @@ dependencies = [
  "num-traits",
  "pkcs1",
  "pkcs8",
- "rand_core",
+ "rand_core 0.6.4",
  "signature",
  "spki",
  "subtle",
@@ -2105,9 +2150,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.10.1"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
+checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
 
 [[package]]
 name = "rustls-webpki"
@@ -2128,9 +2173,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
 
 [[package]]
 name = "ryu"
-version = "1.0.18"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
 
 [[package]]
 name = "scopeguard"
@@ -2171,9 +2216,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.137"
+version = "1.0.138"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
+checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
 dependencies = [
  "itoa",
  "memchr",
@@ -2256,7 +2301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
 dependencies = [
  "digest",
- "rand_core",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
@@ -2422,7 +2467,7 @@ dependencies = [
  "memchr",
  "once_cell",
  "percent-encoding",
- "rand",
+ "rand 0.8.5",
  "rsa",
  "serde",
  "sha1",
@@ -2461,7 +2506,7 @@ dependencies = [
  "md-5",
  "memchr",
  "once_cell",
- "rand",
+ "rand 0.8.5",
  "serde",
  "serde_json",
  "sha2",
@@ -2588,13 +2633,13 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.15.0"
+version = "3.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
+checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
 dependencies = [
  "cfg-if",
  "fastrand",
- "getrandom",
+ "getrandom 0.3.1",
  "once_cell",
  "rustix",
  "windows-sys 0.59.0",
@@ -2921,9 +2966,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.14"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
 
 [[package]]
 name = "unicode-normalization"
@@ -3011,6 +3056,15 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 name = "wasite"
 version = "0.1.0"
@@ -3315,6 +3369,15 @@ dependencies = [
  "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]]
 name = "write16"
 version = "1.0.0"
@@ -3358,7 +3421,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 dependencies = [
  "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]]
@@ -3372,6 +3444,17 @@ dependencies = [
  "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]]
 name = "zerofrom"
 version = "0.1.5"
diff --git a/TODO.md b/TODO.md
index 4f59ace..5163115 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -1,9 +1,14 @@
 * 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
 * Define the UI (mockups).
     * Two CSS: one for desktop and one for mobile
     * Use CSS flex/grid to define a good design/layout
 * CSS for toast and modal dialog
+* Calendar: Choose the first day of the week 
 * Make a search page
     Use FTS5:
         https://sqlite.org/fts5.html
 * Make the home page: Define what to display to the user
 * 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] Force tags in lowercase
 [ok] Remove the given language to recipe_edit and replace it by tr (like the header)
index cc6a499..b52bb93 100644 (file)
@@ -16,7 +16,7 @@ tower-http = { version = "0.6", features = ["fs", "trace"] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
 
-chrono = "0.4"
+chrono = { version = "0.4", features = ["serde"] }
 
 # Rust object notation, to load configuration files.
 ron = "0.8"
@@ -30,8 +30,8 @@ sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
 rinja = { version = "0.3" }
 
 argon2 = { version = "0.5", features = ["default", "std"] }
-rand_core = { version = "0.6", features = ["std"] }
-rand = "0.8"
+rand_core = { version = "0.9", features = ["std"] }
+rand = "0.9"
 strum = "0.26"
 strum_macros = "0.26"
 
index 6500a85..77e5a9b 100644 (file)
             width: 14%;
             text-align: center;
             margin: 0;
+
+            &.current-month {
+                background-color: blue;
+            }
         }
     }
 }
\ No newline at end of file
index 0100ee3..07d7d60 100644 (file)
@@ -172,6 +172,8 @@ CREATE TABLE [RecipeScheduled] (
     FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
 );
 
+CREATE INDEX [RecipeScheduled_date_index] ON [RecipeScheduled]([date]);
+
 CREATE TABLE [ShoppingEntry] (
     [id] INTEGER PRIMARY KEY,
     [user_id] INTEGER NOT NULL,
index d1e1a8f..d7b8978 100644 (file)
@@ -1,10 +1,9 @@
-use chrono::prelude::*;
+use chrono::{prelude::*, Days};
+use common::ron_api::Difficulty;
 use itertools::Itertools;
 
 use super::{Connection, DBError, Result};
-use crate::data::model;
-
-use common::ron_api::Difficulty;
+use crate::{data::model, user_authentication};
 
 impl Connection {
     /// Returns all the recipe titles where recipe is written in the given language.
@@ -106,11 +105,10 @@ SELECT COUNT(*)
 FROM [Recipe]
 INNER JOIN [User] ON [User].[id] = [Recipe].[user_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
         );
-
         let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
         for id in group_ids {
             query = query.bind(id);
@@ -147,11 +145,10 @@ FROM [Recipe]
 INNER JOIN [User] ON [User].[id] = [Recipe].[user_id]
 INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[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
         );
-
         let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
         for id in steps_ids {
             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 [Ingredient] ON [Ingredient].[step_id] = [Step].[id]
 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
         );
-
         let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
         for id in ingredients_ids {
             query = query.bind(id);
@@ -755,6 +751,73 @@ VALUES ($1, $2)
 
         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)]
@@ -884,4 +947,83 @@ VALUES
 
         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(())
+    }
 }
index 4b7d2b4..4e1768e 100644 (file)
@@ -1,5 +1,5 @@
 use chrono::{prelude::*, Duration};
-use rand::distributions::{Alphanumeric, DistString};
+use rand::distr::{Alphanumeric, SampleString};
 use sqlx::Sqlite;
 
 use super::{Connection, DBError, Result};
@@ -57,7 +57,7 @@ pub enum ResetPasswordResult {
 }
 
 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 {
index c5bcb30..4c07c00 100644 (file)
@@ -177,6 +177,10 @@ async fn main() {
             "/recipe/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);
 
     let fragments_routes = Router::new().route(
index a6c6238..bedccb1 100644 (file)
@@ -5,6 +5,7 @@ use axum::{
     response::{ErrorResponse, IntoResponse, Result},
 };
 use axum_extra::extract::cookie::{Cookie, CookieJar};
+use chrono::NaiveDate;
 use serde::Deserialize;
 // use tracing::{event, Level};
 
@@ -183,11 +184,11 @@ async fn check_user_rights_recipe_ingredient(
 async fn check_user_rights_recipe_ingredients(
     connection: &db::Connection,
     user: &Option<model::User>,
-    step_ids: &[i64],
+    ingredient_ids: &[i64],
 ) -> Result<()> {
     if user.is_none()
         || !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?
     {
         Err(ErrorResponse::from(ron_error(
@@ -599,7 +600,39 @@ pub async fn set_ingredients_order(
     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]
 pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
     ron_error(StatusCode::NOT_FOUND, "Not found")
index 0e4383f..9fc14c5 100644 (file)
@@ -1,12 +1,13 @@
 use std::{borrow::Borrow, fs::File, sync::LazyLock};
 
+use common::utils;
 use ron::de::from_reader;
 use serde::Deserialize;
 use strum::EnumCount;
 use strum_macros::EnumCount;
 use tracing::{event, Level};
 
-use crate::{consts, utils};
+use crate::consts;
 
 #[derive(Debug, Clone, EnumCount, Deserialize)]
 pub enum Sentence {
@@ -109,6 +110,10 @@ pub enum Sentence {
     RecipeIngredientQuantity,
     RecipeIngredientUnit,
     RecipeIngredientComment,
+    RecipeDeleteConfirmation,
+    RecipeGroupDeleteConfirmation,
+    RecipeStepDeleteConfirmation,
+    RecipeIngredientDeleteConfirmation,
 
     // View Recipe.
     RecipeOneServing,
index fddeb71..f8715f3 100644 (file)
@@ -39,44 +39,3 @@ pub fn get_url_from_host(host: &str) -> String {
         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");
-    }
-}
index 5183ea5..435e48e 100644 (file)
@@ -38,7 +38,7 @@
     <ul class="days">
         {% for i in 0..7 %}
             {% 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 %}
     </ul>
index c14c8fc..502b37e 100644 (file)
         </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>
 
index 168d8de..8089431 100644 (file)
             (RecipeIngredientQuantity, "Quantity"),
             (RecipeIngredientUnit, "Unit"),
             (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"),
             (RecipeSomeServings, "{} servings"),
             (RecipeIngredientQuantity, "Quantité"),
             (RecipeIngredientUnit, "Unité"),
             (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"),
             (RecipeSomeServings, "pour {} personnes"),
index efb39b9..ea07afa 100644 (file)
@@ -7,3 +7,4 @@ edition = "2021"
 [dependencies]
 ron = "0.8"
 serde = { version = "1.0", features = ["derive"] }
+chrono = { version = "0.4", features = ["serde"] }
index a088220..5ca697f 100644 (file)
@@ -1,3 +1,4 @@
+use chrono::NaiveDate;
 use ron::ser::{to_string_pretty, PrettyConfig};
 use serde::{Deserialize, Serialize};
 
@@ -16,7 +17,7 @@ pub struct Id {
     pub id: i64,
 }
 
-///// RECIPE /////
+/// RECIPE ///
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct SetRecipeTitle {
@@ -158,7 +159,7 @@ pub struct Ingredient {
     pub quantity_unit: String,
 }
 
-///// PROFILE /////
+/// PROFILE ///
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct UpdateProfile {
@@ -174,3 +175,11 @@ where
     // TODO: handle'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)>,
+}
index 8b24810..2efe599 100644 (file)
@@ -12,3 +12,44 @@ pub fn validate_password(password: &str) -> PasswordValidation {
         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");
+    }
+}
index 4214b2f..adc06e1 100644 (file)
@@ -13,7 +13,7 @@ default = ["console_error_panic_hook"]
 [dependencies]
 common = { path = "../common" }
 
-chrono = "0.4"
+chrono = { version = "0.4", features = ["serde"] }
 
 ron = "0.8"
 serde = { version = "1.0", features = ["derive"] }
index 7ace6d3..b715117 100644 (file)
-use std::{
-    ops::{AddAssign, SubAssign},
-    sync::{
-        atomic::{AtomicI32, AtomicU32, Ordering},
-        Arc,
-    },
+use std::sync::{
+    atomic::{AtomicI32, AtomicU32, Ordering},
+    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 wasm_bindgen::prelude::*;
 use wasm_bindgen_futures::spawn_local;
 use web_sys::Element;
 
-use crate::utils::{by_id, SelectorExt};
+use crate::{
+    request,
+    utils::{by_id, selector, SelectorExt},
+};
+
+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) {
+pub fn setup(calendar: Element) {
     let prev: Element = calendar.selector(".prev");
     let next: Element = calendar.selector(".next");
 
-    let current_month = Arc::new(AtomicU32::new(Local::now().month()));
-    let current_year = Arc::new(AtomicI32::new(Local::now().year()));
+    let state = CalendarState::new();
 
-    display_month(calendar, Local::now().year(), Local::now().month());
+    display_month(&calendar, state.get_current_date());
 
     let calendar_clone = calendar.clone();
-    let current_month_clone = current_month.clone();
-    let current_year_clone = current_year.clone();
+    let state_clone = state.clone();
     EventListener::new(&prev, "click", move |_event| {
-        let mut m = current_month_clone.load(Ordering::Relaxed) - 1;
-        if m == 0 {
-            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,
-        );
+        let m = state_clone.to_previous_month();
+        display_month(&calendar_clone, m);
     })
     .forget();
 
     let calendar_clone = calendar.clone();
-    let current_month_clone = current_month.clone();
-    let current_year_clone = current_year.clone();
+    let state_clone = state.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,
-        );
+        let m = state_clone.to_next_month();
+        display_month(&calendar_clone, m);
     })
     .forget();
 
-    // now.weekday()
-
-    // console!(now.to_string());
+    // 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();
+
+    // 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 {
-//     match
-// }
-
-fn display_month(calendar: &Element, year: i32, month: u32) {
-    log!(year, month);
+const NB_CALENDAR_ROW: u64 = 5;
 
+fn display_month(calendar: &Element, date: DateTime<Local>) {
     calendar
         .selector::<Element>(".year")
-        .set_inner_html(&year.to_string());
+        .set_inner_html(&date.year().to_string());
 
     for (i, m) in calendar
         .selector_all::<Element>(".month")
         .into_iter()
         .enumerate()
     {
-        if i as u32 + 1 == month {
+        if i as u32 + 1 == date.month() {
             m.set_class_name("month current");
         } else {
             m.set_class_name("month");
         }
     }
 
-    // calendar
-    //     .selector::<Element>(".month")
-    //     .set_inner_html(&month.to_string());
-
-    let mut current = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
+    let mut current = date;
 
-    // let mut day = Local:: ;
-    while (current - Days::new(1)).month() == month {
+    while (current - Days::new(1)).month() == date.month() {
         current = current - Days::new(1);
     }
 
@@ -102,20 +146,46 @@ fn display_month(calendar: &Element, year: i32, month: u32) {
         current = current - Days::new(1);
     }
 
+    let first_day = current;
+
     for i in 0..7 {
-        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() {
-                li.set_class_name("current-month today");
-            } else if current.month() == month {
-                li.set_class_name("current-month");
+        for j in 0..NB_CALENDAR_ROW {
+            let day_element: Element = by_id(&format!("day-{}{}", i, j));
+            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 {
-                li.set_class_name("");
+                day_element.set_class_name("");
             }
 
             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);
+    });
 }
index 588be31..4ac25e3 100644 (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 modal_dialog;
 mod on_click;
@@ -7,14 +14,6 @@ mod request;
 mod toast;
 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)]
 pub fn main() -> Result<(), JsValue> {
     console_error_panic_hook::set_once();
index ac83ab9..477df12 100644 (file)
@@ -6,17 +6,13 @@ use crate::{
     utils::{by_id, selector_and_clone, SelectorExt},
 };
 
-pub enum DialogContent<'a, T>
-where
-    T: Fn(&Element),
-{
-    Text(&'a str),
-    CloneFromElement(&'a str, T),
+pub async fn show(element_selector: &str) -> bool {
+    show_and_initialize(element_selector, async |_| {}).await
 }
 
-pub async fn show<T>(content: DialogContent<'_, T>) -> bool
+pub async fn show_and_initialize<T>(element_selector: &str, initializer: T) -> bool
 where
-    T: Fn(&Element),
+    T: AsyncFn(Element),
 {
     let dialog: HtmlDialogElement = by_id("modal-dialog");
 
@@ -25,15 +21,10 @@ where
 
     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);
-            content_element.set_inner_html("");
-            content_element.append_child(&element).unwrap();
-            initilizer(&element);
-        }
-    }
+    let element: Element = selector_and_clone(element_selector);
+    content_element.set_inner_html("");
+    content_element.append_child(&element).unwrap();
+    initializer(element).await;
 
     dialog.show_modal().unwrap();
 
index d1a4e4c..d9c2c5a 100644 (file)
@@ -1,5 +1,6 @@
 use std::{cell::RefCell, rc, sync::Mutex};
 
+use common::{ron_api, utils::substitute};
 use gloo::{
     events::{EventListener, EventListenerOptions},
     net::http::Request,
@@ -12,14 +13,17 @@ use web_sys::{
     KeyboardEvent,
 };
 
-use common::ron_api;
-
 use crate::{
     modal_dialog, request,
     toast::{self, Level},
     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> {
     // Title.
     {
@@ -248,12 +252,18 @@ pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
     // Delete recipe button.
     let delete_button: HtmlInputElement = by_id("input-delete");
     EventListener::new(&delete_button, "click", move |_event| {
-        let title: HtmlInputElement = by_id("input-title");
         spawn_local(async move {
-            if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
-                "Are you sure to delete the recipe '{}'",
-                title.value()
-            )))
+            if modal_dialog::show_and_initialize(
+                "#hidden-templates .recipe-delete-confirmation",
+                async |element| {
+                    let title: HtmlInputElement = by_id("input-title");
+                    element.set_inner_html(&substitute(
+                        &element.inner_html(),
+                        "{}",
+                        &[&title.value()],
+                    ));
+                },
+            )
             .await
             {
                 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 delete_button: HtmlInputElement = group_element.selector(".input-group-delete");
     EventListener::new(&delete_button, "click", move |_event| {
-        let name = group_element_cloned
-            .selector::<HtmlInputElement>(".input-group-name")
-            .value();
+        // FIXME: How to avoid cloning twice?
+        let group_element_cloned = group_element_cloned.clone();
         spawn_local(async move {
-            if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
-                "Are you sure to delete the group '{}'",
-                name
-            )))
+            if modal_dialog::show_and_initialize(
+                "#hidden-templates .recipe-group-delete-confirmation",
+                async move |element| {
+                    let name = group_element_cloned
+                        .selector::<HtmlInputElement>(".input-group-name")
+                        .value();
+                    element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&name]));
+                },
+            )
             .await
             {
                 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 delete_button: HtmlInputElement = step_element.selector(".input-step-delete");
     EventListener::new(&delete_button, "click", move |_event| {
-        let action = step_element_cloned
-            .selector::<HtmlTextAreaElement>(".text-area-step-action")
-            .value();
+        // FIXME: How to avoid cloning twice?
+        let step_element_cloned = step_element_cloned.clone();
         spawn_local(async move {
-            if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
-                "Are you sure to delete the step '{}'",
-                action
-            )))
+            if modal_dialog::show_and_initialize(
+                "#hidden-templates .recipe-step-delete-confirmation",
+                async move |element| {
+                    let action = step_element_cloned
+                        .selector::<HtmlTextAreaElement>(".text-area-step-action")
+                        .value();
+                    element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&action]));
+                },
+            )
             .await
             {
                 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 delete_button: HtmlInputElement = ingredient_element.selector(".input-ingredient-delete");
     EventListener::new(&delete_button, "click", move |_event| {
-        let name = ingredient_element_cloned
-            .selector::<HtmlInputElement>(".input-ingredient-name")
-            .value();
+        // FIXME: How to avoid cloning twice?
+        let ingredient_element_cloned = ingredient_element_cloned.clone();
         spawn_local(async move {
-            if modal_dialog::show(modal_dialog::DialogContent::<fn(&Element)>::Text(&format!(
-                "Are you sure to delete the ingredient '{}'",
-                name
-            )))
+            if modal_dialog::show_and_initialize(
+                "#hidden-templates .recipe-ingredient-delete-confirmation",
+                async move |element| {
+                    let name = ingredient_element_cloned
+                        .selector::<HtmlInputElement>(".input-ingredient-name")
+                        .value();
+                    element.set_inner_html(&substitute(&element.inner_html(), "{}", &[&name]));
+                },
+            )
             .await
             {
                 let body = ron_api::Id { id: ingredient_id };
index 488e459..ba30b4d 100644 (file)
@@ -1,3 +1,6 @@
+use std::future::Future;
+
+use common::ron_api;
 use gloo::{
     console::console,
     events::EventListener,
@@ -11,8 +14,6 @@ use web_sys::{
     KeyboardEvent,
 };
 
-use common::ron_api;
-
 use crate::{
     calendar, modal_dialog, request,
     toast::{self, Level},
@@ -22,15 +23,10 @@ use crate::{
 pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
     let add_to_planner: Element = selector("#recipe-view .add-to-planner");
     EventListener::new(&add_to_planner, "click", move |_event| {
-        // console!("CLICK".to_string());
         spawn_local(async move {
-            modal_dialog::show(modal_dialog::DialogContent::CloneFromElement(
-                "#hidden-templates .calendar",
-                |element| {
-                    // console!("SETUP...".to_string());
-                    calendar::setup(element);
-                },
-            ))
+            modal_dialog::show_and_initialize("#hidden-templates .calendar", async |element| {
+                calendar::setup(element);
+            })
             .await;
         });
     })
index ab4a398..306f96b 100644 (file)
@@ -1,9 +1,8 @@
+use common::ron_api;
 use gloo::net::http::{Request, RequestBuilder};
 use serde::{de::DeserializeOwned, Serialize};
 use thiserror::Error;
 
-use common::ron_api;
-
 use crate::toast::{self, Level};
 
 #[derive(Error, Debug)]