diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css index a129c91..f241273 100644 --- a/clients/web/assets/style.css +++ b/clients/web/assets/style.css @@ -378,7 +378,7 @@ a:hover { text-decoration: underline; } /* ── Game overlay (full-screen, covers portal during play) ───────── */ .game-overlay { position: fixed; - inset: 0; + inset: 54px 0 0 0; background: #8a7050; background-image: radial-gradient(ellipse at 20% 10%, rgba(80,48,16,0.35) 0%, transparent 60%), @@ -1776,213 +1776,3 @@ a:hover { text-decoration: underline; } color: var(--ui-red-accent); font-style: italic; } - -/* ── Nickname modal (anonymous player name chooser) ─────────────────── */ -.nickname-backdrop { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 300; -} - -.nickname-modal { - background: var(--ui-parchment); - border-radius: 8px; - padding: 2rem 2rem 1.75rem; - width: min(360px, 90vw); - display: flex; - flex-direction: column; - gap: 1rem; - box-shadow: - 0 20px 60px rgba(0,0,0,0.55), - 0 0 0 1px rgba(200,164,72,0.35), - 0 0 0 5px rgba(42,21,8,0.9), - 0 0 0 6px rgba(200,164,72,0.2); - animation: game-over-appear 0.25s cubic-bezier(0.22, 0.61, 0.36, 1); -} - -.nickname-modal-title { - font-family: var(--font-display); - font-size: 1.5rem; - font-weight: 600; - color: var(--ui-ink); - text-align: center; - letter-spacing: 0.04em; -} - -.nickname-modal-hint { - font-family: var(--font-ui); - font-size: 0.8rem; - color: rgba(42,26,8,0.6); - text-align: center; - margin-bottom: -0.25rem; -} - -.nickname-modal-alt { - text-align: center; - font-size: 0.8rem; - color: rgba(42,26,8,0.55); - padding-top: 0.5rem; - border-top: 1px solid rgba(138,106,40,0.2); -} - -.nickname-modal-alt a { - color: var(--ui-gold-dark); - text-decoration: none; - font-weight: 500; -} - -.nickname-modal-alt a:hover { text-decoration: underline; } - -/* ── Game hamburger button (☰ → ✕ animation) ────────────────────────── */ -.game-hamburger { - position: fixed; - top: 0.6rem; - left: 0.6rem; - z-index: 251; - width: 36px; - height: 36px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 5px; - background: var(--board-rail); - border: 1px solid rgba(200,164,72,0.35); - border-radius: 5px; - cursor: pointer; - transition: background 0.15s, border-color 0.15s; -} -.game-hamburger:hover { - background: #3d1f0a; - border-color: rgba(200,164,72,0.65); -} - -.hb-bar { - display: block; - width: 16px; - height: 2px; - background: var(--ui-parchment); - border-radius: 1px; - transition: transform 0.25s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.2s; - transform-origin: center; -} -/* Top bar rotates down to form \ */ -.game-hamburger-open .hb-top { transform: translateY(7px) rotate(45deg); } -/* Middle bar fades out */ -.game-hamburger-open .hb-mid { opacity: 0; transform: scaleX(0); } -/* Bottom bar rotates up to form / */ -.game-hamburger-open .hb-bot { transform: translateY(-7px) rotate(-45deg); } - -/* ── Game sidebar ────────────────────────────────────────────────────── */ -.game-sidebar { - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 280px; - z-index: 250; - background: var(--board-rail); - border-right: 1px solid rgba(200,164,72,0.25); - display: flex; - flex-direction: column; - transform: translateX(-100%); - transition: transform 0.25s cubic-bezier(0.22, 0.61, 0.36, 1); - overflow-y: auto; -} -.game-sidebar-open { - transform: translateX(0); -} - -.game-sidebar-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 1rem; - border-bottom: 1px solid rgba(200,164,72,0.2); - flex-shrink: 0; -} - -.game-sidebar-brand { - font-family: var(--font-display); - font-size: 1.3rem; - font-weight: 600; - color: var(--ui-gold); - letter-spacing: 0.06em; - margin-left: 45px; -} - -.game-sidebar-close { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: 1px solid rgba(200,164,72,0.25); - border-radius: 4px; - color: var(--ui-parchment); - font-size: 0.85rem; - cursor: pointer; - opacity: 0.65; - transition: opacity 0.15s; -} -.game-sidebar-close:hover { opacity: 1; } - -.game-sidebar-section { - padding: 0.9rem 1rem; - border-bottom: 1px solid rgba(200,164,72,0.12); - display: flex; - flex-direction: column; - gap: 0.55rem; -} - -.game-sidebar-label { - font-size: 0.7rem; - font-family: var(--font-ui); - letter-spacing: 0.07em; - text-transform: uppercase; - color: rgba(242,232,208,0.45); -} - -.game-sidebar-link { - font-family: var(--font-ui); - font-size: 0.85rem; - color: var(--ui-parchment); - text-decoration: none; - opacity: 0.8; - transition: opacity 0.15s; -} -.game-sidebar-link:hover { opacity: 1; text-decoration: underline; text-underline-offset: 2px; } - -.game-sidebar-btn { - font-family: var(--font-ui); - font-size: 0.82rem; - padding: 0.4rem 0.75rem; - border: 1px solid rgba(200,164,72,0.35); - border-radius: 4px; - background: rgba(200,164,72,0.1); - color: var(--ui-parchment); - cursor: pointer; - text-align: left; - transition: background 0.15s; -} -.game-sidebar-btn:hover { background: rgba(200,164,72,0.22); } - -.game-sidebar-btn-newgame { - background: rgba(58,107,42,0.25); - border-color: rgba(58,107,42,0.55); - font-weight: 500; -} -.game-sidebar-btn-newgame:hover { background: rgba(58,107,42,0.42); } - -.game-sidebar-qr { - width: 100%; - height: auto; - aspect-ratio: 1; - max-width: 200px; - margin: 0 auto; -} diff --git a/clients/web/locales/en.json b/clients/web/locales/en.json index 4e41d9a..2d7ca43 100644 --- a/clients/web/locales/en.json +++ b/clients/web/locales/en.json @@ -121,12 +121,5 @@ "scan_qr": "or scan the QR code", "join_code_label": "Join by code", "join_code_placeholder": "Room code", - "share_btn": "Share", - "nickname_modal_title": "Choose your nickname", - "nickname_modal_hint": "You will play as:", - "nickname_modal_play": "Play", - "nickname_modal_or": "or", - "nickname_modal_sign_in": "Sign in", - "nickname_modal_register": "Create account", - "new_game": "New game" + "share_btn": "Share" } diff --git a/clients/web/locales/fr.json b/clients/web/locales/fr.json index 1d2a8d0..3ff78aa 100644 --- a/clients/web/locales/fr.json +++ b/clients/web/locales/fr.json @@ -121,12 +121,5 @@ "scan_qr": "ou scannez le QR code", "join_code_label": "Rejoindre par code", "join_code_placeholder": "Code de 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_register": "Créer un compte", - "new_game": "Nouvelle partie" + "share_btn": "Partager" } diff --git a/clients/web/src/app.rs b/clients/web/src/app.rs index de8d55f..5aa0f80 100644 --- a/clients/web/src/app.rs +++ b/clients/web/src/app.rs @@ -3,7 +3,7 @@ use futures::{FutureExt, StreamExt}; use gloo_storage::{LocalStorage, Storage}; use leptos::prelude::*; use leptos::task::spawn_local; -use leptos_router::components::{Route, Router, Routes, A}; +use leptos_router::components::{Route, Router, Routes}; use leptos_router::hooks::use_location; use leptos_router::path; use serde::{Deserialize, Serialize}; @@ -19,9 +19,14 @@ use crate::game::session::{ use crate::game::trictrac::backend::TrictracBackend; use crate::game::trictrac::types::{GameDelta, PlayerAction, ScoredEvent, SerStage, ViewState}; use crate::i18n::*; +use crate::nav::SiteNav; use crate::portal::{ - account::AccountPage, forgot_password::ForgotPasswordPage, game_detail::GameDetailPage, - lobby::LobbyPage, profile::ProfilePage, reset_password::ResetPasswordPage, + account::AccountPage, + forgot_password::ForgotPasswordPage, + game_detail::GameDetailPage, + lobby::LobbyPage, + profile::ProfilePage, + reset_password::ResetPasswordPage, verify_email::VerifyEmailPage, }; use trictrac_store::CheckerMove; @@ -149,19 +154,11 @@ pub fn App() -> impl IntoView { let auth_email_verified: RwSignal = RwSignal::new(false); provide_context(auth_username); provide_context(auth_email_verified); - // Set to true once get_me resolves (success or failure) so lobby can - // decide immediately whether to show the nickname modal. - let auth_loaded: RwSignal = RwSignal::new(false); - provide_context(auth_loaded); - // Nickname chosen by an anonymous player; used instead of "Anonymous". - let anon_nickname: RwSignal> = RwSignal::new(None); - provide_context(anon_nickname); spawn_local(async move { if let Ok(me) = api::get_me().await { auth_username.set(Some(me.username)); auth_email_verified.set(me.email_verified); } - auth_loaded.set(true); }); let (cmd_tx, mut cmd_rx) = mpsc::unbounded::(); @@ -245,7 +242,6 @@ pub fn App() -> impl IntoView { if remote_config.is_none() { let player_name = auth_username .get_untracked() - .or_else(|| anon_nickname.get_untracked()) .unwrap_or_else(|| untrack(|| t_string!(i18n, anonymous_name).to_string())); loop { let restart = @@ -291,11 +287,7 @@ pub fn App() -> impl IntoView { let reconnect_token = session.reconnect_token; let my_name = auth_username .get_untracked() - .or_else(|| anon_nickname.get_untracked()) .unwrap_or_else(|| t_string!(i18n, anonymous_name).to_string()); - // Announce our name to the host backend so it can broadcast it to - // the opponent. Done once immediately after connecting. - session.send_action(PlayerAction::SetName(my_name.clone())); let mut vs = ViewState::default_with_names("", ""); let mut result_submitted = false; @@ -375,7 +367,8 @@ pub fn App() -> impl IntoView { view! { - + +
"Page not found."

}> @@ -428,105 +421,6 @@ fn GameOverlay( } } -/// Persistent hamburger button + left sidebar — visible on every page. -#[component] -fn SiteHamburger() -> impl IntoView { - let i18n = use_i18n(); - let auth_username = - use_context::>>().unwrap_or_else(|| RwSignal::new(None)); - let screen = use_context::>().expect("Screen context not found"); - let cmd_tx = use_context::>() - .expect("cmd_tx not found in context"); - - let sidebar_open = RwSignal::new(false); - - let cmd_tx_newgame = cmd_tx.clone(); - - view! { - // ── Hamburger button (☰ → ✕ animation) ─────────────────────────────── - - - // ── Left sidebar ────────────────────────────────────────────────────── -
- -
- "Trictrac" -
- - // Language switcher -
- "Language" -
- - -
-
- - // Auth -
- {move || match auth_username.get() { - Some(u) => { - let href = format!("/profile/{u}"); - view! { - - {u} - - - }.into_any() - }, - None => view! { - - {t!(i18n, sign_in)} - - }.into_any(), - }} -
- - // New game — only shown while a game is in progress - {move || { - if matches!(screen.get(), Screen::Playing(_) | Screen::Connecting) { - let tx = cmd_tx_newgame.clone(); - Some(view! { -
- -
- }) - } else { - None - } - }} -
- } -} - #[cfg(test)] mod tests { use super::*; diff --git a/clients/web/src/game/components/game_screen.rs b/clients/web/src/game/components/game_screen.rs index d55284f..2a1d761 100644 --- a/clients/web/src/game/components/game_screen.rs +++ b/clients/web/src/game/components/game_screen.rs @@ -19,6 +19,8 @@ use super::scoring::ScoringPanel; pub fn GameScreen(state: GameUiState) -> impl IntoView { let i18n = use_i18n(); + let auth_username = + use_context::>>().unwrap_or_else(|| RwSignal::new(None)); let vs = state.view_state.clone(); let player_id = state.player_id; let is_my_turn = vs.active_mp_player == Some(player_id); @@ -98,6 +100,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { // ── Button senders ───────────────────────────────────────────────────────── let cmd_tx_go = cmd_tx.clone(); + let cmd_tx_quit = cmd_tx.clone(); let cmd_tx_end_quit = cmd_tx.clone(); let cmd_tx_end_replay = cmd_tx.clone(); // Only show the fallback Go button when there is no ScoringPanel showing it. @@ -243,52 +246,61 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let opp_name_end = opp_score.name.clone(); let opp_holes_end = opp_score.holes; - let sidebar_copied = 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() }; + 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! { - // ── Game container ────────────────────────────────────────────────────
- // ── Share popover (while waiting for opponent) ─────────────────── - {(!is_bot_game && stage == SerStage::PreGame).then(|| { - let url_label = share_url.clone(); - let url_copy = share_url.clone(); - let svg = share_svg.clone(); - view! { -