201 lines
6.5 KiB
Plaintext
201 lines
6.5 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 "fmt"
|
||
import "sort"
|
||
import "time"
|
||
|
||
templ SeasonLeagueFixturesPage(season *db.Season, league *db.League, fixtures []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult) {
|
||
@SeasonLeagueLayout("fixtures", season, league) {
|
||
@SeasonLeagueFixtures(season, league, fixtures, scheduleMap, resultMap)
|
||
}
|
||
}
|
||
|
||
templ SeasonLeagueFixtures(season *db.Season, league *db.League, fixtures []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult) {
|
||
{{
|
||
permCache := contexts.Permissions(ctx)
|
||
canManage := permCache.HasPermission(permissions.FixturesManage)
|
||
|
||
// Group fixtures by game week (only allocated ones)
|
||
type gameWeekGroup struct {
|
||
Week int
|
||
Fixtures []*db.Fixture
|
||
}
|
||
groups := []gameWeekGroup{}
|
||
groupMap := map[int]int{} // week -> index in groups
|
||
for _, f := range fixtures {
|
||
if f.GameWeek == nil {
|
||
continue
|
||
}
|
||
idx, exists := groupMap[*f.GameWeek]
|
||
if !exists {
|
||
idx = len(groups)
|
||
groupMap[*f.GameWeek] = idx
|
||
groups = append(groups, gameWeekGroup{Week: *f.GameWeek, Fixtures: []*db.Fixture{}})
|
||
}
|
||
groups[idx].Fixtures = append(groups[idx].Fixtures, f)
|
||
}
|
||
|
||
// Sort fixtures within each group by scheduled time
|
||
// Scheduled fixtures first (by time), then TBD last
|
||
farFuture := time.Date(9999, 1, 1, 0, 0, 0, 0, time.UTC)
|
||
for i := range groups {
|
||
sort.Slice(groups[i].Fixtures, func(a, b int) bool {
|
||
ta := farFuture
|
||
tb := farFuture
|
||
if sa, ok := scheduleMap[groups[i].Fixtures[a].ID]; ok && sa.ScheduledTime != nil {
|
||
ta = *sa.ScheduledTime
|
||
}
|
||
if sb, ok := scheduleMap[groups[i].Fixtures[b].ID]; ok && sb.ScheduledTime != nil {
|
||
tb = *sb.ScheduledTime
|
||
}
|
||
return ta.Before(tb)
|
||
})
|
||
}
|
||
}}
|
||
<div>
|
||
if canManage {
|
||
<div class="flex justify-end mb-4">
|
||
<a
|
||
href={ templ.SafeURL(fmt.Sprintf("/seasons/%s/leagues/%s/fixtures/manage", season.ShortName, league.ShortName)) }
|
||
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center text-sm
|
||
bg-blue hover:bg-blue/80 text-mantle transition"
|
||
>
|
||
Manage Fixtures
|
||
</a>
|
||
</div>
|
||
}
|
||
if len(groups) == 0 {
|
||
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
|
||
<p class="text-subtext0 text-lg">No fixtures scheduled yet.</p>
|
||
</div>
|
||
} else {
|
||
<div class="space-y-4">
|
||
for _, group := range groups {
|
||
{{
|
||
playedCount := 0
|
||
for _, f := range group.Fixtures {
|
||
if res, ok := resultMap[f.ID]; ok && res.Finalized {
|
||
playedCount++
|
||
}
|
||
}
|
||
hasPlayed := playedCount > 0
|
||
allPlayed := playedCount == len(group.Fixtures)
|
||
}}
|
||
<div
|
||
class="bg-surface0 border border-surface1 rounded-lg overflow-hidden"
|
||
x-data="{ showPlayed: false }"
|
||
>
|
||
<div class="bg-mantle border-b border-surface1 px-4 py-3 flex items-center justify-between">
|
||
<h3 class="text-lg font-bold text-text">Game Week { fmt.Sprint(group.Week) }</h3>
|
||
if hasPlayed {
|
||
<button
|
||
type="button"
|
||
@click="showPlayed = !showPlayed"
|
||
class="text-xs px-2.5 py-1 rounded-lg transition cursor-pointer
|
||
bg-surface1 hover:bg-surface2 text-subtext0 hover:text-text"
|
||
>
|
||
<span x-show="!showPlayed">Show played</span>
|
||
<span x-show="showPlayed" x-cloak>Hide played</span>
|
||
</button>
|
||
}
|
||
</div>
|
||
<div class="divide-y divide-surface1">
|
||
for _, fixture := range group.Fixtures {
|
||
{{
|
||
sched, hasSchedule := scheduleMap[fixture.ID]
|
||
_ = sched
|
||
res, hasResult := resultMap[fixture.ID]
|
||
_ = res
|
||
isPlayed := hasResult && res.Finalized
|
||
}}
|
||
if isPlayed {
|
||
<a
|
||
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d", fixture.ID)) }
|
||
x-show="showPlayed"
|
||
x-cloak
|
||
class="px-4 py-3 flex items-center justify-between hover:bg-surface1 transition hover:cursor-pointer block"
|
||
>
|
||
@fixtureListItem(fixture, sched, hasSchedule, res, hasResult)
|
||
</a>
|
||
} else {
|
||
<a
|
||
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d", fixture.ID)) }
|
||
class="px-4 py-3 flex items-center justify-between hover:bg-surface1 transition hover:cursor-pointer block"
|
||
>
|
||
@fixtureListItem(fixture, sched, hasSchedule, res, hasResult)
|
||
</a>
|
||
}
|
||
}
|
||
</div>
|
||
if allPlayed {
|
||
<div
|
||
x-show="!showPlayed"
|
||
class="px-4 py-3 text-center text-xs text-subtext1 italic"
|
||
>
|
||
All fixtures played
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
templ fixtureListItem(fixture *db.Fixture, sched *db.FixtureSchedule, hasSchedule bool, res *db.FixtureResult, hasResult bool) {
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-xs font-mono text-subtext0 bg-mantle px-2 py-0.5 rounded">
|
||
R{ fmt.Sprint(fixture.Round) }
|
||
</span>
|
||
<span class="text-text">
|
||
{ fixture.HomeTeam.Name }
|
||
</span>
|
||
<span class="text-subtext0 text-sm">vs</span>
|
||
<span class="text-text">
|
||
{ fixture.AwayTeam.Name }
|
||
</span>
|
||
</div>
|
||
if hasResult {
|
||
if res.IsForfeit {
|
||
<span class="flex items-center gap-2">
|
||
if res.ForfeitType != nil && *res.ForfeitType == "mutual" {
|
||
<span class="px-2 py-0.5 bg-peach/20 text-peach rounded text-xs font-medium">
|
||
Mutual Forfeit
|
||
</span>
|
||
} else {
|
||
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
|
||
Forfeit
|
||
</span>
|
||
}
|
||
</span>
|
||
} else {
|
||
<span class="flex items-center gap-2">
|
||
if res.Winner == "home" {
|
||
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||
<span class="text-xs text-subtext0">–</span>
|
||
<span class="text-sm text-subtext0">{ fmt.Sprint(res.AwayScore) }</span>
|
||
} else if res.Winner == "away" {
|
||
<span class="text-sm text-subtext0">{ fmt.Sprint(res.HomeScore) }</span>
|
||
<span class="text-xs text-subtext0">–</span>
|
||
<span class="text-sm font-bold text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||
} else {
|
||
<span class="text-sm text-text">{ fmt.Sprint(res.HomeScore) }</span>
|
||
<span class="text-xs text-subtext0">–</span>
|
||
<span class="text-sm text-text">{ fmt.Sprint(res.AwayScore) }</span>
|
||
}
|
||
</span>
|
||
}
|
||
} else if hasSchedule && sched.ScheduledTime != nil {
|
||
<span class="text-xs text-green font-medium">
|
||
@localtime(sched.ScheduledTime, "short")
|
||
</span>
|
||
} else {
|
||
<span class="text-xs text-subtext1">
|
||
TBD
|
||
</span>
|
||
}
|
||
}
|