Compare commits
No commits in common. "da10ddee2b1ddd3efa7df2edb9ab7d4ceba1ce1a" and "ad356fe8326438e2cb8771b9d9818335cd2ab147" have entirely different histories.
da10ddee2b
...
ad356fe832
18 changed files with 79 additions and 125 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -189,7 +189,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backbone-lib"
|
name = "backbone-lib"
|
||||||
version = "0.2.11"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"ewebsock",
|
"ewebsock",
|
||||||
|
|
@ -2649,7 +2649,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protocol"
|
name = "protocol"
|
||||||
version = "0.2.11"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
@ -2883,7 +2883,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "relay-server"
|
name = "relay-server"
|
||||||
version = "0.2.11"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
@ -3893,7 +3893,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trictrac-store"
|
name = "trictrac-store"
|
||||||
version = "0.2.11"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
|
|
@ -3906,7 +3906,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trictrac-web"
|
name = "trictrac-web"
|
||||||
version = "0.2.11"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backbone-lib",
|
"backbone-lib",
|
||||||
"futures",
|
"futures",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
[workspace.package]
|
|
||||||
version = "0.2.11"
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "backbone-lib"
|
name = "backbone-lib"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "trictrac-web"
|
name = "trictrac-web"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[package.metadata.leptos-i18n]
|
[package.metadata.leptos-i18n]
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ body {
|
||||||
/* ── Stats grid ──────────────────────────────────────────────────── */
|
/* ── Stats grid ──────────────────────────────────────────────────── */
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
@ -2076,19 +2076,3 @@ a:hover { text-decoration: underline; }
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
margin: 0 auto;
|
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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 le",
|
"member_since": "Membre depuis",
|
||||||
"stat_games": "Parties",
|
"stat_games": "Parties",
|
||||||
"stat_wins": "Victoires",
|
"stat_wins": "Victoires",
|
||||||
"stat_losses": "Défaites",
|
"stat_losses": "Défaites",
|
||||||
|
|
|
||||||
|
|
@ -244,53 +244,10 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(),
|
||||||
|
|
||||||
// ── Utilities ─────────────────────────────────────────────────────────────────
|
// ── Utilities ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Maps to the `Intl.DateTimeFormat` options object accepted by `Date.toLocaleString`.
|
pub fn format_ts(ts: i64) -> String {
|
||||||
/// `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(locale, &opts.to_js_value())
|
date.to_locale_string("en-GB", &wasm_bindgen::JsValue::UNDEFINED)
|
||||||
.as_string()
|
.as_string()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ use std::collections::VecDeque;
|
||||||
const RELAY_URL: &str = "ws://localhost:8080/ws";
|
const RELAY_URL: &str = "ws://localhost:8080/ws";
|
||||||
const GAME_ID: &str = "trictrac";
|
const GAME_ID: &str = "trictrac";
|
||||||
const STORAGE_KEY: &str = "trictrac_session";
|
const STORAGE_KEY: &str = "trictrac_session";
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
/// The state the UI needs to render the game screen.
|
/// The state the UI needs to render the game screen.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
|
|
@ -622,9 +621,6 @@ fn SiteHamburger() -> impl IntoView {
|
||||||
sidebar_open.set(false);
|
sidebar_open.set(false);
|
||||||
}>{t!(i18n, replay_snapshot)}</button>
|
}>{t!(i18n, replay_snapshot)}</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span class="site-nav-version">"v" {VERSION}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// ── Replay snapshot modal ─────────────────────────────────────────────
|
// ── Replay snapshot modal ─────────────────────────────────────────────
|
||||||
|
|
|
||||||
46
clients/web/src/nav.rs
Normal file
46
clients/web/src/nav.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
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::<RwSignal<Option<String>>>().expect("auth_username context not found");
|
||||||
|
|
||||||
|
let logout = move |_| {
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = api::post_logout().await;
|
||||||
|
auth_username.set(None);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<nav class="site-nav">
|
||||||
|
<A href="/" attr:class="site-nav-brand">"Trictrac"</A>
|
||||||
|
<div class="site-nav-spacer" />
|
||||||
|
<div class="lang-switcher">
|
||||||
|
<button
|
||||||
|
class:lang-active=move || i18n.get_locale() == Locale::en
|
||||||
|
on:click=move |_| i18n.set_locale(Locale::en)
|
||||||
|
>"EN"</button>
|
||||||
|
<button
|
||||||
|
class:lang-active=move || i18n.get_locale() == Locale::fr
|
||||||
|
on:click=move |_| i18n.set_locale(Locale::fr)
|
||||||
|
>"FR"</button>
|
||||||
|
</div>
|
||||||
|
{move || match auth_username.get() {
|
||||||
|
Some(u) => view! {
|
||||||
|
<A href=format!("/profile/{u}")>{ u.clone() }</A>
|
||||||
|
<button class="site-nav-btn" on:click=logout>{t!(i18n, sign_out)}</button>
|
||||||
|
}.into_any(),
|
||||||
|
None => view! {
|
||||||
|
<A href="/account">{t!(i18n, sign_in)}</A>
|
||||||
|
}.into_any(),
|
||||||
|
}}
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,12 +32,8 @@ 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 locale_tag = match i18n.get_locale() {
|
let started = api::format_ts(game.started_at);
|
||||||
Locale::en => "en-GB",
|
let ended = game.ended_at.map(api::format_ts)
|
||||||
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,16 +37,7 @@ 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 locale_tag = match i18n.get_locale() {
|
let joined = api::format_ts(profile.created_at);
|
||||||
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">
|
||||||
|
|
@ -66,6 +57,10 @@ 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>
|
||||||
|
|
||||||
|
|
@ -89,10 +84,6 @@ 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;
|
||||||
|
|
||||||
|
|
@ -109,8 +100,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, locale_tag, &api::DateFormatOptions::date_only());
|
let started = api::format_ts(g.started_at);
|
||||||
let ended = g.ended_at.map(|ts| api::format_ts(ts, locale_tag, &api::DateFormatOptions::date_only())).unwrap_or_else(|| "—".into());
|
let ended = g.ended_at.map(api::format_ts).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",
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
|
|
||||||
trictrac = with final; rustPlatform.buildRustPackage {
|
trictrac = with final; rustPlatform.buildRustPackage {
|
||||||
pname = "trictrac";
|
pname = "trictrac";
|
||||||
version = "0.2.11"; # trictrac-version
|
version = "0.2.1";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
nativeBuildInputs = [ pkg-config ];
|
nativeBuildInputs = [ pkg-config ];
|
||||||
|
|
|
||||||
11
justfile
11
justfile
|
|
@ -2,17 +2,6 @@
|
||||||
# ^ A shebang isn't required, but allows a justfile to be executed
|
# ^ A shebang isn't required, but allows a justfile to be executed
|
||||||
# like a script, with `./justfile test`, for example.
|
# 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 <version>
|
|
||||||
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:
|
doc:
|
||||||
cargo doc --no-deps
|
cargo doc --no-deps
|
||||||
shell:
|
shell:
|
||||||
|
|
|
||||||
16
module.nix
16
module.nix
|
|
@ -123,7 +123,6 @@ 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}" = {
|
||||||
|
|
@ -131,19 +130,18 @@ 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 =
|
||||||
{ addr = "0.0.0.0"; port = listenPort; ssl = withSSL; }
|
if withSSL then [
|
||||||
{ addr = "[::]"; port = listenPort; ssl = withSSL; }
|
{ 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; }
|
||||||
];
|
];
|
||||||
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;
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "protocol"
|
name = "protocol"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "relay-server"
|
name = "relay-server"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "trictrac-store"
|
name = "trictrac-store"
|
||||||
version.workspace = true
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue