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
{ 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 {

    Teams not yet determined

    } 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 {

    No time scheduled

    } 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 {

    No time confirmed

    }
    } 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 {

    No players on roster.

    } else {
    for _, p := range players {
    @links.PlayerLink(p.Player) if p.IsManager { ★ Manager } if p.IsFreeAgent { FREE AGENT }
    }
    }
    }