Add default servings to profile + choose servings when scheduling
authorGreg Burri <greg.burri@gmail.com>
Mon, 3 Feb 2025 22:42:07 +0000 (23:42 +0100)
committerGreg Burri <greg.burri@gmail.com>
Mon, 3 Feb 2025 22:42:07 +0000 (23:42 +0100)
13 files changed:
Cargo.lock
backend/src/data/db/user.rs
backend/src/data/model.rs
backend/src/html_templates.rs
backend/src/main.rs
backend/src/services/recipe.rs
backend/src/services/user.rs
backend/src/translation.rs
backend/templates/profile.html
backend/templates/recipe_view.html
backend/translation.ron
frontend/src/calendar.rs
frontend/src/recipe_view.rs

index aef3812..01a5811 100644 (file)
@@ -327,9 +327,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.9.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
 
 [[package]]
 name = "cc"
@@ -1438,9 +1438,9 @@ dependencies = [
 
 [[package]]
 name = "lettre"
-version = "0.11.11"
+version = "0.11.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5"
+checksum = "e882e1489810a45919477602194312b1a7df0e5acc30a6188be7b520268f63f8"
 dependencies = [
  "async-trait",
  "base64 0.22.1",
@@ -1742,18 +1742,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
 name = "pin-project"
-version = "1.1.8"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
+checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.8"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
+checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
 dependencies = [
  "proc-macro2",
  "quote",
index 4e1768e..f057d8c 100644 (file)
@@ -77,7 +77,7 @@ FROM [UserLoginToken] WHERE [token] = $1
 
     pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
         sqlx::query_as(
-            "SELECT [id], [email], [name], [lang], [is_admin] FROM [User] WHERE [id] = $1",
+            "SELECT [id], [email], [name], [default_servings], [lang], [is_admin] FROM [User] WHERE [id] = $1",
         )
         .bind(user_id)
         .fetch_optional(&self.pool)
@@ -92,13 +92,17 @@ FROM [UserLoginToken] WHERE [token] = $1
         user_id: i64,
         new_email: Option<&str>,
         new_name: Option<&str>,
+        new_default_servings: Option<u32>,
         new_password: Option<&str>,
     ) -> Result<UpdateUserResult> {
         let mut tx = self.tx().await?;
         let hashed_new_password = new_password.map(|p| hash(p).unwrap());
 
-        let (email, name, hashed_password) = sqlx::query_as::<_, (String, String, String)>(
-            "SELECT [email], [name], [password] FROM [User] WHERE [id] = $1",
+        let (email, name, default_servings, hashed_password) = sqlx::query_as::<
+            _,
+            (String, String, u32, String),
+        >(
+            "SELECT [email], [name], [default_servings], [password] FROM [User] WHERE [id] = $1",
         )
         .bind(user_id)
         .fetch_one(&mut *tx)
@@ -144,13 +148,14 @@ WHERE [id] = $1
         sqlx::query(
             r#"
 UPDATE [User]
-SET [email] = $2, [name] = $3, [password] = $4
+SET [email] = $2, [name] = $3, [default_servings] = $4, [password] = $5
 WHERE [id] = $1
             "#,
         )
         .bind(user_id)
         .bind(new_email.unwrap_or(&email))
         .bind(new_name.map(str::trim).unwrap_or(&name))
+        .bind(new_default_servings.unwrap_or(default_servings))
         .bind(hashed_new_password.unwrap_or(hashed_password))
         .execute(&mut *tx)
         .await?;
index ceb7128..a6834c8 100644 (file)
@@ -7,6 +7,7 @@ pub struct User {
     pub id: i64,
     pub name: String,
     pub email: String,
+    pub default_servings: u32,
     pub lang: String,
     pub is_admin: bool,
 }
index 3ee4ff1..294f4ae 100644 (file)
@@ -112,6 +112,7 @@ pub struct ProfileTemplate<'a> {
 
     pub username: &'a str,
     pub email: &'a str,
+    pub default_servings: u32,
     pub message: &'a str,
     pub message_email: &'a str,
     pub message_password: &'a str,
index 4c07c00..5ac03eb 100644 (file)
@@ -212,7 +212,7 @@ async fn main() {
         )
         // Recipes.
         .route("/recipe/new", get(services::recipe::create))
-        .route("/recipe/edit/{id}", get(services::recipe::edit_recipe))
+        .route("/recipe/edit/{id}", get(services::recipe::edit))
         .route("/recipe/view/{id}", get(services::recipe::view))
         // User.
         .route(
index 09eb138..799ffee 100644 (file)
@@ -28,7 +28,7 @@ pub async fn create(
 }
 
 #[debug_handler]
-pub async fn edit_recipe(
+pub async fn edit(
     State(connection): State<db::Connection>,
     Extension(user): Extension<Option<model::User>>,
     Extension(tr): Extension<translation::Tr>,
index 985dc7b..ede66c5 100644 (file)
@@ -655,6 +655,7 @@ pub async fn edit_user_get(
             ProfileTemplate {
                 username: &user.name,
                 email: &user.email,
+                default_servings: user.default_servings,
                 message: "",
                 message_email: "",
                 message_password: "",
@@ -673,6 +674,7 @@ pub async fn edit_user_get(
 pub struct EditUserForm {
     name: String,
     email: String,
+    default_servings: u32,
     password_1: String,
     password_2: String,
 }
@@ -712,6 +714,7 @@ pub async fn edit_user_post(
                     user: Some(user),
                     username: &form_data.name,
                     email: &form_data.email,
+                    default_servings: form_data.default_servings,
                     message_email: match error {
                         ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail),
                         ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken),
@@ -760,6 +763,7 @@ pub async fn edit_user_post(
                 user.id,
                 Some(email_trimmed),
                 Some(&form_data.name),
+                Some(form_data.default_servings),
                 new_password,
             )
             .await
@@ -815,6 +819,7 @@ pub async fn edit_user_post(
                 user,
                 username: &form_data.name,
                 email: &form_data.email,
+                default_servings: form_data.default_servings,
                 message,
                 message_email: "",
                 message_password: "",
index 9fc14c5..1e272cb 100644 (file)
@@ -76,6 +76,7 @@ pub enum Sentence {
     // Profile
     ProfileTitle,
     ProfileEmail,
+    ProfileDefaultServings,
     ProfileNewPassword,
     ProfileFollowEmailLink,
     ProfileEmailSent,
index 1ec5955..8bc7ec9 100644 (file)
@@ -2,8 +2,7 @@
 
 {% block main_container %}
 
-{% match user %}
-{% when Some with (user) %}
+{% if let Some(user) = user %}
 
 <div class="content" id="user-edit">
     <h1>{{ tr.t(Sentence::ProfileTitle) }}</h1>
 
         {{ message_email }}
 
+        <label for="input-servings">{{ tr.t(Sentence::ProfileDefaultServings) }}</label>
+        <input
+            id="input-servings"
+            type="number"
+            step="1" min="1" max="100"
+            name="default_servings"
+            value="{{ default_servings }}"/>
+
         <label for="input-password-1">{{ tr.tp(Sentence::ProfileNewPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}</label>
         <input id="input-password-1" type="password" name="password_1" autocomplete="new-password" />
 
@@ -40,7 +47,6 @@
     {{ message }}
 </div>
 
-{% when None %}
-{% endmatch %}
+{% endif %}
 
 {% endblock %}
\ No newline at end of file
index 3a2d487..231f85c 100644 (file)
     {% endfor %}
 
     <div id="hidden-templates">
-        {% include "calendar.html" %}
+        {# To create a modal dialog to choose a date and and servings #}
+        {% if let Some(user) = user %}
+            <div class="date-and-servings" >
+                {% include "calendar.html" %}
+                <label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label>
+                <input
+                    id="input-servings"
+                    type="number"
+                    step="1" min="1" max="100"
+                    value="{{ user.default_servings }}"/>
+            </div>
+        {% endif %}
     </div>
 </div>
 
index 8089431..f99b3a3 100644 (file)
@@ -62,6 +62,7 @@
 
             (ProfileTitle, "Profile"),
             (ProfileEmail, "Email (need to be revalidated if changed)"),
+            (ProfileDefaultServings, "Default servings"),
             (ProfileNewPassword, "New password (minimum {} characters)"),
             (ProfileFollowEmailLink, "Follow this link to validate this email address, {}"),
             (ProfileEmailSent, "An email has been sent, follow the link to validate your new email"),
 
             (ProfileTitle, "Profile"),
             (ProfileEmail, "Email (doit être revalidé si changé)"),
+            (ProfileDefaultServings, "Nombre de portions par défaut"),
             (ProfileNewPassword, "Nouveau mot de passe (minimum {} caractères)"),
             (ProfileFollowEmailLink, "Suivez ce lien pour valider l'adresse email, {}"),
             (ProfileEmailSent, "Un email a été envoyé, suivez le lien pour valider la nouvelle adresse email"),
             (RecipeNotFound, "Recette non-trouvée"),
             (RecipeTitle, "Titre"),
             (RecipeDescription, "Description"),
-            (RecipeServings, "Nombre de personnes"),
+            (RecipeServings, "Nombre de portions"),
             (RecipeEstimatedTime, "Temps estimé"),
             (RecipeDifficulty, "Difficulté"),
             (RecipeDifficultyEasy, "Facile"),
index dec6540..64c16e5 100644 (file)
@@ -1,4 +1,4 @@
-use std::sync::{Arc, Mutex};
+use std::{cell::RefCell, rc::Rc};
 
 use chrono::{offset::Local, DateTime, Datelike, Days, Months, Weekday};
 use common::ron_api;
@@ -19,14 +19,14 @@ struct CalendarStateInternal {
 
 #[derive(Clone)]
 struct CalendarState {
-    internal_state: Arc<Mutex<CalendarStateInternal>>,
+    internal_state: Rc<RefCell<CalendarStateInternal>>,
 }
 
 impl CalendarState {
     pub fn new() -> Self {
         let current_date = Local::now();
         Self {
-            internal_state: Arc::new(Mutex::new(CalendarStateInternal {
+            internal_state: Rc::new(RefCell::new(CalendarStateInternal {
                 displayed_date: current_date,
                 selected_date: current_date,
             })),
@@ -34,25 +34,25 @@ impl CalendarState {
     }
 
     pub fn displayed_date_next_month(&self) {
-        let mut locker = self.internal_state.lock().unwrap();
-        locker.displayed_date = locker.displayed_date + Months::new(1);
+        let mut state_borrowed = self.internal_state.borrow_mut();
+        state_borrowed.displayed_date = state_borrowed.displayed_date + Months::new(1);
     }
 
     pub fn displayed_date_previous_month(&self) {
-        let mut locker = self.internal_state.lock().unwrap();
-        locker.displayed_date = locker.displayed_date - Months::new(1);
+        let mut state_borrowed = self.internal_state.borrow_mut();
+        state_borrowed.displayed_date = state_borrowed.displayed_date - Months::new(1);
     }
 
     pub fn get_displayed_date(&self) -> DateTime<Local> {
-        self.internal_state.lock().unwrap().displayed_date
+        self.internal_state.borrow().displayed_date
     }
 
     pub fn get_selected_date(&self) -> DateTime<Local> {
-        self.internal_state.lock().unwrap().selected_date
+        self.internal_state.borrow().selected_date
     }
 
     pub fn set_selected_date(&self, date: DateTime<Local>) {
-        self.internal_state.lock().unwrap().selected_date = date;
+        self.internal_state.borrow_mut().selected_date = date;
     }
 }
 
@@ -173,7 +173,8 @@ fn display_month(calendar: &Element, state: CalendarState) {
         .unwrap();
 
         for recipe in scheduled_recipes.recipes {
-            log!(recipe.1);
+            // log!(recipe.1);
+            // TODO
         }
     });
 }
index ba30b4d..bee05c6 100644 (file)
@@ -24,9 +24,12 @@ 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| {
         spawn_local(async move {
-            modal_dialog::show_and_initialize("#hidden-templates .calendar", async |element| {
-                calendar::setup(element);
-            })
+            modal_dialog::show_and_initialize(
+                "#hidden-templates .date-and-servings",
+                async |element| {
+                    calendar::setup(element.selector(".calendar"));
+                },
+            )
             .await;
         });
     })