* Support for lang in URL as /fr/recipe/view/42

* Create a pages module in the frontend crate
This commit is contained in:
Greg Burri 2025-03-26 01:49:02 +01:00
parent b812525f4b
commit 418d31a127
12 changed files with 117 additions and 80 deletions

51
Cargo.lock generated
View file

@ -330,9 +330,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.16" version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -537,9 +537,9 @@ dependencies = [
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
] ]
@ -1226,14 +1226,15 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.61" version = "0.1.62"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
"iana-time-zone-haiku", "iana-time-zone-haiku",
"js-sys", "js-sys",
"log",
"wasm-bindgen", "wasm-bindgen",
"windows-core", "windows-core",
] ]
@ -1512,9 +1513,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.26" version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "matchers" name = "matchers"
@ -1829,7 +1830,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [ dependencies = [
"zerocopy 0.8.23", "zerocopy 0.8.24",
] ]
[[package]] [[package]]
@ -1906,7 +1907,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rand_core 0.9.3", "rand_core 0.9.3",
"zerocopy 0.8.23", "zerocopy 0.8.24",
] ]
[[package]] [[package]]
@ -2165,9 +2166,9 @@ checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.0" version = "0.103.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f" checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -2664,9 +2665,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.19.0" version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.2", "getrandom 0.3.2",
@ -2727,9 +2728,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.40" version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
@ -2748,9 +2749,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.21" version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@ -3194,9 +3195,9 @@ dependencies = [
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "1.5.2" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
dependencies = [ dependencies = [
"redox_syscall", "redox_syscall",
"wasite", "wasite",
@ -3462,11 +3463,11 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.23" version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
dependencies = [ dependencies = [
"zerocopy-derive 0.8.23", "zerocopy-derive 0.8.24",
] ]
[[package]] [[package]]
@ -3482,9 +3483,9 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.23" version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -8,13 +8,15 @@
* Default number is the user setting user.default_servings * Default number is the user setting user.default_servings
* A symbol show the native recipe servings number * A symbol show the native recipe servings number
* Check position of message error in profile/sign in/sign up with flex grid layout * Check position of message error in profile/sign in/sign up with flex grid layout
* Replace Rinja by Askama when Askma 0.13 is out (Rinja has been merged with Askama)
* Define the UI (mockups). * Define the UI (mockups).
* Two CSS: one for desktop and one for mobile * Two CSS: one for desktop and one for mobile
* Use CSS flex/grid to define a good design/layout * Use CSS flex/grid to define a good design/layout
* CSS for dark mode + autodetect * CSS for dark mode + autodetect
* CSS for toast and modal dialog * CSS for toast and modal dialog
* Calendar: Choose the first day of the week * Calendar: Choose the first day of the week
* i18n: prefix uri with the language: /fr/recipe/view/2 * i18n: prefix uri with the language: /fr/recipe/view/2 (do it for all intern href)
* Redirect with the correct prefix when the current language is changed
* Make a search page * Make a search page
Use FTS5: Use FTS5:
https://sqlite.org/fts5.html https://sqlite.org/fts5.html

View file

@ -1,10 +1,10 @@
use std::{net::SocketAddr, path::Path}; use std::{net::SocketAddr, path::Path};
use axum::{ use axum::{
BoxError, Router, BoxError, Router, ServiceExt,
error_handling::HandleErrorLayer, error_handling::HandleErrorLayer,
extract::{ConnectInfo, Extension, FromRef, Request, State}, extract::{ConnectInfo, Extension, FromRef, Request, State},
http::StatusCode, http::{StatusCode, Uri},
middleware::{self, Next}, middleware::{self, Next},
response::Response, response::Response,
routing::{delete, get, patch, post, put}, routing::{delete, get, patch, post, put},
@ -14,6 +14,7 @@ use chrono::prelude::*;
use clap::Parser; use clap::Parser;
use config::Config; use config::Config;
use itertools::Itertools; use itertools::Itertools;
use tower::layer::Layer;
use tower::{ServiceBuilder, buffer::BufferLayer, limit::RateLimitLayer}; use tower::{ServiceBuilder, buffer::BufferLayer, limit::RateLimitLayer};
use tower_http::{ use tower_http::{
services::{ServeDir, ServeFile}, services::{ServeDir, ServeFile},
@ -290,13 +291,20 @@ async fn main() {
.with_state(state) .with_state(state)
.nest_service("/favicon.ico", ServeFile::new("static/favicon.ico")) .nest_service("/favicon.ico", ServeFile::new("static/favicon.ico"))
.nest_service("/static", ServeDir::new("static")) .nest_service("/static", ServeDir::new("static"))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http());
.into_make_service_with_connect_info::<SocketAddr>();
let url_rewriting_middleware = tower::util::MapRequestLayer::new(url_rewriting);
let app_with_url_rewriting = url_rewriting_middleware.layer(app);
let addr = SocketAddr::from(([0, 0, 0, 0], port)); let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap(); axum::serve(
listener,
app_with_url_rewriting.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
} }
async fn user_authentication( async fn user_authentication(
@ -312,54 +320,65 @@ async fn user_authentication(
Ok(next.run(req).await) Ok(next.run(req).await)
} }
#[derive(Debug, Clone)]
struct Lang(Option<String>);
fn url_rewriting(mut req: Request) -> Request {
// Here we are extracting the language from the url then rewriting it.
// For example:
// "/fr/recipe/view/1"
// lang = "fr" and uri rewritten as = "/recipe/view/1"
let lang_and_new_uri = 'lang_and_new_uri: {
if let Some(path_query) = req.uri().path_and_query() {
let mut parts = path_query.path().split('/');
let _ = parts.next(); // Empty part due to the first '/'.
if let Some(lang) = parts.next() {
let available_codes = translation::available_codes();
if available_codes.contains(&lang) {
let mut rest: String = String::from("");
for part in parts {
rest.push('/');
rest.push_str(part);
}
if let Some(query) = path_query.query() {
rest.push('?');
rest.push_str(query);
}
if let Ok(new_uri) = rest.parse::<Uri>() {
break 'lang_and_new_uri Some((lang.to_string(), new_uri));
}
}
}
}
None
};
if let Some((lang, new_uri)) = lang_and_new_uri {
*req.uri_mut() = new_uri;
req.extensions_mut().insert(Lang(Some(lang)));
} else {
req.extensions_mut().insert(Lang(None));
}
req
}
/// The language of the current HTTP request is defined in the current order: /// The language of the current HTTP request is defined in the current order:
/// - Extraction from the url: like in '/fr/recipe/view/42' (Not yet implemented). /// - Extraction from the url: like in '/fr/recipe/view/42'
/// - Get from the user database record. /// - Get from the user database record.
/// - Get from the cookie. /// - Get from the cookie.
/// - Get from the HTTP header `accept-language`. /// - Get from the HTTP header `accept-language`.
/// - Set as `translation::DEFAULT_LANGUAGE_CODE`. /// - Set as `translation::DEFAULT_LANGUAGE_CODE`.
async fn translation( async fn translation(
Extension(lang): Extension<Lang>,
Extension(user): Extension<Option<model::User>>, Extension(user): Extension<Option<model::User>>,
mut req: Request, mut req: Request,
next: Next, next: Next,
) -> Result<Response> { ) -> Result<Response> {
// Here we are extracting the language from the url then rewriting it. let language = if let Some(lang) = lang.0 {
// For example: lang
// "/fr/recipe/view/1" } else if let Some(user) = user {
// lang = "fr" and uri rewritten as = "/recipe/view/1"
// Disable because it doesn't work at this level, see:
// https://docs.rs/axum/latest/axum/middleware/index.html#rewriting-request-uri-in-middleware
// let lang_and_new_uri = 'lang_from_uri: {
// if let Some(path_query) = req.uri().path_and_query() {
// event!(Level::INFO, "path: {:?}", path_query.path());
// let mut parts = path_query.path().split('/');
// let _ = parts.next(); // Empty part due to the first '/'.
// if let Some(lang) = parts.next() {
// let available_codes = translation::available_codes();
// if available_codes.contains(&lang) {
// let mut rest: String = String::from("");
// for part in parts {
// rest.push('/');
// rest.push_str(part);
// }
// // let uri_builder = Uri::builder()
// if let Ok(new_uri) = rest.parse::<Uri>() {
// event!(Level::INFO, "path rewrite: {:?}", new_uri.path());
// break 'lang_from_uri Some((lang.to_string(), new_uri));
// }
// }
// }
// }
// None
// };
// let language = if let Some((lang, uri)) = lang_and_new_uri {
// *req.uri_mut() = uri; // Replace the URI without the language.
// event!(Level::INFO, "URI: {:?}", req.uri());
// lang
// } else
let language = if let Some(user) = user {
user.lang user.lang
} else { } else {
let available_codes = translation::available_codes(); let available_codes = translation::available_codes();

View file

@ -7,10 +7,10 @@ use rinja::Template;
// use tracing::{event, Level}; // use tracing::{event, Level};
use crate::{ use crate::{
Result,
data::{db, model}, data::{db, model},
html_templates::*, html_templates::*,
translation::{self, Sentence}, translation::{self, Sentence},
Result,
}; };
#[debug_handler] #[debug_handler]

View file

@ -18,8 +18,6 @@ def main [host: string, destination: string, ssh_key: path] {
} }
cd frontend cd frontend
# source frontend/deploy.nu
# main true
trunk build --release trunk build --release
cd .. cd ..

View file

@ -9,12 +9,10 @@ use crate::utils::selector;
mod calendar; mod calendar;
mod error; mod error;
mod home;
mod modal_dialog; mod modal_dialog;
mod on_click; mod on_click;
mod recipe_edit; mod pages;
mod recipe_scheduler; mod recipe_scheduler;
mod recipe_view;
mod request; mod request;
mod shopping_list; mod shopping_list;
mod toast; mod toast;
@ -24,8 +22,14 @@ mod utils;
pub fn main() -> Result<(), JsValue> { pub fn main() -> Result<(), JsValue> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let lang = utils::get_current_lang();
let location = window().location().pathname()?; let location = window().location().pathname()?;
let path: Vec<&str> = location.split('/').skip(1).collect(); let path: Vec<&str> = location
.split('/')
.skip(1)
.skip_while(|part| *part == lang)
.collect();
let is_user_logged = selector::<HtmlElement>("html") let is_user_logged = selector::<HtmlElement>("html")
.dataset() .dataset()
@ -36,14 +40,14 @@ pub fn main() -> Result<(), JsValue> {
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.
recipe_edit::setup_page(id) pages::recipe_edit::setup_page(id)
} }
["recipe", "view", id] => { ["recipe", "view", id] => {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap. let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
recipe_view::setup_page(id, is_user_logged) pages::recipe_view::setup_page(id, is_user_logged)
} }
// Home. // Home.
[""] => home::setup_page(is_user_logged), [""] => pages::home::setup_page(is_user_logged),
_ => log!("Path unknown: ", location), _ => log!("Path unknown: ", location),
} }

View file

@ -0,0 +1,3 @@
pub mod home;
pub mod recipe_edit;
pub mod recipe_view;

View file

@ -1,7 +1,7 @@
use chrono::{Datelike, Days, Months, NaiveDate}; use chrono::{Datelike, Days, Months, NaiveDate};
use common::ron_api; use common::ron_api;
use gloo::storage::{LocalStorage, Storage}; use gloo::storage::{LocalStorage, Storage};
use ron::ser::{to_string_pretty, PrettyConfig}; use ron::ser::{PrettyConfig, to_string_pretty};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;

View file

@ -99,6 +99,16 @@ where
.unwrap() .unwrap()
} }
pub fn get_current_lang() -> String {
selector::<Element>("html")
.get_attribute("lang")
.unwrap()
.split("-")
.next()
.unwrap()
.to_string()
}
pub fn get_locale() -> Locale { pub fn get_locale() -> Locale {
let lang_and_territory = selector::<Element>("html") let lang_and_territory = selector::<Element>("html")
.get_attribute("lang") .get_attribute("lang")