436 lines
14 KiB
Plaintext
436 lines
14 KiB
Plaintext
package seasonsview
|
|
|
|
import "git.haelnorr.com/h/oslstats/internal/db"
|
|
import "git.haelnorr.com/h/oslstats/internal/view/component/links"
|
|
import "fmt"
|
|
import "sort"
|
|
|
|
// fixtureMatchPreviewTab renders the full Match Preview tab content.
|
|
// Shows team standings comparison, recent form, and full rosters side-by-side.
|
|
templ fixtureMatchPreviewTab(
|
|
fixture *db.Fixture,
|
|
rosters map[string][]*db.PlayerWithPlayStatus,
|
|
preview *db.MatchPreviewData,
|
|
) {
|
|
<div class="space-y-6">
|
|
<!-- Team Comparison Header -->
|
|
@matchPreviewHeader(fixture, preview)
|
|
|
|
<!-- Form Guide (Last 5 Games) -->
|
|
@matchPreviewFormGuide(fixture, preview)
|
|
|
|
<!-- Team Rosters -->
|
|
@matchPreviewRosters(fixture, rosters)
|
|
</div>
|
|
}
|
|
|
|
// matchPreviewHeader renders the broadcast-style team comparison with standings.
|
|
templ matchPreviewHeader(fixture *db.Fixture, preview *db.MatchPreviewData) {
|
|
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
|
<div class="bg-surface0 border-b border-surface1 px-6 py-3">
|
|
<h2 class="text-lg font-bold text-text">Team Comparison</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
<!-- Team Names and VS -->
|
|
<div class="flex items-center justify-center gap-4 sm:gap-8 mb-8">
|
|
<!-- Home Team -->
|
|
<div class="flex flex-col items-center text-center flex-1">
|
|
if fixture.HomeTeam.Color != "" {
|
|
<div
|
|
class="w-14 h-14 rounded-full border-2 border-surface1 mb-3 shrink-0"
|
|
style={ "background-color: " + templ.SafeCSS(fixture.HomeTeam.Color) }
|
|
></div>
|
|
}
|
|
<h3 class="text-xl sm:text-2xl font-bold text-text">
|
|
@links.TeamNameLinkInSeason(fixture.HomeTeam, fixture.Season, fixture.League)
|
|
</h3>
|
|
<span class="text-subtext0 text-sm font-mono mt-1">{ fixture.HomeTeam.ShortName }</span>
|
|
</div>
|
|
<!-- VS Divider -->
|
|
<div class="flex flex-col items-center shrink-0">
|
|
<span class="text-3xl sm:text-4xl font-bold text-subtext0">VS</span>
|
|
</div>
|
|
<!-- Away Team -->
|
|
<div class="flex flex-col items-center text-center flex-1">
|
|
if fixture.AwayTeam.Color != "" {
|
|
<div
|
|
class="w-14 h-14 rounded-full border-2 border-surface1 mb-3 shrink-0"
|
|
style={ "background-color: " + templ.SafeCSS(fixture.AwayTeam.Color) }
|
|
></div>
|
|
}
|
|
<h3 class="text-xl sm:text-2xl font-bold text-text">
|
|
@links.TeamNameLinkInSeason(fixture.AwayTeam, fixture.Season, fixture.League)
|
|
</h3>
|
|
<span class="text-subtext0 text-sm font-mono mt-1">{ fixture.AwayTeam.ShortName }</span>
|
|
</div>
|
|
</div>
|
|
<!-- Stats Comparison Grid -->
|
|
{{
|
|
homePos := ordinal(preview.HomePosition)
|
|
awayPos := ordinal(preview.AwayPosition)
|
|
if preview.HomePosition == 0 {
|
|
homePos = "N/A"
|
|
}
|
|
if preview.AwayPosition == 0 {
|
|
awayPos = "N/A"
|
|
}
|
|
}}
|
|
<div class="space-y-0">
|
|
<!-- Position -->
|
|
@previewStatRow(
|
|
homePos,
|
|
"Position",
|
|
awayPos,
|
|
preview.HomePosition > 0 && preview.HomePosition < preview.AwayPosition,
|
|
preview.AwayPosition > 0 && preview.AwayPosition < preview.HomePosition,
|
|
)
|
|
<!-- Points -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.Points),
|
|
"Points",
|
|
fmt.Sprint(preview.AwayRecord.Points),
|
|
preview.HomeRecord.Points > preview.AwayRecord.Points,
|
|
preview.AwayRecord.Points > preview.HomeRecord.Points,
|
|
)
|
|
<!-- Played -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.Played),
|
|
"Played",
|
|
fmt.Sprint(preview.AwayRecord.Played),
|
|
false,
|
|
false,
|
|
)
|
|
<!-- Wins -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.Wins),
|
|
"Wins",
|
|
fmt.Sprint(preview.AwayRecord.Wins),
|
|
preview.HomeRecord.Wins > preview.AwayRecord.Wins,
|
|
preview.AwayRecord.Wins > preview.HomeRecord.Wins,
|
|
)
|
|
<!-- OT Wins -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.OvertimeWins),
|
|
"OT Wins",
|
|
fmt.Sprint(preview.AwayRecord.OvertimeWins),
|
|
preview.HomeRecord.OvertimeWins > preview.AwayRecord.OvertimeWins,
|
|
preview.AwayRecord.OvertimeWins > preview.HomeRecord.OvertimeWins,
|
|
)
|
|
<!-- OT Losses -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.OvertimeLosses),
|
|
"OT Losses",
|
|
fmt.Sprint(preview.AwayRecord.OvertimeLosses),
|
|
preview.HomeRecord.OvertimeLosses < preview.AwayRecord.OvertimeLosses,
|
|
preview.AwayRecord.OvertimeLosses < preview.HomeRecord.OvertimeLosses,
|
|
)
|
|
<!-- Losses -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.Losses),
|
|
"Losses",
|
|
fmt.Sprint(preview.AwayRecord.Losses),
|
|
preview.HomeRecord.Losses < preview.AwayRecord.Losses,
|
|
preview.AwayRecord.Losses < preview.HomeRecord.Losses,
|
|
)
|
|
<!-- Goals For -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.GoalsFor),
|
|
"Goals For",
|
|
fmt.Sprint(preview.AwayRecord.GoalsFor),
|
|
preview.HomeRecord.GoalsFor > preview.AwayRecord.GoalsFor,
|
|
preview.AwayRecord.GoalsFor > preview.HomeRecord.GoalsFor,
|
|
)
|
|
<!-- Goals Against -->
|
|
@previewStatRow(
|
|
fmt.Sprint(preview.HomeRecord.GoalsAgainst),
|
|
"Goals Against",
|
|
fmt.Sprint(preview.AwayRecord.GoalsAgainst),
|
|
preview.HomeRecord.GoalsAgainst < preview.AwayRecord.GoalsAgainst,
|
|
preview.AwayRecord.GoalsAgainst < preview.HomeRecord.GoalsAgainst,
|
|
)
|
|
<!-- Goal Difference -->
|
|
{{
|
|
homeDiff := preview.HomeRecord.GoalsFor - preview.HomeRecord.GoalsAgainst
|
|
awayDiff := preview.AwayRecord.GoalsFor - preview.AwayRecord.GoalsAgainst
|
|
homeDiffStr := fmt.Sprintf("%+d", homeDiff)
|
|
awayDiffStr := fmt.Sprintf("%+d", awayDiff)
|
|
}}
|
|
@previewStatRow(
|
|
homeDiffStr,
|
|
"Goal Diff",
|
|
awayDiffStr,
|
|
homeDiff > awayDiff,
|
|
awayDiff > homeDiff,
|
|
)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// previewStatRow renders a single comparison stat row in the broadcast-style layout.
|
|
// The stat label is centered, with home value on the left and away value on the right.
|
|
// homeHighlight/awayHighlight indicate which side has the better value.
|
|
templ previewStatRow(homeValue, label, awayValue string, homeHighlight, awayHighlight bool) {
|
|
<div class="flex items-center py-2.5 border-b border-surface1 last:border-b-0">
|
|
<!-- Home Value -->
|
|
<div class="flex-1 text-right pr-4">
|
|
<span class={ "text-lg font-bold", templ.KV("text-green", homeHighlight), templ.KV("text-text", !homeHighlight) }>
|
|
{ homeValue }
|
|
</span>
|
|
</div>
|
|
<!-- Label -->
|
|
<div class="w-28 sm:w-36 text-center shrink-0">
|
|
<span class="text-xs font-semibold text-subtext0 uppercase tracking-wider">{ label }</span>
|
|
</div>
|
|
<!-- Away Value -->
|
|
<div class="flex-1 text-left pl-4">
|
|
<span class={ "text-lg font-bold", templ.KV("text-green", awayHighlight), templ.KV("text-text", !awayHighlight) }>
|
|
{ awayValue }
|
|
</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// matchPreviewFormGuide renders the recent form section with last 5 game outcome icons.
|
|
templ matchPreviewFormGuide(fixture *db.Fixture, preview *db.MatchPreviewData) {
|
|
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
|
<div class="bg-surface0 border-b border-surface1 px-6 py-3">
|
|
<h2 class="text-lg font-bold text-text">Recent Form</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<!-- Home Team Form -->
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-4">
|
|
if fixture.HomeTeam.Color != "" {
|
|
<span
|
|
class="w-3 h-3 rounded-full shrink-0"
|
|
style={ "background-color: " + templ.SafeCSS(fixture.HomeTeam.Color) }
|
|
></span>
|
|
}
|
|
<h3 class="text-md font-bold text-text">{ fixture.HomeTeam.Name }</h3>
|
|
</div>
|
|
if len(preview.HomeRecentGames) == 0 {
|
|
<p class="text-subtext1 text-sm">No recent matches played</p>
|
|
} else {
|
|
<!-- Outcome Icons: chronological (oldest → newest, left → right) -->
|
|
<div class="flex items-center gap-1.5 mb-4">
|
|
for _, g := range preview.HomeRecentGames {
|
|
@gameOutcomeIcon(g)
|
|
}
|
|
</div>
|
|
<!-- Recent Results List: most recent first -->
|
|
<div class="space-y-1.5">
|
|
for i := len(preview.HomeRecentGames) - 1; i >= 0; i-- {
|
|
@recentGameRow(preview.HomeRecentGames[i])
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
<!-- Away Team Form -->
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-4">
|
|
if fixture.AwayTeam.Color != "" {
|
|
<span
|
|
class="w-3 h-3 rounded-full shrink-0"
|
|
style={ "background-color: " + templ.SafeCSS(fixture.AwayTeam.Color) }
|
|
></span>
|
|
}
|
|
<h3 class="text-md font-bold text-text">{ fixture.AwayTeam.Name }</h3>
|
|
</div>
|
|
if len(preview.AwayRecentGames) == 0 {
|
|
<p class="text-subtext1 text-sm">No recent matches played</p>
|
|
} else {
|
|
<!-- Outcome Icons: chronological (oldest → newest, left → right) -->
|
|
<div class="flex items-center gap-1.5 mb-4">
|
|
for _, g := range preview.AwayRecentGames {
|
|
@gameOutcomeIcon(g)
|
|
}
|
|
</div>
|
|
<!-- Recent Results List: most recent first -->
|
|
<div class="space-y-1.5">
|
|
for i := len(preview.AwayRecentGames) - 1; i >= 0; i-- {
|
|
@recentGameRow(preview.AwayRecentGames[i])
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// outcomeStyle holds the styling info for a game outcome type.
|
|
type outcomeStyle struct {
|
|
iconBg string // Background class for the icon badge
|
|
rowBg string // Background class for the row
|
|
text string // Text color class
|
|
label string // Short label (W, L, OW, OL, D, F)
|
|
fullLabel string // Full label for row display (W, OTW, OTL, L, D, FF)
|
|
desc string // Human-readable description (Win, Loss, etc.)
|
|
}
|
|
|
|
func getOutcomeStyle(outcomeType string) outcomeStyle {
|
|
switch outcomeType {
|
|
case "W":
|
|
return outcomeStyle{"bg-green/20", "bg-green/10", "text-green", "W", "W", "Win"}
|
|
case "OTW":
|
|
return outcomeStyle{"bg-yellow/20", "bg-yellow/10", "text-yellow", "OW", "OTW", "OT Win"}
|
|
case "OTL":
|
|
return outcomeStyle{"bg-peach/20", "bg-peach/10", "text-peach", "OL", "OTL", "OT Loss"}
|
|
case "L":
|
|
return outcomeStyle{"bg-red/20", "bg-red/10", "text-red", "L", "L", "Loss"}
|
|
case "D":
|
|
return outcomeStyle{"bg-overlay0/20", "bg-overlay0/10", "text-overlay0", "D", "D", "Draw"}
|
|
case "F":
|
|
return outcomeStyle{"bg-red/30", "bg-red/15", "text-red", "F", "FF", "Forfeit"}
|
|
default:
|
|
return outcomeStyle{"bg-surface1", "bg-surface0", "text-subtext0", "?", "?", "Unknown"}
|
|
}
|
|
}
|
|
|
|
// gameOutcomeIcon renders a single game outcome as a colored badge.
|
|
templ gameOutcomeIcon(outcome *db.GameOutcome) {
|
|
{{
|
|
style := getOutcomeStyle(outcome.Type)
|
|
tooltip := ""
|
|
if outcome.Opponent != nil {
|
|
tooltip = fmt.Sprintf("%s vs %s", style.desc, outcome.Opponent.Name)
|
|
if outcome.IsForfeit {
|
|
tooltip += " (Forfeit)"
|
|
} else if outcome.Score != "" {
|
|
tooltip += fmt.Sprintf(" (%s)", outcome.Score)
|
|
}
|
|
}
|
|
}}
|
|
<span
|
|
class={ "inline-flex items-center justify-center w-9 h-9 rounded-md text-xs font-bold cursor-default", style.iconBg, style.text }
|
|
title={ tooltip }
|
|
>
|
|
{ style.label }
|
|
</span>
|
|
}
|
|
|
|
// recentGameRow renders a single recent game result as a compact row.
|
|
templ recentGameRow(outcome *db.GameOutcome) {
|
|
{{
|
|
style := getOutcomeStyle(outcome.Type)
|
|
opponentName := "Unknown"
|
|
if outcome.Opponent != nil {
|
|
opponentName = outcome.Opponent.Name
|
|
}
|
|
}}
|
|
<div class={ "flex items-center gap-3 px-3 py-2 rounded-lg", style.rowBg }>
|
|
<span class={ "text-xs font-bold w-8 text-center", style.text }>{ style.fullLabel }</span>
|
|
<span class="text-sm text-text">vs { opponentName }</span>
|
|
if outcome.IsForfeit {
|
|
<span class="text-xs text-red ml-auto font-medium">Forfeit</span>
|
|
} else if outcome.Score != "" {
|
|
<span class="text-sm text-subtext0 ml-auto font-mono">{ outcome.Score }</span>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// matchPreviewRosters renders team rosters side-by-side for the match preview.
|
|
templ matchPreviewRosters(
|
|
fixture *db.Fixture,
|
|
rosters map[string][]*db.PlayerWithPlayStatus,
|
|
) {
|
|
{{
|
|
homePlayers := rosters["home"]
|
|
awayPlayers := rosters["away"]
|
|
}}
|
|
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
|
<div class="bg-surface0 border-b border-surface1 px-6 py-3">
|
|
<h2 class="text-lg font-bold text-text">Team Rosters</h2>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- Home Team Roster -->
|
|
@previewRosterColumn(fixture.HomeTeam, homePlayers, fixture.Season, fixture.League)
|
|
<!-- Away Team Roster -->
|
|
@previewRosterColumn(fixture.AwayTeam, awayPlayers, fixture.Season, fixture.League)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// previewRosterColumn renders a single team's roster for the match preview.
|
|
templ previewRosterColumn(
|
|
team *db.Team,
|
|
players []*db.PlayerWithPlayStatus,
|
|
season *db.Season,
|
|
league *db.League,
|
|
) {
|
|
{{
|
|
// Separate managers and regular players
|
|
var managers []*db.PlayerWithPlayStatus
|
|
var roster []*db.PlayerWithPlayStatus
|
|
for _, p := range players {
|
|
if p.IsManager {
|
|
managers = append(managers, p)
|
|
} else {
|
|
roster = append(roster, p)
|
|
}
|
|
}
|
|
// Sort roster alphabetically by display name
|
|
sort.Slice(roster, func(i, j int) bool {
|
|
return roster[i].Player.DisplayName() < roster[j].Player.DisplayName()
|
|
})
|
|
}}
|
|
<div>
|
|
<!-- Team Header -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-2">
|
|
if team.Color != "" {
|
|
<span
|
|
class="w-3.5 h-3.5 rounded-full shrink-0 border border-surface1"
|
|
style={ "background-color: " + templ.SafeCSS(team.Color) }
|
|
></span>
|
|
}
|
|
<h3 class="text-md font-bold">
|
|
@links.TeamNameLinkInSeason(team, season, league)
|
|
</h3>
|
|
</div>
|
|
<span class="text-xs text-subtext0">
|
|
{ fmt.Sprint(len(players)) } players
|
|
</span>
|
|
</div>
|
|
if len(players) == 0 {
|
|
<p class="text-subtext1 text-sm text-center py-4">No players on roster.</p>
|
|
} else {
|
|
<div class="space-y-1">
|
|
<!-- Manager(s) -->
|
|
for _, p := range managers {
|
|
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface0 border border-surface1">
|
|
<span class="px-1.5 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium shrink-0">
|
|
★
|
|
</span>
|
|
<span class="text-sm font-medium">
|
|
@links.PlayerLink(p.Player)
|
|
</span>
|
|
if p.IsFreeAgent {
|
|
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
|
|
FA
|
|
</span>
|
|
}
|
|
</div>
|
|
}
|
|
<!-- Regular Players -->
|
|
for _, p := range roster {
|
|
<div class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-surface0 transition">
|
|
<span class="text-sm">
|
|
@links.PlayerLink(p.Player)
|
|
</span>
|
|
if p.IsFreeAgent {
|
|
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
|
|
FA
|
|
</span>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|