Compare commits
3 commits
c7bb3a3291
...
4003fc0ef2
| Author | SHA1 | Date | |
|---|---|---|---|
| 4003fc0ef2 | |||
| 25554126a8 | |||
| 9443a04ad6 |
7 changed files with 80 additions and 22 deletions
|
|
@ -161,7 +161,7 @@ body {
|
||||||
/* ── Stats grid ──────────────────────────────────────────────────── */
|
/* ── Stats grid ──────────────────────────────────────────────────── */
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
"resend_verification": "Renvoyer l'email de vérification",
|
"resend_verification": "Renvoyer l'email de vérification",
|
||||||
"verification_email_resent": "Email de vérification envoyé.",
|
"verification_email_resent": "Email de vérification envoyé.",
|
||||||
"loading": "Chargement…",
|
"loading": "Chargement…",
|
||||||
"member_since": "Membre depuis",
|
"member_since": "Membre depuis le",
|
||||||
"stat_games": "Parties",
|
"stat_games": "Parties",
|
||||||
"stat_wins": "Victoires",
|
"stat_wins": "Victoires",
|
||||||
"stat_losses": "Défaites",
|
"stat_losses": "Défaites",
|
||||||
|
|
|
||||||
|
|
@ -244,10 +244,53 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(),
|
||||||
|
|
||||||
// ── Utilities ─────────────────────────────────────────────────────────────────
|
// ── Utilities ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn format_ts(ts: i64) -> String {
|
/// Maps to the `Intl.DateTimeFormat` options object accepted by `Date.toLocaleString`.
|
||||||
|
/// `Default` passes no options (browser default: full date + time).
|
||||||
|
pub struct DateFormatOptions {
|
||||||
|
/// "full" | "long" | "medium" | "short" — omit to suppress date part
|
||||||
|
pub date_style: Option<&'static str>,
|
||||||
|
/// "full" | "long" | "medium" | "short" — omit to suppress time part
|
||||||
|
pub time_style: Option<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DateFormatOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { date_style: None, time_style: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateFormatOptions {
|
||||||
|
pub fn date_only() -> Self {
|
||||||
|
Self { date_style: Some("short"), time_style: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_only() -> Self {
|
||||||
|
Self { date_style: None, time_style: Some("short") }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date_time() -> Self {
|
||||||
|
Self { date_style: Some("short"), time_style: Some("short") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_js_value(&self) -> wasm_bindgen::JsValue {
|
||||||
|
if self.date_style.is_none() && self.time_style.is_none() {
|
||||||
|
return wasm_bindgen::JsValue::UNDEFINED;
|
||||||
|
}
|
||||||
|
let obj = js_sys::Object::new();
|
||||||
|
if let Some(v) = self.date_style {
|
||||||
|
let _ = js_sys::Reflect::set(&obj, &"dateStyle".into(), &v.into());
|
||||||
|
}
|
||||||
|
if let Some(v) = self.time_style {
|
||||||
|
let _ = js_sys::Reflect::set(&obj, &"timeStyle".into(), &v.into());
|
||||||
|
}
|
||||||
|
obj.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_ts(ts: i64, locale: &str, opts: &DateFormatOptions) -> String {
|
||||||
let ms = (ts * 1000) as f64;
|
let ms = (ts * 1000) as f64;
|
||||||
let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(ms));
|
let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(ms));
|
||||||
date.to_locale_string("en-GB", &wasm_bindgen::JsValue::UNDEFINED)
|
date.to_locale_string(locale, &opts.to_js_value())
|
||||||
.as_string()
|
.as_string()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,12 @@ pub fn GameDetailPage() -> impl IntoView {
|
||||||
#[component]
|
#[component]
|
||||||
fn GameDetailView(game: GameDetail) -> impl IntoView {
|
fn GameDetailView(game: GameDetail) -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
let started = api::format_ts(game.started_at);
|
let locale_tag = match i18n.get_locale() {
|
||||||
let ended = game.ended_at.map(api::format_ts)
|
Locale::en => "en-GB",
|
||||||
|
Locale::fr => "fr-FR",
|
||||||
|
};
|
||||||
|
let started = api::format_ts(game.started_at, locale_tag, &api::DateFormatOptions::date_only());
|
||||||
|
let ended = game.ended_at.map(|ts| api::format_ts(ts, locale_tag, &api::DateFormatOptions::date_only()))
|
||||||
.unwrap_or_else(|| t_string!(i18n, game_ongoing).to_string());
|
.unwrap_or_else(|| t_string!(i18n, game_ongoing).to_string());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,16 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
|
||||||
async move { api::get_user_games(&u, p).await }
|
async move { api::get_user_games(&u, p).await }
|
||||||
});
|
});
|
||||||
|
|
||||||
let joined = api::format_ts(profile.created_at);
|
let locale_tag = match i18n.get_locale() {
|
||||||
|
Locale::en => "en-GB",
|
||||||
|
Locale::fr => "fr-FR",
|
||||||
|
};
|
||||||
|
let date_format = api::DateFormatOptions {
|
||||||
|
date_style: Some("long"),
|
||||||
|
time_style: None,
|
||||||
|
};
|
||||||
|
let joined = api::format_ts(profile.created_at, locale_tag, &date_format);
|
||||||
|
// let joined = api::format_ts(profile.created_at, locale_tag, &api::DateFormatOptions::date_only());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="portal-card">
|
<div class="portal-card">
|
||||||
|
|
@ -57,10 +66,6 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
|
||||||
<div class="value outcome-loss">{ profile.losses }</div>
|
<div class="value outcome-loss">{ profile.losses }</div>
|
||||||
<div class="label">{t!(i18n, stat_losses)}</div>
|
<div class="label">{t!(i18n, stat_losses)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-box">
|
|
||||||
<div class="value outcome-draw">{ profile.draws }</div>
|
|
||||||
<div class="label">{t!(i18n, stat_draws)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -84,6 +89,10 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
|
||||||
#[component]
|
#[component]
|
||||||
fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> impl IntoView {
|
fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> impl IntoView {
|
||||||
let i18n = use_i18n();
|
let i18n = use_i18n();
|
||||||
|
let locale_tag = match i18n.get_locale() {
|
||||||
|
Locale::en => "en-GB",
|
||||||
|
Locale::fr => "fr-FR",
|
||||||
|
};
|
||||||
let rows = games.clone();
|
let rows = games.clone();
|
||||||
let has_next = games.len() == 20;
|
let has_next = games.len() == 20;
|
||||||
|
|
||||||
|
|
@ -100,8 +109,8 @@ fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> impl IntoView {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.into_iter().map(|g| {
|
{rows.into_iter().map(|g| {
|
||||||
let started = api::format_ts(g.started_at);
|
let started = api::format_ts(g.started_at, locale_tag, &api::DateFormatOptions::date_only());
|
||||||
let ended = g.ended_at.map(api::format_ts).unwrap_or_else(|| "—".into());
|
let ended = g.ended_at.map(|ts| api::format_ts(ts, locale_tag, &api::DateFormatOptions::date_only())).unwrap_or_else(|| "—".into());
|
||||||
let outcome_class = match g.outcome.as_deref() {
|
let outcome_class = match g.outcome.as_deref() {
|
||||||
Some("win") => "outcome-win",
|
Some("win") => "outcome-win",
|
||||||
Some("loss") => "outcome-loss",
|
Some("loss") => "outcome-loss",
|
||||||
|
|
|
||||||
16
module.nix
16
module.nix
|
|
@ -123,6 +123,7 @@ in
|
||||||
proxy_read_timeout 3600s;
|
proxy_read_timeout 3600s;
|
||||||
'';
|
'';
|
||||||
withSSL = cfg.protocol == "https";
|
withSSL = cfg.protocol == "https";
|
||||||
|
listenPort = if withSSL then 443 else 80;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
"${cfg.hostname}" = {
|
"${cfg.hostname}" = {
|
||||||
|
|
@ -130,18 +131,19 @@ in
|
||||||
forceSSL = withSSL;
|
forceSSL = withSSL;
|
||||||
# Explicit listen so this vhost isn't shadowed by a default_server
|
# Explicit listen so this vhost isn't shadowed by a default_server
|
||||||
# created by other virtual hosts with forceSSL = true.
|
# created by other virtual hosts with forceSSL = true.
|
||||||
listen =
|
listen = [
|
||||||
if withSSL then [
|
{ addr = "0.0.0.0"; port = listenPort; ssl = withSSL; }
|
||||||
{ addr = "0.0.0.0"; port = 443; ssl = true; }
|
{ addr = "[::]"; port = listenPort; ssl = withSSL; }
|
||||||
{ addr = "[::]"; port = 443; ssl = true; }
|
|
||||||
] else [
|
|
||||||
{ addr = "0.0.0.0"; port = 80; ssl = false; }
|
|
||||||
{ addr = "[::]"; port = 80; ssl = false; }
|
|
||||||
];
|
];
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
extraConfig = proxyConfig;
|
extraConfig = proxyConfig;
|
||||||
proxyPass = "http://trictrac-api/";
|
proxyPass = "http://trictrac-api/";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extraConfig = ''
|
||||||
|
error_log /var/log/nginx/trictrac_error.log;
|
||||||
|
access_log /var/log/nginx/trictrac_access.log;
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,9 @@ async fn main() {
|
||||||
.route("/ws", get(websocket_handler))
|
.route("/ws", get(websocket_handler))
|
||||||
.merge(http::router())
|
.merge(http::router())
|
||||||
.with_state(app_state)
|
.with_state(app_state)
|
||||||
|
.fallback_service(ServeDir::new(".").not_found_service(ServeFile::new("index.html")))
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.layer(cors)
|
.layer(cors);
|
||||||
.fallback_service(ServeDir::new(".").not_found_service(ServeFile::new("index.html")));
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue