added league stats

This commit is contained in:
2026-03-06 21:37:02 +11:00
parent ce659f7d56
commit b96aeef32e
5 changed files with 349 additions and 110 deletions

View File

@@ -437,6 +437,99 @@ func GetAggregatedPlayerStatsForTeam(
return stats, nil return stats, nil
} }
// LeaguePlayerStats holds all aggregated stats for a player in a season-league.
type LeaguePlayerStats struct {
PlayerID int `bun:"player_id"`
PlayerName string `bun:"player_name"`
TeamID int `bun:"team_id"`
TeamName string `bun:"team_name"`
TeamColor string `bun:"team_color"`
GamesPlayed int `bun:"games_played"`
PeriodsPlayed int `bun:"total_periods_played"`
Goals int `bun:"total_goals"`
Assists int `bun:"total_assists"`
PrimaryAssists int `bun:"total_primary_assists"`
SecondaryAssists int `bun:"total_secondary_assists"`
Saves int `bun:"total_saves"`
Shots int `bun:"total_shots"`
Blocks int `bun:"total_blocks"`
Passes int `bun:"total_passes"`
Score int `bun:"total_score"`
}
// GetAllLeaguePlayerStats returns aggregated stats for all players in a season-league.
// Stats are combined across all teams a player may have played on,
// and the player's current roster team is shown.
func GetAllLeaguePlayerStats(
ctx context.Context,
tx bun.Tx,
seasonID, leagueID int,
) ([]*LeaguePlayerStats, error) {
if seasonID == 0 {
return nil, errors.New("seasonID not provided")
}
if leagueID == 0 {
return nil, errors.New("leagueID not provided")
}
var stats []*LeaguePlayerStats
err := tx.NewRaw(`
SELECT
agg.player_id,
agg.player_name,
COALESCE(tr.team_id, 0) AS team_id,
COALESCE(t.name, '') AS team_name,
COALESCE(t.color, '') AS team_color,
agg.games_played,
agg.total_periods_played,
agg.total_goals,
agg.total_assists,
agg.total_primary_assists,
agg.total_secondary_assists,
agg.total_saves,
agg.total_shots,
agg.total_blocks,
agg.total_passes,
agg.total_score
FROM (
SELECT
frps.player_id AS player_id,
COALESCE(p.name, frps.player_username) AS player_name,
COUNT(DISTINCT frps.fixture_result_id) AS games_played,
COALESCE(SUM(frps.periods_played), 0) AS total_periods_played,
COALESCE(SUM(frps.goals), 0) AS total_goals,
COALESCE(SUM(frps.assists), 0) AS total_assists,
COALESCE(SUM(frps.primary_assists), 0) AS total_primary_assists,
COALESCE(SUM(frps.secondary_assists), 0) AS total_secondary_assists,
COALESCE(SUM(frps.saves), 0) AS total_saves,
COALESCE(SUM(frps.shots), 0) AS total_shots,
COALESCE(SUM(frps.blocks), 0) AS total_blocks,
COALESCE(SUM(frps.passes), 0) AS total_passes,
COALESCE(SUM(frps.score), 0) AS total_score
FROM fixture_result_player_stats frps
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
JOIN fixtures f ON f.id = fr.fixture_id
LEFT JOIN players p ON p.id = frps.player_id
WHERE fr.finalized = true
AND f.season_id = ?
AND f.league_id = ?
AND frps.period_num = 3
AND frps.player_id IS NOT NULL
GROUP BY frps.player_id, COALESCE(p.name, frps.player_username)
) agg
LEFT JOIN team_rosters tr
ON tr.player_id = agg.player_id
AND tr.season_id = ?
AND tr.league_id = ?
LEFT JOIN teams t ON t.id = tr.team_id
ORDER BY agg.total_score DESC
`, seasonID, leagueID, seasonID, leagueID).Scan(ctx, &stats)
if err != nil {
return nil, errors.Wrap(err, "tx.NewRaw")
}
return stats, nil
}
// LeagueTopGoalScorer holds aggregated goal scoring stats for a player in a season-league. // LeagueTopGoalScorer holds aggregated goal scoring stats for a player in a season-league.
type LeagueTopGoalScorer struct { type LeagueTopGoalScorer struct {
PlayerID int `bun:"player_id"` PlayerID int `bun:"player_id"`

View File

@@ -705,6 +705,9 @@
.justify-end { .justify-end {
justify-content: flex-end; justify-content: flex-end;
} }
.gap-0\.5 {
gap: calc(var(--spacing) * 0.5);
}
.gap-1 { .gap-1 {
gap: calc(var(--spacing) * 1); gap: calc(var(--spacing) * 1);
} }
@@ -768,6 +771,13 @@
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
} }
} }
.space-y-8 {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)));
}
}
.gap-x-2 { .gap-x-2 {
column-gap: calc(var(--spacing) * 2); column-gap: calc(var(--spacing) * 2);
} }

View File

@@ -0,0 +1,36 @@
function sortableTable(initField, initDir) {
return {
sortField: initField || "score",
sortDir: initDir || "desc",
sort(field) {
if (this.sortField === field) {
this.sortDir = this.sortDir === "asc" ? "desc" : "asc";
} else {
this.sortField = field;
this.sortDir = "desc";
}
this.reorder();
},
reorder() {
const tbody = this.$refs.tbody;
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll("tr"));
const field = this.sortField;
const dir = this.sortDir === "asc" ? 1 : -1;
rows.sort((a, b) => {
const aVal = parseFloat(a.dataset[field]) || 0;
const bVal = parseFloat(b.dataset[field]) || 0;
if (aVal !== bVal) return (aVal - bVal) * dir;
// Tiebreak: alphabetical by player name
const aName = (a.dataset.name || "").toLowerCase();
const bName = (b.dataset.name || "").toLowerCase();
return aName < bName ? -1 : aName > bName ? 1 : 0;
});
rows.forEach((row) => tbody.appendChild(row));
},
};
}

View File

@@ -25,6 +25,7 @@ func SeasonLeagueStatsPage(
var topGoals []*db.LeagueTopGoalScorer var topGoals []*db.LeagueTopGoalScorer
var topAssists []*db.LeagueTopAssister var topAssists []*db.LeagueTopAssister
var topSaves []*db.LeagueTopSaver var topSaves []*db.LeagueTopSaver
var allStats []*db.LeaguePlayerStats
if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error var err error
@@ -52,15 +53,20 @@ func SeasonLeagueStatsPage(
return false, errors.Wrap(err, "db.GetTopSavers") return false, errors.Wrap(err, "db.GetTopSavers")
} }
allStats, err = db.GetAllLeaguePlayerStats(ctx, tx, sl.SeasonID, sl.LeagueID)
if err != nil {
return false, errors.Wrap(err, "db.GetAllLeaguePlayerStats")
}
return true, nil return true, nil
}); !ok { }); !ok {
return return
} }
if r.Method == "GET" { if r.Method == "GET" {
renderSafely(seasonsview.SeasonLeagueStatsPage(sl.Season, sl.League, topGoals, topAssists, topSaves), s, r, w) renderSafely(seasonsview.SeasonLeagueStatsPage(sl.Season, sl.League, topGoals, topAssists, topSaves, allStats), s, r, w)
} else { } else {
renderSafely(seasonsview.SeasonLeagueStats(sl.Season, sl.League, topGoals, topAssists, topSaves), s, r, w) renderSafely(seasonsview.SeasonLeagueStats(sl.Season, sl.League, topGoals, topAssists, topSaves, allStats), s, r, w)
} }
}) })
} }

View File

@@ -10,9 +10,10 @@ templ SeasonLeagueStatsPage(
topGoals []*db.LeagueTopGoalScorer, topGoals []*db.LeagueTopGoalScorer,
topAssists []*db.LeagueTopAssister, topAssists []*db.LeagueTopAssister,
topSaves []*db.LeagueTopSaver, topSaves []*db.LeagueTopSaver,
allStats []*db.LeaguePlayerStats,
) { ) {
@SeasonLeagueLayout("stats", season, league) { @SeasonLeagueLayout("stats", season, league) {
@SeasonLeagueStats(season, league, topGoals, topAssists, topSaves) @SeasonLeagueStats(season, league, topGoals, topAssists, topSaves, allStats)
} }
} }
@@ -22,28 +23,45 @@ templ SeasonLeagueStats(
topGoals []*db.LeagueTopGoalScorer, topGoals []*db.LeagueTopGoalScorer,
topAssists []*db.LeagueTopAssister, topAssists []*db.LeagueTopAssister,
topSaves []*db.LeagueTopSaver, topSaves []*db.LeagueTopSaver,
allStats []*db.LeaguePlayerStats,
) { ) {
if len(topGoals) == 0 && len(topAssists) == 0 && len(topSaves) == 0 { if len(topGoals) == 0 && len(topAssists) == 0 && len(topSaves) == 0 && len(allStats) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center"> <div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No stats available yet.</p> <p class="text-subtext0 text-lg">No stats available yet.</p>
<p class="text-subtext1 text-sm mt-2">Player statistics will appear here once games are finalized.</p> <p class="text-subtext1 text-sm mt-2">Player statistics will appear here once games are finalized.</p>
</div> </div>
} else { } else {
<!-- Triangle layout: two side-by-side on wide screens, saves centered below --> <div class="space-y-8">
<div class="flex flex-col items-center gap-6"> <!-- Trophy Leaders Section -->
<!-- Top row: Goals and Assists side by side when room allows --> if len(topGoals) > 0 || len(topAssists) > 0 || len(topSaves) > 0 {
<div class="flex flex-col lg:flex-row gap-6 w-full justify-center items-center lg:items-start"> <div class="space-y-4">
@topGoalScorersTable(season, league, topGoals) <h2 class="text-xl font-bold text-text text-center">Trophy Leaders</h2>
@topAssistersTable(season, league, topAssists) <!-- Triangle layout: two side-by-side on wide screens, saves centered below -->
</div> <div class="flex flex-col items-center gap-6">
<!-- Bottom row: Saves centered --> <!-- Top row: Goals and Assists side by side when room allows -->
@topSaversTable(season, league, topSaves) <div class="flex flex-col lg:flex-row gap-6 justify-center items-center lg:items-start">
@topGoalScorersTable(season, league, topGoals)
@topAssistersTable(season, league, topAssists)
</div>
<!-- Bottom row: Saves centered -->
@topSaversTable(season, league, topSaves)
</div>
</div>
}
<!-- All Stats Section -->
if len(allStats) > 0 {
<div class="space-y-4">
<h2 class="text-xl font-bold text-text text-center">All Stats</h2>
@allStatsTable(season, league, allStats)
</div>
}
</div> </div>
} }
<script src="/static/js/sortable-table.js" defer></script>
} }
templ topGoalScorersTable(season *db.Season, league *db.League, goals []*db.LeagueTopGoalScorer) { templ topGoalScorersTable(season *db.Season, league *db.League, goals []*db.LeagueTopGoalScorer) {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden w-full max-w-lg"> <div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
<div class="bg-mantle border-b border-surface1 px-4 py-3"> <div class="bg-mantle border-b border-surface1 px-4 py-3">
<h3 class="text-sm font-semibold text-text"> <h3 class="text-sm font-semibold text-text">
Top Goal Scorers Top Goal Scorers
@@ -61,44 +79,42 @@ templ topGoalScorersTable(season *db.Season, league *db.League, goals []*db.Leag
<p class="text-subtext0 text-sm">No goal data available yet.</p> <p class="text-subtext0 text-sm">No goal data available yet.</p>
</div> </div>
} else { } else {
<div class="overflow-x-auto"> <table>
<table class="w-full"> <thead class="bg-mantle border-b border-surface1">
<thead class="bg-mantle border-b border-surface1"> <tr>
<tr> <th class="px-3 py-2 text-center text-xs font-semibold text-subtext0 w-10">#</th>
<th class="px-3 py-2 text-center text-xs font-semibold text-subtext0 w-10">#</th> <th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th> <th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th> <th class="px-2 py-2 text-center text-xs font-semibold text-blue" title="Goals">G</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-blue" title="Goals">G</th> <th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th> <th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Shots">SH</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Shots">SH</th> </tr>
</thead>
<tbody class="divide-y divide-surface1">
for i, gs := range goals {
<tr class="hover:bg-surface1 transition-colors">
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
{ fmt.Sprint(i + 1) }
</td>
<td class="px-3 py-2 text-sm font-medium whitespace-nowrap">
@links.PlayerLinkFromStats(gs.PlayerID, gs.PlayerName)
</td>
<td class="px-3 py-2 text-sm whitespace-nowrap">
@teamColorName(gs.TeamID, gs.TeamName, gs.TeamColor, season, league)
</td>
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(gs.Goals) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(gs.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(gs.Shots) }</td>
</tr> </tr>
</thead> }
<tbody class="divide-y divide-surface1"> </tbody>
for i, gs := range goals { </table>
<tr class="hover:bg-surface1 transition-colors">
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
{ fmt.Sprint(i + 1) }
</td>
<td class="px-3 py-2 text-sm font-medium">
@links.PlayerLinkFromStats(gs.PlayerID, gs.PlayerName)
</td>
<td class="px-3 py-2 text-sm">
@teamColorName(gs.TeamID, gs.TeamName, gs.TeamColor, season, league)
</td>
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(gs.Goals) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(gs.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(gs.Shots) }</td>
</tr>
}
</tbody>
</table>
</div>
} }
</div> </div>
} }
templ topAssistersTable(season *db.Season, league *db.League, assists []*db.LeagueTopAssister) { templ topAssistersTable(season *db.Season, league *db.League, assists []*db.LeagueTopAssister) {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden w-full max-w-lg"> <div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
<div class="bg-mantle border-b border-surface1 px-4 py-3"> <div class="bg-mantle border-b border-surface1 px-4 py-3">
<h3 class="text-sm font-semibold text-text"> <h3 class="text-sm font-semibold text-text">
Top Assisters Top Assisters
@@ -116,44 +132,42 @@ templ topAssistersTable(season *db.Season, league *db.League, assists []*db.Leag
<p class="text-subtext0 text-sm">No assist data available yet.</p> <p class="text-subtext0 text-sm">No assist data available yet.</p>
</div> </div>
} else { } else {
<div class="overflow-x-auto"> <table>
<table class="w-full"> <thead class="bg-mantle border-b border-surface1">
<thead class="bg-mantle border-b border-surface1"> <tr>
<tr> <th class="px-3 py-2 text-center text-xs font-semibold text-subtext0 w-10">#</th>
<th class="px-3 py-2 text-center text-xs font-semibold text-subtext0 w-10">#</th> <th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th> <th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th> <th class="px-2 py-2 text-center text-xs font-semibold text-blue" title="Assists">A</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-blue" title="Assists">A</th> <th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th> <th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Primary Assists">PA</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Primary Assists">PA</th> </tr>
</thead>
<tbody class="divide-y divide-surface1">
for i, as := range assists {
<tr class="hover:bg-surface1 transition-colors">
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
{ fmt.Sprint(i + 1) }
</td>
<td class="px-3 py-2 text-sm font-medium whitespace-nowrap">
@links.PlayerLinkFromStats(as.PlayerID, as.PlayerName)
</td>
<td class="px-3 py-2 text-sm whitespace-nowrap">
@teamColorName(as.TeamID, as.TeamName, as.TeamColor, season, league)
</td>
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(as.Assists) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(as.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(as.PrimaryAssists) }</td>
</tr> </tr>
</thead> }
<tbody class="divide-y divide-surface1"> </tbody>
for i, as := range assists { </table>
<tr class="hover:bg-surface1 transition-colors">
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
{ fmt.Sprint(i + 1) }
</td>
<td class="px-3 py-2 text-sm font-medium">
@links.PlayerLinkFromStats(as.PlayerID, as.PlayerName)
</td>
<td class="px-3 py-2 text-sm">
@teamColorName(as.TeamID, as.TeamName, as.TeamColor, season, league)
</td>
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(as.Assists) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(as.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(as.PrimaryAssists) }</td>
</tr>
}
</tbody>
</table>
</div>
} }
</div> </div>
} }
templ topSaversTable(season *db.Season, league *db.League, saves []*db.LeagueTopSaver) { templ topSaversTable(season *db.Season, league *db.League, saves []*db.LeagueTopSaver) {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden w-full max-w-lg"> <div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
<div class="bg-mantle border-b border-surface1 px-4 py-3"> <div class="bg-mantle border-b border-surface1 px-4 py-3">
<h3 class="text-sm font-semibold text-text"> <h3 class="text-sm font-semibold text-text">
Top Saves Top Saves
@@ -171,42 +185,122 @@ templ topSaversTable(season *db.Season, league *db.League, saves []*db.LeagueTop
<p class="text-subtext0 text-sm">No save data available yet.</p> <p class="text-subtext0 text-sm">No save data available yet.</p>
</div> </div>
} else { } else {
<div class="overflow-x-auto"> <table>
<table class="w-full"> <thead class="bg-mantle border-b border-surface1">
<thead class="bg-mantle border-b border-surface1"> <tr>
<tr> <th class="px-3 py-2 text-center text-xs font-semibold text-subtext0 w-10">#</th>
<th class="px-3 py-2 text-center text-xs font-semibold text-subtext0 w-10">#</th> <th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th> <th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th> <th class="px-2 py-2 text-center text-xs font-semibold text-blue" title="Saves">SV</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-blue" title="Saves">SV</th> <th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th> <th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Blocks">BLK</th>
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Blocks">BLK</th> </tr>
</thead>
<tbody class="divide-y divide-surface1">
for i, sv := range saves {
<tr class="hover:bg-surface1 transition-colors">
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
{ fmt.Sprint(i + 1) }
</td>
<td class="px-3 py-2 text-sm font-medium whitespace-nowrap">
@links.PlayerLinkFromStats(sv.PlayerID, sv.PlayerName)
</td>
<td class="px-3 py-2 text-sm whitespace-nowrap">
@teamColorName(sv.TeamID, sv.TeamName, sv.TeamColor, season, league)
</td>
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(sv.Saves) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(sv.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(sv.Blocks) }</td>
</tr> </tr>
</thead> }
<tbody class="divide-y divide-surface1"> </tbody>
for i, sv := range saves { </table>
<tr class="hover:bg-surface1 transition-colors">
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
{ fmt.Sprint(i + 1) }
</td>
<td class="px-3 py-2 text-sm font-medium">
@links.PlayerLinkFromStats(sv.PlayerID, sv.PlayerName)
</td>
<td class="px-3 py-2 text-sm">
@teamColorName(sv.TeamID, sv.TeamName, sv.TeamColor, season, league)
</td>
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(sv.Saves) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(sv.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(sv.Blocks) }</td>
</tr>
}
</tbody>
</table>
</div>
} }
</div> </div>
} }
templ allStatsTable(season *db.Season, league *db.League, allStats []*db.LeaguePlayerStats) {
<div
class="bg-surface0 border border-surface1 rounded-lg overflow-hidden"
x-data="sortableTable('score', 'desc')"
>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-mantle border-b border-surface1">
<tr>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th>
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th>
@sortableCol("gp", "GP", "Games Played")
@sortableCol("pp", "PP", "Periods Played")
@sortableCol("score", "SC", "Score")
@sortableCol("goals", "G", "Goals")
@sortableCol("assists", "A", "Assists")
@sortableCol("pa", "PA", "Primary Assists")
@sortableCol("sa", "SA", "Secondary Assists")
@sortableCol("saves", "SV", "Saves")
@sortableCol("shots", "SH", "Shots")
@sortableCol("blocks", "BLK", "Blocks")
@sortableCol("passes", "PAS", "Passes")
</tr>
</thead>
<tbody class="divide-y divide-surface1" x-ref="tbody">
for _, ps := range allStats {
<tr
class="hover:bg-surface1 transition-colors"
data-name={ ps.PlayerName }
data-team={ ps.TeamName }
data-gp={ fmt.Sprint(ps.GamesPlayed) }
data-pp={ fmt.Sprint(ps.PeriodsPlayed) }
data-score={ fmt.Sprint(ps.Score) }
data-goals={ fmt.Sprint(ps.Goals) }
data-assists={ fmt.Sprint(ps.Assists) }
data-pa={ fmt.Sprint(ps.PrimaryAssists) }
data-sa={ fmt.Sprint(ps.SecondaryAssists) }
data-saves={ fmt.Sprint(ps.Saves) }
data-shots={ fmt.Sprint(ps.Shots) }
data-blocks={ fmt.Sprint(ps.Blocks) }
data-passes={ fmt.Sprint(ps.Passes) }
>
<td class="px-3 py-2 text-sm font-medium">
@links.PlayerLinkFromStats(ps.PlayerID, ps.PlayerName)
</td>
<td class="px-3 py-2 text-sm">
@teamColorName(ps.TeamID, ps.TeamName, ps.TeamColor, season, league)
</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.GamesPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-text font-medium">{ fmt.Sprint(ps.Score) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Goals) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Assists) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.PrimaryAssists) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.SecondaryAssists) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Saves) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.Shots) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.Blocks) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.Passes) }</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
templ sortableCol(field string, label string, title string) {
<th
class="px-2 py-2 text-center text-xs font-semibold text-text select-none hover:cursor-pointer hover:text-blue transition-colors"
title={ title }
@click={ fmt.Sprintf("sort('%s')", field) }
>
<span class="inline-flex items-center gap-0.5">
{ label }
<template x-if={ fmt.Sprintf("sortField === '%s'", field) }>
<span class="text-blue" x-text={ "sortDir === 'asc' ? '↑' : '↓'" }></span>
</template>
</span>
</th>
}
templ teamColorName(teamID int, teamName string, teamColor string, season *db.Season, league *db.League) { templ teamColorName(teamID int, teamName string, teamColor string, season *db.Season, league *db.League) {
if teamID > 0 && teamName != "" { if teamID > 0 && teamName != "" {
<a <a
@@ -219,7 +313,7 @@ templ teamColorName(teamID int, teamName string, teamColor string, season *db.Se
style={ "background-color: " + templ.SafeCSS(teamColor) } style={ "background-color: " + templ.SafeCSS(teamColor) }
></span> ></span>
} }
<span class="text-sm font-medium">{ teamName }</span> <span class="text-sm font-medium whitespace-nowrap">{ teamName }</span>
</a> </a>
} else { } else {
<span class="text-sm text-subtext0 italic">—</span> <span class="text-sm text-subtext0 italic">—</span>