updated team stats
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user