Add first day of the week feature to user settings and calendar functionality

This commit is contained in:
Greg Burri 2025-04-19 12:01:46 +02:00
parent 39f5b968b4
commit fdbf2e4f27
15 changed files with 191 additions and 42 deletions

View file

@ -17,6 +17,8 @@ CREATE TABLE [User] (
[default_servings] INTEGER DEFAULT 4, [default_servings] INTEGER DEFAULT 4,
[lang] TEXT NOT NULL DEFAULT 'en', [lang] TEXT NOT NULL DEFAULT 'en',
-- 0: Monday, 1: Tuesday, 2: Wednesday, 3: Thursday, 4: Friday, 5: Saturday, 6: Sunday.
[first_day_of_the_week] INTEGER DEFAULT 0,
[password] TEXT NOT NULL, -- argon2(password_plain, salt). [password] TEXT NOT NULL, -- argon2(password_plain, salt).
@ -34,6 +36,17 @@ CREATE TABLE [User] (
CREATE INDEX [validation_token_index] ON [User]([validation_token]); CREATE INDEX [validation_token_index] ON [User]([validation_token]);
CREATE UNIQUE INDEX [User_email_index] ON [User]([email]); CREATE UNIQUE INDEX [User_email_index] ON [User]([email]);
CREATE TRIGGER [User_trigger_update_first_day_of_the_week]
BEFORE UPDATE OF [first_day_of_the_week]
ON [User]
BEGIN
SELECT
CASE
WHEN NEW.[first_day_of_the_week] < 0 OR NEW.[first_day_of_the_week] > 6 THEN
RAISE (ABORT, 'Invalid [first_day_of_the_week] value')
END;
END;
CREATE TABLE [UserLoginToken] ( CREATE TABLE [UserLoginToken] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[user_id] INTEGER NOT NULL, [user_id] INTEGER NOT NULL,

View file

@ -77,7 +77,7 @@ FROM [UserLoginToken] WHERE [token] = $1
pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> { pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
sqlx::query_as( sqlx::query_as(
"SELECT [id], [email], [name], [default_servings], [lang], [is_admin] FROM [User] WHERE [id] = $1", "SELECT [id], [email], [name], [default_servings], [first_day_of_the_week], [lang], [is_admin] FROM [User] WHERE [id] = $1",
) )
.bind(user_id) .bind(user_id)
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
@ -93,16 +93,17 @@ FROM [UserLoginToken] WHERE [token] = $1
new_email: Option<&str>, new_email: Option<&str>,
new_name: Option<&str>, new_name: Option<&str>,
new_default_servings: Option<u32>, new_default_servings: Option<u32>,
new_first_day_of_the_week: Option<Weekday>,
new_password: Option<&str>, new_password: Option<&str>,
) -> Result<UpdateUserResult> { ) -> Result<UpdateUserResult> {
let mut tx = self.tx().await?; let mut tx = self.tx().await?;
let hashed_new_password = new_password.map(|p| hash(p).unwrap()); let hashed_new_password = new_password.map(|p| hash(p).unwrap());
let (email, name, default_servings, hashed_password) = sqlx::query_as::< let (email, name, default_servings, first_day_of_the_week, hashed_password) = sqlx::query_as::<
_, _,
(String, String, u32, String), (String, String, u32, u8, String),
>( >(
"SELECT [email], [name], [default_servings], [password] FROM [User] WHERE [id] = $1", "SELECT [email], [name], [default_servings], [first_day_of_the_week], [password] FROM [User] WHERE [id] = $1",
) )
.bind(user_id) .bind(user_id)
.fetch_one(&mut *tx) .fetch_one(&mut *tx)
@ -148,7 +149,7 @@ WHERE [id] = $1
sqlx::query( sqlx::query(
r#" r#"
UPDATE [User] UPDATE [User]
SET [email] = $2, [name] = $3, [default_servings] = $4, [password] = $5 SET [email] = $2, [name] = $3, [default_servings] = $4, [first_day_of_the_week] = $5, [password] = $6
WHERE [id] = $1 WHERE [id] = $1
"#, "#,
) )
@ -156,6 +157,7 @@ WHERE [id] = $1
.bind(new_email.unwrap_or(&email)) .bind(new_email.unwrap_or(&email))
.bind(new_name.map(str::trim).unwrap_or(&name)) .bind(new_name.map(str::trim).unwrap_or(&name))
.bind(new_default_servings.unwrap_or(default_servings)) .bind(new_default_servings.unwrap_or(default_servings))
.bind(new_first_day_of_the_week.map(|d| d as u8).unwrap_or(first_day_of_the_week))
.bind(hashed_new_password.unwrap_or(hashed_password)) .bind(hashed_new_password.unwrap_or(hashed_password))
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
@ -179,8 +181,13 @@ WHERE [id] = $1
.map_err(DBError::from) .map_err(DBError::from)
} }
pub async fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResult> { pub async fn sign_up(
self.sign_up_with_given_time(email, password, Utc::now()) &self,
email: &str,
password: &str,
first_day_of_the_week: Weekday,
) -> Result<SignUpResult> {
self.sign_up_with_given_time(email, password, first_day_of_the_week, Utc::now())
.await .await
} }
@ -188,6 +195,7 @@ WHERE [id] = $1
&self, &self,
email: &str, email: &str,
password: &str, password: &str,
first_day_of_the_week: Weekday,
datetime: DateTime<Utc>, datetime: DateTime<Utc>,
) -> Result<SignUpResult> { ) -> Result<SignUpResult> {
let mut tx = self.tx().await?; let mut tx = self.tx().await?;
@ -229,11 +237,12 @@ WHERE [id] = $1
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO [User] INSERT INTO [User]
([email], [creation_datetime], [validation_token], [validation_token_datetime], [password]) ([email], [first_day_of_the_week], [creation_datetime], [validation_token], [validation_token_datetime], [password])
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5, $6)
"#, "#,
) )
.bind(email) .bind(email)
.bind(first_day_of_the_week as u8)
.bind(Utc::now()) .bind(Utc::now())
.bind(&token) .bind(&token)
.bind(datetime) .bind(datetime)
@ -525,7 +534,10 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn sign_up() -> Result<()> { async fn sign_up() -> Result<()> {
let connection = Connection::new_in_memory().await?; let connection = Connection::new_in_memory().await?;
match connection.sign_up("paul@atreides.com", "12345").await? { match connection
.sign_up("paul@atreides.com", "12345", Weekday::Mon)
.await?
{
SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case. SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
} }
@ -550,7 +562,10 @@ INSERT INTO
NULL NULL
); );
"#)).await?; "#)).await?;
match connection.sign_up("paul@atreides.com", "12345").await? { match connection
.sign_up("paul@atreides.com", "12345", Weekday::Mon)
.await?
{
SignUpResult::UserAlreadyExists => (), // Nominal case. SignUpResult::UserAlreadyExists => (), // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
} }
@ -564,7 +579,7 @@ INSERT INTO
let email = "paul@atreides.com"; let email = "paul@atreides.com";
let password = "12345"; let password = "12345";
match connection.sign_up(email, password).await? { match connection.sign_up(email, password, Weekday::Mon).await? {
SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case. SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
} }
@ -600,7 +615,10 @@ VALUES (
) )
"# "#
).bind(token)).await?; ).bind(token)).await?;
match connection.sign_up("paul@atreides.com", "12345").await? { match connection
.sign_up("paul@atreides.com", "12345", Weekday::Mon)
.await?
{
SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case. SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
} }
@ -610,7 +628,10 @@ VALUES (
#[tokio::test] #[tokio::test]
async fn sign_up_then_send_validation_at_time() -> Result<()> { async fn sign_up_then_send_validation_at_time() -> Result<()> {
let connection = Connection::new_in_memory().await?; let connection = Connection::new_in_memory().await?;
let validation_token = match connection.sign_up("paul@atreides.com", "12345").await? { let validation_token = match connection
.sign_up("paul@atreides.com", "12345", Weekday::Mon)
.await?
{
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
}; };
@ -633,7 +654,12 @@ VALUES (
async fn sign_up_then_send_validation_too_late() -> Result<()> { async fn sign_up_then_send_validation_too_late() -> Result<()> {
let connection = Connection::new_in_memory().await?; let connection = Connection::new_in_memory().await?;
let validation_token = match connection let validation_token = match connection
.sign_up_with_given_time("paul@atreides.com", "12345", Utc::now() - Duration::days(1)) .sign_up_with_given_time(
"paul@atreides.com",
"12345",
Weekday::Mon,
Utc::now() - Duration::days(1),
)
.await? .await?
{ {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
@ -657,7 +683,10 @@ VALUES (
#[tokio::test] #[tokio::test]
async fn sign_up_then_send_validation_with_bad_token() -> Result<()> { async fn sign_up_then_send_validation_with_bad_token() -> Result<()> {
let connection = Connection::new_in_memory().await?; let connection = Connection::new_in_memory().await?;
let _validation_token = match connection.sign_up("paul@atreides.com", "12345").await? { let _validation_token = match connection
.sign_up("paul@atreides.com", "12345", Weekday::Mon)
.await?
{
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
}; };
@ -685,7 +714,7 @@ VALUES (
let password = "12345"; let password = "12345";
// Sign up. // Sign up.
let validation_token = match connection.sign_up(email, password).await? { let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
}; };
@ -724,7 +753,7 @@ VALUES (
let password = "12345"; let password = "12345";
// Sign up. // Sign up.
let validation_token = match connection.sign_up(email, password).await? { let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
}; };
@ -778,7 +807,7 @@ VALUES (
let password = "12345"; let password = "12345";
// Sign up. // Sign up.
let validation_token = match connection.sign_up(email, password).await? { let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
}; };
@ -855,7 +884,7 @@ VALUES (
let new_password = "54321"; let new_password = "54321";
// Sign up. // Sign up.
let validation_token = match connection.sign_up(email, password).await? { let validation_token = match connection.sign_up(email, password, Weekday::Mon).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case. SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other), other => panic!("{:?}", other),
}; };
@ -946,6 +975,7 @@ VALUES
Some("muaddib@fremen.com"), Some("muaddib@fremen.com"),
Some("muaddib"), Some("muaddib"),
None, None,
None,
Some("Chani"), Some("Chani"),
) )
.await? .await?

View file

@ -8,6 +8,10 @@ pub struct User {
pub name: String, pub name: String,
pub email: String, pub email: String,
pub default_servings: u32, pub default_servings: u32,
#[sqlx(try_from = "u8")]
pub first_day_of_the_week: Weekday,
pub lang: String, pub lang: String,
pub is_admin: bool, pub is_admin: bool,
} }

View file

@ -89,6 +89,16 @@ struct Context {
dark_theme: bool, dark_theme: bool,
} }
impl Context {
pub fn first_day_of_the_week(&self) -> Weekday {
if let Some(user) = &self.user {
user.first_day_of_the_week
} else {
self.tr.first_day_of_week()
}
}
}
// TODO: Should main returns 'Result'? // TODO: Should main returns 'Result'?
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {

View file

@ -141,7 +141,11 @@ pub async fn sign_up_post(
} }
match connection match connection
.sign_up(&form_data.email, &form_data.password_1) .sign_up(
&form_data.email,
&form_data.password_1,
context.tr.first_day_of_week(),
)
.await .await
{ {
Ok(db::user::SignUpResult::UserAlreadyExists) => { Ok(db::user::SignUpResult::UserAlreadyExists) => {
@ -691,6 +695,7 @@ pub struct EditUserForm {
name: String, name: String,
email: String, email: String,
default_servings: u32, default_servings: u32,
first_day_of_the_week: chrono::Weekday,
password_1: String, password_1: String,
password_2: String, password_2: String,
} }
@ -713,6 +718,11 @@ pub async fn edit_user_post(
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
Form(form_data): Form<EditUserForm>, Form(form_data): Form<EditUserForm>,
) -> Result<Response> { ) -> Result<Response> {
event!(
Level::DEBUG,
"First day of the week: {:?}",
form_data.first_day_of_the_week
);
if let Some(ref user) = context.user { if let Some(ref user) = context.user {
fn error_response( fn error_response(
error: ProfileUpdateError, error: ProfileUpdateError,
@ -783,6 +793,7 @@ pub async fn edit_user_post(
Some(email_trimmed), Some(email_trimmed),
Some(&form_data.name), Some(&form_data.name),
Some(form_data.default_servings), Some(form_data.default_servings),
Some(form_data.first_day_of_the_week),
new_password, new_password,
) )
.await .await

View file

@ -1,5 +1,6 @@
use std::{borrow::Borrow, fs::File, sync::LazyLock}; use std::{borrow::Borrow, fs::File, sync::LazyLock};
use chrono::Weekday;
use common::utils; use common::utils;
use ron::de::from_reader; use ron::de::from_reader;
use serde::Deserialize; use serde::Deserialize;
@ -80,6 +81,7 @@ pub enum Sentence {
ProfileTitle, ProfileTitle,
ProfileEmail, ProfileEmail,
ProfileDefaultServings, ProfileDefaultServings,
ProfileFirstDayOfWeek,
ProfileNewPassword, ProfileNewPassword,
ProfileFollowEmailTitle, ProfileFollowEmailTitle,
ProfileFollowEmailLink, ProfileFollowEmailLink,
@ -126,6 +128,13 @@ pub enum Sentence {
RecipeEstimatedTimeMinAbbreviation, RecipeEstimatedTimeMinAbbreviation,
// Calendar. // Calendar.
CalendarMonday,
CalendarTuesday,
CalendarWednesday,
CalendarThursday,
CalendarFriday,
CalendarSaturday,
CalendarSunday,
CalendarMondayAbbreviation, CalendarMondayAbbreviation,
CalendarTuesdayAbbreviation, CalendarTuesdayAbbreviation,
CalendarWednesdayAbbreviation, CalendarWednesdayAbbreviation,
@ -197,6 +206,13 @@ impl Tr {
pub fn current_lang_and_territory_code(&self) -> String { pub fn current_lang_and_territory_code(&self) -> String {
format!("{}-{}", self.lang.code, self.lang.territory) format!("{}-{}", self.lang.code, self.lang.territory)
} }
pub fn first_day_of_week(&self) -> Weekday {
match (self.lang.code.as_ref(), self.lang.territory.as_ref()) {
("en", "US") => Weekday::Sun,
_ => Weekday::Mon,
}
}
} }
// #[macro_export] // #[macro_export]

View file

@ -1,5 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ context.tr.current_lang_and_territory_code() }}" data-user-logged="{{ context.user.is_some() }}" > <html lang="{{ context.tr.current_lang_and_territory_code() }}"
data-user-logged="{{ context.user.is_some() }}"
data-user-first-day-of-the-week="{{ context.first_day_of_the_week().to_string() }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">

View file

@ -26,7 +26,7 @@
</div> </div>
<ul class="weekdays"> <ul class="weekdays">
{% for day in [ {% let day_names = [
Sentence::CalendarMondayAbbreviation, Sentence::CalendarMondayAbbreviation,
Sentence::CalendarTuesdayAbbreviation, Sentence::CalendarTuesdayAbbreviation,
Sentence::CalendarWednesdayAbbreviation, Sentence::CalendarWednesdayAbbreviation,
@ -34,9 +34,16 @@
Sentence::CalendarFridayAbbreviation, Sentence::CalendarFridayAbbreviation,
Sentence::CalendarSaturdayAbbreviation, Sentence::CalendarSaturdayAbbreviation,
Sentence::CalendarSundayAbbreviation, Sentence::CalendarSundayAbbreviation,
] %} ] %}
<li class="weekday">{{ context.tr.t(*day) }}</li>
{% for i in 0..7 %}
<li class="weekday">
{{ context.tr.t(*day_names[
(i + context.first_day_of_the_week().num_days_from_monday() as usize) % 7
]) }}
</li>
{% endfor %} {% endfor %}
</ul> </ul>
<ul class="days"> <ul class="days">

View file

@ -26,6 +26,14 @@
autocapitalize="none" autocomplete="email" autofocus="autofocus"> autocapitalize="none" autocomplete="email" autofocus="autofocus">
<span class="user-message">{{ message_email }}</span> <span class="user-message">{{ message_email }}</span>
<label for="input-password-1">{{ context.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">
<span></span>
<label for="input-password-2">{{ context.tr.t(Sentence::ReEnterPassword) }}</label>
<input id="input-password-2" type="password" name="password_2" autocomplete="new-password">
<span class="user-message">{{ message_password }}</span>
<label for="input-servings">{{ context.tr.t(Sentence::ProfileDefaultServings) }}</label> <label for="input-servings">{{ context.tr.t(Sentence::ProfileDefaultServings) }}</label>
<input <input
id="input-servings" id="input-servings"
@ -35,13 +43,30 @@
value="{{ default_servings }}"> value="{{ default_servings }}">
<span></span> <span></span>
<label for="input-password-1">{{ context.tr.tp(Sentence::ProfileNewPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}</label> <label for="input-first-day-of-week">{{ context.tr.t(Sentence::ProfileFirstDayOfWeek) }}</label>
<input id="input-password-1" type="password" name="password_1" autocomplete="new-password"> <select id="input-first-day-of-week" name="first_day_of_the_week">
<span></span> <option value="Mon"
{%~ if user.first_day_of_the_week == chrono::Weekday::Mon %}
<label for="input-password-2">{{ context.tr.t(Sentence::ReEnterPassword) }}</label> selected
<input id="input-password-2" type="password" name="password_2" autocomplete="new-password"> {% endif %}
<span class="user-message">{{ message_password }}</span> >
{{ context.tr.t(Sentence::CalendarMonday) }}
</option>
<option value="Sat"
{%~ if user.first_day_of_the_week == chrono::Weekday::Sat %}
selected
{% endif %}
>
{{ context.tr.t(Sentence::CalendarSaturday) }}
</option>
<option value="Sun"
{%~ if user.first_day_of_the_week == chrono::Weekday::Sun %}
selected
{% endif %}
>
{{ context.tr.t(Sentence::CalendarSunday) }}
</option>
</select>
<input type="submit" name="commit" value="{{ context.tr.t(Sentence::Save) }}"> <input type="submit" name="commit" value="{{ context.tr.t(Sentence::Save) }}">
</form> </form>

View file

@ -67,6 +67,7 @@
(ProfileTitle, "Profile"), (ProfileTitle, "Profile"),
(ProfileEmail, "Email (need to be revalidated if changed)"), (ProfileEmail, "Email (need to be revalidated if changed)"),
(ProfileDefaultServings, "Default servings"), (ProfileDefaultServings, "Default servings"),
(ProfileFirstDayOfWeek, "First day of the week"),
(ProfileNewPassword, "New password (minimum {} characters)"), (ProfileNewPassword, "New password (minimum {} characters)"),
(ProfileFollowEmailTitle, "Cooking Recipes: Email validation"), (ProfileFollowEmailTitle, "Cooking Recipes: Email validation"),
(ProfileFollowEmailLink, "Follow this link to validate this email address, {}"), (ProfileFollowEmailLink, "Follow this link to validate this email address, {}"),
@ -110,6 +111,13 @@
(RecipeSomeServings, "{} servings"), (RecipeSomeServings, "{} servings"),
(RecipeEstimatedTimeMinAbbreviation, "min"), (RecipeEstimatedTimeMinAbbreviation, "min"),
(CalendarMonday, "Monday"),
(CalendarTuesday, "Tuesday"),
(CalendarWednesday, "Wednesday"),
(CalendarThursday, "Thursday"),
(CalendarFriday, "Friday"),
(CalendarSaturday, "Saturday"),
(CalendarSunday, "Sunday"),
(CalendarMondayAbbreviation, "Mon"), (CalendarMondayAbbreviation, "Mon"),
(CalendarTuesdayAbbreviation, "Tue"), (CalendarTuesdayAbbreviation, "Tue"),
(CalendarWednesdayAbbreviation, "Wed"), (CalendarWednesdayAbbreviation, "Wed"),
@ -207,6 +215,7 @@
(ProfileTitle, "Profile"), (ProfileTitle, "Profile"),
(ProfileEmail, "Email (doit être revalidé si changé)"), (ProfileEmail, "Email (doit être revalidé si changé)"),
(ProfileDefaultServings, "Nombre de portions par défaut"), (ProfileDefaultServings, "Nombre de portions par défaut"),
(ProfileFirstDayOfWeek, "Premier jour de la semaine"),
(ProfileNewPassword, "Nouveau mot de passe (minimum {} caractères)"), (ProfileNewPassword, "Nouveau mot de passe (minimum {} caractères)"),
(ProfileFollowEmailTitle, "Recettes de Cuisine: Validation de l'adresse email"), (ProfileFollowEmailTitle, "Recettes de Cuisine: Validation de l'adresse email"),
(ProfileFollowEmailLink, "Suivez ce lien pour valider l'adresse email, {}"), (ProfileFollowEmailLink, "Suivez ce lien pour valider l'adresse email, {}"),
@ -250,6 +259,13 @@
(RecipeSomeServings, "pour {} personnes"), (RecipeSomeServings, "pour {} personnes"),
(RecipeEstimatedTimeMinAbbreviation, "min"), (RecipeEstimatedTimeMinAbbreviation, "min"),
(CalendarMonday, "lundi"),
(CalendarTuesday, "mardi"),
(CalendarWednesday, "mercredi"),
(CalendarThursday, "jeudi"),
(CalendarFriday, "vendredi"),
(CalendarSaturday, "samedi"),
(CalendarSunday, "dimanche"),
(CalendarMondayAbbreviation, "Lun"), (CalendarMondayAbbreviation, "Lun"),
(CalendarTuesdayAbbreviation, "Mar"), (CalendarTuesdayAbbreviation, "Mar"),
(CalendarWednesdayAbbreviation, "Mer"), (CalendarWednesdayAbbreviation, "Mer"),

View file

@ -63,7 +63,8 @@ pub struct SetRecipeEstimatedTime {
pub estimated_time: Option<u32>, pub estimated_time: Option<u32>,
} }
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[repr(u32)]
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)]
pub enum Difficulty { pub enum Difficulty {
Unknown = 0, Unknown = 0,
Easy = 1, Easy = 1,

View file

@ -66,6 +66,7 @@ impl CalendarState {
pub struct CalendarOptions { pub struct CalendarOptions {
pub can_select_date: bool, pub can_select_date: bool,
pub with_link_and_remove: bool, pub with_link_and_remove: bool,
pub first_day_of_the_week: Weekday,
} }
pub fn setup( pub fn setup(
@ -118,7 +119,10 @@ pub fn setup(
// gloo::console::log!(event); // TODO: Remove. // gloo::console::log!(event); // TODO: Remove.
if target.class_name() == "number" && options.can_select_date { if target.class_name() == "number" && options.can_select_date {
let first_day = first_grid_day(state_clone.get_displayed_date()); let first_day = first_grid_day(
state_clone.get_displayed_date(),
options.first_day_of_the_week,
);
let day_grid_id = target.parent_element().unwrap().id(); let day_grid_id = target.parent_element().unwrap().id();
let day_offset = day_grid_id[9..10].parse::<u64>().unwrap() * 7 let day_offset = day_grid_id[9..10].parse::<u64>().unwrap() * 7
+ day_grid_id[10..11].parse::<u64>().unwrap(); + day_grid_id[10..11].parse::<u64>().unwrap();
@ -212,7 +216,7 @@ fn display_month(
} }
} }
let first_day = first_grid_day(date); let first_day = first_grid_day(date, options.first_day_of_the_week);
let mut current = first_day; let mut current = first_day;
for i in 0..NB_CALENDAR_ROW { for i in 0..NB_CALENDAR_ROW {
@ -302,11 +306,11 @@ fn display_month(
}); });
} }
fn first_grid_day(mut date: NaiveDate) -> NaiveDate { fn first_grid_day(mut date: NaiveDate, first_day_of_the_week: Weekday) -> NaiveDate {
while (date - Days::new(1)).month() == date.month() { while (date - Days::new(1)).month() == date.month() {
date = date - Days::new(1); date = date - Days::new(1);
} }
while date.weekday() != Weekday::Mon { while date.weekday() != first_day_of_the_week {
date = date - Days::new(1); date = date - Days::new(1);
} }
date date

View file

@ -44,6 +44,12 @@ pub fn main() -> Result<(), JsValue> {
.map(|v| v == "true") .map(|v| v == "true")
.unwrap_or_default(); .unwrap_or_default();
let first_day_of_the_week = selector::<HtmlElement>("html")
.dataset()
.get("userFirstDayOfTheWeek")
.map(|v| v.parse().unwrap_or(chrono::Weekday::Mon))
.unwrap_or(chrono::Weekday::Mon);
match path[..] { match path[..] {
["recipe", "edit", id] => { ["recipe", "edit", id] => {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap. let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
@ -51,11 +57,11 @@ pub fn main() -> Result<(), JsValue> {
} }
["recipe", "view", id] => { ["recipe", "view", id] => {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap. let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
pages::recipe_view::setup_page(id, is_user_logged) pages::recipe_view::setup_page(id, is_user_logged, first_day_of_the_week)
} }
["dev_panel"] => pages::dev_panel::setup_page(), ["dev_panel"] => pages::dev_panel::setup_page(),
// Home. // Home.
[""] => pages::home::setup_page(is_user_logged), [""] => pages::home::setup_page(is_user_logged, first_day_of_the_week),
_ => log!("Path unknown: ", location), _ => log!("Path unknown: ", location),
} }

View file

@ -1,3 +1,4 @@
use chrono::Weekday;
use gloo::events::EventListener; use gloo::events::EventListener;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
@ -10,7 +11,7 @@ use crate::{
utils::{SelectorExt, by_id, get_current_lang, get_locale, selector}, utils::{SelectorExt, by_id, get_current_lang, get_locale, selector},
}; };
pub fn setup_page(is_user_logged: bool) { pub fn setup_page(is_user_logged: bool, first_day_of_the_week: Weekday) {
let recipe_scheduler = RecipeScheduler::new(!is_user_logged); let recipe_scheduler = RecipeScheduler::new(!is_user_logged);
calendar::setup( calendar::setup(
@ -18,6 +19,7 @@ pub fn setup_page(is_user_logged: bool) {
calendar::CalendarOptions { calendar::CalendarOptions {
can_select_date: false, can_select_date: false,
with_link_and_remove: true, with_link_and_remove: true,
first_day_of_the_week,
}, },
recipe_scheduler, recipe_scheduler,
); );

View file

@ -1,7 +1,8 @@
use chrono::Weekday;
use common::utils::substitute_with_names; use common::utils::substitute_with_names;
use gloo::events::EventListener; use gloo::events::EventListener;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
use web_sys::{Element, HtmlInputElement}; use web_sys::{Element, HtmlElement, HtmlInputElement};
use crate::{ use crate::{
calendar, modal_dialog, calendar, modal_dialog,
@ -10,7 +11,7 @@ use crate::{
utils::{SelectorExt, get_locale, selector}, utils::{SelectorExt, get_locale, selector},
}; };
pub fn setup_page(recipe_id: i64, is_user_logged: bool) { pub fn setup_page(recipe_id: i64, is_user_logged: bool, first_day_of_the_week: Weekday) {
let recipe_scheduler = RecipeScheduler::new(!is_user_logged); let recipe_scheduler = RecipeScheduler::new(!is_user_logged);
let add_to_planner: Element = selector("#recipe-view .add-to-planner"); let add_to_planner: Element = selector("#recipe-view .add-to-planner");
@ -26,6 +27,7 @@ pub fn setup_page(recipe_id: i64, is_user_logged: bool) {
calendar::CalendarOptions { calendar::CalendarOptions {
can_select_date: true, can_select_date: true,
with_link_and_remove: false, with_link_and_remove: false,
first_day_of_the_week,
}, },
recipe_scheduler, recipe_scheduler,
) )