661 lines
21 KiB
Plaintext
661 lines
21 KiB
Plaintext
package seasonsview
|
||
|
||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||
import "git.haelnorr.com/h/oslstats/internal/permissions"
|
||
import "git.haelnorr.com/h/oslstats/internal/contexts"
|
||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||
import "git.haelnorr.com/h/oslstats/internal/view/component/links"
|
||
import "fmt"
|
||
import "sort"
|
||
import "strings"
|
||
|
||
// seriesTeamName returns a display name for a team in the series, or "TBD" if nil
|
||
func seriesTeamName(team *db.Team) string {
|
||
if team == nil {
|
||
return "TBD"
|
||
}
|
||
return team.Name
|
||
}
|
||
|
||
// seriesTeamShortName returns a short name for a team in the series, or "TBD" if nil
|
||
func seriesTeamShortName(team *db.Team) string {
|
||
if team == nil {
|
||
return "TBD"
|
||
}
|
||
return team.ShortName
|
||
}
|
||
|
||
// roundDisplayName converts a round slug to a human-readable name
|
||
func roundDisplayName(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 Final"
|
||
case "semi_final":
|
||
return "Semi Final"
|
||
case "elimination_final":
|
||
return "Elimination Final"
|
||
case "qualifying_final":
|
||
return "Qualifying Final"
|
||
case "preliminary_final":
|
||
return "Preliminary Final"
|
||
case "third_place":
|
||
return "Third Place Playoff"
|
||
case "grand_final":
|
||
return "Grand Final"
|
||
default:
|
||
return strings.ReplaceAll(round, "_", " ")
|
||
}
|
||
}
|
||
|
||
// SeriesDetailLayout renders the series detail page layout with header and
|
||
// tab navigation. Tab content is rendered as children.
|
||
templ SeriesDetailLayout(activeTab string, series *db.PlayoffSeries, schedule *db.PlayoffSeriesSchedule) {
|
||
{{
|
||
backURL := fmt.Sprintf("/seasons/%s/leagues/%s/finals",
|
||
series.Bracket.Season.ShortName, series.Bracket.League.ShortName)
|
||
isCompleted := series.Status == db.SeriesStatusCompleted
|
||
team1Name := seriesTeamName(series.Team1)
|
||
team2Name := seriesTeamName(series.Team2)
|
||
boLabel := fmt.Sprintf("BO%d", series.MatchesToWin*2-1)
|
||
}}
|
||
@baseview.Layout(fmt.Sprintf("%s — %s vs %s", series.Label, team1Name, team2Name)) {
|
||
<div class="max-w-screen-lg mx-auto px-4 py-8">
|
||
<!-- Header -->
|
||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden mb-6">
|
||
<div class="bg-surface0 border-b border-surface1 px-6 py-8">
|
||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||
<div>
|
||
<div class="flex items-center gap-4 mb-2">
|
||
<h1 class="text-3xl font-bold text-text">
|
||
{ team1Name }
|
||
<span class="text-subtext0 font-normal">vs</span>
|
||
{ team2Name }
|
||
</h1>
|
||
</div>
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<span class="px-2 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
|
||
{ series.Label }
|
||
</span>
|
||
<span class="px-2 py-0.5 bg-surface1 text-subtext0 rounded text-xs font-mono">
|
||
{ boLabel }
|
||
</span>
|
||
if series.Team1Seed != nil || series.Team2Seed != nil {
|
||
<span class="px-2 py-0.5 bg-surface1 text-subtext0 rounded text-xs font-mono">
|
||
if series.Team1Seed != nil && series.Team2Seed != nil {
|
||
Seed { fmt.Sprint(*series.Team1Seed) } vs { fmt.Sprint(*series.Team2Seed) }
|
||
} else if series.Team1Seed != nil {
|
||
Seed { fmt.Sprint(*series.Team1Seed) }
|
||
} else if series.Team2Seed != nil {
|
||
Seed { fmt.Sprint(*series.Team2Seed) }
|
||
}
|
||
</span>
|
||
}
|
||
<span class="text-subtext1 text-sm">
|
||
{ series.Bracket.Season.Name } — { series.Bracket.League.Name }
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<a
|
||
href={ templ.SafeURL(backURL) }
|
||
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center
|
||
bg-surface1 hover:bg-surface2 text-text transition"
|
||
>
|
||
Back to Bracket
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<!-- Tab Navigation -->
|
||
<nav class="bg-surface0 border-b border-surface1" data-tab-nav="series-detail-content">
|
||
<ul class="flex flex-wrap">
|
||
@seriesTabItem("overview", "Overview", activeTab, series)
|
||
if isCompleted {
|
||
@seriesTabItem("analysis", "Match Analysis", activeTab, series)
|
||
} else {
|
||
@seriesTabItem("preview", "Match Preview", activeTab, series)
|
||
@seriesTabItem("scheduling", "Schedule", activeTab, series)
|
||
}
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
<!-- Content Area -->
|
||
<main id="series-detail-content">
|
||
{ children... }
|
||
</main>
|
||
</div>
|
||
<script src="/static/js/tabs.js" defer></script>
|
||
}
|
||
}
|
||
|
||
templ seriesTabItem(section string, label string, activeTab string, series *db.PlayoffSeries) {
|
||
{{
|
||
isActive := section == activeTab
|
||
baseClasses := "inline-block px-6 py-3 transition-colors cursor-pointer border-b-2"
|
||
activeClasses := "border-blue text-blue font-semibold"
|
||
inactiveClasses := "border-transparent text-subtext0 hover:text-text hover:border-surface2"
|
||
url := fmt.Sprintf("/series/%d/%s", series.ID, section)
|
||
}}
|
||
<li class="inline-block">
|
||
<a
|
||
href={ templ.SafeURL(url) }
|
||
hx-post={ url }
|
||
hx-target="#series-detail-content"
|
||
hx-swap="innerHTML"
|
||
hx-push-url={ url }
|
||
class={ baseClasses, templ.KV(activeClasses, isActive), templ.KV(inactiveClasses, !isActive) }
|
||
>
|
||
{ label }
|
||
</a>
|
||
</li>
|
||
}
|
||
|
||
// ==================== Full page wrappers (for GET requests / direct navigation) ====================
|
||
|
||
templ SeriesDetailOverviewPage(
|
||
series *db.PlayoffSeries,
|
||
currentSchedule *db.PlayoffSeriesSchedule,
|
||
canSchedule bool,
|
||
userTeamID int,
|
||
rosters map[string][]*db.PlayerWithPlayStatus,
|
||
) {
|
||
@SeriesDetailLayout("overview", series, currentSchedule) {
|
||
@SeriesDetailOverviewContent(series, currentSchedule, canSchedule, userTeamID, rosters)
|
||
}
|
||
}
|
||
|
||
templ SeriesDetailPreviewPage(
|
||
series *db.PlayoffSeries,
|
||
currentSchedule *db.PlayoffSeriesSchedule,
|
||
rosters map[string][]*db.PlayerWithPlayStatus,
|
||
previewData *db.MatchPreviewData,
|
||
) {
|
||
@SeriesDetailLayout("preview", series, currentSchedule) {
|
||
@SeriesDetailPreviewContent(series, rosters, previewData)
|
||
}
|
||
}
|
||
|
||
templ SeriesDetailAnalysisPage(
|
||
series *db.PlayoffSeries,
|
||
currentSchedule *db.PlayoffSeriesSchedule,
|
||
rosters map[string][]*db.PlayerWithPlayStatus,
|
||
previewData *db.MatchPreviewData,
|
||
) {
|
||
@SeriesDetailLayout("analysis", series, currentSchedule) {
|
||
@SeriesDetailAnalysisContent(series, rosters, previewData)
|
||
}
|
||
}
|
||
|
||
templ SeriesDetailSchedulePage(
|
||
series *db.PlayoffSeries,
|
||
currentSchedule *db.PlayoffSeriesSchedule,
|
||
history []*db.PlayoffSeriesSchedule,
|
||
canSchedule bool,
|
||
userTeamID int,
|
||
) {
|
||
@SeriesDetailLayout("scheduling", series, currentSchedule) {
|
||
@SeriesDetailScheduleContent(series, currentSchedule, history, canSchedule, userTeamID)
|
||
}
|
||
}
|
||
|
||
// ==================== Tab content components (for POST requests / HTMX swaps) ====================
|
||
|
||
templ SeriesDetailOverviewContent(
|
||
series *db.PlayoffSeries,
|
||
currentSchedule *db.PlayoffSeriesSchedule,
|
||
canSchedule bool,
|
||
userTeamID int,
|
||
rosters map[string][]*db.PlayerWithPlayStatus,
|
||
) {
|
||
{{
|
||
permCache := contexts.Permissions(ctx)
|
||
canManage := permCache.HasPermission(permissions.PlayoffsManage)
|
||
}}
|
||
@seriesOverviewTab(series, currentSchedule, rosters, canSchedule, canManage, userTeamID)
|
||
}
|
||
|
||
templ SeriesDetailPreviewContent(
|
||
series *db.PlayoffSeries,
|
||
rosters map[string][]*db.PlayerWithPlayStatus,
|
||
previewData *db.MatchPreviewData,
|
||
) {
|
||
@seriesMatchPreviewTab(series, rosters, previewData)
|
||
}
|
||
|
||
templ SeriesDetailAnalysisContent(
|
||
series *db.PlayoffSeries,
|
||
rosters map[string][]*db.PlayerWithPlayStatus,
|
||
previewData *db.MatchPreviewData,
|
||
) {
|
||
@seriesMatchAnalysisTab(series, rosters, previewData)
|
||
}
|
||
|
||
templ SeriesDetailScheduleContent(
|
||
series *db.PlayoffSeries,
|
||
currentSchedule *db.PlayoffSeriesSchedule,
|
||
history []*db.PlayoffSeriesSchedule,
|
||
canSchedule bool,
|
||
userTeamID int,
|
||
) {
|
||
{{
|
||
permCache := contexts.Permissions(ctx)
|
||
canManage := permCache.HasPermission(permissions.PlayoffsManage)
|
||
}}
|
||
@seriesScheduleTab(series, currentSchedule, history, canSchedule, canManage, userTeamID)
|
||
}
|
||
|
||
// ==================== Overview Tab ====================
|
||
templ seriesOverviewTab(
|
||
series *db.PlayoffSeries,
|
||
currentSchedule *db.PlayoffSeriesSchedule,
|
||
rosters map[string][]*db.PlayerWithPlayStatus,
|
||
canSchedule bool,
|
||
canManage bool,
|
||
userTeamID int,
|
||
) {
|
||
{{
|
||
isCompleted := series.Status == db.SeriesStatusCompleted
|
||
isBye := series.Status == db.SeriesStatusBye
|
||
bothTeamsAssigned := series.Team1ID != nil && series.Team2ID != nil
|
||
showUploadPrompt := canManage && !isCompleted && !isBye && bothTeamsAssigned
|
||
}}
|
||
<div class="space-y-6">
|
||
<!-- Series Score + Schedule Row -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<div class="lg:col-span-2">
|
||
@seriesScoreDisplay(series)
|
||
</div>
|
||
<div>
|
||
@seriesScheduleSummary(series, currentSchedule)
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Upload Prompt (for admins when series is in progress) -->
|
||
if showUploadPrompt {
|
||
@seriesUploadPrompt(series)
|
||
}
|
||
|
||
<!-- Match List -->
|
||
if len(series.Matches) > 0 {
|
||
@seriesMatchList(series)
|
||
}
|
||
|
||
<!-- Series Context -->
|
||
@seriesContextCard(series)
|
||
|
||
<!-- Team Rosters -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
if series.Team1 != nil {
|
||
@seriesTeamSection(series.Team1, rosters["team1"], series.Bracket.Season, series.Bracket.League)
|
||
}
|
||
if series.Team2 != nil {
|
||
@seriesTeamSection(series.Team2, rosters["team2"], series.Bracket.Season, series.Bracket.League)
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ seriesUploadPrompt(series *db.PlayoffSeries) {
|
||
{{
|
||
// Check if there are pending results waiting for review
|
||
hasPendingMatches := false
|
||
for _, match := range series.Matches {
|
||
if match.FixtureID != nil && match.Status == "pending" {
|
||
hasPendingMatches = true
|
||
break
|
||
}
|
||
}
|
||
}}
|
||
<div class="bg-mantle border border-surface1 rounded-lg p-6 text-center">
|
||
if hasPendingMatches {
|
||
<div class="text-4xl mb-3">📋</div>
|
||
<p class="text-lg text-text font-medium mb-2">Results Pending Review</p>
|
||
<p class="text-sm text-subtext1 mb-4">Uploaded results are waiting to be reviewed and finalized.</p>
|
||
<a
|
||
href={ templ.SafeURL(fmt.Sprintf("/series/%d/results/review", series.ID)) }
|
||
class="inline-block px-4 py-2 bg-green hover:bg-green/75 text-mantle rounded-lg
|
||
font-medium transition hover:cursor-pointer"
|
||
>
|
||
Review Results
|
||
</a>
|
||
} else {
|
||
<div class="text-4xl mb-3">📋</div>
|
||
<p class="text-lg text-text font-medium mb-2">No Results Uploaded</p>
|
||
<p class="text-sm text-subtext1 mb-4">Upload match log files to record the series results.</p>
|
||
<a
|
||
href={ templ.SafeURL(fmt.Sprintf("/series/%d/results/upload", series.ID)) }
|
||
class="inline-block px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg
|
||
font-medium transition hover:cursor-pointer"
|
||
>
|
||
Upload Match Logs
|
||
</a>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
templ seriesScoreDisplay(series *db.PlayoffSeries) {
|
||
{{
|
||
isCompleted := series.Status == db.SeriesStatusCompleted
|
||
team1Won := series.WinnerTeamID != nil && series.Team1ID != nil && *series.WinnerTeamID == *series.Team1ID
|
||
team2Won := series.WinnerTeamID != nil && series.Team2ID != nil && *series.WinnerTeamID == *series.Team2ID
|
||
isBye := series.Status == db.SeriesStatusBye
|
||
}}
|
||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
||
<div class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between">
|
||
<h2 class="text-lg font-bold text-text">Series Score</h2>
|
||
<div class="flex items-center gap-2">
|
||
@seriesStatusBadge(series.Status)
|
||
@seriesFormatBadge(series.MatchesToWin)
|
||
</div>
|
||
</div>
|
||
<div class="p-6">
|
||
if isBye {
|
||
<div class="text-center py-4">
|
||
<p class="text-lg text-subtext0">Bye — team advances automatically</p>
|
||
</div>
|
||
} else if series.Team1 == nil && series.Team2 == nil {
|
||
<div class="text-center py-4">
|
||
<p class="text-lg text-subtext0">Teams not yet determined</p>
|
||
</div>
|
||
} else {
|
||
<div class="flex items-center justify-center gap-6 py-4">
|
||
<div class="flex items-center gap-3">
|
||
if team1Won {
|
||
<span class="text-2xl">🏆</span>
|
||
}
|
||
if series.Team1 != nil && series.Team1.Color != "" {
|
||
<span
|
||
class="px-2.5 py-1 rounded text-sm font-bold"
|
||
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
|
||
series.Team1.Color, series.Team1.Color, series.Team1.Color) }
|
||
>
|
||
{ seriesTeamShortName(series.Team1) }
|
||
</span>
|
||
} else {
|
||
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
|
||
{ seriesTeamShortName(series.Team1) }
|
||
</span>
|
||
}
|
||
<span class="text-4xl font-bold text-text">{ fmt.Sprint(series.Team1Wins) }</span>
|
||
</div>
|
||
<div class="flex flex-col items-center">
|
||
<span class="text-4xl text-subtext0 font-light leading-none">–</span>
|
||
if isCompleted {
|
||
<span class="px-1.5 py-0.5 bg-green/20 text-green rounded text-xs font-semibold mt-1">
|
||
FINAL
|
||
</span>
|
||
}
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-4xl font-bold text-text">{ fmt.Sprint(series.Team2Wins) }</span>
|
||
if series.Team2 != nil && series.Team2.Color != "" {
|
||
<span
|
||
class="px-2.5 py-1 rounded text-sm font-bold"
|
||
style={ fmt.Sprintf("background-color: %s22; color: %s; border: 1px solid %s44",
|
||
series.Team2.Color, series.Team2.Color, series.Team2.Color) }
|
||
>
|
||
{ seriesTeamShortName(series.Team2) }
|
||
</span>
|
||
} else {
|
||
<span class="px-2.5 py-1 bg-surface1 text-text rounded text-sm font-bold">
|
||
{ seriesTeamShortName(series.Team2) }
|
||
</span>
|
||
}
|
||
if team2Won {
|
||
<span class="text-2xl">🏆</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ seriesScheduleSummary(series *db.PlayoffSeries, schedule *db.PlayoffSeriesSchedule) {
|
||
{{
|
||
isCompleted := series.Status == db.SeriesStatusCompleted
|
||
}}
|
||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden h-full">
|
||
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
|
||
<h2 class="text-lg font-bold text-text">Schedule</h2>
|
||
</div>
|
||
<div class="p-4 flex flex-col justify-center h-[calc(100%-3rem)]">
|
||
if schedule == nil {
|
||
<div class="text-center">
|
||
<p class="text-subtext1 text-sm">No time scheduled</p>
|
||
</div>
|
||
} else if schedule.Status == db.ScheduleStatusAccepted && schedule.ScheduledTime != nil {
|
||
<div class="text-center space-y-2">
|
||
if isCompleted {
|
||
<span class="px-2 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium">
|
||
Played
|
||
</span>
|
||
} else {
|
||
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
|
||
Confirmed
|
||
</span>
|
||
}
|
||
<p class="text-text font-medium">
|
||
@localtime(schedule.ScheduledTime, "date")
|
||
</p>
|
||
<p class="text-text text-lg font-bold">
|
||
@localtime(schedule.ScheduledTime, "time")
|
||
</p>
|
||
</div>
|
||
} else if schedule.Status == db.ScheduleStatusPending && schedule.ScheduledTime != nil {
|
||
<div class="text-center space-y-2">
|
||
<span class="px-2 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
|
||
Proposed
|
||
</span>
|
||
<p class="text-text font-medium">
|
||
@localtime(schedule.ScheduledTime, "date")
|
||
</p>
|
||
<p class="text-text text-lg font-bold">
|
||
@localtime(schedule.ScheduledTime, "time")
|
||
</p>
|
||
<p class="text-subtext1 text-xs">Awaiting confirmation</p>
|
||
</div>
|
||
} else if schedule.Status == db.ScheduleStatusCancelled {
|
||
<div class="text-center space-y-2">
|
||
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
|
||
Cancelled
|
||
</span>
|
||
if schedule.RescheduleReason != nil {
|
||
<p class="text-subtext1 text-xs">{ *schedule.RescheduleReason }</p>
|
||
}
|
||
</div>
|
||
} else {
|
||
<div class="text-center">
|
||
<p class="text-subtext1 text-sm">No time confirmed</p>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ seriesMatchList(series *db.PlayoffSeries) {
|
||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
||
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
|
||
<h2 class="text-lg font-bold text-text">Matches</h2>
|
||
</div>
|
||
<div class="divide-y divide-surface1">
|
||
for _, match := range series.Matches {
|
||
@seriesMatchRow(series, match)
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ seriesMatchRow(series *db.PlayoffSeries, match *db.PlayoffMatch) {
|
||
{{
|
||
matchLabel := fmt.Sprintf("Game %d", match.MatchNumber)
|
||
isPending := match.Status == "pending"
|
||
isCompleted := match.Status == "completed"
|
||
hasFixture := match.FixtureID != nil
|
||
_ = hasFixture
|
||
}}
|
||
<div class="flex items-center justify-between px-4 py-3 hover:bg-surface0 transition-colors">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm font-medium text-text">{ matchLabel }</span>
|
||
if isPending {
|
||
<span class="px-1.5 py-0.5 bg-surface1 text-subtext0 rounded text-xs">
|
||
Pending
|
||
</span>
|
||
} else if isCompleted {
|
||
<span class="px-1.5 py-0.5 bg-green/20 text-green rounded text-xs">
|
||
Complete
|
||
</span>
|
||
} else {
|
||
<span class="px-1.5 py-0.5 bg-blue/20 text-blue rounded text-xs">
|
||
{ match.Status }
|
||
</span>
|
||
}
|
||
</div>
|
||
if match.FixtureID != nil {
|
||
<a
|
||
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d/overview", *match.FixtureID)) }
|
||
class="px-3 py-1 bg-surface1 hover:bg-surface2 text-text rounded text-xs
|
||
font-medium transition hover:cursor-pointer"
|
||
>
|
||
View Details
|
||
</a>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
templ seriesContextCard(series *db.PlayoffSeries) {
|
||
{{
|
||
// Determine advancement info
|
||
winnerAdvances := ""
|
||
loserAdvances := ""
|
||
|
||
if series.WinnerNextID != nil {
|
||
// Look through bracket series for the target
|
||
if series.Bracket != nil {
|
||
for _, s := range series.Bracket.Series {
|
||
if s.ID == *series.WinnerNextID {
|
||
winnerAdvances = s.Label
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if winnerAdvances == "" {
|
||
winnerAdvances = "next round"
|
||
}
|
||
}
|
||
if series.LoserNextID != nil {
|
||
if series.Bracket != nil {
|
||
for _, s := range series.Bracket.Series {
|
||
if s.ID == *series.LoserNextID {
|
||
loserAdvances = s.Label
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if loserAdvances == "" {
|
||
loserAdvances = "next round"
|
||
}
|
||
}
|
||
}}
|
||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
||
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
|
||
<h2 class="text-lg font-bold text-text">Series Info</h2>
|
||
</div>
|
||
<div class="p-4 space-y-3">
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-subtext0 w-24 shrink-0">Round</span>
|
||
<span class="text-sm font-medium text-text">{ roundDisplayName(series.Round) }</span>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-subtext0 w-24 shrink-0">Format</span>
|
||
<span class="text-sm font-medium text-text">Best of { fmt.Sprint(series.MatchesToWin*2 - 1) } (first to { fmt.Sprint(series.MatchesToWin) })</span>
|
||
</div>
|
||
if series.Team1Seed != nil && series.Team2Seed != nil {
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-subtext0 w-24 shrink-0">Seeding</span>
|
||
<span class="text-sm font-medium text-text">
|
||
{ ordinal(*series.Team1Seed) } seed vs { ordinal(*series.Team2Seed) } seed
|
||
</span>
|
||
</div>
|
||
}
|
||
if winnerAdvances != "" {
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-subtext0 w-24 shrink-0">Winner →</span>
|
||
<span class="text-sm font-medium text-green">{ winnerAdvances }</span>
|
||
</div>
|
||
} else {
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-subtext0 w-24 shrink-0">Winner →</span>
|
||
<span class="text-sm font-medium text-yellow">Champion</span>
|
||
</div>
|
||
}
|
||
if loserAdvances != "" {
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-subtext0 w-24 shrink-0">Loser →</span>
|
||
<span class="text-sm font-medium text-peach">{ loserAdvances }</span>
|
||
</div>
|
||
} else if series.WinnerNextID != nil {
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-subtext0 w-24 shrink-0">Loser →</span>
|
||
<span class="text-sm font-medium text-red">Eliminated</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
templ seriesTeamSection(team *db.Team, players []*db.PlayerWithPlayStatus, season *db.Season, league *db.League) {
|
||
{{
|
||
// Sort with managers first
|
||
sort.SliceStable(players, func(i, j int) bool {
|
||
return players[i].IsManager && !players[j].IsManager
|
||
})
|
||
}}
|
||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
||
<div class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between">
|
||
<h3 class="text-md font-bold">
|
||
@links.TeamNameLinkInSeason(team, season, league)
|
||
</h3>
|
||
if team.Color != "" {
|
||
<span
|
||
class="w-4 h-4 rounded-full border border-surface1"
|
||
style={ fmt.Sprintf("background-color: %s", team.Color) }
|
||
></span>
|
||
}
|
||
</div>
|
||
if len(players) == 0 {
|
||
<div class="p-4">
|
||
<p class="text-subtext1 text-sm text-center py-2">No players on roster.</p>
|
||
</div>
|
||
} else {
|
||
<div class="p-4">
|
||
<div class="space-y-1">
|
||
for _, p := range players {
|
||
<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-surface0 transition">
|
||
<span class="text-sm">
|
||
@links.PlayerLink(p.Player)
|
||
</span>
|
||
if p.IsManager {
|
||
<span class="px-1.5 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
|
||
★ Manager
|
||
</span>
|
||
}
|
||
if p.IsFreeAgent {
|
||
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
|
||
FREE AGENT
|
||
</span>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|