diff --git a/Cargo.lock b/Cargo.lock
index de6765c..c1257d6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -189,7 +189,7 @@ dependencies = [
[[package]]
name = "backbone-lib"
-version = "0.2.11"
+version = "0.2.12"
dependencies = [
"bytes",
"ewebsock",
@@ -738,7 +738,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -926,6 +926,15 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "getopts"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+dependencies = [
+ "unicode-width",
+]
+
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -2649,11 +2658,30 @@ dependencies = [
[[package]]
name = "protocol"
-version = "0.2.11"
+version = "0.2.12"
dependencies = [
"serde",
]
+[[package]]
+name = "pulldown-cmark"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
+dependencies = [
+ "bitflags",
+ "getopts",
+ "memchr",
+ "pulldown-cmark-escape",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
+
[[package]]
name = "qrcodegen"
version = "1.8.0"
@@ -2883,7 +2911,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "relay-server"
-version = "0.2.11"
+version = "0.2.12"
dependencies = [
"argon2",
"axum",
@@ -3893,7 +3921,7 @@ dependencies = [
[[package]]
name = "trictrac-store"
-version = "0.2.11"
+version = "0.2.12"
dependencies = [
"anyhow",
"base64 0.21.7",
@@ -3906,7 +3934,7 @@ dependencies = [
[[package]]
name = "trictrac-web"
-version = "0.2.11"
+version = "0.2.12"
dependencies = [
"backbone-lib",
"futures",
@@ -3918,6 +3946,7 @@ dependencies = [
"leptos",
"leptos_i18n",
"leptos_router",
+ "pulldown-cmark",
"qrcodegen",
"rand 0.9.4",
"serde",
@@ -4034,6 +4063,12 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
[[package]]
name = "unicode-xid"
version = "0.2.6"
@@ -4358,7 +4393,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.61.2",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index d722f4f..a468cd0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[workspace.package]
-version = "0.2.12"
+version = "0.2.13"
[workspace]
resolver = "2"
diff --git a/README.md b/README.md
index ca4c0de..f9485c7 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,6 @@
This is a game of [Trictrac](https://en.wikipedia.org/wiki/Trictrac) rust implementation.
-The project is still on its early stages.
-
## Usage
Install [devenv](https://devenv.sh/getting-started/), start a devenv shell `devenv shell`, and run the following commands.
@@ -17,118 +15,18 @@ just run-relay # listens on :8080
just dev
```
-Open two browser windows at `http://127.0.0.1:9091`. In one, create a room; in the other, join with the same room name.
-
-Playing with the cli against the 'random' bot: `cargo run --bin=client_cli -- --bot random`
-
-## Roadmap
-
-- [x] rules
-- [x] command line interface
-- [x] basic bot (random play)
-- [ ] web client (in progress)
-- [ ] network game (in progress)
-- [ ] AI bot
+Open a browser window at `http://127.0.0.1:9091`. You can play against a very basic bot, or invite an other player to connect at the same address.
## Code structure
- game rules and game state are implemented in the _store/_ folder.
+- a server for the network game is implemented in _server/relay-server_, which uses _server/protocol_
+- the web client is in _clients/web_, it connects to the server using the _clients/backbone-lib_ library
- the command-line application is implemented in _clients/cli/_; it allows you to play against a bot, or to have two bots play against each other
-- the bots algorithms and the training of their models are implemented in the _bot/_ and _spiel_bot_ folders.
+- the bots algorithms and the training of their models are implemented in the _bot/_ and _spiel_bot_ folders. This is a work in progress, they are not performant at all.
-### _store_ package
+## Inspirations
-The game state is defined by the `GameState` struct in _store/src/game.rs_. The `to_string_id()` method allows this state to be encoded compactly in a string (without the played moves history). For a more readable textual representation, the `fmt::Display` trait is implemented.
+The multiplayer game architecture, implemented in packages _clients/backbone-lib_, _clients/web/game_, _server/protocol_, _server/relay-server_ is a Leptos-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. It is a multiplayer game system in Rust targeting browser-based board games compiled as WASM. The original project used Macroquad with a polling-based transport layer; this version replaces that with an async session API built for [Leptos](https://leptos.dev/).
-### _clients/cli_ package
-
-`clients/cli/src/game_runner.rs` contains the logic to make two bots play against each other.
-
-### _bot_ package
-
-- `bot/src/strategy/default.rs` contains the code for a basic bot strategy: it determines the list of valid moves (using the `get_possible_moves_sequences` method of `store::MoveRules`) and simply executes the first move in the list.
-- `bot/src/strategy/dqnburn.rs` is another bot strategy that uses a reinforcement learning trained model with the DQN algorithm via the burn library ().
-- `bot/scripts/trains.sh` allows you to train agents using different algorithms (DQN, PPO, SAC).
-
-### multiplayer game
-
-Packages "clients/backbone-lib", "clients/web/game", "server/protocol", "server/relay-server" are a Leptos-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. It is a multiplayer game system in Rust targeting browser-based board games compiled as WASM. The original project used Macroquad with a polling-based transport layer; this version replaces that with an async session API built for [Leptos](https://leptos.dev/).
-
-The system consists of:
-
-- A **relay server** (Axum/Tokio) that routes messages between players and manages rooms, without knowing anything about game rules.
-- A **backbone library** that handles WebSocket connection, handshake, and message routing, exposing an async API to the game frontend.
-- Game-specific **backend logic** implementing the `BackEndArchitecture` trait, which runs only on the hosting client.
-- A **Leptos frontend** that connects to a session and reacts to state updates.
-
-There is no dedicated game server. One of the players acts as the host: their browser runs the game backend locally. The relay server only forwards messages — it never touches game state.
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Host Client │
-│ ┌─────────────┐ ┌──────────────────┐ ┌────────────┐ │
-│ │ Leptos UI │◄──►│ GameSession │◄──►│ Backend │ │
-│ └─────────────┘ └────────┬─────────┘ └────────────┘ │
-└───────────────────────────── │ ────────────────────────────┘
- │ WebSocket
- ┌──────▼──────┐
- │ Relay Server│
- └──────┬──────┘
- │ WebSocket
-┌───────────────────────────────│────────────────────────────┐
-│ ┌─────────────┐ ┌─────────▼────────┐ │
-│ │ Leptos UI │◄──►│ GameSession │ (no backend) │
-│ └─────────────┘ └──────────────────┘ │
-│ Remote Client │
-└─────────────────────────────────────────────────────────────┘
-```
-
-#### Data flow
-
-- **Actions** (e.g. "place stone at B3") flow from the UI to the host backend via `GameSession::send_action()`.
-- **State updates** flow back as `ViewStateUpdate::Full` (full snapshot, on join or reset) or `ViewStateUpdate::Incremental` (delta, for animations).
-- **Timers** are managed by the host's background task (wall-clock, no polling required from the game).
-
-#### backbone-lib session API
-
-The key design choice: `backbone-lib` owns a background async task per session. The Leptos app never drives a loop — it just awaits on events.
-
-#### Workspace
-
-**server/protocol**
-
-Shared message-type constants and the `JoinRequest` struct used during the WebSocket handshake.
-
-**server/relay-server**
-
-Listens on port 8080. Loads `GameConfig.json` on startup to know which games exist and their player limits:
-
-```json
-[{ "name": "trictrac", "max_players": 10 }]
-```
-
-Games can be added at runtime via the `/reload` endpoint. `/enlist` lists active rooms. A watchdog cleans up inactive rooms every 20 minutes.
-
-For production, put it behind a reverse proxy with SSL (the browser requires `wss://` on HTTPS pages). Example Caddy config:
-
-```
-your-domain.com {
- handle_path /api/* {
- reverse_proxy localhost:8080
- }
- file_server
-}
-```
-
-**clients/backbone-lib**
-
-Modules:
-
-| Module | Purpose |
-| ---------- | ---------------------------------------------------------------------------------------------------------- |
-| `session` | `GameSession`, `connect()`, `SessionEvent`, `RoomConfig` |
-| `host` | Background async task for the hosting client (drives `BackEndArchitecture`, manages timers) |
-| `client` | Background async task for non-hosting clients |
-| `protocol` | Wire encoding/decoding helpers (postcard + message-type bytes) |
-| `platform` | `spawn_task` / `sleep_ms` abstractions (WASM: `spawn_local` + gloo-timers; native: thread + thread::sleep) |
-| `traits` | `BackEndArchitecture`, `BackendCommand`, `ViewStateUpdate`, `SerializationCap` |
+The web client UX/UI is inspired by https://playtiao.com.
diff --git a/clients/web/Cargo.toml b/clients/web/Cargo.toml
index 1edb9eb..4b82427 100644
--- a/clients/web/Cargo.toml
+++ b/clients/web/Cargo.toml
@@ -19,6 +19,7 @@ futures = "0.3"
rand = "0.9"
gloo-storage = "0.3"
qrcodegen = "1.8"
+pulldown-cmark = "0.13"
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2.118"
diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css
index 09b21e9..1d4cc77 100644
--- a/clients/web/assets/style.css
+++ b/clients/web/assets/style.css
@@ -305,6 +305,62 @@ a:hover { text-decoration: underline; }
.portal-error { color: var(--ui-red-accent); font-size: 0.875rem; margin-top: 0.5rem; }
.portal-success { color: var(--ui-green-accent); font-size: 0.875rem; margin-top: 0.5rem; }
+.flash-banner {
+ position: fixed;
+ top: 1.25rem;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 500;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.75rem 1.25rem;
+ background: var(--ui-green-accent);
+ color: #f5edd8;
+ border-radius: 6px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.35);
+ font-family: var(--font-ui);
+ font-size: 0.95rem;
+ max-width: 90vw;
+ animation: flash-in 0.2s ease;
+}
+@keyframes flash-in {
+ from { opacity: 0; transform: translateX(-50%) translateY(-0.5rem); }
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
+}
+.flash-dismiss {
+ background: none;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ font-size: 1rem;
+ opacity: 0.75;
+ padding: 0;
+ line-height: 1;
+}
+.flash-dismiss:hover { opacity: 1; }
+
+.portal-danger-zone {
+ border: 1px solid rgba(122, 30, 42, 0.4);
+ background: rgba(122, 30, 42, 0.04);
+}
+.portal-danger-zone h2 {
+ color: var(--ui-red-accent);
+}
+.portal-danger-btn {
+ padding: 0.5rem 1.25rem;
+ font-family: var(--font-ui);
+ font-size: 0.9rem;
+ background: var(--ui-red-accent);
+ color: #f5edd8;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: opacity 0.15s;
+}
+.portal-danger-btn:hover { opacity: 0.85; }
+.portal-danger-btn:disabled { opacity: 0.45; cursor: not-allowed; }
+
.portal-link {
color: var(--ui-gold);
text-decoration: none;
@@ -2045,6 +2101,7 @@ a:hover { text-decoration: underline; }
text-decoration: none;
opacity: 0.8;
transition: opacity 0.15s;
+ cursor: pointer;
}
.game-sidebar-link:hover { opacity: 1; text-decoration: underline; text-underline-offset: 2px; }
@@ -2078,13 +2135,27 @@ a:hover { text-decoration: underline; }
}
/* Push the version wrapper to the bottom of the sidebar flex column */
-.game-sidebar > div:has(.site-nav-version) {
+.sidebar-footer {
margin-top: auto;
- padding: 0.75rem 1rem;
border-top: 1px solid rgba(200,164,72,0.12);
}
+.site-nav-infolinks {
+ margin: 2em 0 1em;
+ text-align: center;
+ font-size: 0.9rem;
+ color: rgba(200,164,72,0.4);
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.site-nav-infolinks > a {
+ width: 100%;
+}
+
.site-nav-version {
+ margin: 2em 0 1em;
display: block;
text-align: center;
font-family: var(--font-ui);
@@ -2092,3 +2163,91 @@ a:hover { text-decoration: underline; }
letter-spacing: 0.06em;
color: rgba(200,164,72,0.4);
}
+
+/* ── Content pages (markdown-rendered) ─────────────────────────────────────── */
+
+.content-page h1 {
+ font-family: var(--font-display);
+ font-size: 2rem;
+ font-weight: 600;
+ color: var(--ui-ink);
+ letter-spacing: 0.04em;
+ margin-bottom: 0.5rem;
+}
+.content-page h2 {
+ font-family: var(--font-display);
+ font-size: 1.4rem;
+ font-weight: 600;
+ color: var(--ui-ink);
+ margin: 1.75rem 0 0.5rem;
+ border-bottom: 1px solid rgba(200,164,72,0.25);
+ padding-bottom: 0.25rem;
+}
+.content-page h3 {
+ font-family: var(--font-display);
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--ui-ink);
+ margin: 1.25rem 0 0.4rem;
+}
+.content-page p {
+ line-height: 1.7;
+ margin-bottom: 0.9rem;
+ color: var(--ui-ink);
+}
+.content-page ul,
+.content-page ol {
+ margin: 0.5rem 0 1rem 1.5rem;
+ line-height: 1.7;
+ color: var(--ui-ink);
+}
+.content-page li {
+ margin-bottom: 0.25rem;
+}
+.content-page a {
+ color: var(--ui-gold-dark);
+ text-decoration: underline;
+}
+.content-page a:hover {
+ color: var(--ui-ink);
+}
+.content-page code {
+ font-family: monospace;
+ background: rgba(0,0,0,0.07);
+ border-radius: 3px;
+ padding: 0.1em 0.35em;
+ font-size: 0.88em;
+}
+.content-page pre {
+ background: rgba(0,0,0,0.07);
+ border-radius: 5px;
+ padding: 1rem 1.25rem;
+ overflow-x: auto;
+ margin-bottom: 1rem;
+}
+.content-page pre code {
+ background: none;
+ padding: 0;
+}
+.content-page blockquote {
+ border-left: 3px solid rgba(200,164,72,0.5);
+ margin: 0.75rem 0;
+ padding: 0.25rem 1rem;
+ color: #665544;
+ font-style: italic;
+}
+.content-page table {
+ border-collapse: collapse;
+ width: 100%;
+ margin-bottom: 1rem;
+}
+.content-page th,
+.content-page td {
+ border: 1px solid rgba(200,164,72,0.3);
+ padding: 0.4rem 0.75rem;
+ text-align: left;
+}
+.content-page th {
+ background: rgba(200,164,72,0.1);
+ font-weight: 600;
+}
diff --git a/clients/web/locales/en.json b/clients/web/locales/en.json
index 03ba37c..1e5fbc2 100644
--- a/clients/web/locales/en.json
+++ b/clients/web/locales/en.json
@@ -140,5 +140,14 @@
"nickname_modal_sign_in": "Sign in",
"nickname_modal_register": "Create account",
"new_game": "New game",
- "language": "Language"
+ "language": "Language",
+ "delete_account_title": "Danger zone",
+ "delete_account_btn": "Delete my account",
+ "delete_account_warning": "This action is irreversible. Your account will be permanently deleted.",
+ "delete_account_confirm_label": "Type your username to confirm:",
+ "delete_account_confirm_btn": "Delete permanently",
+ "delete_account_mismatch": "Username does not match.",
+ "account_deleted": "Your account has been permanently deleted.",
+ "about": "About",
+ "legal": "Legal notices"
}
diff --git a/clients/web/locales/fr.json b/clients/web/locales/fr.json
index 569d66b..28ae43c 100644
--- a/clients/web/locales/fr.json
+++ b/clients/web/locales/fr.json
@@ -130,15 +130,22 @@
"copy_link": "Copier le lien",
"link_copied": "Copié !",
"scan_qr": "ou scannez le QR code",
- "join_code_label": "Rejoindre avec un code",
- "join_code_placeholder": "Code de la salle",
"share_btn": "Partager",
"nickname_modal_title": "Choisissez votre pseudo",
"nickname_modal_hint": "Vous jouerez sous le nom de :",
"nickname_modal_play": "Jouer",
"nickname_modal_or": "ou",
- "nickname_modal_sign_in": "Se connecter",
+ "nickname_modal_sign_in": "connectez-vous",
"nickname_modal_register": "Créer un compte",
"new_game": "Nouvelle partie",
- "language": "Langue"
+ "language": "Langue",
+ "delete_account_title": "Zone de danger",
+ "delete_account_btn": "Supprimer mon compte",
+ "delete_account_warning": "Cette action est irréversible. Votre compte sera définitivement supprimé.",
+ "delete_account_confirm_label": "Tapez votre nom d'utilisateur pour confirmer :",
+ "delete_account_confirm_btn": "Supprimer définitivement",
+ "delete_account_mismatch": "Le nom d'utilisateur ne correspond pas.",
+ "account_deleted": "Votre compte a été définitivement supprimé.",
+ "about": "À propos",
+ "legal": "Mentions légales"
}
diff --git a/clients/web/pages/about/en.md b/clients/web/pages/about/en.md
new file mode 100644
index 0000000..27a382e
--- /dev/null
+++ b/clients/web/pages/about/en.md
@@ -0,0 +1,12 @@
+# About
+
+This application allows you to play [trictrac](https://en.wikipedia.org/wiki/Trictrac) against a friend online or locally against a bot.
+
+The source code is available at [github.com/mmai/trictrac](https://github.com/mmai/trictrac)
+The application is self-hosted and runs on a simple Raspberry Pi.
+
+## Contact & bug Report
+
+For any questions, bug reports, or feedback, you can contact me at rhumbs@rhumbs.fr.
+
+If you encounter an issue during gameplay, you can copy the context of a game by clicking _Take snapshot_ then paste the resulting code into your message, specifying the expected behavior and the incorrect behavior you observed.
diff --git a/clients/web/pages/about/fr.md b/clients/web/pages/about/fr.md
new file mode 100644
index 0000000..1c3ec74
--- /dev/null
+++ b/clients/web/pages/about/fr.md
@@ -0,0 +1,12 @@
+# À propos
+
+Cette application vous permet de jouer au [trictrac](https://fr.wikipedia.org/wiki/Trictrac) contre un ami en ligne ou localement contre un bot.
+
+Le code source est disponible sur [github.com/mmai/trictrac](https://github.com/mmai/trictrac).
+L'application est auto hébergée et tourne sur un simple Raspberry Pi.
+
+## Contact et rapport de bogue
+
+Pour toute question, rapport de bogue ou retour d'expérience, vous pouvez me contacter à l'adresse rhumbs@rhumbs.fr.
+
+Si vous constatez une anomalie en cours de jeu, vous pouvez copier le contexte d'une partie en cliquant sur _Prendre un instantané_, puis coller le code obtenu dans le message, en précisant le comportement auquel vous vous attendiez, et le comportement erroné constaté.
diff --git a/clients/web/pages/legal/en.md b/clients/web/pages/legal/en.md
new file mode 100644
index 0000000..ff72761
--- /dev/null
+++ b/clients/web/pages/legal/en.md
@@ -0,0 +1,26 @@
+# Legal Notices
+
+## Data and Privacy
+
+This site does not use third-party analytics or advertising trackers.
+
+If you create an account, your username, email address, and argon2-hashed password are stored in a database on our server. Your email address is used only to send you account-related messages (email verification, password reset). It is never shared with third parties.
+
+Game records (room codes, move history, outcomes) may be stored to display game history on your profile page.
+
+## Cookies and Sessions
+
+A session cookie is stored in your browser when you sign in. It is used solely to keep you authenticated and expires after 30 days of inactivity.
+
+## Contact
+
+The website is created by
+
+Henri Bourcereau\
+7 rue Lugeol\
+33000 Bordeaux\
+France
+
+It is hosted at the same address.
+
+You can contact me at rhumbs@rhumbs.fr
diff --git a/clients/web/pages/legal/fr.md b/clients/web/pages/legal/fr.md
new file mode 100644
index 0000000..43f85d5
--- /dev/null
+++ b/clients/web/pages/legal/fr.md
@@ -0,0 +1,28 @@
+# Mentions légales
+
+## Données et vie privée
+
+Ce site n'utilise pas d'outils d'analyse tiers ni de traceurs publicitaires.
+
+Si vous créez un compte, votre nom d'utilisateur, votre adresse e-mail et votre mot de passe haché (argon2) sont stockés dans une base de données sur notre serveur. Votre adresse e-mail est utilisée uniquement pour vous envoyer des messages liés à votre compte (vérification d'e-mail, réinitialisation de mot de passe). Elle n'est jamais partagée avec des tiers.
+
+Les enregistrements de parties (codes de salle, historique des coups, résultats) peuvent être conservés afin d'afficher l'historique des parties sur votre page de profil.
+
+Vous pouvez supprimer votre compte et la totalité des données associées depuis votre page de profil.
+
+## Cookies et sessions
+
+Un cookie de session est stocké dans votre navigateur lorsque vous vous connectez. Il sert uniquement à maintenir votre authentification et expire après 30 jours d'inactivité.
+
+## Contact
+
+Le site est réalisé par
+
+Henri Bourcereau\
+7 rue Lugeol\
+33000 Bordeaux\
+France
+
+Il est hébergé à la même adresse.
+
+Vous pouvez me contacter à l'adresse rhumbs@rhumbs.fr
diff --git a/clients/web/pages/readme.txt b/clients/web/pages/readme.txt
new file mode 100644
index 0000000..ea3df35
--- /dev/null
+++ b/clients/web/pages/readme.txt
@@ -0,0 +1 @@
+Sync this folder to the PAGES_DIR directory of the server running `relay-server`.
diff --git a/clients/web/src/api.rs b/clients/web/src/api.rs
index d826165..2452b67 100644
--- a/clients/web/src/api.rs
+++ b/clients/web/src/api.rs
@@ -64,6 +64,12 @@ pub struct GameDetail {
pub participants: Vec,
}
+#[derive(Clone, Debug, Deserialize)]
+pub struct PageContent {
+ pub title: String,
+ pub content: String,
+}
+
// ── Request bodies ────────────────────────────────────────────────────────────
#[derive(Serialize)]
@@ -141,6 +147,19 @@ pub async fn post_logout() -> Result<(), String> {
}
}
+pub async fn delete_account() -> Result<(), String> {
+ let resp = gloo_net::http::Request::delete(&url("/auth/account"))
+ .credentials(web_sys::RequestCredentials::Include)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+ if resp.status() == 204 {
+ Ok(())
+ } else {
+ Err(format!("status {}", resp.status()))
+ }
+}
+
pub async fn get_user_profile(username: &str) -> Result {
let resp = gloo_net::http::Request::get(&url(&format!("/users/{username}")))
.credentials(web_sys::RequestCredentials::Include)
@@ -242,6 +261,18 @@ pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(),
}
}
+pub async fn get_page(slug: &str, lang: &str) -> Result {
+ let resp = gloo_net::http::Request::get(&url(&format!("/pages/{slug}?lang={lang}")))
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+ if resp.status() == 200 {
+ resp.json::().await.map_err(|e| e.to_string())
+ } else {
+ Err(format!("status {}", resp.status()))
+ }
+}
+
// ── Utilities ─────────────────────────────────────────────────────────────────
/// Maps to the `Intl.DateTimeFormat` options object accepted by `Date.toLocaleString`.
diff --git a/clients/web/src/app.rs b/clients/web/src/app.rs
index 5c38d33..ba90a54 100644
--- a/clients/web/src/app.rs
+++ b/clients/web/src/app.rs
@@ -21,15 +21,37 @@ use crate::game::trictrac::backend::TrictracBackend;
use crate::game::trictrac::types::{GameDelta, PlayerAction, ScoredEvent, SerStage, ViewState};
use crate::i18n::*;
use crate::portal::{
- account::AccountPage, forgot_password::ForgotPasswordPage, game_detail::GameDetailPage,
- lobby::LobbyPage, profile::ProfilePage, reset_password::ResetPasswordPage,
- verify_email::VerifyEmailPage,
+ account::AccountPage, content_page::ContentPage, forgot_password::ForgotPasswordPage,
+ game_detail::GameDetailPage, lobby::LobbyPage, profile::ProfilePage,
+ reset_password::ResetPasswordPage, verify_email::VerifyEmailPage,
};
use trictrac_store::CheckerMove;
use std::collections::VecDeque;
-const RELAY_URL: &str = "ws://localhost:8080/ws";
+/// Newtype wrappers so context lookup can distinguish signals of the same inner type.
+#[derive(Clone, Copy)]
+pub(crate) struct AnonNickname(pub RwSignal
+ }
+ })
+ }
+}
+
/// Renders the full-screen game overlay, but only when the current route is "/".
/// This lets the user navigate to profile/account pages while a game is running.
#[component]
@@ -544,47 +598,60 @@ fn SiteHamburger() -> impl IntoView {
// Auth
-
-
-
{move || match auth_username.get() {
Some(u) => {
let href = format!("/profile/{u}");
view! {
+