finals generation added
This commit is contained in:
268
internal/view/seasonsview/finals_setup_form.templ
Normal file
268
internal/view/seasonsview/finals_setup_form.templ
Normal file
@@ -0,0 +1,268 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/datepicker"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/component/links"
|
||||
import "fmt"
|
||||
|
||||
templ FinalsSetupForm(
|
||||
season *db.Season,
|
||||
league *db.League,
|
||||
leaderboard []*db.LeaderboardEntry,
|
||||
unplayedFixtures []*db.Fixture,
|
||||
) {
|
||||
{{
|
||||
// Determine the recommended format value for the default Alpine state
|
||||
defaultFormat := ""
|
||||
if len(leaderboard) >= 10 && len(leaderboard) <= 15 {
|
||||
defaultFormat = string(db.PlayoffFormat10to15)
|
||||
} else if len(leaderboard) >= 7 && len(leaderboard) <= 9 {
|
||||
defaultFormat = string(db.PlayoffFormat7to9)
|
||||
} else if len(leaderboard) >= 5 && len(leaderboard) <= 6 {
|
||||
defaultFormat = string(db.PlayoffFormat5to6)
|
||||
}
|
||||
}}
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div
|
||||
class="bg-surface0 border border-surface1 rounded-lg overflow-hidden"
|
||||
x-data={ fmt.Sprintf("{ selectedFormat: '%s' }", defaultFormat) }
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="bg-mantle border-b border-surface1 px-6 py-4">
|
||||
<h2 class="text-xl font-bold text-text flex items-center gap-2">
|
||||
<span class="text-yellow">★</span>
|
||||
Begin Finals Setup
|
||||
</h2>
|
||||
<p class="text-sm text-subtext0 mt-1">
|
||||
Configure playoff format and dates for { league.Name }
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
hx-post={ fmt.Sprintf("/seasons/%s/leagues/%s/finals/setup", season.ShortName, league.ShortName) }
|
||||
hx-swap="none"
|
||||
class="p-6 space-y-6"
|
||||
>
|
||||
<!-- Date Fields -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
@datepicker.DatePicker(
|
||||
"regular_season_end_date",
|
||||
"regular_season_end_date",
|
||||
"Regular Season End Date",
|
||||
"DD/MM/YYYY",
|
||||
true,
|
||||
"",
|
||||
)
|
||||
<p class="text-xs text-subtext0 mt-1">Games after this date will be forfeited</p>
|
||||
</div>
|
||||
<div>
|
||||
@datepicker.DatePicker(
|
||||
"finals_start_date",
|
||||
"finals_start_date",
|
||||
"Finals Start Date",
|
||||
"DD/MM/YYYY",
|
||||
true,
|
||||
"",
|
||||
)
|
||||
<p class="text-xs text-subtext0 mt-1">First playoff matches begin on this date</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Format Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-3">Playoff Format</label>
|
||||
<div class="space-y-3">
|
||||
@formatOption(
|
||||
string(db.PlayoffFormat5to6),
|
||||
"5-6 Teams",
|
||||
"Top 5 qualify. 1st earns a bye, 2nd vs 3rd (upper), 4th vs 5th (lower). Double-chance for top seeds.",
|
||||
len(leaderboard) >= 5 && len(leaderboard) <= 6,
|
||||
len(leaderboard),
|
||||
)
|
||||
@formatOption(
|
||||
string(db.PlayoffFormat7to9),
|
||||
"7-9 Teams",
|
||||
"Top 6 qualify. 1st & 2nd placed into semis. 3rd vs 6th and 4th vs 5th in quarter finals.",
|
||||
len(leaderboard) >= 7 && len(leaderboard) <= 9,
|
||||
len(leaderboard),
|
||||
)
|
||||
@formatOption(
|
||||
string(db.PlayoffFormat10to15),
|
||||
"10-15 Teams",
|
||||
"Top 8 qualify. Top 4 get a second chance in qualifying finals.",
|
||||
len(leaderboard) >= 10 && len(leaderboard) <= 15,
|
||||
len(leaderboard),
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
<!-- Per-Round Best-of-N Configuration -->
|
||||
<div x-show="selectedFormat !== ''" x-cloak>
|
||||
<label class="block text-sm font-medium mb-3">Series Format (Best-of-N per Round)</label>
|
||||
<div class="bg-mantle border border-surface1 rounded-lg p-4 space-y-3">
|
||||
<!-- 5-6 Teams rounds -->
|
||||
<template x-if={ fmt.Sprintf("selectedFormat === '%s'", string(db.PlayoffFormat5to6)) }>
|
||||
<div class="space-y-3">
|
||||
@boRoundDropdown("bo_upper_bracket", "Upper Bracket", "2nd vs 3rd")
|
||||
@boRoundDropdown("bo_lower_bracket", "Lower Bracket", "4th vs 5th (elimination)")
|
||||
@boRoundDropdown("bo_upper_final", "Upper Final", "1st vs Winner of Upper Bracket")
|
||||
@boRoundDropdown("bo_lower_final", "Lower Final", "Loser of Upper Final vs Winner of Lower Bracket")
|
||||
@boRoundDropdown("bo_grand_final", "Grand Final", "Winner of Upper Final vs Winner of Lower Final")
|
||||
</div>
|
||||
</template>
|
||||
<!-- 7-9 Teams rounds -->
|
||||
<template x-if={ fmt.Sprintf("selectedFormat === '%s'", string(db.PlayoffFormat7to9)) }>
|
||||
<div class="space-y-3">
|
||||
@boRoundDropdown("bo_quarter_final", "Quarter Finals", "3rd vs 6th, 4th vs 5th")
|
||||
@boRoundDropdown("bo_semi_final", "Semi Finals", "1st vs QF winner, 2nd vs QF winner")
|
||||
@boRoundDropdown("bo_third_place", "Third Place Playoff", "SF1 loser vs SF2 loser")
|
||||
@boRoundDropdown("bo_grand_final", "Grand Final", "SF1 winner vs SF2 winner")
|
||||
</div>
|
||||
</template>
|
||||
<!-- 10-15 Teams rounds -->
|
||||
<template x-if={ fmt.Sprintf("selectedFormat === '%s'", string(db.PlayoffFormat10to15)) }>
|
||||
<div class="space-y-3">
|
||||
@boRoundDropdown("bo_qualifying_final", "Qualifying Finals", "1st vs 4th, 2nd vs 3rd (losers get second chance)")
|
||||
@boRoundDropdown("bo_elimination_final", "Elimination Finals", "5th vs 8th, 6th vs 7th (losers eliminated)")
|
||||
@boRoundDropdown("bo_semi_final", "Semi Finals", "QF losers vs EF winners")
|
||||
@boRoundDropdown("bo_preliminary_final", "Preliminary Finals", "QF winners vs SF winners")
|
||||
@boRoundDropdown("bo_third_place", "Third Place Playoff", "PF1 loser vs PF2 loser")
|
||||
@boRoundDropdown("bo_grand_final", "Grand Final", "PF1 winner vs PF2 winner")
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Unplayed Fixtures Warning -->
|
||||
if len(unplayedFixtures) > 0 {
|
||||
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-yellow mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-yellow mb-1">
|
||||
{ fmt.Sprintf("%d unplayed fixture(s) found", len(unplayedFixtures)) }
|
||||
</p>
|
||||
<p class="text-xs text-subtext0 mb-3">
|
||||
These fixtures will be recorded as a mutual forfeit when you begin finals.
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div class="max-h-40 overflow-y-auto space-y-1">
|
||||
for _, fixture := range unplayedFixtures {
|
||||
<div class="text-xs text-subtext1 flex items-center gap-2">
|
||||
<span class="text-subtext0">GW{ fmt.Sprint(*fixture.GameWeek) }</span>
|
||||
<span>{ fixture.HomeTeam.Name }</span>
|
||||
<span class="text-subtext0">vs</span>
|
||||
<span>{ fixture.AwayTeam.Name }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Standings Preview -->
|
||||
if len(leaderboard) > 0 {
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-3">Current Standings</label>
|
||||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-surface0 border-b border-surface1">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-subtext0 w-10">#</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Team</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-text">GP</th>
|
||||
<th class="px-3 py-2 text-center text-xs font-semibold text-blue">PTS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-surface1">
|
||||
for _, entry := range leaderboard {
|
||||
@standingsPreviewRow(entry, season, league)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-surface1">
|
||||
<button
|
||||
type="button"
|
||||
hx-post={ fmt.Sprintf("/seasons/%s/leagues/%s/finals", season.ShortName, league.ShortName) }
|
||||
hx-target="#finals-content"
|
||||
hx-swap="innerHTML"
|
||||
class="px-4 py-2 bg-surface1 hover:bg-surface2 text-text rounded-lg font-medium transition hover:cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-6 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg font-semibold transition hover:cursor-pointer"
|
||||
>
|
||||
Begin Finals
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ formatOption(value, label, description string, recommended bool, teamCount int) {
|
||||
<label
|
||||
class="flex items-start gap-3 p-3 bg-mantle border border-surface1 rounded-lg hover:bg-surface0 transition hover:cursor-pointer"
|
||||
x-bind:class={ fmt.Sprintf("selectedFormat === '%s' && 'border-blue/50 bg-blue/5'", value) }
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="format"
|
||||
value={ value }
|
||||
if recommended {
|
||||
checked
|
||||
}
|
||||
x-model="selectedFormat"
|
||||
class="mt-1 text-blue focus:ring-blue hover:cursor-pointer"
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-text">{ label }</span>
|
||||
if recommended {
|
||||
<span class="ml-2 px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
|
||||
Recommended for { fmt.Sprint(teamCount) } teams
|
||||
</span>
|
||||
}
|
||||
<p class="text-xs text-subtext0 mt-0.5">{ description }</p>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
|
||||
templ boRoundDropdown(name, label, description string) {
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="text-sm font-medium text-text">{ label }</span>
|
||||
<p class="text-xs text-subtext0 truncate">{ description }</p>
|
||||
</div>
|
||||
<select
|
||||
name={ name }
|
||||
class="w-24 px-3 py-1.5 bg-surface0 border border-surface1 rounded text-sm text-text focus:border-blue focus:outline-none hover:cursor-pointer"
|
||||
>
|
||||
<option value="1" selected>BO1</option>
|
||||
<option value="2">BO3</option>
|
||||
<option value="3">BO5</option>
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ standingsPreviewRow(entry *db.LeaderboardEntry, season *db.Season, league *db.League) {
|
||||
<tr class="hover:bg-surface0/50 transition-colors">
|
||||
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
|
||||
{ fmt.Sprint(entry.Position) }
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
@links.TeamLinkInSeason(entry.Team, season, league)
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-sm text-subtext0">
|
||||
{ fmt.Sprint(entry.Record.Played) }
|
||||
</td>
|
||||
<td class="px-3 py-2 text-center text-sm font-semibold text-blue">
|
||||
{ fmt.Sprint(entry.Record.Points) }
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
165
internal/view/seasonsview/playoff_bracket.templ
Normal file
165
internal/view/seasonsview/playoff_bracket.templ
Normal file
@@ -0,0 +1,165 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/component/links"
|
||||
import "fmt"
|
||||
|
||||
// PlayoffBracketView renders the full bracket visualization
|
||||
templ PlayoffBracketView(season *db.Season, league *db.League, bracket *db.PlayoffBracket) {
|
||||
<div class="space-y-6">
|
||||
<!-- Bracket Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-text flex items-center gap-2">
|
||||
<span class="text-yellow">★</span>
|
||||
Finals Bracket
|
||||
</h2>
|
||||
<p class="text-sm text-subtext0 mt-1">
|
||||
{ formatLabel(bracket.Format) }
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@playoffStatusBadge(bracket.Status)
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bracket Series List -->
|
||||
<div class="space-y-4">
|
||||
@bracketRounds(season, league, bracket)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
// bracketRounds groups series by round and renders them
|
||||
templ bracketRounds(season *db.Season, league *db.League, bracket *db.PlayoffBracket) {
|
||||
{{
|
||||
// Group series by round
|
||||
rounds := groupSeriesByRound(bracket.Series)
|
||||
roundOrder := getRoundOrder(bracket.Format)
|
||||
}}
|
||||
for _, roundName := range roundOrder {
|
||||
if series, ok := rounds[roundName]; ok {
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-semibold text-subtext0 uppercase tracking-wider">
|
||||
{ formatRoundName(roundName) }
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
for _, s := range series {
|
||||
@seriesCard(season, league, s)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templ seriesCard(season *db.Season, league *db.League, series *db.PlayoffSeries) {
|
||||
<div class={ "bg-surface0 border rounded-lg overflow-hidden",
|
||||
templ.KV("border-blue/50", series.Status == db.SeriesStatusInProgress),
|
||||
templ.KV("border-surface1", series.Status != db.SeriesStatusInProgress) }>
|
||||
<!-- Series Header -->
|
||||
<div class="bg-mantle px-4 py-2 flex items-center justify-between border-b border-surface1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold text-subtext0">{ series.Label }</span>
|
||||
@seriesFormatBadge(series.MatchesToWin)
|
||||
</div>
|
||||
@seriesStatusBadge(series.Status)
|
||||
</div>
|
||||
<!-- Teams -->
|
||||
<div class="divide-y divide-surface1">
|
||||
@seriesTeamRow(season, league, series.Team1, series.Team1Seed, series.Team1Wins,
|
||||
series.WinnerTeamID, series.MatchesToWin)
|
||||
@seriesTeamRow(season, league, series.Team2, series.Team2Seed, series.Team2Wins,
|
||||
series.WinnerTeamID, series.MatchesToWin)
|
||||
</div>
|
||||
<!-- Series Score -->
|
||||
if series.MatchesToWin > 1 {
|
||||
<div class="bg-mantle px-4 py-1.5 text-center text-xs text-subtext0 border-t border-surface1">
|
||||
{ fmt.Sprint(series.Team1Wins) } - { fmt.Sprint(series.Team2Wins) }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ seriesTeamRow(season *db.Season, league *db.League, team *db.Team, seed *int, wins int, winnerID *int, matchesToWin int) {
|
||||
{{
|
||||
isWinner := false
|
||||
if team != nil && winnerID != nil {
|
||||
isWinner = team.ID == *winnerID
|
||||
}
|
||||
isTBD := team == nil
|
||||
}}
|
||||
<div class={ "flex items-center justify-between px-4 py-2.5",
|
||||
templ.KV("bg-green/5", isWinner) }>
|
||||
<div class="flex items-center gap-2">
|
||||
if seed != nil {
|
||||
<span class="text-xs font-mono text-subtext0 w-5 text-right">
|
||||
{ fmt.Sprint(*seed) }
|
||||
</span>
|
||||
} else {
|
||||
<span class="text-xs font-mono text-subtext0 w-5 text-right">-</span>
|
||||
}
|
||||
if isTBD {
|
||||
<span class="text-sm text-subtext1 italic">TBD</span>
|
||||
} else {
|
||||
@links.TeamLinkInSeason(team, season, league)
|
||||
if isWinner {
|
||||
<span class="text-green text-xs">✓</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
if matchesToWin > 1 {
|
||||
<span class={ "text-sm font-mono",
|
||||
templ.KV("text-text", !isWinner),
|
||||
templ.KV("text-green font-bold", isWinner) }>
|
||||
{ fmt.Sprint(wins) }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ playoffStatusBadge(status db.PlayoffStatus) {
|
||||
switch status {
|
||||
case db.PlayoffStatusUpcoming:
|
||||
<span class="px-2 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium">
|
||||
Upcoming
|
||||
</span>
|
||||
case db.PlayoffStatusInProgress:
|
||||
<span class="px-2 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
|
||||
In Progress
|
||||
</span>
|
||||
case db.PlayoffStatusCompleted:
|
||||
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
|
||||
Completed
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
templ seriesFormatBadge(matchesToWin int) {
|
||||
{{
|
||||
label := fmt.Sprintf("BO%d", matchesToWin*2-1)
|
||||
}}
|
||||
<span class="px-1.5 py-0.5 bg-surface1 text-subtext1 rounded text-xs font-mono">
|
||||
{ label }
|
||||
</span>
|
||||
}
|
||||
|
||||
templ seriesStatusBadge(status db.SeriesStatus) {
|
||||
switch status {
|
||||
case db.SeriesStatusPending:
|
||||
<span class="px-1.5 py-0.5 bg-surface1 text-subtext0 rounded text-xs">
|
||||
Pending
|
||||
</span>
|
||||
case db.SeriesStatusInProgress:
|
||||
<span class="px-1.5 py-0.5 bg-blue/20 text-blue rounded text-xs">
|
||||
Live
|
||||
</span>
|
||||
case db.SeriesStatusCompleted:
|
||||
<span class="px-1.5 py-0.5 bg-green/20 text-green rounded text-xs">
|
||||
Complete
|
||||
</span>
|
||||
case db.SeriesStatusBye:
|
||||
<span class="px-1.5 py-0.5 bg-surface1 text-subtext0 rounded text-xs">
|
||||
Bye
|
||||
</span>
|
||||
}
|
||||
}
|
||||
88
internal/view/seasonsview/playoff_helpers.go
Normal file
88
internal/view/seasonsview/playoff_helpers.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
|
||||
// groupSeriesByRound groups playoff series by their round field
|
||||
func groupSeriesByRound(series []*db.PlayoffSeries) map[string][]*db.PlayoffSeries {
|
||||
grouped := make(map[string][]*db.PlayoffSeries)
|
||||
for _, s := range series {
|
||||
grouped[s.Round] = append(grouped[s.Round], s)
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
// getRoundOrder returns the display order of rounds for a given format
|
||||
func getRoundOrder(format db.PlayoffFormat) []string {
|
||||
switch format {
|
||||
case db.PlayoffFormat5to6:
|
||||
return []string{
|
||||
"upper_bracket",
|
||||
"lower_bracket",
|
||||
"upper_final",
|
||||
"lower_final",
|
||||
"grand_final",
|
||||
}
|
||||
case db.PlayoffFormat7to9:
|
||||
return []string{
|
||||
"quarter_final",
|
||||
"semi_final",
|
||||
"third_place",
|
||||
"grand_final",
|
||||
}
|
||||
case db.PlayoffFormat10to15:
|
||||
return []string{
|
||||
"qualifying_final",
|
||||
"elimination_final",
|
||||
"semi_final",
|
||||
"preliminary_final",
|
||||
"third_place",
|
||||
"grand_final",
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// formatRoundName converts a round slug to a human-readable name
|
||||
func formatRoundName(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 Finals"
|
||||
case "semi_final":
|
||||
return "Semi Finals"
|
||||
case "qualifying_final":
|
||||
return "Qualifying Finals"
|
||||
case "elimination_final":
|
||||
return "Elimination Finals"
|
||||
case "preliminary_final":
|
||||
return "Preliminary Finals"
|
||||
case "third_place":
|
||||
return "Third Place Playoff"
|
||||
case "grand_final":
|
||||
return "Grand Final"
|
||||
default:
|
||||
return round
|
||||
}
|
||||
}
|
||||
|
||||
// formatLabel returns a human-readable format description
|
||||
func formatLabel(format db.PlayoffFormat) string {
|
||||
switch format {
|
||||
case db.PlayoffFormat5to6:
|
||||
return "Top 5 qualify"
|
||||
case db.PlayoffFormat7to9:
|
||||
return "Top 6 qualify"
|
||||
case db.PlayoffFormat10to15:
|
||||
return "Top 8 qualify"
|
||||
default:
|
||||
return string(format)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,59 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/contexts"
|
||||
import "git.haelnorr.com/h/oslstats/internal/permissions"
|
||||
import "fmt"
|
||||
|
||||
templ SeasonLeagueFinalsPage(season *db.Season, league *db.League) {
|
||||
templ SeasonLeagueFinalsPage(season *db.Season, league *db.League, bracket *db.PlayoffBracket) {
|
||||
@SeasonLeagueLayout("finals", season, league) {
|
||||
@SeasonLeagueFinals()
|
||||
@SeasonLeagueFinals(season, league, bracket)
|
||||
}
|
||||
}
|
||||
|
||||
templ SeasonLeagueFinals() {
|
||||
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
|
||||
<p class="text-subtext0 text-lg">Coming Soon...</p>
|
||||
templ SeasonLeagueFinals(season *db.Season, league *db.League, bracket *db.PlayoffBracket) {
|
||||
{{
|
||||
status := season.GetStatus()
|
||||
permCache := contexts.Permissions(ctx)
|
||||
canManagePlayoffs := permCache != nil && permCache.HasPermission(permissions.PlayoffsManage)
|
||||
}}
|
||||
<div id="finals-content">
|
||||
if bracket != nil {
|
||||
@PlayoffBracketView(season, league, bracket)
|
||||
} else if status == db.StatusInProgress || status == db.StatusUpcoming {
|
||||
@finalsRegularSeasonInProgress(season, league, canManagePlayoffs)
|
||||
} else {
|
||||
@finalsNotConfigured()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ finalsRegularSeasonInProgress(season *db.Season, league *db.League, canManagePlayoffs bool) {
|
||||
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
|
||||
<div class="mb-4">
|
||||
<svg class="w-12 h-12 mx-auto text-subtext0 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 013 3h-15a3 3 0 013-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 01-.982-3.172M9.497 14.25a7.454 7.454 0 00.981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 007.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M18.75 4.236c.982.143 1.954.317 2.916.52A6.003 6.003 0 0016.27 9.728M18.75 4.236V4.5c0 2.108-.966 3.99-2.48 5.228m0 0a6.003 6.003 0 01-2.77.836 6.003 6.003 0 01-2.77-.836"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-text text-lg font-semibold mb-2">Regular Season in Progress</p>
|
||||
<p class="text-subtext0 mb-6">
|
||||
Finals will be available once the regular season is complete.
|
||||
</p>
|
||||
if canManagePlayoffs {
|
||||
<button
|
||||
hx-get={ fmt.Sprintf("/seasons/%s/leagues/%s/finals/setup", season.ShortName, league.ShortName) }
|
||||
hx-target="#finals-content"
|
||||
hx-swap="innerHTML"
|
||||
class="px-6 py-3 bg-blue hover:bg-blue/80 text-mantle rounded-lg font-semibold transition hover:cursor-pointer"
|
||||
>
|
||||
Begin Finals
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ finalsNotConfigured() {
|
||||
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
|
||||
<p class="text-subtext0 text-lg">No finals configured for this league.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user