package seasonsview
import "git.haelnorr.com/h/oslstats/internal/db"
import "git.haelnorr.com/h/oslstats/internal/permissions"
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/component/links"
import "fmt"
import "sort"
import "strings"
// seriesTeamName returns a display name for a team in the series, or "TBD" if nil
func seriesTeamName(team *db.Team) string {
if team == nil {
return "TBD"
}
return team.Name
}
// seriesTeamShortName returns a short name for a team in the series, or "TBD" if nil
func seriesTeamShortName(team *db.Team) string {
if team == nil {
return "TBD"
}
return team.ShortName
}
// roundDisplayName converts a round slug to a human-readable name
func roundDisplayName(round string) string {
switch round {
case "upper_bracket":
return "Upper Bracket"
case "lower_bracket":
return "Lower Bracket"
case "upper_final":
return "Upper Final"
case "lower_final":
return "Lower Final"
case "quarter_final":
return "Quarter Final"
case "semi_final":
return "Semi Final"
case "elimination_final":
return "Elimination Final"
case "qualifying_final":
return "Qualifying Final"
case "preliminary_final":
return "Preliminary Final"
case "third_place":
return "Third Place Playoff"
case "grand_final":
return "Grand Final"
default:
return strings.ReplaceAll(round, "_", " ")
}
}
// SeriesDetailLayout renders the series detail page layout with header and
// tab navigation. Tab content is rendered as children.
templ SeriesDetailLayout(activeTab string, series *db.PlayoffSeries, schedule *db.PlayoffSeriesSchedule) {
{{
backURL := fmt.Sprintf("/seasons/%s/leagues/%s/finals",
series.Bracket.Season.ShortName, series.Bracket.League.ShortName)
isCompleted := series.Status == db.SeriesStatusCompleted
team1Name := seriesTeamName(series.Team1)
team2Name := seriesTeamName(series.Team2)
boLabel := fmt.Sprintf("BO%d", series.MatchesToWin*2-1)
}}
@baseview.Layout(fmt.Sprintf("%s — %s vs %s", series.Label, team1Name, team2Name)) {
{ team1Name }
vs
{ team2Name }
{ series.Label }
{ boLabel }
if series.Team1Seed != nil || series.Team2Seed != nil {
if series.Team1Seed != nil && series.Team2Seed != nil {
Seed { fmt.Sprint(*series.Team1Seed) } vs { fmt.Sprint(*series.Team2Seed) }
} else if series.Team1Seed != nil {
Seed { fmt.Sprint(*series.Team1Seed) }
} else if series.Team2Seed != nil {
Seed { fmt.Sprint(*series.Team2Seed) }
}
}
{ series.Bracket.Season.Name } — { series.Bracket.League.Name }
Back to Bracket
@seriesTabItem("overview", "Overview", activeTab, series)
if isCompleted {
@seriesTabItem("analysis", "Match Analysis", activeTab, series)
} else {
@seriesTabItem("preview", "Match Preview", activeTab, series)
@seriesTabItem("scheduling", "Schedule", activeTab, series)
}
{ children... }
}
}
templ seriesTabItem(section string, label string, activeTab string, series *db.PlayoffSeries) {
{{
isActive := section == activeTab
baseClasses := "inline-block px-6 py-3 transition-colors cursor-pointer border-b-2"
activeClasses := "border-blue text-blue font-semibold"
inactiveClasses := "border-transparent text-subtext0 hover:text-text hover:border-surface2"
url := fmt.Sprintf("/series/%d/%s", series.ID, section)
}}
{ label }
}
// ==================== Full page wrappers (for GET requests / direct navigation) ====================
templ SeriesDetailOverviewPage(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
canSchedule bool,
userTeamID int,
rosters map[string][]*db.PlayerWithPlayStatus,
) {
@SeriesDetailLayout("overview", series, currentSchedule) {
@SeriesDetailOverviewContent(series, currentSchedule, canSchedule, userTeamID, rosters)
}
}
templ SeriesDetailPreviewPage(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
rosters map[string][]*db.PlayerWithPlayStatus,
previewData *db.MatchPreviewData,
) {
@SeriesDetailLayout("preview", series, currentSchedule) {
@SeriesDetailPreviewContent(series, rosters, previewData)
}
}
templ SeriesDetailAnalysisPage(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
rosters map[string][]*db.PlayerWithPlayStatus,
previewData *db.MatchPreviewData,
) {
@SeriesDetailLayout("analysis", series, currentSchedule) {
@SeriesDetailAnalysisContent(series, rosters, previewData)
}
}
templ SeriesDetailSchedulePage(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
history []*db.PlayoffSeriesSchedule,
canSchedule bool,
userTeamID int,
) {
@SeriesDetailLayout("scheduling", series, currentSchedule) {
@SeriesDetailScheduleContent(series, currentSchedule, history, canSchedule, userTeamID)
}
}
// ==================== Tab content components (for POST requests / HTMX swaps) ====================
templ SeriesDetailOverviewContent(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
canSchedule bool,
userTeamID int,
rosters map[string][]*db.PlayerWithPlayStatus,
) {
{{
permCache := contexts.Permissions(ctx)
canManage := permCache.HasPermission(permissions.PlayoffsManage)
}}
@seriesOverviewTab(series, currentSchedule, rosters, canSchedule, canManage, userTeamID)
}
templ SeriesDetailPreviewContent(
series *db.PlayoffSeries,
rosters map[string][]*db.PlayerWithPlayStatus,
previewData *db.MatchPreviewData,
) {
@seriesMatchPreviewTab(series, rosters, previewData)
}
templ SeriesDetailAnalysisContent(
series *db.PlayoffSeries,
rosters map[string][]*db.PlayerWithPlayStatus,
previewData *db.MatchPreviewData,
) {
@seriesMatchAnalysisTab(series, rosters, previewData)
}
templ SeriesDetailScheduleContent(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
history []*db.PlayoffSeriesSchedule,
canSchedule bool,
userTeamID int,
) {
{{
permCache := contexts.Permissions(ctx)
canManage := permCache.HasPermission(permissions.PlayoffsManage)
}}
@seriesScheduleTab(series, currentSchedule, history, canSchedule, canManage, userTeamID)
}
// ==================== Overview Tab ====================
templ seriesOverviewTab(
series *db.PlayoffSeries,
currentSchedule *db.PlayoffSeriesSchedule,
rosters map[string][]*db.PlayerWithPlayStatus,
canSchedule bool,
canManage bool,
userTeamID int,
) {
{{
isCompleted := series.Status == db.SeriesStatusCompleted
isBye := series.Status == db.SeriesStatusBye
bothTeamsAssigned := series.Team1ID != nil && series.Team2ID != nil
showUploadPrompt := canManage && !isCompleted && !isBye && bothTeamsAssigned
}}
@seriesScoreDisplay(series)
@seriesScheduleSummary(series, currentSchedule)
if showUploadPrompt {
@seriesUploadPrompt(series)
}
if len(series.Matches) > 0 {
@seriesMatchList(series)
}
@seriesContextCard(series)
if series.Team1 != nil {
@seriesTeamSection(series.Team1, rosters["team1"], series.Bracket.Season, series.Bracket.League)
}
if series.Team2 != nil {
@seriesTeamSection(series.Team2, rosters["team2"], series.Bracket.Season, series.Bracket.League)
}
}
templ seriesUploadPrompt(series *db.PlayoffSeries) {
{{
// Check if there are pending results waiting for review
hasPendingMatches := false
for _, match := range series.Matches {
if match.FixtureID != nil && match.Status == "pending" {
hasPendingMatches = true
break
}
}
}}
if hasPendingMatches {
📋
Results Pending Review
Uploaded results are waiting to be reviewed and finalized.
Review Results
} else {
📋
No Results Uploaded
Upload match log files to record the series results.
Upload Match Logs
}
}
templ seriesScoreDisplay(series *db.PlayoffSeries) {
{{
isCompleted := series.Status == db.SeriesStatusCompleted
team1Won := series.WinnerTeamID != nil && series.Team1ID != nil && *series.WinnerTeamID == *series.Team1ID
team2Won := series.WinnerTeamID != nil && series.Team2ID != nil && *series.WinnerTeamID == *series.Team2ID
isBye := series.Status == db.SeriesStatusBye
}}
Series Score
@seriesStatusBadge(series.Status)
@seriesFormatBadge(series.MatchesToWin)
if isBye {
Bye — team advances automatically
} else if series.Team1 == nil && series.Team2 == nil {
} else {
if team1Won {
🏆
}
if series.Team1 != nil && series.Team1.Color != "" {
{ seriesTeamShortName(series.Team1) }
} else {
{ seriesTeamShortName(series.Team1) }
}
{ fmt.Sprint(series.Team1Wins) }
–
if isCompleted {
FINAL
}
{ fmt.Sprint(series.Team2Wins) }
if series.Team2 != nil && series.Team2.Color != "" {
{ seriesTeamShortName(series.Team2) }
} else {
{ seriesTeamShortName(series.Team2) }
}
if team2Won {
🏆
}
}
}
templ seriesScheduleSummary(series *db.PlayoffSeries, schedule *db.PlayoffSeriesSchedule) {
{{
isCompleted := series.Status == db.SeriesStatusCompleted
}}
Schedule
if schedule == nil {
} else if schedule.Status == db.ScheduleStatusAccepted && schedule.ScheduledTime != nil {
if isCompleted {
Played
} else {
Confirmed
}
@localtime(schedule.ScheduledTime, "date")
@localtime(schedule.ScheduledTime, "time")
} else if schedule.Status == db.ScheduleStatusPending && schedule.ScheduledTime != nil {
Proposed
@localtime(schedule.ScheduledTime, "date")
@localtime(schedule.ScheduledTime, "time")
Awaiting confirmation
} else if schedule.Status == db.ScheduleStatusCancelled {
Cancelled
if schedule.RescheduleReason != nil {
{ *schedule.RescheduleReason }
}
} else {
}
}
templ seriesMatchList(series *db.PlayoffSeries) {
Matches
for _, match := range series.Matches {
@seriesMatchRow(series, match)
}
}
templ seriesMatchRow(series *db.PlayoffSeries, match *db.PlayoffMatch) {
{{
matchLabel := fmt.Sprintf("Game %d", match.MatchNumber)
isPending := match.Status == "pending"
isCompleted := match.Status == "completed"
hasFixture := match.FixtureID != nil
_ = hasFixture
}}
{ matchLabel }
if isPending {
Pending
} else if isCompleted {
Complete
} else {
{ match.Status }
}
if match.FixtureID != nil {
View Details
}
}
templ seriesContextCard(series *db.PlayoffSeries) {
{{
// Determine advancement info
winnerAdvances := ""
loserAdvances := ""
if series.WinnerNextID != nil {
// Look through bracket series for the target
if series.Bracket != nil {
for _, s := range series.Bracket.Series {
if s.ID == *series.WinnerNextID {
winnerAdvances = s.Label
break
}
}
}
if winnerAdvances == "" {
winnerAdvances = "next round"
}
}
if series.LoserNextID != nil {
if series.Bracket != nil {
for _, s := range series.Bracket.Series {
if s.ID == *series.LoserNextID {
loserAdvances = s.Label
break
}
}
}
if loserAdvances == "" {
loserAdvances = "next round"
}
}
}}
Series Info
Round
{ roundDisplayName(series.Round) }
Format
Best of { fmt.Sprint(series.MatchesToWin*2 - 1) } (first to { fmt.Sprint(series.MatchesToWin) })
if series.Team1Seed != nil && series.Team2Seed != nil {
Seeding
{ ordinal(*series.Team1Seed) } seed vs { ordinal(*series.Team2Seed) } seed
}
if winnerAdvances != "" {
Winner →
{ winnerAdvances }
} else {
Winner →
Champion
}
if loserAdvances != "" {
Loser →
{ loserAdvances }
} else if series.WinnerNextID != nil {
Loser →
Eliminated
}
}
templ seriesTeamSection(team *db.Team, players []*db.PlayerWithPlayStatus, season *db.Season, league *db.League) {
{{
// Sort with managers first
sort.SliceStable(players, func(i, j int) bool {
return players[i].IsManager && !players[j].IsManager
})
}}
@links.TeamNameLinkInSeason(team, season, league)
if team.Color != "" {
}
if len(players) == 0 {
} else {
for _, p := range players {
@links.PlayerLink(p.Player)
if p.IsManager {
★ Manager
}
if p.IsFreeAgent {
FREE AGENT
}
}
}
}