Offer a Continue link for ongoing games in the lobby
A player who accidentally closes the game page can now get back to it from the lobby. Backend: - GameStore.list_ongoing() returns live unfinished sessions (newest first), skipping expired keys and finished matches; - GET /api/me/games lists the games the caller is seated in, with the game name/description, players, open seats and your seat (no join code); - store, platform route and app integration tests. Frontend: - MyGame/MyGamesPage model plus api::my_games(); - the lobby shows a "Your games" card with phase, seats, players and a Continue button linking to /game/<id>; hidden when empty or logged out.
This commit is contained in:
9 files changed
+446
-4
No files matched your search
+69
-1
@@ -16,7 +16,7 @@ use crate::components::card::card_back;
|
||||
use crate::components::toast::toast;
|
||||
use crate::model::{
|
||||
fallback_game_types, is_individual_game, AuthProvider, GameTypeInfo, JoinPreview,
|
||||
OptionField, OptionKind, PlayerRating, User,
|
||||
MyGame, OptionField, OptionKind, PlayerRating, User,
|
||||
};
|
||||
|
||||
/// Human-readable label for a provider name ("google" -> "Google").
|
||||
@@ -292,6 +292,8 @@ pub fn LobbyPage() -> View {
|
||||
let auth_providers = create_signal(Vec::<AuthProvider>::new());
|
||||
// The player's Elo ratings, one row per game type played.
|
||||
let ratings = create_signal(Vec::<PlayerRating>::new());
|
||||
// Live games the player is seated in, for the "Continue" list.
|
||||
let my_games = create_signal(Vec::<MyGame>::new());
|
||||
let error = create_signal(Option::<String>::None);
|
||||
let code = create_signal(String::new());
|
||||
let game_types = create_signal(fallback_game_types());
|
||||
@@ -386,6 +388,9 @@ pub fn LobbyPage() -> View {
|
||||
if let Ok(p) = api::my_ratings().await {
|
||||
ratings.set(p.results);
|
||||
}
|
||||
if let Ok(games) = api::my_games().await {
|
||||
my_games.set(games);
|
||||
}
|
||||
});
|
||||
}
|
||||
user.set(Some(me));
|
||||
@@ -549,6 +554,69 @@ pub fn LobbyPage() -> View {
|
||||
"Play scopone scientifico and briscola with friends."
|
||||
}
|
||||
}
|
||||
// Ongoing games: a "Continue" link back into a
|
||||
// match whose page was accidentally closed.
|
||||
(move || {
|
||||
let games = my_games.get_clone();
|
||||
if games.is_empty() {
|
||||
return view! {};
|
||||
}
|
||||
let types = game_types.get_clone();
|
||||
let rows = games
|
||||
.into_iter()
|
||||
.map(|g| {
|
||||
let name = if g.game_name.is_empty() {
|
||||
types
|
||||
.iter()
|
||||
.find(|t| t.id == g.game_type)
|
||||
.map(|t| t.name.clone())
|
||||
.unwrap_or_else(|| g.game_type.clone())
|
||||
} else {
|
||||
g.game_name.clone()
|
||||
};
|
||||
let phase = g.phase_label();
|
||||
let seats = g.seats_text();
|
||||
let players = g.players_text();
|
||||
let players_note = if players.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" · {players}")
|
||||
};
|
||||
let href = format!("/game/{}", g.id);
|
||||
view! {
|
||||
li(
|
||||
class="list-group-item d-flex flex-wrap \
|
||||
justify-content-between \
|
||||
align-items-center gap-2"
|
||||
) {
|
||||
div {
|
||||
div(class="fw-semibold") { (name) }
|
||||
div(class="text-secondary small") {
|
||||
(phase) " · " (seats)
|
||||
(players_note)
|
||||
}
|
||||
}
|
||||
a(
|
||||
class="btn btn-sm btn-warning",
|
||||
href=href,
|
||||
) {
|
||||
"Continue"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
view! {
|
||||
div(class="card mb-4") {
|
||||
div(class="card-header") { "Your games" }
|
||||
div(class="card-body p-0") {
|
||||
ul(class="list-group list-group-flush") {
|
||||
(rows)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
div(class="row g-4") {
|
||||
div(class="col-lg-7") {
|
||||
div(class="card h-100") {
|
||||
|
||||
Reference in new issue
Block a user