Add first day of the week feature to user settings and calendar functionality
This commit is contained in:
parent
39f5b968b4
commit
fdbf2e4f27
15 changed files with 191 additions and 42 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue