269 lines
10 KiB
Plaintext
269 lines
10 KiB
Plaintext
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>
|
|
}
|