Files
oslstats/internal/view/seasonsview/finals_setup_form.templ
2026-03-15 17:43:39 +11:00

281 lines
11 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)
}
// Prefill dates from existing season values
endDateDefault := ""
if !season.EndDate.IsZero() {
endDateDefault = season.EndDate.Time.Format("02/01/2006")
}
finalsStartDefault := ""
if !season.FinalsStartDate.IsZero() {
finalsStartDefault = season.FinalsStartDate.Time.Format("02/01/2006")
}
}}
<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">&#9733;</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.DatePickerWithDefault(
"regular_season_end_date",
"regular_season_end_date",
"Regular Season End Date",
"DD/MM/YYYY",
true,
"",
endDateDefault,
)
<p class="text-xs text-subtext0 mt-1">Last day of the regular season (inclusive)</p>
</div>
<div>
@datepicker.DatePickerWithDefault(
"finals_start_date",
"finals_start_date",
"Finals Start Date",
"DD/MM/YYYY",
true,
"",
finalsStartDefault,
)
<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>
}