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>
|
||||
}
|
||||
Reference in New Issue
Block a user