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",
"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",
]

View file

@ -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 = [

View file

@ -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(

View file

@ -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::{

View file

@ -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<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 ***/
#[debug_handler]

View file

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

View file

@ -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(),
))
}
}

View file

@ -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<dyn ToString + Send>]) -> String {
let text = self.lang.get(sentence);
let params_as_string: Vec<String> = 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<Sentence>,
{
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
}

View file

@ -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"),

View file

@ -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"] }

View file

@ -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";

View file

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

View file

@ -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<T> {
pub value: T,
}
// A value associated with an id.
#[derive(Serialize, Deserialize, Clone)]
pub struct KeyValue<T> {
pub id: i64,
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",
"DomRect",
"KeyboardEvent",
"History",
"Element",
"DomStringMap",
"HtmlDocument",

View file

@ -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::<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.
let select_language: HtmlSelectElement = by_id("select-website-language");
EventListener::new(&select_language.clone(), "input", move |_event| {

View file

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

View file

@ -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].

View file

@ -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::<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
}