Give the match history table fixed columns
CI / Build and push docker image (push) Successful in 1m6s
CI / Build and push docker image (push) Successful in 1m6s
Rows rendered only their own match's team labels while the header was the union across all loaded matches. Scopone and briscola have two team labels and briscola chiamata has three, so a mixed history produced a five-column header above rows with two or three team cells - every cell after them (score, winner, you, elo) landed under the wrong header, and the columns changed with the set of game types loaded. Replace the variable team columns with a single "Teams" column: the table is now Finished | Game | Teams | Score | Winner | You | Elo for every game. The cell lists each team or role on its own line, ordered Chiamante/Socio/Difensori for chiamata and alphabetically (Team A, Team B, ...) otherwise, with team-less players last; names read "Bob, Carol & Dave".
This commit is contained in:
+147
-44
@@ -12,29 +12,50 @@ use crate::api;
|
||||
use crate::components::toast::toast;
|
||||
use crate::model::{GameTypeInfo, MatchSummary, MatchesPage};
|
||||
|
||||
/// Distinct team labels in seat order; players without a team render in
|
||||
/// a shared "Players" column.
|
||||
fn team_columns(m: &MatchSummary) -> Vec<Option<String>> {
|
||||
let mut teams = BTreeSet::new();
|
||||
/// The match's teams, each as a `(label, names)` pair, in a readable
|
||||
/// order: briscola chiamata's roles first (Chiamante, Socio, Difensori),
|
||||
/// then other team labels alphabetically (A, B, ...), with team-less
|
||||
/// players last.
|
||||
fn team_groups(m: &MatchSummary) -> Vec<(String, String)> {
|
||||
let mut labels: BTreeSet<Option<String>> = BTreeSet::new();
|
||||
for p in &m.players {
|
||||
teams.insert(p.team.clone());
|
||||
labels.insert(p.team.clone());
|
||||
}
|
||||
let mut ordered: Vec<Option<String>> = teams.into_iter().collect();
|
||||
// Seated teams first (in first-seat order), then the team-less column.
|
||||
ordered.sort_by_key(|t| match t {
|
||||
Some(_) => (0, String::new()),
|
||||
None => (1, String::new()),
|
||||
});
|
||||
let mut ordered: Vec<Option<String>> = labels.into_iter().collect();
|
||||
ordered.sort_by_key(team_sort_key);
|
||||
ordered
|
||||
.into_iter()
|
||||
.map(|label| (team_header(&label), team_names(m, &label)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sort key for team labels: chiamata roles in play order, other labels
|
||||
/// alphabetically, team-less players last.
|
||||
fn team_sort_key(label: &Option<String>) -> (u8, String) {
|
||||
match label.as_deref() {
|
||||
Some("Chiamante") => (0, String::new()),
|
||||
Some("Socio") => (1, String::new()),
|
||||
Some("Difensori") => (2, String::new()),
|
||||
Some(other) => (0, other.to_string()),
|
||||
None => (9, String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn team_names(m: &MatchSummary, team: &Option<String>) -> String {
|
||||
m.players
|
||||
let names = m
|
||||
.players
|
||||
.iter()
|
||||
.filter(|p| &p.team == team)
|
||||
.map(|p| p.display_name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" & ")
|
||||
.collect::<Vec<_>>();
|
||||
match names.len() {
|
||||
0 => String::new(),
|
||||
1 => names[0].clone(),
|
||||
_ => {
|
||||
let (last, rest) = names.split_last().expect("non-empty");
|
||||
format!("{} & {}", rest.join(", "), last)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn team_header(team: &Option<String>) -> String {
|
||||
@@ -175,10 +196,18 @@ pub fn HistoryPage() -> View {
|
||||
.map(|m| {
|
||||
let finished = local_finished(&m.finished_at);
|
||||
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) } })
|
||||
let teams: Vec<View> = team_groups(&m)
|
||||
.into_iter()
|
||||
.map(|(label, names)| {
|
||||
view! {
|
||||
div(class="match-team") {
|
||||
span(class="text-secondary") {
|
||||
(label) ": "
|
||||
}
|
||||
(names)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let score = score_text(&m);
|
||||
let winner = winner_text(&m);
|
||||
@@ -198,37 +227,18 @@ pub fn HistoryPage() -> View {
|
||||
} else {
|
||||
"text-secondary"
|
||||
};
|
||||
let mut row_cells = vec![
|
||||
let row_cells = vec![
|
||||
view! { td { (finished) } },
|
||||
view! { td { (game) } },
|
||||
view! { td { (teams) } },
|
||||
view! { td { (score) } },
|
||||
view! { td { (winner) } },
|
||||
view! { td(class=cls) { (outcome) } },
|
||||
view! { td(class=cls) { (elo_delta) } },
|
||||
];
|
||||
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") {
|
||||
@@ -236,7 +246,7 @@ pub fn HistoryPage() -> View {
|
||||
tr {
|
||||
th { "Finished" }
|
||||
th { "Game" }
|
||||
(header_cells)
|
||||
th { "Teams" }
|
||||
th { "Score" }
|
||||
th { "Winner" }
|
||||
th { "You" }
|
||||
@@ -261,3 +271,96 @@ pub fn HistoryPage() -> View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn match_summary(players: serde_json::Value) -> MatchSummary {
|
||||
serde_json::from_value(json!({
|
||||
"id": "m1",
|
||||
"started_at": "2026-01-01T00:00:00+00:00",
|
||||
"finished_at": "2026-01-01T01:00:00+00:00",
|
||||
"players": players,
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_groups_order_chiamata_roles() {
|
||||
let m = match_summary(json!([
|
||||
{"user_sub": "a", "display_name": "Alice", "seat": 0,
|
||||
"team": "Difensori", "won": false},
|
||||
{"user_sub": "b", "display_name": "Bob", "seat": 1,
|
||||
"team": "Chiamante", "won": true},
|
||||
{"user_sub": "c", "display_name": "Carol", "seat": 2,
|
||||
"team": "Difensori", "won": false},
|
||||
{"user_sub": "d", "display_name": "Dave", "seat": 3,
|
||||
"team": "Socio", "won": true},
|
||||
{"user_sub": "e", "display_name": "Erin", "seat": 4,
|
||||
"team": "Difensori", "won": false},
|
||||
]));
|
||||
assert_eq!(
|
||||
vec![
|
||||
("Chiamante".to_string(), "Bob".to_string()),
|
||||
("Socio".to_string(), "Dave".to_string()),
|
||||
(
|
||||
"Difensori".to_string(),
|
||||
"Alice, Carol & Erin".to_string()
|
||||
),
|
||||
],
|
||||
team_groups(&m)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_groups_keep_a_and_b_in_order() {
|
||||
let m = match_summary(json!([
|
||||
{"user_sub": "a", "display_name": "Alice", "seat": 0,
|
||||
"team": "A", "won": true},
|
||||
{"user_sub": "b", "display_name": "Bob", "seat": 1,
|
||||
"team": "B", "won": false},
|
||||
]));
|
||||
assert_eq!(
|
||||
vec![
|
||||
("Team A".to_string(), "Alice".to_string()),
|
||||
("Team B".to_string(), "Bob".to_string()),
|
||||
],
|
||||
team_groups(&m)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_groups_put_teamless_players_last() {
|
||||
let m = match_summary(json!([
|
||||
{"user_sub": "a", "display_name": "Alice", "seat": 0,
|
||||
"team": null, "won": false},
|
||||
{"user_sub": "b", "display_name": "Bob", "seat": 1,
|
||||
"team": "A", "won": true},
|
||||
]));
|
||||
assert_eq!(
|
||||
vec![
|
||||
("Team A".to_string(), "Bob".to_string()),
|
||||
("Players".to_string(), "Alice".to_string()),
|
||||
],
|
||||
team_groups(&m)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_names_join_three_names_readably() {
|
||||
let m = match_summary(json!([
|
||||
{"user_sub": "a", "display_name": "Alice", "seat": 0,
|
||||
"team": "A", "won": true},
|
||||
{"user_sub": "b", "display_name": "Bob", "seat": 1,
|
||||
"team": "A", "won": true},
|
||||
{"user_sub": "c", "display_name": "Carol", "seat": 2,
|
||||
"team": "A", "won": true},
|
||||
]));
|
||||
assert_eq!(
|
||||
"Alice, Bob & Carol",
|
||||
team_names(&m, &Some("A".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,3 +871,11 @@ body {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- match history ---------- */
|
||||
|
||||
/* One line per team inside the "Teams" cell of the history table. */
|
||||
.match-team {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user