Merge branch 'release/v0.2.11'
This commit is contained in:
commit
da10ddee2b
18 changed files with 125 additions and 79 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
[workspace.package]
|
||||
version = "0.2.11"
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "backbone-lib"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "trictrac-web"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.leptos-i18n]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}</button>
|
||||
</div>
|
||||
<div>
|
||||
<span class="site-nav-version">"v" {VERSION}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// ── Replay snapshot modal ─────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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::<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,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! {
|
||||
|
|
|
|||
|
|
@ -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! {
|
||||
<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="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>
|
||||
|
||||
|
|
@ -84,6 +89,10 @@ 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;
|
||||
|
||||
|
|
@ -100,8 +109,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);
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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 ];
|
||||
|
|
|
|||
11
justfile
11
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 <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,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;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "protocol"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "relay-server"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
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)
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue