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]]
|
||||
name = "backbone-lib"
|
||||
version = "0.2.11"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"ewebsock",
|
||||
|
|
@ -2649,7 +2649,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "protocol"
|
||||
version = "0.2.11"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
|
@ -2883,7 +2883,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
|||
|
||||
[[package]]
|
||||
name = "relay-server"
|
||||
version = "0.2.11"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum",
|
||||
|
|
@ -3893,7 +3893,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "trictrac-store"
|
||||
version = "0.2.11"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.7",
|
||||
|
|
@ -3906,7 +3906,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "trictrac-web"
|
||||
version = "0.2.11"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"backbone-lib",
|
||||
"futures",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
[workspace.package]
|
||||
version = "0.2.11"
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "backbone-lib"
|
||||
version.workspace = true
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "trictrac-web"
|
||||
version.workspace = true
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.leptos-i18n]
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ body {
|
|||
/* ── Stats grid ──────────────────────────────────────────────────── */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
|
@ -2076,19 +2076,3 @@ 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 le",
|
||||
"member_since": "Membre depuis",
|
||||
"stat_games": "Parties",
|
||||
"stat_wins": "Victoires",
|
||||
"stat_losses": "Défaites",
|
||||
|
|
|
|||
|
|
@ -244,53 +244,10 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(),
|
|||
|
||||
// ── Utilities ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 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 {
|
||||
pub fn format_ts(ts: i64) -> String {
|
||||
let ms = (ts * 1000) as f64;
|
||||
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()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ 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)]
|
||||
|
|
@ -622,9 +621,6 @@ fn SiteHamburger() -> impl IntoView {
|
|||
sidebar_open.set(false);
|
||||
}>{t!(i18n, replay_snapshot)}</button>
|
||||
</div>
|
||||
<div>
|
||||
<span class="site-nav-version">"v" {VERSION}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// ── 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]
|
||||
fn GameDetailView(game: GameDetail) -> impl IntoView {
|
||||
let i18n = use_i18n();
|
||||
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()))
|
||||
let started = api::format_ts(game.started_at);
|
||||
let ended = game.ended_at.map(api::format_ts)
|
||||
.unwrap_or_else(|| t_string!(i18n, game_ongoing).to_string());
|
||||
|
||||
view! {
|
||||
|
|
|
|||
|
|
@ -37,16 +37,7 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
|
|||
async move { api::get_user_games(&u, p).await }
|
||||
});
|
||||
|
||||
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());
|
||||
let joined = api::format_ts(profile.created_at);
|
||||
|
||||
view! {
|
||||
<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="label">{t!(i18n, stat_losses)}</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>
|
||||
|
||||
|
|
@ -89,10 +84,6 @@ fn ProfileContent(profile: UserProfile, username: String) -> impl IntoView {
|
|||
#[component]
|
||||
fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> 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;
|
||||
|
||||
|
|
@ -109,8 +100,8 @@ fn GamesTable(games: Vec<GameSummary>, page: RwSignal<i64>) -> impl IntoView {
|
|||
</thead>
|
||||
<tbody>
|
||||
{rows.into_iter().map(|g| {
|
||||
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 started = api::format_ts(g.started_at);
|
||||
let ended = g.ended_at.map(api::format_ts).unwrap_or_else(|| "—".into());
|
||||
let outcome_class = match g.outcome.as_deref() {
|
||||
Some("win") => "outcome-win",
|
||||
Some("loss") => "outcome-loss",
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
|
||||
trictrac = with final; rustPlatform.buildRustPackage {
|
||||
pname = "trictrac";
|
||||
version = "0.2.11"; # trictrac-version
|
||||
version = "0.2.1";
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [ pkg-config ];
|
||||
|
|
|
|||
11
justfile
11
justfile
|
|
@ -2,17 +2,6 @@
|
|||
# ^ 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 <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:
|
||||
cargo doc --no-deps
|
||||
shell:
|
||||
|
|
|
|||
16
module.nix
16
module.nix
|
|
@ -123,7 +123,6 @@ in
|
|||
proxy_read_timeout 3600s;
|
||||
'';
|
||||
withSSL = cfg.protocol == "https";
|
||||
listenPort = if withSSL then 443 else 80;
|
||||
in
|
||||
{
|
||||
"${cfg.hostname}" = {
|
||||
|
|
@ -131,19 +130,18 @@ in
|
|||
forceSSL = withSSL;
|
||||
# Explicit listen so this vhost isn't shadowed by a default_server
|
||||
# created by other virtual hosts with forceSSL = true.
|
||||
listen = [
|
||||
{ addr = "0.0.0.0"; port = listenPort; ssl = withSSL; }
|
||||
{ addr = "[::]"; port = listenPort; ssl = withSSL; }
|
||||
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; }
|
||||
];
|
||||
locations."/" = {
|
||||
extraConfig = proxyConfig;
|
||||
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]
|
||||
name = "protocol"
|
||||
version.workspace = true
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "relay-server"
|
||||
version.workspace = true
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
.layer(cors)
|
||||
.fallback_service(ServeDir::new(".").not_found_service(ServeFile::new("index.html")));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "trictrac-store"
|
||||
version.workspace = true
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# 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