Files
tavolo/web/src/pages/lobby.rs
T
woggioni ab4130a4ca
CI / Build and push docker image (push) Successful in 3m5s
Add preliminary support for multiple card games
A game-type registry (server/src/tavolo/games.py) is now the single
source of truth for the games the platform can host; only scopone
scientifico is registered so far. GET /api/game-types exposes it for the
lobby's new game dropdown, and POST /api/games accepts a validated
game_type (default scopone_scientifico) which is carried on the live
GameState and onto each finished Match row (new indexed column,
migration 1_20260916235833_update), so statistics can be scoped per
game: /api/me/matches and /api/leaderboard take an optional game_type
filter and every serialized match includes its game_type.

Game states serialized before this change still load with the default
game type.
2026-09-17 08:26:46 +08:00

128 lines
5.0 KiB
Rust

//! Lobby page: login prompt, match creation and joining by code.
use wasm_bindgen_futures::spawn_local;
use sycamore::prelude::*;
use sycamore_router::navigate;
use crate::api;
use crate::model::{GameTypeInfo, User};
/// Used when the game-types fetch fails: match creation must still work.
fn fallback_game_types() -> Vec<GameTypeInfo> {
vec![GameTypeInfo {
id: "scopone_scientifico".to_string(),
name: "Scopone scientifico".to_string(),
description: String::new(),
}]
}
#[component]
pub fn LobbyPage() -> View {
// Outer None = still loading; Some(None) = logged out.
let user = create_signal(Option::<Option<User>>::None);
let error = create_signal(Option::<String>::None);
let code = create_signal(String::new());
let game_types = create_signal(fallback_game_types());
let selected_game = create_signal("scopone_scientifico".to_string());
spawn_local(async move {
match api::me().await {
Ok(me) => user.set(Some(me)),
Err(e) => {
error.set(Some(e));
user.set(Some(None));
}
}
});
spawn_local(async move {
match api::game_types().await {
Ok(types) if !types.is_empty() => {
selected_game.set(types[0].id.clone());
game_types.set(types);
}
_ => {} // keep the scopone fallback
}
});
let on_create = move |target: i32| {
let game_type = selected_game.get_clone();
spawn_local(async move {
match api::create_game(&game_type, target).await {
Ok(game) => navigate(&format!("/game/{}", game.id)),
Err(e) => error.set(Some(e)),
}
});
};
let on_join = move |_| {
let value = code.get_clone().trim().to_uppercase();
if value.is_empty() {
return;
}
spawn_local(async move {
match api::join_game(&value).await {
Ok(game) => navigate(&format!("/game/{}", game.id)),
Err(e) => error.set(Some(e)),
}
});
};
view! {
div(class="lobby") {
h1 { "Scopone scientifico" }
(move || error.get_clone().map(|e| view! { div(class="toast") { (e) } }))
(move || match user.get_clone() {
None => view! { p(class="status") { "Loading…" } },
Some(None) => view! {
div(class="panel login-panel") {
p { "Log in with your account to play." }
// rel="external" opts out of the SPA router's click
// interception: this must be a full page navigation
// to the backend's OIDC login endpoint.
a(class="button primary", href="/auth/login", rel="external") { "Log in" }
}
},
Some(Some(me)) => view! {
div(class="lobby-grid") {
nav(class="top-nav") {
span(class="whoami") { "Signed in as " strong { (me.name.clone()) } }
a(href="/history") { "My matches" }
a(href="/leaderboard") { "Leaderboard" }
a(href="/auth/logout", rel="external") { "Log out" }
}
div(class="panel") {
h2 { "New match" }
label(r#for="game-type") { "Game" }
select(id="game-type", bind:value=selected_game) {
Keyed(
list=game_types,
view=|g| view! { option(value=g.id.clone()) { (g.name) } },
key=|g| g.id.clone(),
)
}
p { "First team to reach the target score wins." }
div(class="target-buttons") {
button(class="button", on:click=move |_| on_create(11)) { "Target 11" }
button(class="button", on:click=move |_| on_create(16)) { "Target 16" }
button(class="button", on:click=move |_| on_create(21)) { "Target 21" }
}
}
div(class="panel") {
h2 { "Join with a code" }
div(class="join-form") {
input(
r#type="text",
placeholder="6-letter code",
maxlength="6",
bind:value=code,
)
button(class="button primary", on:click=on_join) { "Join" }
}
}
}
},
})
}
}
}