Update dependencies, add translation endpoint, and display a user message when logged in

This commit is contained in:
Greg Burri 2025-05-05 01:59:30 +02:00
parent 6b043620b8
commit 348b0f24e9
19 changed files with 170 additions and 34 deletions

27
Cargo.lock generated
View file

@ -166,7 +166,7 @@ dependencies = [
"memchr", "memchr",
"serde", "serde",
"serde_derive", "serde_derive",
"winnow 0.7.8", "winnow 0.7.9",
] ]
[[package]] [[package]]
@ -499,9 +499,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.20" version = "1.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -592,13 +592,14 @@ dependencies = [
"chrono", "chrono",
"ron", "ron",
"serde", "serde",
"strum",
] ]
[[package]] [[package]]
name = "comrak" name = "comrak"
version = "0.38.0" version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f690706b5db081dccea6206d7f6d594bb9895599abea9d1a0539f13888781ae8" checksum = "d5c834ca54c5a20588b358f34d1533b4b498ddb5fd979cec6b22d0e8867a2449"
dependencies = [ dependencies = [
"bon", "bon",
"caseless", "caseless",
@ -1836,9 +1837,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]] [[package]]
name = "libm" name = "libm"
version = "0.2.13" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" checksum = "a25169bd5913a4b437588a7e3d127cd6e90127b60e0ffbd834a38f1599e016b8"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
@ -2536,7 +2537,6 @@ dependencies = [
"serde", "serde",
"sqlx", "sqlx",
"strum", "strum",
"strum_macros",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tower", "tower",
@ -2548,9 +2548,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.11" version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
] ]
@ -3268,6 +3268,9 @@ name = "strum"
version = "0.27.1" version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros",
]
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
@ -4222,9 +4225,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.8" version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b" checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -31,13 +31,12 @@ sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
async-compression = { version = "0.4", features = ["tokio", "gzip"] } async-compression = { version = "0.4", features = ["tokio", "gzip"] }
askama = "0.14" askama = "0.14"
comrak = "0.38" comrak = "0.39"
argon2 = { version = "0.5", features = ["default", "std"] } argon2 = { version = "0.5", features = ["default", "std"] }
rand_core = { version = "0.9", features = ["std"] } rand_core = { version = "0.9", features = ["std"] }
rand = "0.9" rand = "0.9"
strum = "0.27" strum = { version = "0.27", features = ["derive"] }
strum_macros = "0.27"
async-trait = "0.1" async-trait = "0.1"
lettre = { version = "0.11", default-features = false, features = [ lettre = { version = "0.11", default-features = false, features = [

View file

@ -233,6 +233,7 @@ pub fn make_service(
"/shopping_list/checked", "/shopping_list/checked",
patch(services::ron::shopping_list::set_entry_checked), patch(services::ron::shopping_list::set_entry_checked),
) )
.route("/translation", get(services::ron::get_translation))
.fallback(services::ron::not_found); .fallback(services::ron::not_found);
let fragments_routes = Router::new().route( let fragments_routes = Router::new().route(

View file

@ -1,7 +1,7 @@
use chrono::{Duration, prelude::*}; use chrono::{Duration, prelude::*};
use rand::distr::{Alphanumeric, SampleString}; use rand::distr::{Alphanumeric, SampleString};
use sqlx::Sqlite; use sqlx::Sqlite;
use strum_macros::Display; use strum::Display;
use super::{Connection, DBError, Result}; use super::{Connection, DBError, Result};
use crate::{ use crate::{

View file

@ -1,13 +1,18 @@
use axum::{ use axum::{
debug_handler, debug_handler,
extract::{Extension, State}, extract::{Extension, Query, State},
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
response::{IntoResponse, Result}, response::{IntoResponse, Result},
}; };
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
use common::ron_api;
use crate::{ 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; pub mod calendar;
@ -36,6 +41,16 @@ pub async fn set_lang(
Ok((jar, StatusCode::OK)) Ok((jar, StatusCode::OK))
} }
#[debug_handler]
pub async fn get_translation(
Extension(context): Extension<Context>,
translation_id: Query<ron_api::Id>,
) -> Result<impl IntoResponse> {
Ok(ron_response_ok(ron_api::Value {
value: context.tr.t_from_id(translation_id.id),
}))
}
/*** 404 ***/ /*** 404 ***/
#[debug_handler] #[debug_handler]

View file

@ -58,7 +58,7 @@ pub async fn get(
pub async fn set_entry_checked( pub async fn set_entry_checked(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Value<bool>>, ExtractRon(ron): ExtractRon<ron_api::KeyValue<bool>>,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
check_user_rights_shopping_list_entry(&connection, &context.user, ron.id).await?; check_user_rights_shopping_list_entry(&connection, &context.user, ron.id).await?;
Ok(ron_response_ok( Ok(ron_response_ok(

View file

@ -367,7 +367,15 @@ pub async fn sign_in_post(
.same_site(cookie::SameSite::Strict); .same_site(cookie::SameSite::Strict);
Ok(( Ok((
jar.add(cookie), 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(),
)) ))
} }
} }

View file

@ -5,11 +5,11 @@ use common::utils;
use ron::de::from_reader; use ron::de::from_reader;
use serde::Deserialize; use serde::Deserialize;
use strum::EnumCount; use strum::EnumCount;
use strum_macros::EnumCount;
use tracing::warn; use tracing::warn;
use crate::consts; use crate::consts;
#[repr(i64)]
#[derive(Debug, Clone, EnumCount, Deserialize)] #[derive(Debug, Clone, EnumCount, Deserialize)]
pub enum Sentence { pub enum Sentence {
MainTitle = 0, MainTitle = 0,
@ -34,6 +34,7 @@ pub enum Sentence {
SignInMenu, SignInMenu,
SignInTitle, SignInTitle,
SignInButton, SignInButton,
SignInSuccess,
WrongEmailOrPassword, WrongEmailOrPassword,
// Sign up page. // Sign up page.
@ -188,6 +189,11 @@ impl Tr {
self.lang.get(sentence) 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<dyn ToString + Send>]) -> String { pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
let text = self.lang.get(sentence); let text = self.lang.get(sentence);
let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect(); let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect();
@ -246,6 +252,7 @@ impl Tr {
| "VE" // Venezuela. | "VE" // Venezuela.
| "ZW" // Zimbabwe. | "ZW" // Zimbabwe.
=> Weekday::Sun, => Weekday::Sun,
"AF" // Afghanistan. "AF" // Afghanistan.
| "DZ" // Algeria. | "DZ" // Algeria.
| "BH" // Bahrain. | "BH" // Bahrain.
@ -262,6 +269,7 @@ impl Tr {
| "AE" // United Arab Emirates. | "AE" // United Arab Emirates.
| "YE" // Yemen. | "YE" // Yemen.
=> Weekday::Sat, => Weekday::Sat,
_ => Weekday::Mon, _ => Weekday::Mon,
} }
} }
@ -320,14 +328,17 @@ impl Language {
T: Borrow<Sentence>, T: Borrow<Sentence>,
{ {
let sentence_cloned: Sentence = sentence.borrow().clone(); 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, None => UNABLE_TO_FIND_TRANSLATION_MESSAGE,
Some(text) => text, Some(text) => text,
}; };
if text.is_empty() && self.code != DEFAULT_LANGUAGE_CODE { 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 text
} }

View file

@ -25,7 +25,9 @@
(SignInMenu, "Sign in"), (SignInMenu, "Sign in"),
(SignInTitle, "Sign in"), (SignInTitle, "Sign in"),
(SignInButton, "Sign in"), (SignInButton, "Sign in"),
(SignInSuccess, "Sign in successful"),
(WrongEmailOrPassword, "Wrong email or password"), (WrongEmailOrPassword, "Wrong email or password"),
(AccountMustBeValidatedFirst, "This account must be validated first"), (AccountMustBeValidatedFirst, "This account must be validated first"),
(InvalidEmail, "Invalid email"), (InvalidEmail, "Invalid email"),
(PasswordDontMatch, "Passwords don't match"), (PasswordDontMatch, "Passwords don't match"),
@ -175,7 +177,9 @@
(SignInMenu, "Se connecter"), (SignInMenu, "Se connecter"),
(SignInTitle, "Se connecter"), (SignInTitle, "Se connecter"),
(SignInButton, "Se connecter"), (SignInButton, "Se connecter"),
(SignInSuccess, "Connexion réussie"),
(WrongEmailOrPassword, "Mot de passe ou email invalide"), (WrongEmailOrPassword, "Mot de passe ou email invalide"),
(AccountMustBeValidatedFirst, "Ce compte doit d'abord être validé"), (AccountMustBeValidatedFirst, "Ce compte doit d'abord être validé"),
(InvalidEmail, "Adresse email invalide"), (InvalidEmail, "Adresse email invalide"),
(PasswordDontMatch, "Les mots de passe ne correspondent pas"), (PasswordDontMatch, "Les mots de passe ne correspondent pas"),

View file

@ -8,3 +8,4 @@ edition = "2024"
ron = "0.10" ron = "0.10"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
strum = { version = "0.27", features = ["derive"] }

View file

@ -1,2 +1,5 @@
pub const MIN_PASSWORD_SIZE: usize = 8; pub const MIN_PASSWORD_SIZE: usize = 8;
pub const COOKIE_DARK_THEME: &str = "dark_theme"; 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";

View file

@ -1,3 +1,4 @@
pub mod consts; pub mod consts;
pub mod ron_api; pub mod ron_api;
pub mod toast;
pub mod utils; pub mod utils;

View file

@ -19,9 +19,15 @@ pub struct Id {
pub id: i64, pub id: i64,
} }
// A value associated with an id. // A simple value.
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct Value<T> { pub struct Value<T> {
pub value: T,
}
// A value associated with an id.
#[derive(Serialize, Deserialize, Clone)]
pub struct KeyValue<T> {
pub id: i64, pub id: i64,
pub value: T, pub value: T,
} }

10
common/src/toast.rs Normal file
View file

@ -0,0 +1,10 @@
use strum::FromRepr;
#[derive(FromRepr)]
pub enum Level {
Success,
Error,
Info,
Warning,
Unknown,
}

View file

@ -36,6 +36,7 @@ web-sys = { version = "0.3", features = [
"DataTransfer", "DataTransfer",
"DomRect", "DomRect",
"KeyboardEvent", "KeyboardEvent",
"History",
"Element", "Element",
"DomStringMap", "DomStringMap",
"HtmlDocument", "HtmlDocument",

View file

@ -65,6 +65,44 @@ pub fn main() -> Result<(), JsValue> {
_ => log!("Path unknown: ", location), _ => 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::<i64>().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::<usize>().ok()
} else {
None
}
});
// Request the message to display.
spawn_local(async move {
let translation: ron_api::Value<String> =
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. // Language handling.
let select_language: HtmlSelectElement = by_id("select-website-language"); let select_language: HtmlSelectElement = by_id("select-website-language");
EventListener::new(&select_language.clone(), "input", move |_event| { EventListener::new(&select_language.clone(), "input", move |_event| {

View file

@ -39,7 +39,7 @@ impl ShoppingList {
} else { } else {
request::patch( request::patch(
"shopping_list/checked", "shopping_list/checked",
ron_api::Value { ron_api::KeyValue {
id: item_id, id: item_id,
value: is_checked, value: is_checked,
}, },

View file

@ -1,16 +1,9 @@
pub use common::toast::Level;
use gloo::{events::EventListener, timers::callback::Timeout}; use gloo::{events::EventListener, timers::callback::Timeout};
use web_sys::{Element, HtmlElement, HtmlImageElement}; use web_sys::{Element, HtmlElement, HtmlImageElement};
use crate::utils::{SelectorExt, by_id, selector_and_clone}; 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_ANIMATION: u32 = 500; // [ms].
const TIME_DISPLAYED: u32 = 5_000; // [ms]. const TIME_DISPLAYED: u32 = 5_000; // [ms].

View file

@ -1,7 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use chrono::Locale; use chrono::Locale;
use gloo::utils::document; use gloo::utils::{document, window};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use web_sys::Element; use web_sys::Element;
@ -133,3 +133,45 @@ pub fn get_locale() -> Locale {
.replace("-", "_"); .replace("-", "_");
Locale::from_str(&lang_and_territory).unwrap_or_default() 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::<Vec<&str>>()[..] {
[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
}