Files
oslstats/internal/view/seasonsview/season_league_team_detail.templ

716 lines
26 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "time"
templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture, available []*db.Player, scheduleMap map[int]*db.FixtureSchedule, resultMap map[int]*db.FixtureResult, record *db.TeamRecord, playerStats []*db.AggregatedPlayerStats, position int, totalTeams int) {
{{
team := twr.Team
season := twr.Season
league := twr.League
}}
@baseview.Layout(fmt.Sprintf("%s - %s - %s", team.Name, league.Name, season.Name)) {
<div class="max-w-screen-2xl mx-auto px-4 py-8">
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<!-- Header Section -->
<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 class="flex items-center gap-4">
if team.Color != "" {
<div
class="w-12 h-12 rounded-full border-2 border-surface1 shrink-0"
style={ "background-color: " + templ.SafeCSS(team.Color) }
></div>
}
<div>
<h1 class="text-4xl font-bold text-text">{ team.Name }</h1>
<div class="flex items-center gap-2 mt-2 flex-wrap">
<span class="px-2 py-1 bg-mantle rounded text-subtext0 font-mono text-sm">
{ team.ShortName }
</span>
<span class="px-2 py-1 bg-mantle rounded text-subtext0 font-mono text-sm">
{ team.AltShortName }
</span>
<span class="text-subtext1 text-sm">
{ season.Name } — { league.Name }
</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<a
href={ templ.SafeURL(fmt.Sprintf("/teams/%d", team.ID)) }
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center
bg-surface0 border border-surface1 hover:bg-surface1 text-subtext0 hover:text-text transition text-sm"
>
View All Seasons
</a>
<a
href={ templ.SafeURL(fmt.Sprintf("/seasons/%s/leagues/%s/teams", season.ShortName, league.ShortName)) }
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center
bg-surface1 hover:bg-surface2 text-text transition"
>
Back to Teams
</a>
</div>
</div>
</div>
<!-- Content -->
<div class="bg-crust p-6">
{{
// Split fixtures into upcoming and completed
var upcoming []*db.Fixture
var completed []*db.Fixture
for _, f := range fixtures {
if _, hasResult := resultMap[f.ID]; hasResult {
completed = append(completed, f)
} else {
upcoming = append(upcoming, f)
}
}
// Sort completed by scheduled time descending (most recent first)
sort.Slice(completed, func(i, j int) bool {
ti := time.Time{}
tj := time.Time{}
if si, ok := scheduleMap[completed[i].ID]; ok && si.ScheduledTime != nil {
ti = *si.ScheduledTime
}
if sj, ok := scheduleMap[completed[j].ID]; ok && sj.ScheduledTime != nil {
tj = *sj.ScheduledTime
}
return ti.After(tj)
})
// Limit to 5 most recent results
recentResults := completed
if len(recentResults) > 5 {
recentResults = recentResults[:5]
}
}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Top Left: Team Standing -->
@teamRecordCard(record, position, totalTeams)
<!-- Top Right: Results -->
@teamResultsSection(twr.Team, recentResults, resultMap)
<!-- Bottom Left: Roster -->
@TeamRosterSection(twr, available)
<!-- Bottom Right: Upcoming -->
@teamUpcomingSection(twr.Team, upcoming, scheduleMap)
</div>
<!-- Player Stats (full width) -->
<div class="mt-6">
@playerStatsSection(playerStats)
</div>
</div>
</div>
</div>
<script src="/static/vendored/sortablejs@1.15.6.min.js"></script>
}
}
// TeamRosterSection renders the roster section — exported so it can be used for HTMX swaps
templ TeamRosterSection(twr *db.TeamWithRoster, available []*db.Player) {
{{
permCache := contexts.Permissions(ctx)
canManagePlayers := permCache.HasPermission(permissions.TeamsManagePlayers)
// Build the non-manager player list for display
rosterPlayers := []*db.Player{}
for _, p := range twr.Players {
if p != nil && (twr.Manager == nil || p.ID != twr.Manager.ID) {
rosterPlayers = append(rosterPlayers, p)
}
}
hasRoster := twr.Manager != nil || len(rosterPlayers) > 0
}}
<section
id="team-roster-section"
x-data="{ showManageRosterModal: false }"
>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">Roster</h2>
if canManagePlayers {
<button
@click="showManageRosterModal = true"
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center text-sm
bg-blue hover:bg-blue/80 text-mantle transition"
>
Manage Players
</button>
}
</div>
if !hasRoster {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No players on this roster yet.</p>
if canManagePlayers {
<p class="text-subtext1 text-sm mt-2">Click "Manage Players" to add players to this team.</p>
}
</div>
} else {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden divide-y divide-surface1">
if twr.Manager != nil {
<div class="px-4 py-3 flex items-center justify-between">
<span class="font-medium">
@links.PlayerLink(twr.Manager)
</span>
<span class="text-xs px-2 py-0.5 bg-yellow/20 text-yellow rounded font-medium">
&#9733; Manager
</span>
</div>
}
for _, player := range rosterPlayers {
<div class="px-4 py-3">
@links.PlayerLink(player)
</div>
}
</div>
}
if canManagePlayers {
@manageRosterModal(twr, available, rosterPlayers)
}
</section>
}
templ manageRosterModal(twr *db.TeamWithRoster, available []*db.Player, rosterPlayers []*db.Player) {
<div
x-show="showManageRosterModal"
@keydown.escape.window="showManageRosterModal = false"
class="fixed inset-0 z-50 overflow-y-auto"
style="display: none;"
x-data="rosterManager()"
x-init="$watch('showManageRosterModal', val => { if (val) $nextTick(() => init()) })"
>
<!-- Backdrop -->
<div
class="fixed inset-0 bg-crust/80 transition-opacity"
@click="showManageRosterModal = false"
></div>
<!-- Modal -->
<div class="flex min-h-full items-center justify-center p-4">
<div
class="relative bg-mantle border-2 border-surface1 rounded-xl shadow-xl max-w-4xl w-full p-6"
@click.stop
>
<div class="flex justify-between items-center mb-6">
<h3 class="text-2xl font-bold text-text">Manage Players</h3>
<button
@click="showManageRosterModal = false"
class="text-subtext0 hover:text-text transition hover:cursor-pointer"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Two column layout -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Left: Available Players -->
<div>
<h4 class="text-lg font-semibold text-text mb-3">Available Players</h4>
<input
type="text"
x-model="search"
@input="applySearch()"
placeholder="Search players..."
class="w-full px-3 py-2 bg-base border border-surface1 rounded-lg text-text
focus:border-blue focus:outline-none mb-3 text-sm"
/>
<div
id="available-players-list"
class="bg-base border border-surface1 rounded-lg p-2 min-h-48 max-h-80 overflow-y-auto space-y-1"
>
for _, player := range available {
<div
class="roster-player-chip px-3 py-2 bg-surface0 border border-surface1 rounded
text-text text-sm cursor-grab hover:bg-surface1 transition"
data-id={ fmt.Sprint(player.ID) }
data-name={ player.Name }
>
{ player.Name }
</div>
}
</div>
<p class="text-subtext1 text-xs mt-2">Drag players to the team roster</p>
</div>
<!-- Right: Team Roster -->
<div>
<h4 class="text-lg font-semibold text-text mb-3">Team Roster</h4>
<!-- Manager slot -->
<div class="mb-4">
<label class="text-sm font-medium text-yellow mb-2 flex items-center gap-1">
&#9733; Manager
</label>
<div
id="manager-drop-zone"
class="bg-base border-2 border-dashed border-yellow/40 rounded-lg p-2 min-h-12
flex items-center justify-center"
>
if twr.Manager != nil {
<div
class="roster-player-chip px-3 py-2 bg-yellow/10 border border-yellow/30 rounded
text-text text-sm cursor-grab w-full"
data-id={ fmt.Sprint(twr.Manager.ID) }
data-name={ twr.Manager.Name }
>
{ twr.Manager.Name }
</div>
}
</div>
</div>
<!-- Players list -->
<div>
<label class="text-sm font-medium text-subtext0 mb-2 block">Players</label>
<div
id="roster-drop-zone"
class="bg-base border-2 border-dashed border-surface1 rounded-lg p-2 min-h-40
max-h-60 overflow-y-auto space-y-1"
>
for _, player := range rosterPlayers {
<div
class="roster-player-chip px-3 py-2 bg-surface0 border border-surface1 rounded
text-text text-sm cursor-grab hover:bg-surface1 transition"
data-id={ fmt.Sprint(player.ID) }
data-name={ player.Name }
>
{ player.Name }
</div>
}
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex justify-between items-center mt-6 pt-4 border-t border-surface1">
<p
x-show="!hasManager"
class="text-sm text-red"
>
A manager is required
</p>
<div class="flex gap-3 ml-auto">
<button
type="button"
@click="showManageRosterModal = false"
class="px-4 py-2 rounded-lg bg-surface0 hover:bg-surface1 text-text transition hover:cursor-pointer"
>
Cancel
</button>
<form
id="roster-submit-form"
hx-post="/teams/manage_roster"
hx-swap="none"
class="inline"
>
<input type="hidden" name="season_id" value={ fmt.Sprint(twr.Season.ID) }/>
<input type="hidden" name="league_id" value={ fmt.Sprint(twr.League.ID) }/>
<input type="hidden" name="team_id" value={ fmt.Sprint(twr.Team.ID) }/>
<input type="hidden" name="manager_id" value="0"/>
<!-- player_ids inputs are added dynamically by submitRoster() -->
<button
type="button"
@click="submitRoster()"
:disabled="!hasManager"
class="px-4 py-2 rounded-lg text-mantle transition"
:class="hasManager ? 'bg-green hover:bg-green/75 hover:cursor-pointer' : 'bg-green/40 cursor-not-allowed'"
>
Save Roster
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
function rosterManager() {
return {
search: '',
hasManager: false,
sortableInstances: [],
updateHasManager() {
const zone = document.getElementById('manager-drop-zone');
this.hasManager = zone ? zone.querySelectorAll('.roster-player-chip').length > 0 : false;
},
init() {
if (typeof Sortable === 'undefined') return;
// Destroy any previous instances
this.sortableInstances.forEach(s => s.destroy());
this.sortableInstances = [];
const self = this;
this.sortableInstances.push(new Sortable(
document.getElementById('available-players-list'), {
group: { name: 'roster', pull: true, put: true },
sort: false,
animation: 150,
onAdd(evt) { self.applySearch(); self.updateHasManager(); },
onRemove(evt) { self.applySearch(); self.updateHasManager(); }
}));
this.sortableInstances.push(new Sortable(
document.getElementById('manager-drop-zone'), {
group: { name: 'roster', pull: true, put: function(to) {
return to.el.querySelectorAll('.roster-player-chip').length === 0;
}},
sort: false,
animation: 150,
onAdd(evt) {
// Style the chip for manager zone
evt.item.classList.remove('bg-surface0', 'border-surface1');
evt.item.classList.add('bg-yellow/10', 'border-yellow/30', 'w-full');
self.updateHasManager();
},
onRemove(evt) {
// Revert style
evt.item.classList.add('bg-surface0', 'border-surface1');
evt.item.classList.remove('bg-yellow/10', 'border-yellow/30', 'w-full');
self.updateHasManager();
}
}));
this.sortableInstances.push(new Sortable(
document.getElementById('roster-drop-zone'), {
group: { name: 'roster', pull: true, put: true },
sort: true,
animation: 150,
onAdd(evt) {
// Ensure surface styling
evt.item.classList.add('bg-surface0', 'border-surface1');
evt.item.classList.remove('bg-yellow/10', 'border-yellow/30', 'w-full');
}
}));
this.updateHasManager();
this.applySearch();
},
applySearch() {
const s = this.search.toLowerCase();
const list = document.getElementById('available-players-list');
if (!list) return;
const chips = list.querySelectorAll('.roster-player-chip');
chips.forEach(chip => {
const name = (chip.dataset.name || '').toLowerCase();
if (s === '' || name.includes(s)) {
chip.style.display = '';
} else {
chip.style.display = 'none';
}
});
},
submitRoster() {
if (!this.hasManager) return;
const managerZone = document.getElementById('manager-drop-zone');
const rosterZone = document.getElementById('roster-drop-zone');
const managerChip = managerZone ? managerZone.querySelector('.roster-player-chip') : null;
const managerID = managerChip ? managerChip.dataset.id : '0';
const rosterChips = rosterZone ? rosterZone.querySelectorAll('.roster-player-chip') : [];
const playerIDs = Array.from(rosterChips).map(el => el.dataset.id);
// Build a hidden form dynamically and submit via HTMX
const form = document.getElementById('roster-submit-form');
// Clear previous dynamic inputs
form.querySelectorAll('.dynamic-input').forEach(el => el.remove());
// Set manager ID
form.querySelector('[name="manager_id"]').value = managerID;
// Add player_ids as multiple hidden inputs
playerIDs.forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'player_ids';
input.value = id;
input.className = 'dynamic-input';
form.appendChild(input);
});
// Trigger HTMX request on the form
htmx.trigger(form, 'submit');
}
}
}
</script>
}
templ teamResultsSection(team *db.Team, recentResults []*db.Fixture, resultMap map[int]*db.FixtureResult) {
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">
Results
<span class="text-sm font-normal text-subtext0">(last 5)</span>
</h2>
</div>
if len(recentResults) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No results yet.</p>
<p class="text-subtext1 text-sm mt-2">Match results will appear here once games are played.</p>
</div>
} else {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden divide-y divide-surface1">
for _, fixture := range recentResults {
@teamResultRow(team, fixture, resultMap)
}
</div>
}
</section>
}
templ teamUpcomingSection(team *db.Team, upcoming []*db.Fixture, scheduleMap map[int]*db.FixtureSchedule) {
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">Upcoming</h2>
</div>
if len(upcoming) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No upcoming fixtures.</p>
</div>
} else {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden divide-y divide-surface1">
for _, fixture := range upcoming {
@teamFixtureRow(team, fixture, scheduleMap)
}
</div>
}
</section>
}
templ teamFixtureRow(team *db.Team, fixture *db.Fixture, scheduleMap map[int]*db.FixtureSchedule) {
{{
isHome := fixture.HomeTeamID == team.ID
var opponent string
if isHome {
opponent = fixture.AwayTeam.Name
} else {
opponent = fixture.HomeTeam.Name
}
sched, hasSchedule := scheduleMap[fixture.ID]
_ = sched
}}
<a
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d", fixture.ID)) }
class="px-4 py-3 flex items-center justify-between gap-3 hover:bg-surface1 transition hover:cursor-pointer block"
>
<div class="flex items-center gap-3 min-w-0">
<span class="text-xs font-mono text-subtext0 bg-mantle px-2 py-0.5 rounded shrink-0">
GW{ fmt.Sprint(*fixture.GameWeek) }
</span>
if isHome {
<span class="text-xs px-2 py-0.5 bg-blue/20 text-blue rounded font-medium shrink-0">
HOME
</span>
} else {
<span class="text-xs px-2 py-0.5 bg-surface1 text-subtext0 rounded font-medium shrink-0">
AWAY
</span>
}
<span class="text-sm text-subtext0 shrink-0">vs</span>
<span class="text-text font-medium truncate">
{ opponent }
</span>
</div>
if hasSchedule && sched.ScheduledTime != nil {
<span class="text-xs text-green font-medium shrink-0">
@localtime(sched.ScheduledTime, "short")
</span>
} else {
<span class="text-xs text-subtext1 shrink-0">
TBD
</span>
}
</a>
}
templ teamResultRow(team *db.Team, fixture *db.Fixture, resultMap map[int]*db.FixtureResult) {
{{
isHome := fixture.HomeTeamID == team.ID
var opponent string
if isHome {
opponent = fixture.AwayTeam.Name
} else {
opponent = fixture.HomeTeam.Name
}
res := resultMap[fixture.ID]
won := (isHome && res.Winner == "home") || (!isHome && res.Winner == "away")
lost := (isHome && res.Winner == "away") || (!isHome && res.Winner == "home")
_ = lost
isForfeit := res.IsForfeit
isMutualForfeit := isForfeit && res.ForfeitType != nil && *res.ForfeitType == "mutual"
}}
<a
href={ templ.SafeURL(fmt.Sprintf("/fixtures/%d", fixture.ID)) }
class="px-4 py-3 flex items-center justify-between gap-3 hover:bg-surface1 transition hover:cursor-pointer block"
>
<div class="flex items-center gap-3 min-w-0">
if isMutualForfeit {
<span class="text-xs font-bold px-2 py-0.5 bg-peach/20 text-peach rounded shrink-0">FF</span>
} else if won {
<span class="text-xs font-bold px-2 py-0.5 bg-green/20 text-green rounded shrink-0">W</span>
} else if lost {
<span class="text-xs font-bold px-2 py-0.5 bg-red/20 text-red rounded shrink-0">L</span>
} else {
<span class="text-xs font-bold px-2 py-0.5 bg-surface1 text-subtext0 rounded shrink-0">D</span>
}
<span class="text-xs font-mono text-subtext0 bg-mantle px-2 py-0.5 rounded shrink-0">
GW{ fmt.Sprint(*fixture.GameWeek) }
</span>
if isHome {
<span class="text-xs px-2 py-0.5 bg-blue/20 text-blue rounded font-medium shrink-0">
HOME
</span>
} else {
<span class="text-xs px-2 py-0.5 bg-surface1 text-subtext0 rounded font-medium shrink-0">
AWAY
</span>
}
<span class="text-sm text-subtext0 shrink-0">vs</span>
<span class="text-text font-medium truncate">
{ opponent }
</span>
</div>
if isForfeit {
<span class="flex items-center gap-2 shrink-0">
if isMutualForfeit {
<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 shrink-0">
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>
}
</a>
}
templ teamRecordCard(record *db.TeamRecord, position int, totalTeams int) {
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">Standing</h2>
</div>
if record.Played == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No games played yet.</p>
</div>
} else {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
<!-- Position & Points Header -->
<div class="flex items-center justify-between px-6 py-5 border-b border-surface1">
<div class="flex items-center gap-3">
<span class="text-4xl font-bold text-text">{ ordinal(position) }</span>
<div>
<p class="text-xs text-subtext0 uppercase font-medium">Position</p>
<p class="text-sm text-subtext1">of { fmt.Sprint(totalTeams) } teams</p>
</div>
</div>
<div class="text-right">
<p class="text-xs text-subtext0 uppercase font-medium">Points</p>
<p class="text-3xl font-bold text-blue">{ fmt.Sprint(record.Points) }</p>
</div>
</div>
<!-- Record Grid -->
<div class="grid grid-cols-4 divide-x divide-surface1">
@statCell("W", fmt.Sprint(record.Wins), "text-green")
@statCell("OTW", fmt.Sprint(record.OvertimeWins), "text-teal")
@statCell("OTL", fmt.Sprint(record.OvertimeLosses), "text-peach")
@statCell("L", fmt.Sprint(record.Losses), "text-red")
</div>
<!-- Goals Row -->
<div class="grid grid-cols-3 divide-x divide-surface1 border-t border-surface1">
@statCell("Played", fmt.Sprint(record.Played), "")
@statCell("GF", fmt.Sprint(record.GoalsFor), "")
@statCell("GA", fmt.Sprint(record.GoalsAgainst), "")
</div>
</div>
}
</section>
}
templ playerStatsSection(playerStats []*db.AggregatedPlayerStats) {
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-text">Player Stats</h2>
</div>
if len(playerStats) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No player stats yet.</p>
<p class="text-subtext1 text-sm mt-2">Player statistics will appear here once games are played.</p>
</div>
} else {
<div class="bg-surface0 border border-surface1 rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-mantle 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="Games Played">GP</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="Score">SC</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>
</tr>
</thead>
<tbody class="divide-y divide-surface1">
for _, ps := range playerStats {
<tr class="hover:bg-surface1 transition-colors">
<td class="px-3 py-2 text-sm">
@links.PlayerLinkFromStats(ps.PlayerID, ps.PlayerName)
</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.GamesPlayed) }</td>
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.PeriodsPlayed) }</td>
<td class="px-2 py-2 text-center text-sm font-medium text-text">{ fmt.Sprint(ps.Score) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Goals) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Assists) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Saves) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Shots) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Blocks) }</td>
<td class="px-2 py-2 text-center text-sm text-text">{ fmt.Sprint(ps.Passes) }</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</section>
}
templ statCell(label string, value string, valueColor string) {
<div class="px-4 py-3 text-center">
<p class="text-xs text-subtext0 font-medium uppercase mb-1">{ label }</p>
<p class={ "text-lg font-bold", templ.KV("text-text", valueColor == ""), templ.KV(valueColor, valueColor != "") }>
{ value }
</p>
</div>
}