diff --git a/Cargo.lock b/Cargo.lock
index 434407c..de6765c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -189,7 +189,7 @@ dependencies = [
[[package]]
name = "backbone-lib"
-version = "0.1.0"
+version = "0.2.11"
dependencies = [
"bytes",
"ewebsock",
@@ -2649,7 +2649,7 @@ dependencies = [
[[package]]
name = "protocol"
-version = "0.1.0"
+version = "0.2.11"
dependencies = [
"serde",
]
@@ -2883,7 +2883,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "relay-server"
-version = "0.1.0"
+version = "0.2.11"
dependencies = [
"argon2",
"axum",
@@ -3893,7 +3893,7 @@ dependencies = [
[[package]]
name = "trictrac-store"
-version = "0.1.0"
+version = "0.2.11"
dependencies = [
"anyhow",
"base64 0.21.7",
@@ -3906,7 +3906,7 @@ dependencies = [
[[package]]
name = "trictrac-web"
-version = "0.1.0"
+version = "0.2.11"
dependencies = [
"backbone-lib",
"futures",
diff --git a/Cargo.toml b/Cargo.toml
index e78d862..9db1a73 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,6 @@
+[workspace.package]
+version = "0.2.11"
+
[workspace]
resolver = "2"
diff --git a/clients/backbone-lib/Cargo.toml b/clients/backbone-lib/Cargo.toml
index 1e57d93..d6ae5c9 100644
--- a/clients/backbone-lib/Cargo.toml
+++ b/clients/backbone-lib/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "backbone-lib"
-version = "0.1.0"
+version.workspace = true
edition = "2024"
[dependencies]
diff --git a/clients/web/Cargo.toml b/clients/web/Cargo.toml
index 4849bec..1edb9eb 100644
--- a/clients/web/Cargo.toml
+++ b/clients/web/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "trictrac-web"
-version = "0.1.0"
+version.workspace = true
edition = "2021"
[package.metadata.leptos-i18n]
diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css
index 428d693..09b21e9 100644
--- a/clients/web/assets/style.css
+++ b/clients/web/assets/style.css
@@ -161,7 +161,7 @@ body {
/* ── Stats grid ──────────────────────────────────────────────────── */
.stats-grid {
display: grid;
- grid-template-columns: repeat(4, 1fr);
+ grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
@@ -2076,3 +2076,19 @@ a:hover { text-decoration: underline; }
max-width: 200px;
margin: 0 auto;
}
+
+/* Push the version wrapper to the bottom of the sidebar flex column */
+.game-sidebar > div:has(.site-nav-version) {
+ margin-top: auto;
+ padding: 0.75rem 1rem;
+ border-top: 1px solid rgba(200,164,72,0.12);
+}
+
+.site-nav-version {
+ display: block;
+ text-align: center;
+ font-family: var(--font-ui);
+ font-size: 0.7rem;
+ letter-spacing: 0.06em;
+ color: rgba(200,164,72,0.4);
+}
diff --git a/clients/web/locales/fr.json b/clients/web/locales/fr.json
index b3a05f0..569d66b 100644
--- a/clients/web/locales/fr.json
+++ b/clients/web/locales/fr.json
@@ -99,7 +99,7 @@
"resend_verification": "Renvoyer l'email de vérification",
"verification_email_resent": "Email de vérification envoyé.",
"loading": "Chargement…",
- "member_since": "Membre depuis",
+ "member_since": "Membre depuis le",
"stat_games": "Parties",
"stat_wins": "Victoires",
"stat_losses": "Défaites",
diff --git a/clients/web/src/api.rs b/clients/web/src/api.rs
index 9e0f57c..d826165 100644
--- a/clients/web/src/api.rs
+++ b/clients/web/src/api.rs
@@ -244,10 +244,53 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(),
// ── 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 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()
.unwrap_or_default()
}
diff --git a/clients/web/src/app.rs b/clients/web/src/app.rs
index 3819b61..5c38d33 100644
--- a/clients/web/src/app.rs
+++ b/clients/web/src/app.rs
@@ -32,6 +32,7 @@ use std::collections::VecDeque;
const RELAY_URL: &str = "ws://localhost:8080/ws";
const GAME_ID: &str = "trictrac";
const STORAGE_KEY: &str = "trictrac_session";
+const VERSION: &str = env!("CARGO_PKG_VERSION");
/// The state the UI needs to render the game screen.
#[derive(Clone, PartialEq)]
@@ -621,6 +622,9 @@ fn SiteHamburger() -> impl IntoView {
sidebar_open.set(false);
}>{t!(i18n, replay_snapshot)}
+
+ "v" {VERSION}
+
// ── Replay snapshot modal ─────────────────────────────────────────────
diff --git a/clients/web/src/nav.rs b/clients/web/src/nav.rs
deleted file mode 100644
index 10ecc36..0000000
--- a/clients/web/src/nav.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use leptos::prelude::*;
-use leptos::task::spawn_local;
-use leptos_router::components::A;
-
-use crate::api;
-use crate::i18n::*;
-
-#[component]
-pub fn SiteNav() -> impl IntoView {
- let i18n = use_i18n();
- let auth_username =
- use_context::>>().expect("auth_username context not found");
-
- let logout = move |_| {
- spawn_local(async move {
- let _ = api::post_logout().await;
- auth_username.set(None);
- });
- };
-
- view! {
-
- }
-}
diff --git a/clients/web/src/portal/game_detail.rs b/clients/web/src/portal/game_detail.rs
index adc3643..d0d17d4 100644
--- a/clients/web/src/portal/game_detail.rs
+++ b/clients/web/src/portal/game_detail.rs
@@ -32,8 +32,12 @@ pub fn GameDetailPage() -> impl IntoView {
#[component]
fn GameDetailView(game: GameDetail) -> impl IntoView {
let i18n = use_i18n();
- let started = api::format_ts(game.started_at);
- let ended = game.ended_at.map(api::format_ts)
+ let locale_tag = match i18n.get_locale() {
+ 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());
view! {
diff --git a/clients/web/src/portal/profile.rs b/clients/web/src/portal/profile.rs
index 9a94b3f..c727bbd 100644
--- a/clients/web/src/portal/profile.rs
+++ b/clients/web/src/portal/profile.rs
@@ -37,7 +37,16 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
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! {
@@ -57,10 +66,6 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
{ profile.losses }
{t!(i18n, stat_losses)}
-
-
{ profile.draws }
-
{t!(i18n, stat_draws)}
-
@@ -84,6 +89,10 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
#[component]
fn GamesTable(games: Vec, page: RwSignal) -> impl IntoView {
let i18n = use_i18n();
+ let locale_tag = match i18n.get_locale() {
+ Locale::en => "en-GB",
+ Locale::fr => "fr-FR",
+ };
let rows = games.clone();
let has_next = games.len() == 20;
@@ -100,8 +109,8 @@ fn GamesTable(games: Vec, page: RwSignal) -> impl IntoView {
{rows.into_iter().map(|g| {
- let started = api::format_ts(g.started_at);
- let ended = g.ended_at.map(api::format_ts).unwrap_or_else(|| "—".into());
+ let started = api::format_ts(g.started_at, locale_tag, &api::DateFormatOptions::date_only());
+ 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() {
Some("win") => "outcome-win",
Some("loss") => "outcome-loss",
diff --git a/flake.nix b/flake.nix
index 54da2ed..cde292d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -103,7 +103,7 @@
trictrac = with final; rustPlatform.buildRustPackage {
pname = "trictrac";
- version = "0.2.1";
+ version = "0.2.11"; # trictrac-version
src = ./.;
nativeBuildInputs = [ pkg-config ];
diff --git a/justfile b/justfile
index fe80391..fe4e52e 100644
--- a/justfile
+++ b/justfile
@@ -2,6 +2,17 @@
# ^ A shebang isn't required, but allows a justfile to be executed
# like a script, with `./justfile test`, for example.
+# Bump the project version and start a git-flow release.
+# Usage: just bump 0.2.12
+# After running, finish with: git flow release finish
+bump version:
+ sed -i '/^\[workspace\.package\]/,/^\[/{s/^version = ".*"/version = "{{version}}"/}' Cargo.toml
+ sed -i 's/version = "[0-9.]*"; # trictrac-version/version = "{{version}}"; # trictrac-version/' flake.nix
+ git flow release start {{version}}
+ git add Cargo.toml flake.nix
+ git commit -m "chore: bump version to {{version}}"
+ @echo "Done. Finish with: git flow release finish {{version}}"
+
doc:
cargo doc --no-deps
shell:
diff --git a/module.nix b/module.nix
index 68b2833..53f77c6 100644
--- a/module.nix
+++ b/module.nix
@@ -123,6 +123,7 @@ in
proxy_read_timeout 3600s;
'';
withSSL = cfg.protocol == "https";
+ listenPort = if withSSL then 443 else 80;
in
{
"${cfg.hostname}" = {
@@ -130,18 +131,19 @@ in
forceSSL = withSSL;
# Explicit listen so this vhost isn't shadowed by a default_server
# created by other virtual hosts with forceSSL = true.
- listen =
- if withSSL then [
- { addr = "0.0.0.0"; port = 443; ssl = true; }
- { addr = "[::]"; port = 443; ssl = true; }
- ] else [
- { addr = "0.0.0.0"; port = 80; ssl = false; }
- { addr = "[::]"; port = 80; ssl = false; }
+ listen = [
+ { addr = "0.0.0.0"; port = listenPort; ssl = withSSL; }
+ { addr = "[::]"; port = listenPort; ssl = withSSL; }
];
locations."/" = {
extraConfig = proxyConfig;
proxyPass = "http://trictrac-api/";
};
+
+ extraConfig = ''
+ error_log /var/log/nginx/trictrac_error.log;
+ access_log /var/log/nginx/trictrac_access.log;
+ '';
};
};
};
diff --git a/server/protocol/Cargo.toml b/server/protocol/Cargo.toml
index 2630fc1..70f4e68 100644
--- a/server/protocol/Cargo.toml
+++ b/server/protocol/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "protocol"
-version = "0.1.0"
+version.workspace = true
edition = "2024"
[dependencies]
diff --git a/server/relay-server/Cargo.toml b/server/relay-server/Cargo.toml
index ccb97fa..1c0112b 100644
--- a/server/relay-server/Cargo.toml
+++ b/server/relay-server/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "relay-server"
-version = "0.1.0"
+version.workspace = true
edition = "2024"
[dependencies]
diff --git a/server/relay-server/src/main.rs b/server/relay-server/src/main.rs
index 0dfea0c..32baf70 100644
--- a/server/relay-server/src/main.rs
+++ b/server/relay-server/src/main.rs
@@ -99,9 +99,9 @@ async fn main() {
.route("/ws", get(websocket_handler))
.merge(http::router())
.with_state(app_state)
+ .fallback_service(ServeDir::new(".").not_found_service(ServeFile::new("index.html")))
.layer(auth_layer)
- .layer(cors)
- .fallback_service(ServeDir::new(".").not_found_service(ServeFile::new("index.html")));
+ .layer(cors);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
diff --git a/store/Cargo.toml b/store/Cargo.toml
index d8a594c..92b1b84 100644
--- a/store/Cargo.toml
+++ b/store/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "trictrac-store"
-version = "0.1.0"
+version.workspace = true
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html