Rebuild the web UI on Bootstrap 5 with the dark felt theme
CI / Build and push docker image (push) Failing after 39s
CI / Build and push docker image (push) Failing after 39s
The lobby was hand-rolled CSS and looked plain. Vendor Bootstrap 5.3 (MIT) under web/vendor so builds stay offline, load it in dark mode and bridge the felt/gold palette onto Bootstrap's CSS variables in a new theme.css; style.css now only owns the game table, cards and scoring summary. Rebuild the lobby, waiting room and shared chrome with Bootstrap components: a shared navbar, creation form as cards with schema-driven controls, roster as a list group with seat/team badges, a seats progress bar and a copy-to-clipboard button on the join code. History, leaderboard, connection/error states and the 404 page use cards, tables and buttons too; the play table itself is unchanged. The copy feedback timer is cancelled when the waiting room is rebuilt by another player joining, fixing a disposed-signal panic that froze the page mid-match.
This commit is contained in:
15 files changed
+774
-486
No files matched your search
+107
-93
@@ -109,104 +109,118 @@ pub fn HistoryPage() -> View {
|
||||
});
|
||||
|
||||
view! {
|
||||
div(class="page") {
|
||||
nav(class="top-nav") {
|
||||
a(href="/") { "← Lobby" }
|
||||
a(href="/leaderboard") { "Leaderboard" }
|
||||
}
|
||||
h1 { "My matches" }
|
||||
(toast(error))
|
||||
(move || match page.get_clone() {
|
||||
None => view! { p(class="status") { "Loading…" } },
|
||||
Some(_) if rows.get_clone().is_empty() => view! {
|
||||
p(class="status") { "No matches played yet." }
|
||||
},
|
||||
Some(_) => {
|
||||
let names = game_types.get_clone();
|
||||
let table_rows = rows
|
||||
.get_clone()
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let finished = m
|
||||
.finished_at
|
||||
.replace('T', " ")
|
||||
.chars()
|
||||
.take(16)
|
||||
.collect::<String>();
|
||||
let game = game_name(&names, &m.game_type);
|
||||
let mut cells: Vec<View> = team_columns(&m)
|
||||
.iter()
|
||||
.map(|t| team_names(&m, t))
|
||||
.map(|c| view! { td { (c) } })
|
||||
.collect();
|
||||
let score = score_text(&m);
|
||||
let winner = winner_text(&m);
|
||||
let outcome = if m.you_won { "Won" } else { "Lost" };
|
||||
let elo_delta = m
|
||||
.your_elo_delta
|
||||
.map(|d| {
|
||||
if d >= 0 {
|
||||
format!("+{d}")
|
||||
} else {
|
||||
d.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let cls = if m.you_won { "won" } else { "lost" };
|
||||
let mut row_cells = vec![
|
||||
view! { td { (finished) } },
|
||||
view! { td { (game) } },
|
||||
];
|
||||
row_cells.append(&mut cells);
|
||||
row_cells.push(view! { td { (score) } });
|
||||
row_cells.push(view! { td { (winner) } });
|
||||
row_cells.push(view! { td(class=cls) { (outcome) } });
|
||||
row_cells.push(view! { td(class=cls) { (elo_delta) } });
|
||||
view! { tr { (row_cells) } }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Team columns are per-row (matches may mix games), so
|
||||
// the header shows the union across the loaded rows.
|
||||
let mut header_teams = BTreeSet::new();
|
||||
for m in rows.get_clone().iter() {
|
||||
for t in team_columns(m) {
|
||||
header_teams.insert(t);
|
||||
div {
|
||||
(crate::components::nav::app_nav("/history", || view! {}))
|
||||
div(class="container page-shell py-4") {
|
||||
h1(class="h3 mb-4") { "My matches" }
|
||||
(toast(error))
|
||||
(move || match page.get_clone() {
|
||||
None => view! {
|
||||
div(class="d-flex justify-content-center p-5") {
|
||||
div(class="spinner-border text-warning", role="status") {
|
||||
span(class="visually-hidden") { "Loading…" }
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut header_teams: Vec<Option<String>> =
|
||||
header_teams.into_iter().collect();
|
||||
header_teams.sort_by_key(|t| match t {
|
||||
Some(_) => (0, String::new()),
|
||||
None => (1, String::new()),
|
||||
});
|
||||
let header_cells: Vec<View> = header_teams
|
||||
.iter()
|
||||
.map(team_header)
|
||||
.map(|h| view! { th { (h) } })
|
||||
.collect();
|
||||
view! {
|
||||
table(class="matches") {
|
||||
thead {
|
||||
tr {
|
||||
th { "Finished" }
|
||||
th { "Game" }
|
||||
(header_cells)
|
||||
th { "Score" }
|
||||
th { "Winner" }
|
||||
th { "You" }
|
||||
th { "Elo" }
|
||||
},
|
||||
Some(_) if rows.get_clone().is_empty() => view! {
|
||||
p(class="text-secondary text-center p-5") { "No matches played yet." }
|
||||
},
|
||||
Some(_) => {
|
||||
let names = game_types.get_clone();
|
||||
let table_rows = rows
|
||||
.get_clone()
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let finished = m
|
||||
.finished_at
|
||||
.replace('T', " ")
|
||||
.chars()
|
||||
.take(16)
|
||||
.collect::<String>();
|
||||
let game = game_name(&names, &m.game_type);
|
||||
let mut cells: Vec<View> = team_columns(&m)
|
||||
.iter()
|
||||
.map(|t| team_names(&m, t))
|
||||
.map(|c| view! { td { (c) } })
|
||||
.collect();
|
||||
let score = score_text(&m);
|
||||
let winner = winner_text(&m);
|
||||
let outcome = if m.you_won { "Won" } else { "Lost" };
|
||||
let elo_delta = m
|
||||
.your_elo_delta
|
||||
.map(|d| {
|
||||
if d >= 0 {
|
||||
format!("+{d}")
|
||||
} else {
|
||||
d.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let cls = if m.you_won {
|
||||
"text-success fw-semibold"
|
||||
} else {
|
||||
"text-secondary"
|
||||
};
|
||||
let mut row_cells = vec![
|
||||
view! { td { (finished) } },
|
||||
view! { td { (game) } },
|
||||
];
|
||||
row_cells.append(&mut cells);
|
||||
row_cells.push(view! { td { (score) } });
|
||||
row_cells.push(view! { td { (winner) } });
|
||||
row_cells.push(view! { td(class=cls) { (outcome) } });
|
||||
row_cells.push(view! { td(class=cls) { (elo_delta) } });
|
||||
view! { tr { (row_cells) } }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Team columns are per-row (matches may mix games), so
|
||||
// the header shows the union across the loaded rows.
|
||||
let mut header_teams = BTreeSet::new();
|
||||
for m in rows.get_clone().iter() {
|
||||
for t in team_columns(m) {
|
||||
header_teams.insert(t);
|
||||
}
|
||||
}
|
||||
let mut header_teams: Vec<Option<String>> =
|
||||
header_teams.into_iter().collect();
|
||||
header_teams.sort_by_key(|t| match t {
|
||||
Some(_) => (0, String::new()),
|
||||
None => (1, String::new()),
|
||||
});
|
||||
let header_cells: Vec<View> = header_teams
|
||||
.iter()
|
||||
.map(team_header)
|
||||
.map(|h| view! { th { (h) } })
|
||||
.collect();
|
||||
view! {
|
||||
div(class="table-responsive") {
|
||||
table(class="table table-striped table-hover align-middle") {
|
||||
thead {
|
||||
tr {
|
||||
th { "Finished" }
|
||||
th { "Game" }
|
||||
(header_cells)
|
||||
th { "Score" }
|
||||
th { "Winner" }
|
||||
th { "You" }
|
||||
th { "Elo" }
|
||||
}
|
||||
}
|
||||
tbody { (table_rows) }
|
||||
}
|
||||
}
|
||||
tbody { (table_rows) }
|
||||
(cursor.get_clone().map(|c| view! {
|
||||
button(
|
||||
class="btn btn-outline-light",
|
||||
on:click=move |_| load2(Some(c.clone())),
|
||||
) {
|
||||
"Load more"
|
||||
}
|
||||
}))
|
||||
}
|
||||
(cursor.get_clone().map(|c| view! {
|
||||
button(class="button", on:click=move |_| load2(Some(c.clone()))) {
|
||||
"Load more"
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user