diff --git a/Cargo.lock b/Cargo.lock index 7098b88..dbc9592 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,7 +166,7 @@ dependencies = [ "memchr", "serde", "serde_derive", - "winnow 0.7.8", + "winnow 0.7.9", ] [[package]] @@ -499,9 +499,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.20" +version = "1.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" dependencies = [ "shlex", ] @@ -592,13 +592,14 @@ dependencies = [ "chrono", "ron", "serde", + "strum", ] [[package]] name = "comrak" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f690706b5db081dccea6206d7f6d594bb9895599abea9d1a0539f13888781ae8" +checksum = "d5c834ca54c5a20588b358f34d1533b4b498ddb5fd979cec6b22d0e8867a2449" dependencies = [ "bon", "caseless", @@ -1836,9 +1837,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" +checksum = "a25169bd5913a4b437588a7e3d127cd6e90127b60e0ffbd834a38f1599e016b8" [[package]] name = "libsqlite3-sys" @@ -2536,7 +2537,6 @@ dependencies = [ "serde", "sqlx", "strum", - "strum_macros", "thiserror 2.0.12", "tokio", "tower", @@ -2548,9 +2548,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags 2.9.0", ] @@ -3268,6 +3268,9 @@ name = "strum" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -4222,9 +4225,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b" +checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" dependencies = [ "memchr", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 48f85b1..d627ad1 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -31,13 +31,12 @@ sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] } async-compression = { version = "0.4", features = ["tokio", "gzip"] } askama = "0.14" -comrak = "0.38" +comrak = "0.39" argon2 = { version = "0.5", features = ["default", "std"] } rand_core = { version = "0.9", features = ["std"] } rand = "0.9" -strum = "0.27" -strum_macros = "0.27" +strum = { version = "0.27", features = ["derive"] } async-trait = "0.1" lettre = { version = "0.11", default-features = false, features = [ diff --git a/backend/src/app.rs b/backend/src/app.rs index 4057bbb..bafaae5 100644 --- a/backend/src/app.rs +++ b/backend/src/app.rs @@ -233,6 +233,7 @@ pub fn make_service( "/shopping_list/checked", patch(services::ron::shopping_list::set_entry_checked), ) + .route("/translation", get(services::ron::get_translation)) .fallback(services::ron::not_found); let fragments_routes = Router::new().route( diff --git a/backend/src/data/db/user.rs b/backend/src/data/db/user.rs index 4ee1c59..10041ff 100644 --- a/backend/src/data/db/user.rs +++ b/backend/src/data/db/user.rs @@ -1,7 +1,7 @@ use chrono::{Duration, prelude::*}; use rand::distr::{Alphanumeric, SampleString}; use sqlx::Sqlite; -use strum_macros::Display; +use strum::Display; use super::{Connection, DBError, Result}; use crate::{ diff --git a/backend/src/services/ron/mod.rs b/backend/src/services/ron/mod.rs index b971d8b..672464e 100644 --- a/backend/src/services/ron/mod.rs +++ b/backend/src/services/ron/mod.rs @@ -1,13 +1,18 @@ use axum::{ debug_handler, - extract::{Extension, State}, + extract::{Extension, Query, State}, http::{HeaderMap, StatusCode}, response::{IntoResponse, Result}, }; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; +use common::ron_api; use crate::{ - app::Context, consts, data::db, data::model, ron_extractor::ExtractRon, ron_utils::ron_error, + app::Context, + consts, + data::{db, model}, + ron_extractor::ExtractRon, + ron_utils::{ron_error, ron_response_ok}, }; pub mod calendar; @@ -36,6 +41,16 @@ pub async fn set_lang( Ok((jar, StatusCode::OK)) } +#[debug_handler] +pub async fn get_translation( + Extension(context): Extension, + translation_id: Query, +) -> Result { + Ok(ron_response_ok(ron_api::Value { + value: context.tr.t_from_id(translation_id.id), + })) +} + /*** 404 ***/ #[debug_handler] diff --git a/backend/src/services/ron/shopping_list.rs b/backend/src/services/ron/shopping_list.rs index 034ce8a..ca3b71a 100644 --- a/backend/src/services/ron/shopping_list.rs +++ b/backend/src/services/ron/shopping_list.rs @@ -58,7 +58,7 @@ pub async fn get( pub async fn set_entry_checked( State(connection): State, Extension(context): Extension, - ExtractRon(ron): ExtractRon>, + ExtractRon(ron): ExtractRon>, ) -> Result { check_user_rights_shopping_list_entry(&connection, &context.user, ron.id).await?; Ok(ron_response_ok( diff --git a/backend/src/services/user.rs b/backend/src/services/user.rs index 31c2630..25c9472 100644 --- a/backend/src/services/user.rs +++ b/backend/src/services/user.rs @@ -367,7 +367,15 @@ pub async fn sign_in_post( .same_site(cookie::SameSite::Strict); Ok(( jar.add(cookie), - Redirect::to(&format!("/{}/", context.tr.current_lang_code())).into_response(), + Redirect::to(&format!( + "/{}/?{}={}&{}={}", + context.tr.current_lang_code(), + common::consts::GET_PARAMETER_USER_MESSAGE, + Sentence::SignInSuccess as i64, + common::consts::GET_PARAMETER_USER_MESSAGE_LEVEL, + common::toast::Level::Success as usize + )) + .into_response(), )) } } diff --git a/backend/src/translation.rs b/backend/src/translation.rs index a28f4e6..c9390ee 100644 --- a/backend/src/translation.rs +++ b/backend/src/translation.rs @@ -5,11 +5,11 @@ use common::utils; use ron::de::from_reader; use serde::Deserialize; use strum::EnumCount; -use strum_macros::EnumCount; use tracing::warn; use crate::consts; +#[repr(i64)] #[derive(Debug, Clone, EnumCount, Deserialize)] pub enum Sentence { MainTitle = 0, @@ -34,6 +34,7 @@ pub enum Sentence { SignInMenu, SignInTitle, SignInButton, + SignInSuccess, WrongEmailOrPassword, // Sign up page. @@ -188,6 +189,11 @@ impl Tr { self.lang.get(sentence) } + pub fn t_from_id(&self, sentence_id: i64) -> &'static str { + self.lang.get_from_id(sentence_id) + } + + /// Translate a sentence with parameters. pub fn tp(&self, sentence: Sentence, params: &[Box]) -> String { let text = self.lang.get(sentence); let params_as_string: Vec = params.iter().map(|p| p.to_string()).collect(); @@ -246,6 +252,7 @@ impl Tr { | "VE" // Venezuela. | "ZW" // Zimbabwe. => Weekday::Sun, + "AF" // Afghanistan. | "DZ" // Algeria. | "BH" // Bahrain. @@ -262,6 +269,7 @@ impl Tr { | "AE" // United Arab Emirates. | "YE" // Yemen. => Weekday::Sat, + _ => Weekday::Mon, } } @@ -320,14 +328,17 @@ impl Language { T: Borrow, { let sentence_cloned: Sentence = sentence.borrow().clone(); + self.get_from_id(sentence_cloned as i64) + } - let text: &str = match self.translation.get(sentence_cloned as usize) { + pub fn get_from_id(&'static self, sentence_id: i64) -> &'static str { + let text: &str = match self.translation.get(sentence_id as usize) { None => UNABLE_TO_FIND_TRANSLATION_MESSAGE, Some(text) => text, }; if text.is_empty() && self.code != DEFAULT_LANGUAGE_CODE { - return get_language_translation(DEFAULT_LANGUAGE_CODE).get(sentence); + return get_language_translation(DEFAULT_LANGUAGE_CODE).get_from_id(sentence_id); } text } diff --git a/backend/translation.ron b/backend/translation.ron index e80e67e..993cc43 100644 --- a/backend/translation.ron +++ b/backend/translation.ron @@ -25,7 +25,9 @@ (SignInMenu, "Sign in"), (SignInTitle, "Sign in"), (SignInButton, "Sign in"), + (SignInSuccess, "Sign in successful"), (WrongEmailOrPassword, "Wrong email or password"), + (AccountMustBeValidatedFirst, "This account must be validated first"), (InvalidEmail, "Invalid email"), (PasswordDontMatch, "Passwords don't match"), @@ -175,7 +177,9 @@ (SignInMenu, "Se connecter"), (SignInTitle, "Se connecter"), (SignInButton, "Se connecter"), + (SignInSuccess, "Connexion réussie"), (WrongEmailOrPassword, "Mot de passe ou email invalide"), + (AccountMustBeValidatedFirst, "Ce compte doit d'abord être validé"), (InvalidEmail, "Adresse email invalide"), (PasswordDontMatch, "Les mots de passe ne correspondent pas"), diff --git a/common/Cargo.toml b/common/Cargo.toml index 1fc4080..5299ea7 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -8,3 +8,4 @@ edition = "2024" ron = "0.10" serde = { version = "1.0", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } +strum = { version = "0.27", features = ["derive"] } diff --git a/common/src/consts.rs b/common/src/consts.rs index f6d3af3..b8b09e7 100644 --- a/common/src/consts.rs +++ b/common/src/consts.rs @@ -1,2 +1,5 @@ pub const MIN_PASSWORD_SIZE: usize = 8; pub const COOKIE_DARK_THEME: &str = "dark_theme"; + +pub const GET_PARAMETER_USER_MESSAGE: &str = "user_message"; +pub const GET_PARAMETER_USER_MESSAGE_LEVEL: &str = "user_message_icon"; diff --git a/common/src/lib.rs b/common/src/lib.rs index 63c9595..07b6d84 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,3 +1,4 @@ pub mod consts; pub mod ron_api; +pub mod toast; pub mod utils; diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index 87ca32c..596fbe3 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -19,9 +19,15 @@ pub struct Id { pub id: i64, } -// A value associated with an id. +// A simple value. #[derive(Serialize, Deserialize, Clone)] pub struct Value { + pub value: T, +} + +// A value associated with an id. +#[derive(Serialize, Deserialize, Clone)] +pub struct KeyValue { pub id: i64, pub value: T, } diff --git a/common/src/toast.rs b/common/src/toast.rs new file mode 100644 index 0000000..4258a87 --- /dev/null +++ b/common/src/toast.rs @@ -0,0 +1,10 @@ +use strum::FromRepr; + +#[derive(FromRepr)] +pub enum Level { + Success, + Error, + Info, + Warning, + Unknown, +} diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index b00b241..7c39b7f 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -36,6 +36,7 @@ web-sys = { version = "0.3", features = [ "DataTransfer", "DomRect", "KeyboardEvent", + "History", "Element", "DomStringMap", "HtmlDocument", diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index e085200..c0567c3 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -65,6 +65,44 @@ pub fn main() -> Result<(), JsValue> { _ => log!("Path unknown: ", location), } + // User message handling. + let user_message_params = utils::extract_get_parameters(&[ + common::consts::GET_PARAMETER_USER_MESSAGE, + common::consts::GET_PARAMETER_USER_MESSAGE_LEVEL, + ]); + if let Some(mess_id) = user_message_params.iter().find_map(|(k, v)| { + if k == common::consts::GET_PARAMETER_USER_MESSAGE { + v.parse::().ok() + } else { + None + } + }) { + let level_id = user_message_params.iter().find_map(|(k, v)| { + if k == common::consts::GET_PARAMETER_USER_MESSAGE_LEVEL { + v.parse::().ok() + } else { + None + } + }); + + // Request the message to display. + spawn_local(async move { + let translation: ron_api::Value = + request::get("translation", ron_api::Id { id: mess_id }) + .await + .unwrap(); + if let Some(level_id) = level_id { + toast::show_message_level( + common::toast::Level::from_repr(level_id) + .unwrap_or(common::toast::Level::Unknown), + &translation.value, + ); + } else { + toast::show_message(&translation.value); + } + }); + } + // Language handling. let select_language: HtmlSelectElement = by_id("select-website-language"); EventListener::new(&select_language.clone(), "input", move |_event| { diff --git a/frontend/src/shopping_list.rs b/frontend/src/shopping_list.rs index 39f59c0..9674b1d 100644 --- a/frontend/src/shopping_list.rs +++ b/frontend/src/shopping_list.rs @@ -39,7 +39,7 @@ impl ShoppingList { } else { request::patch( "shopping_list/checked", - ron_api::Value { + ron_api::KeyValue { id: item_id, value: is_checked, }, diff --git a/frontend/src/toast.rs b/frontend/src/toast.rs index faaff59..dd41a3d 100644 --- a/frontend/src/toast.rs +++ b/frontend/src/toast.rs @@ -1,16 +1,9 @@ +pub use common::toast::Level; use gloo::{events::EventListener, timers::callback::Timeout}; use web_sys::{Element, HtmlElement, HtmlImageElement}; use crate::utils::{SelectorExt, by_id, selector_and_clone}; -pub enum Level { - Success, - Error, - Info, - Warning, - Unknown, -} - const TIME_ANIMATION: u32 = 500; // [ms]. const TIME_DISPLAYED: u32 = 5_000; // [ms]. diff --git a/frontend/src/utils.rs b/frontend/src/utils.rs index 3fbb3bf..ea68d51 100644 --- a/frontend/src/utils.rs +++ b/frontend/src/utils.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use chrono::Locale; -use gloo::utils::document; +use gloo::utils::{document, window}; use wasm_bindgen::prelude::*; use web_sys::Element; @@ -133,3 +133,45 @@ pub fn get_locale() -> Locale { .replace("-", "_"); Locale::from_str(&lang_and_territory).unwrap_or_default() } + +/// Extracts and remove some URL parameters from the current URL. +pub fn extract_get_parameters(names: &[&str]) -> Vec<(String, String)> { + let mut param_values = vec![]; + let location = window().location(); + + if let Ok(search) = location.search() { + if !search.is_empty() && search.starts_with('?') { + let mut search_chars = search.chars(); + search_chars.next(); // To remove the first character which is '?'. + let mut new_search = String::with_capacity(search.len()); + for kv in search_chars.as_str().split('&') { + match kv.split('=').collect::>()[..] { + [key, value] if names.contains(&key) => { + param_values.push((key.to_string(), value.to_string())); + } + _ => { + if !new_search.is_empty() { + new_search.push('&'); + } + new_search.push_str(kv); + } + } + } + + if !param_values.is_empty() { + let mut new_url = location.pathname().unwrap(); + if !new_search.is_empty() { + new_url.push('?'); + new_url.push_str(&new_search); + } + window() + .history() + .unwrap() + .push_state_with_url(&JsValue::null(), "", Some(&new_url)) + .unwrap(); + } + } + } + + param_values +}