CI / Build and push docker image (push) Successful in 3m5s
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.
128 lines
5.0 KiB
Rust
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" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|