//! 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 { 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::>::None); let error = create_signal(Option::::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" } } } } }, }) } } }