added finals log uploads
This commit is contained in:
@@ -216,9 +216,8 @@ templ SeriesDetailOverviewContent(
|
||||
{{
|
||||
permCache := contexts.Permissions(ctx)
|
||||
canManage := permCache.HasPermission(permissions.PlayoffsManage)
|
||||
_ = canManage
|
||||
}}
|
||||
@seriesOverviewTab(series, currentSchedule, rosters, canSchedule, userTeamID)
|
||||
@seriesOverviewTab(series, currentSchedule, rosters, canSchedule, canManage, userTeamID)
|
||||
}
|
||||
|
||||
templ SeriesDetailPreviewContent(
|
||||
@@ -257,8 +256,15 @@ templ seriesOverviewTab(
|
||||
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">
|
||||
@@ -270,6 +276,11 @@ templ seriesOverviewTab(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Prompt (for admins when series is in progress) -->
|
||||
if showUploadPrompt {
|
||||
@seriesUploadPrompt(series)
|
||||
}
|
||||
|
||||
<!-- Match List -->
|
||||
if len(series.Matches) > 0 {
|
||||
@seriesMatchList(series)
|
||||
@@ -290,6 +301,44 @@ templ seriesOverviewTab(
|
||||
</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
|
||||
|
||||
374
internal/view/seasonsview/series_review_result.templ
Normal file
374
internal/view/seasonsview/series_review_result.templ
Normal file
@@ -0,0 +1,374 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/component/links"
|
||||
import "fmt"
|
||||
|
||||
// SeriesGameResult holds the parsed result for a single game in the series review
|
||||
type SeriesGameResult struct {
|
||||
GameNumber int
|
||||
Result *db.FixtureResult
|
||||
UnmappedPlayers []string
|
||||
FreeAgentWarnings []FreeAgentWarning
|
||||
}
|
||||
|
||||
templ SeriesReviewResultPage(
|
||||
series *db.PlayoffSeries,
|
||||
gameResults []*SeriesGameResult,
|
||||
) {
|
||||
{{
|
||||
backURL := fmt.Sprintf("/series/%d", series.ID)
|
||||
team1Name := seriesTeamName(series.Team1)
|
||||
team2Name := seriesTeamName(series.Team2)
|
||||
|
||||
// Calculate series score from the results
|
||||
team1Wins := 0
|
||||
team2Wins := 0
|
||||
for _, gr := range gameResults {
|
||||
if gr.Result != nil {
|
||||
if gr.Result.Winner == "home" {
|
||||
team1Wins++
|
||||
} else {
|
||||
team2Wins++
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
@baseview.Layout(fmt.Sprintf("Review Series Result — %s vs %s", team1Name, team2Name)) {
|
||||
<div class="max-w-screen-xl 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-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-text mb-1">Review Series Result</h1>
|
||||
<p class="text-sm text-subtext1">
|
||||
{ team1Name } vs { team2Name }
|
||||
<span class="text-subtext0 ml-1">{ series.Label }</span>
|
||||
</p>
|
||||
</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 Series
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Series Score Summary -->
|
||||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden mb-6">
|
||||
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
|
||||
<h2 class="text-lg font-bold text-text">Series Result</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-center gap-8 py-2">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
if series.Team1 != nil && series.Team1.Color != "" {
|
||||
<div
|
||||
class="w-10 h-10 rounded-full border-2 border-surface1 mb-2 shrink-0"
|
||||
style={ "background-color: " + templ.SafeCSS(series.Team1.Color) }
|
||||
></div>
|
||||
}
|
||||
<p class="text-sm font-medium text-subtext0 mb-1">{ team1Name }</p>
|
||||
<p class={ "text-5xl font-bold", templ.KV("text-green", team1Wins > team2Wins), templ.KV("text-text", team1Wins <= team2Wins) }>
|
||||
{ fmt.Sprint(team1Wins) }
|
||||
</p>
|
||||
if team1Wins > team2Wins {
|
||||
<span class="mt-1 px-2 py-0.5 bg-green/20 text-green rounded-full text-xs font-bold uppercase tracking-wider">Winner</span>
|
||||
}
|
||||
</div>
|
||||
<span class="text-3xl text-subtext0 font-light">–</span>
|
||||
<div class="flex flex-col items-center text-center">
|
||||
if series.Team2 != nil && series.Team2.Color != "" {
|
||||
<div
|
||||
class="w-10 h-10 rounded-full border-2 border-surface1 mb-2 shrink-0"
|
||||
style={ "background-color: " + templ.SafeCSS(series.Team2.Color) }
|
||||
></div>
|
||||
}
|
||||
<p class="text-sm font-medium text-subtext0 mb-1">{ team2Name }</p>
|
||||
<p class={ "text-5xl font-bold", templ.KV("text-green", team2Wins > team1Wins), templ.KV("text-text", team2Wins <= team1Wins) }>
|
||||
{ fmt.Sprint(team2Wins) }
|
||||
</p>
|
||||
if team2Wins > team1Wins {
|
||||
<span class="mt-1 px-2 py-0.5 bg-green/20 text-green rounded-full text-xs font-bold uppercase tracking-wider">Winner</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center text-sm text-subtext1 mt-3">
|
||||
{ fmt.Sprint(len(gameResults)) } game(s) played
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Per-Game Results -->
|
||||
<div class="space-y-4 mb-6">
|
||||
for _, gr := range gameResults {
|
||||
@seriesReviewGameCard(series, gr)
|
||||
}
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<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">Actions</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
hx-post={ fmt.Sprintf("/series/%d/results/finalize", series.ID) }
|
||||
hx-swap="none"
|
||||
class="px-6 py-3 bg-green hover:bg-green/75 text-mantle rounded-lg
|
||||
font-semibold transition hover:cursor-pointer text-lg"
|
||||
>
|
||||
Finalize Series
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click={ fmt.Sprintf("window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Discard All Results', message: 'Are you sure you want to discard all uploaded results? You will need to re-upload the match logs for every game.', action: () => htmx.ajax('POST', '/series/%d/results/discard', { swap: 'none' }) } }))", series.ID) }
|
||||
class="px-6 py-3 bg-red hover:bg-red/80 text-mantle rounded-lg
|
||||
font-medium transition hover:cursor-pointer text-lg"
|
||||
>
|
||||
Discard & Re-upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ seriesReviewGameCard(series *db.PlayoffSeries, gr *SeriesGameResult) {
|
||||
{{
|
||||
team1Name := seriesTeamName(series.Team1)
|
||||
team2Name := seriesTeamName(series.Team2)
|
||||
result := gr.Result
|
||||
homeWon := result.Winner == "home"
|
||||
winnerName := team2Name
|
||||
if homeWon {
|
||||
winnerName = team1Name
|
||||
}
|
||||
hasWarnings := result.TamperingDetected || len(gr.UnmappedPlayers) > 0 || len(gr.FreeAgentWarnings) > 0
|
||||
}}
|
||||
<div
|
||||
class="bg-mantle border border-surface1 rounded-lg overflow-hidden"
|
||||
x-data="{ expanded: true }"
|
||||
>
|
||||
<!-- Game Header (clickable to expand/collapse) -->
|
||||
<div
|
||||
class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between
|
||||
hover:bg-surface1 transition hover:cursor-pointer"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-md font-bold text-text">Game { fmt.Sprint(gr.GameNumber) }</h3>
|
||||
if hasWarnings {
|
||||
<span class="text-yellow text-sm" title="Has warnings">⚠</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-subtext0">
|
||||
{ team1Name }
|
||||
<span class="font-bold text-text mx-1">{ fmt.Sprint(result.HomeScore) }</span>
|
||||
-
|
||||
<span class="font-bold text-text mx-1">{ fmt.Sprint(result.AwayScore) }</span>
|
||||
{ team2Name }
|
||||
</span>
|
||||
<span class="px-2 py-0.5 bg-green/20 text-green rounded text-xs font-medium">
|
||||
{ winnerName }
|
||||
</span>
|
||||
<!-- Expand/collapse indicator -->
|
||||
<svg
|
||||
class="w-4 h-4 text-subtext0 transition-transform"
|
||||
:class="expanded && 'rotate-180'"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Collapsible Content -->
|
||||
<div x-show="expanded" x-collapse>
|
||||
<!-- Warnings -->
|
||||
if hasWarnings {
|
||||
<div class="p-4 space-y-3 border-b border-surface1">
|
||||
if result.TamperingDetected && result.TamperingReason != nil {
|
||||
<div class="bg-red/10 border border-red/30 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-red font-bold text-sm">⚠ Inconsistent Data Detected</span>
|
||||
</div>
|
||||
<p class="text-red/80 text-sm">{ *result.TamperingReason }</p>
|
||||
<p class="text-red/60 text-xs mt-1">
|
||||
This does not block finalization but should be reviewed carefully.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
if len(gr.FreeAgentWarnings) > 0 {
|
||||
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-yellow font-bold text-sm">⚠ Free Agent Issues</span>
|
||||
</div>
|
||||
<ul class="list-disc list-inside text-yellow/70 text-xs space-y-0.5">
|
||||
for _, fa := range gr.FreeAgentWarnings {
|
||||
<li>
|
||||
<span class="text-yellow font-medium">{ fa.Name }</span>
|
||||
<span class="text-yellow/60"> — { fa.Reason }</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
if len(gr.UnmappedPlayers) > 0 {
|
||||
<div class="bg-yellow/10 border border-yellow/30 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-yellow font-bold text-sm">⚠ Unmapped Players</span>
|
||||
</div>
|
||||
<p class="text-yellow/80 text-sm mb-1">
|
||||
Could not be matched to registered players.
|
||||
</p>
|
||||
<ul class="list-disc list-inside text-yellow/70 text-xs space-y-0.5">
|
||||
for _, p := range gr.UnmappedPlayers {
|
||||
<li>{ p }</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Score Display -->
|
||||
<div class="p-6 border-b border-surface1">
|
||||
<div class="flex items-center justify-center gap-8 py-2">
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-subtext0 mb-1">{ team1Name }</p>
|
||||
<p class={ "text-4xl font-bold", templ.KV("text-green", homeWon), templ.KV("text-text", !homeWon) }>
|
||||
{ fmt.Sprint(result.HomeScore) }
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-2xl text-subtext0 font-light">—</div>
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-subtext0 mb-1">{ team2Name }</p>
|
||||
<p class={ "text-4xl font-bold", templ.KV("text-green", !homeWon), templ.KV("text-text", homeWon) }>
|
||||
{ fmt.Sprint(result.AwayScore) }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-center gap-4 mt-2 text-xs text-subtext1">
|
||||
if result.Arena != "" {
|
||||
<span>{ result.Arena }</span>
|
||||
}
|
||||
if result.EndReason != "" {
|
||||
<span>{ result.EndReason }</span>
|
||||
}
|
||||
<span>
|
||||
Winner: { winnerName }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Player Stats Tables -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-0 lg:divide-x divide-surface1">
|
||||
if series.Team1 != nil {
|
||||
@seriesReviewTeamStats(series.Team1, result, "home", series.Bracket.Season, series.Bracket.League)
|
||||
}
|
||||
if series.Team2 != nil {
|
||||
@seriesReviewTeamStats(series.Team2, result, "away", series.Bracket.Season, series.Bracket.League)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ seriesReviewTeamStats(team *db.Team, result *db.FixtureResult, side string, season *db.Season, league *db.League) {
|
||||
{{
|
||||
type playerStat struct {
|
||||
Username string
|
||||
PlayerID *int
|
||||
Stats *db.FixtureResultPlayerStats
|
||||
}
|
||||
finalStats := []*playerStat{}
|
||||
seen := map[string]bool{}
|
||||
for _, ps := range result.PlayerStats {
|
||||
if ps.Team == side && ps.PeriodNum == 3 {
|
||||
if !seen[ps.PlayerGameUserID] {
|
||||
seen[ps.PlayerGameUserID] = true
|
||||
finalStats = append(finalStats, &playerStat{
|
||||
Username: ps.PlayerUsername,
|
||||
PlayerID: ps.PlayerID,
|
||||
Stats: ps,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
<div>
|
||||
<div class="bg-surface0 border-b border-surface1 px-4 py-2 flex items-center gap-2">
|
||||
if team.Color != "" {
|
||||
<span
|
||||
class="w-3 h-3 rounded-full shrink-0"
|
||||
style={ "background-color: " + templ.SafeCSS(team.Color) }
|
||||
></span>
|
||||
}
|
||||
<h4 class="text-sm font-bold text-text">
|
||||
if side == "home" {
|
||||
Team 1 —
|
||||
} else {
|
||||
Team 2 —
|
||||
}
|
||||
@links.TeamNameLinkInSeason(team, season, league)
|
||||
</h4>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-surface0 border-b border-surface1">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs font-semibold text-text">Player</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Periods Played">PP</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Goals">G</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Assists">A</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Saves">SV</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Shots">SH</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Blocks">BL</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Passes">PA</th>
|
||||
<th class="px-2 py-2 text-center text-xs font-semibold text-text" title="Score">SC</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-surface1">
|
||||
for _, ps := range finalStats {
|
||||
<tr class="hover:bg-surface0 transition-colors">
|
||||
<td class="px-3 py-2 text-sm">
|
||||
<span class="flex items-center gap-1.5">
|
||||
if ps.PlayerID != nil {
|
||||
@links.PlayerLinkFromStats(*ps.PlayerID, ps.Username)
|
||||
} else {
|
||||
<span class="text-text">{ ps.Username }</span>
|
||||
<span class="text-yellow text-xs" title="Unmapped player">?</span>
|
||||
}
|
||||
if ps.Stats.IsFreeAgent {
|
||||
<span class="px-1.5 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
|
||||
FA
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-subtext0">{ intPtrStr(ps.Stats.PeriodsPlayed) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Goals) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Assists) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Saves) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Shots) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Blocks) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Passes) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-text">{ intPtrStr(ps.Stats.Score) }</td>
|
||||
</tr>
|
||||
}
|
||||
if len(finalStats) == 0 {
|
||||
<tr>
|
||||
<td colspan="9" class="px-3 py-4 text-center text-sm text-subtext1">
|
||||
No player stats recorded
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
133
internal/view/seasonsview/series_upload_result.templ
Normal file
133
internal/view/seasonsview/series_upload_result.templ
Normal file
@@ -0,0 +1,133 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||
import "fmt"
|
||||
|
||||
templ SeriesUploadResultPage(series *db.PlayoffSeries) {
|
||||
{{
|
||||
backURL := fmt.Sprintf("/series/%d", series.ID)
|
||||
team1Name := seriesTeamName(series.Team1)
|
||||
team2Name := seriesTeamName(series.Team2)
|
||||
boLabel := fmt.Sprintf("BO%d", series.MatchesToWin*2-1)
|
||||
maxGames := series.MatchesToWin*2 - 1
|
||||
minGames := series.MatchesToWin
|
||||
}}
|
||||
@baseview.Layout(fmt.Sprintf("Upload Series Result — %s vs %s", team1Name, team2Name)) {
|
||||
<div class="max-w-screen-md 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-6">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-text mb-1">Upload Series Results</h1>
|
||||
<p class="text-sm text-subtext1">
|
||||
{ team1Name } vs { team2Name }
|
||||
<span class="text-subtext0 ml-1">
|
||||
{ series.Label } · { boLabel }
|
||||
</span>
|
||||
</p>
|
||||
</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"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload Form -->
|
||||
<div
|
||||
class="bg-mantle border border-surface1 rounded-lg overflow-hidden"
|
||||
x-data={ fmt.Sprintf("{ gameCount: %d }", minGames) }
|
||||
>
|
||||
<div class="bg-surface0 border-b border-surface1 px-4 py-3">
|
||||
<h2 class="text-lg font-bold text-text">Match Log Files</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<p class="text-sm text-subtext1 mb-6">
|
||||
Upload the 3 period match log JSON files for each game in the series.
|
||||
Select the number of games that were actually played.
|
||||
</p>
|
||||
<form
|
||||
hx-post={ fmt.Sprintf("/series/%d/results/upload", series.ID) }
|
||||
hx-swap="none"
|
||||
hx-encoding="multipart/form-data"
|
||||
class="space-y-6"
|
||||
>
|
||||
<!-- Game Count Selector -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text mb-2">
|
||||
Number of Games Played
|
||||
</label>
|
||||
<select
|
||||
name="game_count"
|
||||
x-model="gameCount"
|
||||
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
|
||||
focus:border-blue focus:outline-none hover:cursor-pointer"
|
||||
>
|
||||
for g := minGames; g <= maxGames; g++ {
|
||||
<option
|
||||
value={ fmt.Sprint(g) }
|
||||
if g == minGames {
|
||||
selected
|
||||
}
|
||||
>
|
||||
{ fmt.Sprint(g) } games
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
<p class="text-xs text-subtext0 mt-1">
|
||||
First team to { fmt.Sprint(series.MatchesToWin) } wins takes the series
|
||||
({ fmt.Sprint(minGames) }-{ fmt.Sprint(maxGames) } games possible)
|
||||
</p>
|
||||
</div>
|
||||
<!-- Per-Game File Inputs -->
|
||||
for g := 1; g <= maxGames; g++ {
|
||||
<div
|
||||
x-show={ fmt.Sprintf("gameCount >= %d", g) }
|
||||
x-cloak
|
||||
class="border border-surface1 rounded-lg overflow-hidden"
|
||||
>
|
||||
<div class="bg-surface0 border-b border-surface1 px-4 py-2">
|
||||
<h3 class="text-md font-semibold text-text">Game { fmt.Sprint(g) }</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4">
|
||||
for p := 1; p <= 3; p++ {
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-subtext0 mb-1">
|
||||
Period { fmt.Sprint(p) }
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
name={ fmt.Sprintf("game_%d_period_%d", g, p) }
|
||||
accept=".json"
|
||||
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
|
||||
file:mr-4 file:py-1 file:px-3 file:rounded file:border-0
|
||||
file:text-sm file:font-medium file:bg-blue file:text-mantle
|
||||
file:hover:bg-blue/80 file:hover:cursor-pointer file:transition
|
||||
focus:border-blue focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Submit -->
|
||||
<div class="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full px-4 py-3 bg-blue hover:bg-blue/80 text-mantle rounded-lg
|
||||
font-medium transition hover:cursor-pointer text-lg"
|
||||
>
|
||||
Upload & Validate All Games
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user