Add Sycamore/WASM frontend and restructure into server/ + web/
Repo is now a monorepo:
- server/: the kaya backend, unchanged in behaviour, plus:
- GET /api/me for SPA session detection
- last_move recorded on every play and broadcast in the game state, so
clients can show who played which card the moment they play it
- legal_moves per hand card for the player on turn (rules stay
server-side)
- static catch-all route serving the compiled SPA with index.html
fallback; Tortoise context now bound only for /api/* requests
- configurable OIDC post-login/logout redirects for dev against trunk
- web/: Sycamore 0.9 + WASM frontend (trunk): login via the OIDC flow,
lobby (create match / join by code), live game page over websocket with
card images (CC0 woodcut napoletane deck), capture picker, move banner,
game-over overlay, match history and leaderboard pages
- server/Dockerfile gains a rust+trunk stage building web/dist; the single
app image serves the SPA; compose builds from the repo root with
overridable ports/OIDC env
Verified end-to-end against the compose stack: four OIDC logins, game
creation, three joins by code, websocket play with broadcasts to all
players and out-of-turn rejection. 51 backend tests, mypy and cargo tests
all green.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
//! 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::User;
|
||||
|
||||
#[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());
|
||||
|
||||
spawn_local(async move {
|
||||
match api::me().await {
|
||||
Ok(me) => user.set(Some(me)),
|
||||
Err(e) => {
|
||||
error.set(Some(e));
|
||||
user.set(Some(None));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let on_create = move |target: i32| {
|
||||
spawn_local(async move {
|
||||
match api::create_game(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." }
|
||||
a(class="button primary", href="/auth/login") { "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") { "Log out" }
|
||||
}
|
||||
div(class="panel") {
|
||||
h2 { "New match" }
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user