updated team stats

This commit is contained in:
2026-03-06 19:06:29 +11:00
parent c53fbac281
commit fc219a044c
5 changed files with 230 additions and 125 deletions

View File

@@ -1152,6 +1152,9 @@
.py-4 {
padding-block: calc(var(--spacing) * 4);
}
.py-5 {
padding-block: calc(var(--spacing) * 5);
}
.py-6 {
padding-block: calc(var(--spacing) * 6);
}
@@ -2041,11 +2044,6 @@
scale: var(--tw-scale-x) var(--tw-scale-y);
}
}
.sm\:grid-cols-4 {
@media (width >= 40rem) {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.sm\:flex-row {
@media (width >= 40rem) {
flex-direction: row;
@@ -2177,11 +2175,6 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.lg\:grid-cols-8 {
@media (width >= 64rem) {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
}
.lg\:items-end {
@media (width >= 64rem) {
align-items: flex-end;

View File

@@ -35,6 +35,7 @@ func SeasonLeagueTeamDetailPage(
var scheduleMap map[int]*db.FixtureSchedule
var resultMap map[int]*db.FixtureResult
var playerStats []*db.AggregatedPlayerStats
var leaderboard []*db.LeaderboardEntry
if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error
@@ -72,12 +73,51 @@ func SeasonLeagueTeamDetailPage(
return false, errors.Wrap(err, "db.GetPlayersNotOnTeam")
}
// Get all teams and all fixtures for the league to compute leaderboard
var allTeams []*db.Team
err = tx.NewSelect().
Model(&allTeams).
Join("INNER JOIN team_participations AS tp ON tp.team_id = t.id").
Where("tp.season_id = ? AND tp.league_id = ?", twr.Season.ID, twr.League.ID).
Scan(ctx)
if err != nil {
return false, errors.Wrap(err, "tx.NewSelect allTeams")
}
allFixtures, err := db.GetAllocatedFixtures(ctx, tx, twr.Season.ID, twr.League.ID)
if err != nil {
return false, errors.Wrap(err, "db.GetAllocatedFixtures")
}
allFixtureIDs := make([]int, len(allFixtures))
for i, f := range allFixtures {
allFixtureIDs[i] = f.ID
}
allResultMap, err := db.GetFinalizedResultsForFixtures(ctx, tx, allFixtureIDs)
if err != nil {
return false, errors.Wrap(err, "db.GetFinalizedResultsForFixtures allFixtures")
}
leaderboard = db.ComputeLeaderboard(allTeams, allFixtures, allResultMap)
return true, nil
}); !ok {
return
}
record := db.ComputeTeamRecord(teamID, fixtures, resultMap)
renderSafely(seasonsview.SeasonLeagueTeamDetailPage(twr, fixtures, available, scheduleMap, resultMap, record, playerStats), s, r, w)
// Find this team's position and record from the leaderboard
var position int
var record *db.TeamRecord
for _, entry := range leaderboard {
if entry.Team.ID == teamID {
position = entry.Position
record = entry.Record
break
}
}
if record == nil {
record = &db.TeamRecord{}
}
renderSafely(seasonsview.SeasonLeagueTeamDetailPage(twr, fixtures, available, scheduleMap, resultMap, record, playerStats, position, len(leaderboard)), s, r, w)
})
}

View File

@@ -260,3 +260,20 @@ func intPtrStr(v *int) string {
}
return fmt.Sprint(*v)
}
func ordinal(n int) string {
suffix := "th"
if n%100 >= 11 && n%100 <= 13 {
// 11th, 12th, 13th
} else {
switch n % 10 {
case 1:
suffix = "st"
case 2:
suffix = "nd"
case 3:
suffix = "rd"
}
}
return fmt.Sprintf("%d%s", n, suffix)
}

View File

@@ -8,7 +8,7 @@ import "fmt"
import "sort"
import "time"
templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture, available []*db.Player, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult, record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats) {
templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture, available []*db.Player, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult, record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats, position int, totalTeams int) {
{{
team := twr.Team
season := twr.Season
@@ -62,14 +62,48 @@ templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture,
</div>
<!-- Content -->
<div class="bg-crust p-6">
<!-- Top row: Roster (left) + Fixtures (right) -->
{{
// Split fixtures into upcoming and completed
var upcoming []*db.Fixture
var completed []*db.Fixture
for _, f := range fixtures {
if _, hasResult := resultMap[f.ID]; hasResult {
completed = append(completed, f)
} else {
upcoming = append(upcoming, f)
}
}
// Sort completed by scheduled time descending (most recent first)
sort.Slice(completed, func(i, j int) bool {
ti := time.Time{}
tj := time.Time{}
if si, ok := scheduleMap[completed[i].ID]; ok && si.ScheduledTime != nil {
ti = *si.ScheduledTime
}
if sj, ok := scheduleMap[completed[j].ID]; ok && sj.ScheduledTime != nil {
tj = *sj.ScheduledTime
}
return ti.After(tj)
})
// Limit to 5 most recent results
recentResults := completed
if len(recentResults) > 5 {
recentResults = recentResults[:5]
}
}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Top Left: Team Standing -->
@teamRecordCard(record, position, totalTeams)
<!-- Top Right: Results -->
@teamResultsSection(twr.Team, recentResults, resultMap)
<!-- Bottom Left: Roster -->
@TeamRosterSection(twr, available)
@teamFixturesPane(twr.Team, fixtures, scheduleMap, resultMap)
<!-- Bottom Right: Upcoming -->
@teamUpcomingSection(twr.Team, upcoming, scheduleMap)
</div>
<!-- Stats below both -->
<!-- Player Stats (full width) -->
<div class="mt-6">
@teamStatsSection(record, playerStats)
@playerStatsSection(playerStats)
</div>
</div>
</div>
@@ -405,40 +439,14 @@ templ manageRosterModal(twr *db.TeamWithRoster, available []*db.Player, rosterPl
</script>
}
templ teamFixturesPane(team *db.Team, fixtures []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult) {
{{
// Split fixtures into upcoming and completed
var upcoming []*db.Fixture
var completed []*db.Fixture
for _, f := range fixtures {
if _, hasResult := resultMap[f.ID]; hasResult {
completed = append(completed, f)
} else {
upcoming = append(upcoming, f)
}
}
// Sort completed by scheduled time descending (most recent first)
sort.Slice(completed, func(i, j int) bool {
ti := time.Time{}
tj := time.Time{}
if si, ok := scheduleMap[completed[i].ID]; ok && si.ScheduledTime != nil {
ti = *si.ScheduledTime
}
if sj, ok := scheduleMap[completed[j].ID]; ok && sj.ScheduledTime != nil {
tj = *sj.ScheduledTime
}
return ti.After(tj)
})
// Limit to 5 most recent results
recentResults := completed
if len(recentResults) > 5 {
recentResults = recentResults[:5]
}
}}
<section class="space-y-6">
<!-- Results -->
<div>
<h2 class="text-2xl font-bold text-text mb-4">Results</h2>
templ teamResultsSection(team *db.Team, recentResults []*db.Fixture, resultMap map[int]*db.FixtureResult) {
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">
Results
<span class="text-sm font-normal text-subtext0">(last 5)</span>
</h2>
</div>
if len(recentResults) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No results yet.</p>
@@ -451,10 +459,14 @@ templ teamFixturesPane(team *db.Team, fixtures []*db.Fixture, scheduleMap map[in
}
</div>
}
</section>
}
templ teamUpcomingSection(team *db.Team, upcoming []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule) {
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">Upcoming</h2>
</div>
<!-- Upcoming -->
<div>
<h2 class="text-2xl font-bold text-text mb-4">Upcoming</h2>
if len(upcoming) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No upcoming fixtures.</p>
@@ -466,7 +478,6 @@ templ teamFixturesPane(team *db.Team, fixtures []*db.Fixture, scheduleMap map[in
}
</div>
}
</div>
</section>
}
@@ -595,32 +606,60 @@ templ teamResultRow(team *db.Team, fixture *db.Fixture, resultMap map[int]*db.Fi
</a>
}
templ teamStatsSection(record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats) {
templ teamRecordCard(record *db.TeamRecord, position int, totalTeams int) {
<section>
<div class="mb-4">
<h2 class="text-2xl font-bold text-text">Stats</h2>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">Standing</h2>
</div>
if record.Played == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No stats yet.</p>
<p class="text-subtext1 text-sm mt-2">Team statistics will appear here once games are played.</p>
<p class="text-subtext0 text-lg">No games played yet.</p>
</div>
} else {
<!-- Team Record Summary -->
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden mb-4">
<div class="grid grid-cols-4 sm:grid-cols-4 lg:grid-cols-8 divide-x divide-surface1">
@statCell("Played", fmt.Sprint(record.Played), "")
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
<!-- Position & Points Header -->
<div class="flex items-center justify-between px-6 py-5 border-b border-surface1">
<div class="flex items-center gap-3">
<span class="text-4xl font-bold text-text">{ ordinal(position) }</span>
<div>
<p class="text-xs text-subtext0 uppercase font-medium">Position</p>
<p class="text-sm text-subtext1">of { fmt.Sprint(totalTeams) } teams</p>
</div>
</div>
<div class="text-right">
<p class="text-xs text-subtext0 uppercase font-medium">Points</p>
<p class="text-3xl font-bold text-blue">{ fmt.Sprint(record.Points) }</p>
</div>
</div>
<!-- Record Grid -->
<div class="grid grid-cols-4 divide-x divide-surface1">
@statCell("W", fmt.Sprint(record.Wins), "text-green")
@statCell("OTW", fmt.Sprint(record.OvertimeWins), "text-teal")
@statCell("OTL", fmt.Sprint(record.OvertimeLosses), "text-peach")
@statCell("L", fmt.Sprint(record.Losses), "text-red")
</div>
<!-- Goals Row -->
<div class="grid grid-cols-3 divide-x divide-surface1 border-t border-surface1">
@statCell("Played", fmt.Sprint(record.Played), "")
@statCell("GF", fmt.Sprint(record.GoalsFor), "")
@statCell("GA", fmt.Sprint(record.GoalsAgainst), "")
@statCell("PTS", fmt.Sprint(record.Points), "text-blue")
</div>
</div>
<!-- Player Stats Leaderboard -->
if len(playerStats) > 0 {
}
</section>
}
templ playerStatsSection(playerStats []*db.AggregatedPlayerStats) {
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">Player Stats</h2>
</div>
if len(playerStats) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No player stats yet.</p>
<p class="text-subtext1 text-sm mt-2">Player statistics will appear here once games are played.</p>
</div>
} else {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
@@ -658,7 +697,6 @@ templ teamStatsSection(record *db.TeamRecord, playerStats []*db.AggregatedPlayer
</div>
</div>
}
}
</section>
}

View File

@@ -49,7 +49,7 @@ templ teamSeasonCard(team *db.Team, info *db.TeamSeasonInfo) {
<div class="flex items-center gap-1.5">
<span class="text-xs text-subtext0 uppercase font-medium">Position</span>
<span class="text-2xl font-bold text-text">
{ fmt.Sprint(info.Position) }
{ ordinal(info.Position) }
</span>
<span class="text-sm text-subtext0">
/ { fmt.Sprint(info.TotalTeams) }
@@ -84,3 +84,20 @@ templ teamSeasonCard(team *db.Team, info *db.TeamSeasonInfo) {
</div>
</a>
}
func ordinal(n int) string {
suffix := "th"
if n%100 >= 11 && n%100 <= 13 {
// 11th, 12th, 13th
} else {
switch n % 10 {
case 1:
suffix = "st"
case 2:
suffix = "nd"
case 3:
suffix = "rd"
}
}
return fmt.Sprintf("%d%s", n, suffix)
}