diff --git a/Cargo.lock b/Cargo.lock index e557059..8ce19af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6347,12 +6347,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "qrcodegen" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" - [[package]] name = "quick-error" version = "2.0.1" @@ -8720,7 +8714,6 @@ dependencies = [ "leptos", "leptos_i18n", "leptos_router", - "qrcodegen", "rand 0.9.3", "serde", "serde_json", diff --git a/clients/web/Cargo.toml b/clients/web/Cargo.toml index 71af23b..04857a6 100644 --- a/clients/web/Cargo.toml +++ b/clients/web/Cargo.toml @@ -18,7 +18,6 @@ serde_json = "1" futures = "0.3" rand = "0.9" gloo-storage = "0.3" -qrcodegen = "1.8" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2" @@ -39,7 +38,4 @@ web-sys = { version = "0.3", features = [ "OscillatorType", "BaseAudioContext", "HtmlAudioElement", - "Clipboard", - "Navigator", - "Location", ] } diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css index 3de4a9f..b2d89a4 100644 --- a/clients/web/assets/style.css +++ b/clients/web/assets/style.css @@ -287,74 +287,10 @@ 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; } -/* ── Share URL row (lobby waiting card + game top bar) ──────────── */ -.share-url-row { - display: flex; - align-items: center; - gap: 0.5rem; - background: rgba(0,0,0,0.18); - border: 1px solid rgba(200,164,72,0.25); - border-radius: 5px; - padding: 0.4rem 0.6rem; -} -.share-url-text { - flex: 1; - font-family: var(--font-ui); - font-size: 0.72rem; - color: rgba(242,232,208,0.75); - word-break: break-all; - user-select: all; -} -.share-copy-btn { - flex-shrink: 0; - font-family: var(--font-ui); - font-size: 0.72rem; - padding: 0.2rem 0.6rem; - border: 1px solid rgba(200,164,72,0.4); - border-radius: 3px; - background: rgba(200,164,72,0.1); - color: var(--ui-parchment); - cursor: pointer; - transition: background 0.15s; - white-space: nowrap; -} -.share-copy-btn:hover { background: rgba(200,164,72,0.22); } - -/* ── QR code container ───────────────────────────────────────────── */ -.qr-container { - width: 160px; - height: 160px; - margin: 0 auto; - border-radius: 4px; - overflow: hidden; -} -.qr-container svg { width: 100%; height: 100%; display: block; } - -/* ── Share popover (in-game top bar) ─────────────────────────────── */ -.share-popover { - width: 100%; - background: rgba(0,0,0,0.3); - border: 1px solid rgba(200,164,72,0.2); - border-radius: 6px; - padding: 0.75rem 1rem; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.4rem; - margin-bottom: 0.5rem; -} -.share-popover .qr-container { width: 120px; height: 120px; } -.share-popover-label { - font-size: 0.75rem; - color: rgba(242,232,208,0.6); - text-align: center; - margin: 0; -} - /* ── Game overlay (full-screen, covers portal during play) ───────── */ .game-overlay { position: fixed; - inset: 54px 0 0 0; + inset: 0; background: #8a7050; background-image: radial-gradient(ellipse at 20% 10%, rgba(80,48,16,0.35) 0%, transparent 60%), diff --git a/clients/web/locales/en.json b/clients/web/locales/en.json index 8ff3548..5d5005d 100644 --- a/clients/web/locales/en.json +++ b/clients/web/locales/en.json @@ -58,7 +58,6 @@ "hint_move": "Click a highlighted field to move a checker", "hint_hold_or_go": "Hold to keep points — Go to reset the setting", "hint_continue": "Click Continue when ready", - "anonymous_name": "Anonymous", "login_failed": "Invalid username or password.", "sign_in": "Sign in", "sign_out": "Sign out", @@ -94,12 +93,5 @@ "anonymous_player": "anonymous", "started_label": "Started", "ended_label": "Ended", - "room_detail_title": "Room", - "share_link": "Share this link to invite an opponent", - "copy_link": "Copy link", - "link_copied": "Copied!", - "scan_qr": "or scan the QR code", - "join_code_label": "Join by code", - "join_code_placeholder": "Room code", - "share_btn": "Share" + "room_detail_title": "Room" } diff --git a/clients/web/locales/fr.json b/clients/web/locales/fr.json index 2346395..8cef7df 100644 --- a/clients/web/locales/fr.json +++ b/clients/web/locales/fr.json @@ -58,7 +58,6 @@ "hint_move": "Cliquez un champ surligné pour déplacer", "hint_hold_or_go": "Tenir pour garder les points — S'en aller pour repartir", "hint_continue": "Cliquez Continuer quand vous êtes prêt", - "anonymous_name": "Anonyme", "login_failed": "Identifiant ou mot de passe incorrect.", "sign_in": "Se connecter", "sign_out": "Se déconnecter", @@ -94,12 +93,5 @@ "anonymous_player": "anonyme", "started_label": "Début", "ended_label": "Fin", - "room_detail_title": "Salle", - "share_link": "Partagez ce lien pour inviter un adversaire", - "copy_link": "Copier le lien", - "link_copied": "Copié !", - "scan_qr": "ou scannez le QR code", - "join_code_label": "Rejoindre par code", - "join_code_placeholder": "Code de salle", - "share_btn": "Partager" + "room_detail_title": "Salle" } diff --git a/clients/web/src/app.rs b/clients/web/src/app.rs index b9ce4aa..8d604b6 100644 --- a/clients/web/src/app.rs +++ b/clients/web/src/app.rs @@ -4,7 +4,6 @@ use gloo_storage::{LocalStorage, Storage}; use leptos::prelude::*; use leptos::task::spawn_local; use leptos_router::components::{Route, Router, Routes}; -use leptos_router::hooks::use_location; use leptos_router::path; use serde::{Deserialize, Serialize}; @@ -12,15 +11,15 @@ use backbone_lib::session::{ConnectError, GameSession, RoomConfig, RoomRole, Ses use backbone_lib::traits::ViewStateUpdate; use crate::api; -use crate::i18n::*; use crate::game::components::{ConnectingScreen, GameScreen}; use crate::game::session::{ - compute_last_moves, patch_player_name, push_or_show, run_local_bot_game, + compute_last_moves, push_or_show, run_local_bot_game, }; use crate::game::trictrac::backend::TrictracBackend; use crate::game::trictrac::types::{ GameDelta, PlayerAction, ScoredEvent, SerStage, ViewState, }; +use crate::i18n::I18nContextProvider; use crate::nav::SiteNav; use crate::portal::{account::AccountPage, game_detail::GameDetailPage, lobby::LobbyPage, profile::ProfilePage}; use trictrac_store::CheckerMove; @@ -129,7 +128,6 @@ async fn submit_game_result(room_code: String, game_state: ViewState) { #[component] pub fn App() -> impl IntoView { - let i18n = use_i18n(); let stored = load_session(); let initial_screen = if stored.is_some() { Screen::Connecting @@ -227,10 +225,8 @@ pub fn App() -> impl IntoView { }; if remote_config.is_none() { - let player_name = auth_username.get_untracked() - .unwrap_or_else(|| t_string!(i18n, anonymous_name).to_string()); loop { - let restart = run_local_bot_game(screen, &mut cmd_rx, pending, player_name.clone()).await; + let restart = run_local_bot_game(screen, &mut cmd_rx, pending).await; if !restart { break; } @@ -270,9 +266,7 @@ pub fn App() -> impl IntoView { let is_host = session.is_host; let player_id = session.player_id; let reconnect_token = session.reconnect_token; - let my_name = auth_username.get_untracked() - .unwrap_or_else(|| t_string!(i18n, anonymous_name).to_string()); - let mut vs = ViewState::default_with_names("", ""); + let mut vs = ViewState::default_with_names("Blancs", "Noirs"); let mut result_submitted = false; loop { @@ -296,7 +290,6 @@ pub fn App() -> impl IntoView { ViewStateUpdate::Full(state) => vs = state, ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta), } - patch_player_name(&mut vs, player_id, &my_name); if is_host && !result_submitted && vs.stage == SerStage::Ended { result_submitted = true; @@ -350,52 +343,42 @@ pub fn App() -> impl IntoView { }); view! { - - + + + // Nav: hidden while game overlay is active + -
- "Page not found."

}> - - - - -
-
+ // Portal pages — always mounted for router stability +
+ "Page not found."

}> + + + + +
+
- -
- } -} - -/// 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] -fn GameOverlay( - pending: RwSignal>, - screen: RwSignal, -) -> impl IntoView { - let location = use_location(); - - move || { - if location.pathname.get() != "/" { - return view! { }.into_any(); - } - let q = pending.get(); - let front = q.front().cloned(); - if let Some(state) = front { - return view! { -
- }.into_any(); - } - match screen.get() { - Screen::Playing(state) => view! { -
- }.into_any(), - Screen::Connecting => view! { -
- }.into_any(), - _ => view! { }.into_any(), - } + // Game overlay: fixed, covers portal during play + {move || { + let q = pending.get(); + let front = q.front().cloned(); + if let Some(state) = front { + return view! { +
+ }.into_any(); + } + match screen.get() { + Screen::Playing(state) => view! { +
+ }.into_any(), + Screen::Connecting => view! { +
+ }.into_any(), + _ => view! { }.into_any(), + } + }} +
+ } } diff --git a/clients/web/src/game/components/game_screen.rs b/clients/web/src/game/components/game_screen.rs index 7c811d3..3806a41 100644 --- a/clients/web/src/game/components/game_screen.rs +++ b/clients/web/src/game/components/game_screen.rs @@ -8,7 +8,6 @@ use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, use super::die::Die; use crate::app::{GameUiState, NetCommand, PauseReason}; use crate::i18n::*; -use crate::portal::lobby::{qr_svg, room_url}; use crate::game::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage}; use super::board::Board; @@ -224,10 +223,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let opp_name_end = opp_score.name.clone(); let opp_holes_end = opp_score.holes; - let share_open = RwSignal::new(false); - let share_url = if !is_bot_game { room_url(&room_id) } else { String::new() }; - let share_svg = if !is_bot_game { qr_svg(&share_url) } else { String::new() }; - view! {
// ── Top bar ────────────────────────────────────────────────────── @@ -237,18 +232,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { } else { t_string!(i18n, room_label, id = room_id.as_str()) }} - - {move || (!is_bot_game).then(|| view! { - - })} -
- {move || auth_username.get().map(|u| view! { - "▶ " {u} - })} + {move || auth_username.get().map(|u| view! { + "▶ " {u} + })} impl IntoView { }>{t!(i18n, quit)}
- // ── Share popover ───────────────────────────────────────────────── - {move || share_open.get().then(|| view! { -
-

{t!(i18n, share_link)}

-
- -
-

- {t!(i18n, scan_qr)} -

-
-
- })} - // ── Opponent score (above board) ───────────────────────────────── diff --git a/clients/web/src/game/session.rs b/clients/web/src/game/session.rs index df603ec..e648d01 100644 --- a/clients/web/src/game/session.rs +++ b/clients/web/src/game/session.rs @@ -18,13 +18,12 @@ pub async fn run_local_bot_game( screen: RwSignal, cmd_rx: &mut mpsc::UnboundedReceiver, pending: RwSignal>, - player_name: String, ) -> bool { let mut backend = TrictracBackend::new(0); backend.player_arrival(0); backend.player_arrival(1); - let mut vs = ViewState::default_with_names(&player_name, "Bot"); + let mut vs = ViewState::default_with_names("You", "Bot"); for cmd in backend.drain_commands() { match cmd { BackendCommand::ResetViewState => { @@ -36,7 +35,6 @@ pub async fn run_local_bot_game( _ => {} } } - patch_bot_names(&mut vs, &player_name); screen.set(Screen::Playing(GameUiState { view_state: vs.clone(), player_id: 0, @@ -60,7 +58,6 @@ pub async fn run_local_bot_game( vs.apply_delta(&delta); } } - patch_bot_names(&mut vs, &player_name); let scored = compute_scored_event(&prev_vs, &vs, 0); let opp_scored = compute_scored_event(&prev_vs, &vs, 1); screen.set(Screen::Playing(GameUiState { @@ -89,7 +86,6 @@ pub async fn run_local_bot_game( if let BackendCommand::Delta(delta) = cmd { let delta_prev_vs = vs.clone(); vs.apply_delta(&delta); - patch_bot_names(&mut vs, &player_name); push_or_show( &delta_prev_vs, GameUiState { @@ -114,17 +110,6 @@ pub async fn run_local_bot_game( } } -/// Patches the player names in a ViewState after a backend delta (bot game: slot 0 = human, 1 = Bot). -pub fn patch_bot_names(vs: &mut ViewState, player_name: &str) { - vs.scores[0].name = player_name.to_string(); - vs.scores[1].name = "Bot".to_string(); -} - -/// Patches the local player's name in a ViewState after a backend delta (multiplayer). -pub fn patch_player_name(vs: &mut ViewState, player_id: u16, name: &str) { - vs.scores[player_id as usize].name = name.to_string(); -} - /// Returns the checker moves to animate when the board changed between two ViewStates. pub fn compute_last_moves( prev: &ViewState, diff --git a/clients/web/src/main.rs b/clients/web/src/main.rs index 5d5fbb4..bfd8adf 100644 --- a/clients/web/src/main.rs +++ b/clients/web/src/main.rs @@ -7,13 +7,8 @@ mod nav; mod portal; use app::App; -use i18n::I18nContextProvider; use leptos::prelude::*; fn main() { - mount_to_body(|| view! { - - - - }) + mount_to_body(|| view! { }) } diff --git a/clients/web/src/nav.rs b/clients/web/src/nav.rs index 10ecc36..c5b59fd 100644 --- a/clients/web/src/nav.rs +++ b/clients/web/src/nav.rs @@ -3,14 +3,19 @@ use leptos::task::spawn_local; use leptos_router::components::A; use crate::api; +use crate::app::Screen; use crate::i18n::*; #[component] pub fn SiteNav() -> impl IntoView { let i18n = use_i18n(); + let screen = use_context::>().expect("Screen context not found"); let auth_username = use_context::>>().expect("auth_username context not found"); + let is_game_active = + move || !matches!(screen.get(), Screen::Login { .. }); + let logout = move |_| { spawn_local(async move { let _ = api::post_logout().await; @@ -19,7 +24,7 @@ pub fn SiteNav() -> impl IntoView { }; view! { -