"cfg-if",
"once_cell",
"version_check",
- "zerocopy",
+ "zerocopy 0.7.35",
]
[[package]]
[[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"
"iana-time-zone",
"js-sys",
"num-traits",
+ "serde",
"wasm-bindgen",
"windows-targets 0.52.6",
]
name = "common"
version = "0.1.0"
dependencies = [
+ "chrono",
"ron",
"serde",
]
[[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",
]
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6"
dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
"gloo-events",
"gloo-utils",
"serde",
[[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"
[[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",
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
"num-integer",
"num-iter",
"num-traits",
- "rand",
+ "rand 0.8.5",
"smallvec",
"zeroize",
]
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
- "rand_core",
+ "rand_core 0.6.4",
"subtle",
]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
- "zerocopy",
+ "zerocopy 0.7.35",
]
[[package]]
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]]
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]]
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]]
"derive_more",
"itertools",
"lettre",
- "rand",
- "rand_core",
+ "rand 0.9.0",
+ "rand_core 0.9.0",
"rinja",
"ron",
"serde",
dependencies = [
"cc",
"cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
"libc",
"spin",
"untrusted",
"num-traits",
"pkcs1",
"pkcs8",
- "rand_core",
+ "rand_core 0.6.4",
"signature",
"spki",
"subtle",
[[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"
[[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"
[[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",
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
- "rand_core",
+ "rand_core 0.6.4",
]
[[package]]
"memchr",
"once_cell",
"percent-encoding",
- "rand",
+ "rand 0.8.5",
"rsa",
"serde",
"sha1",
"md-5",
"memchr",
"once_cell",
- "rand",
+ "rand 0.8.5",
"serde",
"serde_json",
"sha2",
[[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",
[[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"
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"
"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"
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]]
"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"
* 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)
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"
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"
width: 14%;
text-align: center;
margin: 0;
+
+ &.current-month {
+ background-color: blue;
+ }
}
}
}
\ No newline at end of file
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,
-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.
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);
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);
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);
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)]
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(())
+ }
}
use chrono::{prelude::*, Duration};
-use rand::distributions::{Alphanumeric, DistString};
+use rand::distr::{Alphanumeric, SampleString};
use sqlx::Sqlite;
use super::{Connection, DBError, Result};
}
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 {
"/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(
response::{ErrorResponse, IntoResponse, Result},
};
use axum_extra::extract::cookie::{Cookie, CookieJar};
+use chrono::NaiveDate;
use serde::Deserialize;
// use tracing::{event, Level};
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(
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")
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 {
RecipeIngredientQuantity,
RecipeIngredientUnit,
RecipeIngredientComment,
+ RecipeDeleteConfirmation,
+ RecipeGroupDeleteConfirmation,
+ RecipeStepDeleteConfirmation,
+ RecipeIngredientDeleteConfirmation,
// View Recipe.
RecipeOneServing,
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");
- }
-}
<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>
</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>
(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"),
[dependencies]
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
+chrono = { version = "0.4", features = ["serde"] }
+use chrono::NaiveDate;
use ron::ser::{to_string_pretty, PrettyConfig};
use serde::{Deserialize, Serialize};
pub id: i64,
}
-///// RECIPE /////
+/// RECIPE ///
#[derive(Serialize, Deserialize, Clone)]
pub struct SetRecipeTitle {
pub quantity_unit: String,
}
-///// PROFILE /////
+/// PROFILE ///
#[derive(Serialize, Deserialize, Clone)]
pub struct UpdateProfile {
// 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)>,
+}
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");
+ }
+}
[dependencies]
common = { path = "../common" }
-chrono = "0.4"
+chrono = { version = "0.4", features = ["serde"] }
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
-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);
}
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(¤t.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(¤t.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);
+ });
}
+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;
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();
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");
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();
use std::{cell::RefCell, rc, sync::Mutex};
+use common::{ron_api, utils::substitute};
use gloo::{
events::{EventListener, EventListenerOptions},
net::http::Request,
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.
{
// 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 };
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 };
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 };
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 };
+use std::future::Future;
+
+use common::ron_api;
use gloo::{
console::console,
events::EventListener,
KeyboardEvent,
};
-use common::ron_api;
-
use crate::{
calendar, modal_dialog, request,
toast::{self, Level},
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;
});
})
+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)]