added stats to team page
This commit is contained in:
@@ -269,6 +269,102 @@ func GetFinalizedResultsForFixtures(ctx context.Context, tx bun.Tx, fixtureIDs [
|
|||||||
return resultMap, nil
|
return resultMap, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AggregatedPlayerStats holds summed stats for a player across multiple fixtures.
|
||||||
|
type AggregatedPlayerStats struct {
|
||||||
|
PlayerID int `bun:"player_id"`
|
||||||
|
PlayerName string `bun:"player_name"`
|
||||||
|
GamesPlayed int `bun:"games_played"`
|
||||||
|
Score int `bun:"total_score"`
|
||||||
|
Goals int `bun:"total_goals"`
|
||||||
|
Assists int `bun:"total_assists"`
|
||||||
|
Saves int `bun:"total_saves"`
|
||||||
|
Shots int `bun:"total_shots"`
|
||||||
|
Blocks int `bun:"total_blocks"`
|
||||||
|
Passes int `bun:"total_passes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAggregatedPlayerStatsForTeam returns aggregated period-3 stats for all mapped
|
||||||
|
// players on a given team across all finalized fixture results.
|
||||||
|
func GetAggregatedPlayerStatsForTeam(
|
||||||
|
ctx context.Context,
|
||||||
|
tx bun.Tx,
|
||||||
|
teamID int,
|
||||||
|
fixtureIDs []int,
|
||||||
|
) ([]*AggregatedPlayerStats, error) {
|
||||||
|
if len(fixtureIDs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats []*AggregatedPlayerStats
|
||||||
|
err := tx.NewRaw(`
|
||||||
|
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.score), 0) AS total_score,
|
||||||
|
COALESCE(SUM(frps.goals), 0) AS total_goals,
|
||||||
|
COALESCE(SUM(frps.assists), 0) AS total_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
|
||||||
|
FROM fixture_result_player_stats frps
|
||||||
|
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
|
||||||
|
LEFT JOIN players p ON p.id = frps.player_id
|
||||||
|
WHERE fr.finalized = true
|
||||||
|
AND fr.fixture_id IN (?)
|
||||||
|
AND frps.team_id = ?
|
||||||
|
AND frps.period_num = 3
|
||||||
|
AND frps.player_id IS NOT NULL
|
||||||
|
GROUP BY frps.player_id, COALESCE(p.name, frps.player_username)
|
||||||
|
ORDER BY total_score DESC
|
||||||
|
`, bun.In(fixtureIDs), teamID).Scan(ctx, &stats)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "tx.NewRaw")
|
||||||
|
}
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamRecord holds win/loss/draw record and goal totals for a team.
|
||||||
|
type TeamRecord struct {
|
||||||
|
Played int
|
||||||
|
Wins int
|
||||||
|
Losses int
|
||||||
|
Draws int
|
||||||
|
GoalsFor int
|
||||||
|
GoalsAgainst int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeTeamRecord calculates W-L-D and GF/GA from fixtures and results.
|
||||||
|
func ComputeTeamRecord(teamID int, fixtures []*Fixture, resultMap map[int]*FixtureResult) *TeamRecord {
|
||||||
|
rec := &TeamRecord{}
|
||||||
|
for _, f := range fixtures {
|
||||||
|
res, ok := resultMap[f.ID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rec.Played++
|
||||||
|
isHome := f.HomeTeamID == teamID
|
||||||
|
if isHome {
|
||||||
|
rec.GoalsFor += res.HomeScore
|
||||||
|
rec.GoalsAgainst += res.AwayScore
|
||||||
|
} else {
|
||||||
|
rec.GoalsFor += res.AwayScore
|
||||||
|
rec.GoalsAgainst += res.HomeScore
|
||||||
|
}
|
||||||
|
won := (isHome && res.Winner == "home") || (!isHome && res.Winner == "away")
|
||||||
|
lost := (isHome && res.Winner == "away") || (!isHome && res.Winner == "home")
|
||||||
|
if won {
|
||||||
|
rec.Wins++
|
||||||
|
} else if lost {
|
||||||
|
rec.Losses++
|
||||||
|
} else {
|
||||||
|
rec.Draws++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
// GetFixtureTeamRosters returns all team players with participation status for a fixture.
|
// GetFixtureTeamRosters returns all team players with participation status for a fixture.
|
||||||
// Returns: map["home"|"away"] -> []*PlayerWithPlayStatus
|
// Returns: map["home"|"away"] -> []*PlayerWithPlayStatus
|
||||||
func GetFixtureTeamRosters(
|
func GetFixtureTeamRosters(
|
||||||
|
|||||||
@@ -793,6 +793,14 @@
|
|||||||
.gap-y-5 {
|
.gap-y-5 {
|
||||||
row-gap: calc(var(--spacing) * 5);
|
row-gap: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.divide-x {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-divide-x-reverse: 0;
|
||||||
|
border-inline-style: var(--tw-border-style);
|
||||||
|
border-inline-start-width: calc(1px * var(--tw-divide-x-reverse));
|
||||||
|
border-inline-end-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
.divide-y {
|
.divide-y {
|
||||||
:where(& > :not(:last-child)) {
|
:where(& > :not(:last-child)) {
|
||||||
--tw-divide-y-reverse: 0;
|
--tw-divide-y-reverse: 0;
|
||||||
@@ -1956,6 +1964,11 @@
|
|||||||
scale: var(--tw-scale-x) var(--tw-scale-y);
|
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 {
|
.sm\:flex-row {
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -2087,6 +2100,11 @@
|
|||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.lg\:grid-cols-6 {
|
||||||
|
@media (width >= 64rem) {
|
||||||
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
.lg\:items-end {
|
.lg\:items-end {
|
||||||
@media (width >= 64rem) {
|
@media (width >= 64rem) {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
@@ -2322,7 +2340,7 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0;
|
initial-value: 0;
|
||||||
}
|
}
|
||||||
@property --tw-divide-y-reverse {
|
@property --tw-divide-x-reverse {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0;
|
initial-value: 0;
|
||||||
@@ -2332,6 +2350,11 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: solid;
|
initial-value: solid;
|
||||||
}
|
}
|
||||||
|
@property --tw-divide-y-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
@property --tw-leading {
|
@property --tw-leading {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -2527,8 +2550,9 @@
|
|||||||
--tw-skew-y: initial;
|
--tw-skew-y: initial;
|
||||||
--tw-space-y-reverse: 0;
|
--tw-space-y-reverse: 0;
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
--tw-divide-y-reverse: 0;
|
--tw-divide-x-reverse: 0;
|
||||||
--tw-border-style: solid;
|
--tw-border-style: solid;
|
||||||
|
--tw-divide-y-reverse: 0;
|
||||||
--tw-leading: initial;
|
--tw-leading: initial;
|
||||||
--tw-font-weight: initial;
|
--tw-font-weight: initial;
|
||||||
--tw-tracking: initial;
|
--tw-tracking: initial;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ func SeasonLeagueTeamDetailPage(
|
|||||||
var fixtures []*db.Fixture
|
var fixtures []*db.Fixture
|
||||||
var available []*db.Player
|
var available []*db.Player
|
||||||
var scheduleMap map[int]*db.FixtureSchedule
|
var scheduleMap map[int]*db.FixtureSchedule
|
||||||
|
var resultMap map[int]*db.FixtureResult
|
||||||
|
var playerStats []*db.AggregatedPlayerStats
|
||||||
|
|
||||||
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
|
||||||
@@ -56,6 +58,14 @@ func SeasonLeagueTeamDetailPage(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.Wrap(err, "db.GetAcceptedSchedulesForFixtures")
|
return false, errors.Wrap(err, "db.GetAcceptedSchedulesForFixtures")
|
||||||
}
|
}
|
||||||
|
resultMap, err = db.GetFinalizedResultsForFixtures(ctx, tx, fixtureIDs)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "db.GetFinalizedResultsForFixtures")
|
||||||
|
}
|
||||||
|
playerStats, err = db.GetAggregatedPlayerStatsForTeam(ctx, tx, teamID, fixtureIDs)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "db.GetAggregatedPlayerStatsForTeam")
|
||||||
|
}
|
||||||
|
|
||||||
available, err = db.GetPlayersNotOnTeam(ctx, tx, twr.Season.ID, twr.League.ID)
|
available, err = db.GetPlayersNotOnTeam(ctx, tx, twr.Season.ID, twr.League.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,6 +77,7 @@ func SeasonLeagueTeamDetailPage(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSafely(seasonsview.SeasonLeagueTeamDetailPage(twr, fixtures, available, scheduleMap), s, r, w)
|
record := db.ComputeTeamRecord(teamID, fixtures, resultMap)
|
||||||
|
renderSafely(seasonsview.SeasonLeagueTeamDetailPage(twr, fixtures, available, scheduleMap, resultMap, record, playerStats), s, r, w)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import "git.haelnorr.com/h/oslstats/internal/permissions"
|
|||||||
import "git.haelnorr.com/h/oslstats/internal/contexts"
|
import "git.haelnorr.com/h/oslstats/internal/contexts"
|
||||||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
import "sort"
|
||||||
|
import "time"
|
||||||
|
|
||||||
templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture, available []*db.Player, scheduleMap map[int]*db.FixtureSchedule) {
|
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) {
|
||||||
{{
|
{{
|
||||||
team := twr.Team
|
team := twr.Team
|
||||||
season := twr.Season
|
season := twr.Season
|
||||||
@@ -54,11 +56,11 @@ templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture,
|
|||||||
<!-- Top row: Roster (left) + Fixtures (right) -->
|
<!-- Top row: Roster (left) + Fixtures (right) -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
@TeamRosterSection(twr, available)
|
@TeamRosterSection(twr, available)
|
||||||
@teamFixturesPane(twr.Team, fixtures, scheduleMap)
|
@teamFixturesPane(twr.Team, fixtures, scheduleMap, resultMap)
|
||||||
</div>
|
</div>
|
||||||
<!-- Stats below both -->
|
<!-- Stats below both -->
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
@teamStatsSection()
|
@teamStatsSection(record, playerStats)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -394,31 +396,68 @@ templ manageRosterModal(twr *db.TeamWithRoster, available []*db.Player, rosterPl
|
|||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ teamFixturesPane(team *db.Team, fixtures []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule) {
|
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">
|
<section class="space-y-6">
|
||||||
|
<!-- Results -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-text mb-4">Results</h2>
|
||||||
|
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>
|
||||||
|
<p class="text-subtext1 text-sm mt-2">Match results will appear here once games are played.</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden divide-y divide-surface1">
|
||||||
|
for _, fixture := range recentResults {
|
||||||
|
@teamResultRow(team, fixture, resultMap)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<!-- Upcoming -->
|
<!-- Upcoming -->
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold text-text mb-4">Upcoming</h2>
|
<h2 class="text-2xl font-bold text-text mb-4">Upcoming</h2>
|
||||||
if len(fixtures) == 0 {
|
if len(upcoming) == 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 upcoming fixtures.</p>
|
<p class="text-subtext0 text-lg">No upcoming fixtures.</p>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden divide-y divide-surface1">
|
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden divide-y divide-surface1">
|
||||||
for _, fixture := range fixtures {
|
for _, fixture := range upcoming {
|
||||||
@teamFixtureRow(team, fixture, scheduleMap)
|
@teamFixtureRow(team, fixture, scheduleMap)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<!-- Results -->
|
|
||||||
<div>
|
|
||||||
<h2 class="text-2xl font-bold text-text mb-4">Results</h2>
|
|
||||||
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
|
|
||||||
<p class="text-subtext0 text-lg">Results coming soon.</p>
|
|
||||||
<p class="text-subtext1 text-sm mt-2">Match results will appear here once game data is recorded.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,14 +507,135 @@ templ teamFixtureRow(team *db.Team, fixture *db.Fixture, scheduleMap map[int]*db
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ teamStatsSection() {
|
templ teamResultRow(team *db.Team, fixture *db.Fixture, resultMap map[int]*db.FixtureResult) {
|
||||||
|
{{
|
||||||
|
isHome := fixture.HomeTeamID == team.ID
|
||||||
|
var opponent string
|
||||||
|
if isHome {
|
||||||
|
opponent = fixture.AwayTeam.Name
|
||||||
|
} else {
|
||||||
|
opponent = fixture.HomeTeam.Name
|
||||||
|
}
|
||||||
|
res := resultMap[fixture.ID]
|
||||||
|
won := (isHome && res.Winner == "home") || (!isHome && res.Winner == "away")
|
||||||
|
lost := (isHome && res.Winner == "away") || (!isHome && res.Winner == "home")
|
||||||
|
_ = lost
|
||||||
|
}}
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d", fixture.ID)) }
|
||||||
|
class="px-4 py-3 flex items-center justify-between gap-3 hover:bg-surface1 transition hover:cursor-pointer block"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
if won {
|
||||||
|
<span class="text-xs font-bold px-2 py-0.5 bg-green/20 text-green rounded shrink-0">W</span>
|
||||||
|
} else if lost {
|
||||||
|
<span class="text-xs font-bold px-2 py-0.5 bg-red/20 text-red rounded shrink-0">L</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-xs font-bold px-2 py-0.5 bg-surface1 text-subtext0 rounded shrink-0">D</span>
|
||||||
|
}
|
||||||
|
<span class="text-xs font-mono text-subtext0 bg-mantle px-2 py-0.5 rounded shrink-0">
|
||||||
|
GW{ fmt.Sprint(*fixture.GameWeek) }
|
||||||
|
</span>
|
||||||
|
if isHome {
|
||||||
|
<span class="text-xs px-2 py-0.5 bg-blue/20 text-blue rounded font-medium shrink-0">
|
||||||
|
HOME
|
||||||
|
</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-xs px-2 py-0.5 bg-surface1 text-subtext0 rounded font-medium shrink-0">
|
||||||
|
AWAY
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<span class="text-sm text-subtext0 shrink-0">vs</span>
|
||||||
|
<span class="text-text font-medium truncate">
|
||||||
|
{ opponent }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="flex items-center gap-2 shrink-0">
|
||||||
|
if res.Winner == "home" {
|
||||||
|
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm text-subtext0">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
} else if res.Winner == "away" {
|
||||||
|
<span class="text-sm text-subtext0">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
} else {
|
||||||
|
<span class="text-sm text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||||||
|
<span class="text-xs text-subtext0">–</span>
|
||||||
|
<span class="text-sm text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ teamStatsSection(record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats) {
|
||||||
<section>
|
<section>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="text-2xl font-bold text-text">Stats</h2>
|
<h2 class="text-2xl font-bold text-text">Stats</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
|
if record.Played == 0 {
|
||||||
<p class="text-subtext0 text-lg">Stats coming soon.</p>
|
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
|
||||||
<p class="text-subtext1 text-sm mt-2">Team statistics will appear here once game data is available.</p>
|
<p class="text-subtext0 text-lg">No stats yet.</p>
|
||||||
</div>
|
<p class="text-subtext1 text-sm mt-2">Team statistics will appear here once games are played.</p>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<!-- Team Record Summary -->
|
||||||
|
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden mb-4">
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 divide-x divide-surface1">
|
||||||
|
@statCell("Played", fmt.Sprint(record.Played), "")
|
||||||
|
@statCell("Record", fmt.Sprintf("%d-%d-%d", record.Wins, record.Losses, record.Draws), "")
|
||||||
|
@statCell("Wins", fmt.Sprint(record.Wins), "text-green")
|
||||||
|
@statCell("Losses", fmt.Sprint(record.Losses), "text-red")
|
||||||
|
@statCell("GF", fmt.Sprint(record.GoalsFor), "")
|
||||||
|
@statCell("GA", fmt.Sprint(record.GoalsAgainst), "")
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Player Stats Leaderboard -->
|
||||||
|
if len(playerStats) > 0 {
|
||||||
|
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
|
||||||
|
<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-2 py-2 text-center text-xs font-semibold text-text" title="Games Played">GP</th>
|
||||||
|
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Score">SC</th>
|
||||||
|
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Goals">G</th>
|
||||||
|
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Assists">A</th>
|
||||||
|
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Saves">SV</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="Blocks">BL</th>
|
||||||
|
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Passes">PA</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-surface1">
|
||||||
|
for _, ps := range playerStats {
|
||||||
|
<tr class="hover:bg-surface1 transition-colors">
|
||||||
|
<td class="px-3 py-2 text-sm text-text">{ ps.PlayerName }</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 font-medium text-text">{ 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-text">{ fmt.Sprint(ps.Saves) }</td>
|
||||||
|
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Shots) }</td>
|
||||||
|
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Blocks) }</td>
|
||||||
|
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Passes) }</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ statCell(label string, value string, valueColor string) {
|
||||||
|
<div class="px-4 py-3 text-center">
|
||||||
|
<p class="text-xs text-subtext0 font-medium uppercase mb-1">{ label }</p>
|
||||||
|
<p class={ "text-lg font-bold", templ.KV("text-text", valueColor == ""), templ.KV(valueColor, valueColor != "") }>
|
||||||
|
{ value }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user