324 lines
11 KiB
Plaintext
324 lines
11 KiB
Plaintext
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 Display -->
|
|
switch bracket.Format {
|
|
case db.PlayoffFormat5to6:
|
|
@bracket5to6(season, league, bracket)
|
|
case db.PlayoffFormat7to9:
|
|
@bracket7to9(season, league, bracket)
|
|
case db.PlayoffFormat10to15:
|
|
@bracket10to15(season, league, bracket)
|
|
}
|
|
<!-- Legend -->
|
|
<div class="flex items-center gap-6 text-xs text-subtext0">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-8 h-0 border-t-2 border-green"></div>
|
|
<span>Winner</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-8 h-0 border-t-2 border-red border-dashed"></div>
|
|
<span>Loser</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="/static/js/bracket-lines.js"></script>
|
|
<script>
|
|
document.querySelectorAll('[data-series-url]').forEach(function(card) {
|
|
card.addEventListener('click', function(e) {
|
|
if (!e.target.closest('a')) {
|
|
window.location.href = card.getAttribute('data-series-url');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 5-6 TEAMS FORMAT
|
|
// ──────────────────────────────────────────────
|
|
// Round 1: [Upper Bracket] [Lower Bracket]
|
|
// Round 2: [Upper Final] [Lower Final]
|
|
// Round 3: [Grand Final]
|
|
templ bracket5to6(season *db.Season, league *db.League, bracket *db.PlayoffBracket) {
|
|
{{
|
|
s := seriesByNumber(bracket.Series)
|
|
conns := connectionsJSON(bracket.Series)
|
|
}}
|
|
<div class="overflow-x-auto">
|
|
<div class="relative min-w-[500px]" data-bracket-lines data-connections={ conns }>
|
|
<svg data-bracket-svg class="absolute inset-0 w-full h-full pointer-events-none" style="z-index: 0;"></svg>
|
|
<div class="relative" style="z-index: 1;">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
@seriesCard(season, league, s[1])
|
|
@seriesCard(season, league, s[2])
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
@seriesCard(season, league, s[3])
|
|
@seriesCard(season, league, s[4])
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<div class="max-w-md mx-auto">
|
|
@seriesCard(season, league, s[5])
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 7-9 TEAMS FORMAT
|
|
// ──────────────────────────────────────────────
|
|
// Round 1 (Quarter Finals): [QF1] [QF2]
|
|
// Round 2 (Semi Finals): [SF1] [SF2]
|
|
// Round 3: [3rd Place] [Grand Final]
|
|
templ bracket7to9(season *db.Season, league *db.League, bracket *db.PlayoffBracket) {
|
|
{{
|
|
s := seriesByNumber(bracket.Series)
|
|
conns := connectionsJSON(bracket.Series)
|
|
}}
|
|
<div class="overflow-x-auto">
|
|
<div class="relative min-w-[500px]" data-bracket-lines data-connections={ conns }>
|
|
<svg data-bracket-svg class="absolute inset-0 w-full h-full pointer-events-none" style="z-index: 0;"></svg>
|
|
<div class="relative" style="z-index: 1;">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
@seriesCard(season, league, s[1])
|
|
@seriesCard(season, league, s[2])
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
@seriesCard(season, league, s[3])
|
|
@seriesCard(season, league, s[4])
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
@seriesCard(season, league, s[5])
|
|
@seriesCard(season, league, s[6])
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// 10-15 TEAMS FORMAT
|
|
// ──────────────────────────────────────────────
|
|
// 4 invisible columns, cards placed into specific cells:
|
|
// Row 1: EF1(col2) EF2(col3)
|
|
// Row 2: QF1(col1) QF2(col4)
|
|
// Row 3: SF1(col2) SF2(col3)
|
|
// Row 4: PF1(col2) PF2(col3)
|
|
// Row 5: 3rd(col2)
|
|
// Row 6: GF(col3)
|
|
templ bracket10to15(season *db.Season, league *db.League, bracket *db.PlayoffBracket) {
|
|
{{
|
|
s := seriesByNumber(bracket.Series)
|
|
conns := connectionsJSON(bracket.Series)
|
|
}}
|
|
<div class="overflow-x-auto">
|
|
<div class="relative min-w-[700px]" data-bracket-lines data-connections={ conns }>
|
|
<svg data-bracket-svg class="absolute inset-0 w-full h-full pointer-events-none" style="z-index: 0;"></svg>
|
|
<div class="relative" style="z-index: 1;">
|
|
<!-- Row 1: EF1(c2) EF2(c3) -->
|
|
<div class="grid grid-cols-4 gap-4">
|
|
<div></div>
|
|
@seriesCard(season, league, s[3])
|
|
@seriesCard(season, league, s[4])
|
|
<div></div>
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<!-- Row 2: QF1(c1) QF2(c4) -->
|
|
<div class="grid grid-cols-4 gap-4">
|
|
@seriesCard(season, league, s[1])
|
|
<div></div>
|
|
<div></div>
|
|
@seriesCard(season, league, s[2])
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<!-- Row 3: SF1(c2) SF2(c3) -->
|
|
<div class="grid grid-cols-4 gap-4">
|
|
<div></div>
|
|
@seriesCard(season, league, s[5])
|
|
@seriesCard(season, league, s[6])
|
|
<div></div>
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<!-- Row 4: PF1(c2) PF2(c3) -->
|
|
<div class="grid grid-cols-4 gap-4">
|
|
<div></div>
|
|
@seriesCard(season, league, s[7])
|
|
@seriesCard(season, league, s[8])
|
|
<div></div>
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<!-- Row 5: 3rd Place(c2) -->
|
|
<div class="grid grid-cols-4 gap-4">
|
|
<div></div>
|
|
@seriesCard(season, league, s[9])
|
|
<div></div>
|
|
<div></div>
|
|
</div>
|
|
<div class="h-16"></div>
|
|
<!-- Row 6: Grand Final(c3) -->
|
|
<div class="grid grid-cols-4 gap-4">
|
|
<div></div>
|
|
<div></div>
|
|
@seriesCard(season, league, s[10])
|
|
<div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// ──────────────────────────────────────────────
|
|
// SHARED COMPONENTS
|
|
// ──────────────────────────────────────────────
|
|
|
|
templ seriesCard(season *db.Season, league *db.League, series *db.PlayoffSeries) {
|
|
{{
|
|
hasTeams := series.Team1 != nil || series.Team2 != nil
|
|
seriesURL := fmt.Sprintf("/series/%d", series.ID)
|
|
}}
|
|
<div
|
|
data-series={ fmt.Sprint(series.SeriesNumber) }
|
|
if hasTeams {
|
|
data-series-url={ seriesURL }
|
|
}
|
|
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),
|
|
templ.KV("hover:bg-surface1 hover:cursor-pointer transition", hasTeams) }
|
|
>
|
|
<!-- Series Header -->
|
|
<div class="bg-mantle px-3 py-1.5 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-3 py-1 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-3 py-2",
|
|
templ.KV("bg-green/5", isWinner) }>
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
if seed != nil {
|
|
<span class="text-xs font-mono text-subtext0 w-4 text-right flex-shrink-0">
|
|
{ fmt.Sprint(*seed) }
|
|
</span>
|
|
} else {
|
|
<span class="text-xs font-mono text-subtext0 w-4 text-right flex-shrink-0">-</span>
|
|
}
|
|
if isTBD {
|
|
<span class="text-sm text-subtext1 italic">TBD</span>
|
|
} else {
|
|
<div class="truncate">
|
|
@links.TeamLinkInSeason(team, season, league)
|
|
</div>
|
|
if isWinner {
|
|
<span class="text-green text-xs flex-shrink-0">✓</span>
|
|
}
|
|
}
|
|
</div>
|
|
if matchesToWin > 1 {
|
|
<span class={ "text-sm font-mono flex-shrink-0 ml-2",
|
|
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>
|
|
}
|
|
}
|