added player stats to profile

This commit is contained in:
2026-03-06 20:48:21 +11:00
parent 71181c43e9
commit e99f10d0f4
10 changed files with 817 additions and 78 deletions

View File

@@ -4,35 +4,94 @@ import "git.haelnorr.com/h/oslstats/internal/db"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
import "fmt"
templ PlayerPage(player *db.Player, isOwner bool) {
templ PlayerLayout(activeSection string, player *db.Player, isOwner bool) {
@baseview.Layout(player.DisplayName() + " - Player Profile") {
<div class="max-w-screen-xl mx-auto px-4 py-8">
<div class="max-w-screen-2xl mx-auto px-4 py-8">
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
<!-- Header -->
<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>
<h1 class="text-4xl font-bold text-text">{ player.DisplayName() }</h1>
<div class="flex items-center gap-2 mt-2 flex-wrap">
if player.SlapID != nil {
<span class="px-2 py-1 bg-mantle rounded text-subtext0 font-mono text-sm">
Slapshot ID: { fmt.Sprintf("%d", *player.SlapID) }
</span>
}
<div class="flex items-center gap-3 mb-2">
<h1 class="text-4xl font-bold text-text">{ player.DisplayName() }</h1>
if isOwner {
<span class="px-2 py-0.5 bg-blue/20 text-blue rounded text-xs font-medium">
Your Profile
</span>
}
</div>
<div class="flex items-center gap-2 flex-wrap">
if player.SlapID != nil {
<span class="px-2 py-1 bg-mantle rounded text-subtext0 font-mono text-sm">
Slapshot ID: { fmt.Sprintf("%d", *player.SlapID) }
</span>
}
</div>
</div>
</div>
</div>
<!-- Content -->
<div class="p-6">
@SlapIDSection(player, isOwner)
</div>
<!-- SlapID Link Prompt (if needed) -->
if player.SlapID == nil && isOwner {
<div class="px-6 pt-6">
@SlapIDSection(player, isOwner)
</div>
}
<!-- Tab Navigation -->
<nav class="bg-surface0 border-b border-surface1" data-tab-nav="player-content">
<ul class="flex flex-wrap">
@playerNavItem("stats", "Stats", activeSection, player)
@playerNavItem("teams", "Teams", activeSection, player)
@playerNavItem("seasons", "Seasons", activeSection, player)
</ul>
</nav>
<!-- Content Area -->
<main class="bg-crust p-6" id="player-content">
{ children... }
</main>
</div>
</div>
<script src="/static/js/tabs.js" defer></script>
}
}
templ playerNavItem(section string, label string, activeSection string, player *db.Player) {
{{
isActive := section == activeSection
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("/players/%d/%s", player.ID, section)
}}
<li class="inline-block">
<a
href={ templ.SafeURL(url) }
hx-post={ url }
hx-target="#player-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 PlayerStatsPage(player *db.Player, isOwner bool, stats *db.PlayerAllTimeStats, seasons []*db.Season, teams []*db.Team) {
@PlayerLayout("stats", player, isOwner) {
@PlayerStatsTab(player, stats, seasons, teams, "", 0)
}
}
templ PlayerTeamsPage(player *db.Player, isOwner bool, teamInfos []*db.PlayerTeamInfo) {
@PlayerLayout("teams", player, isOwner) {
@PlayerTeamsTab(teamInfos)
}
}
templ PlayerSeasonsPage(player *db.Player, isOwner bool, seasonInfos []*db.PlayerSeasonInfo) {
@PlayerLayout("seasons", player, isOwner) {
@PlayerSeasonsTab(seasonInfos)
}
}

View File

@@ -0,0 +1,73 @@
package playersview
import "git.haelnorr.com/h/oslstats/internal/db"
import "fmt"
templ PlayerSeasonsTab(seasonInfos []*db.PlayerSeasonInfo) {
if len(seasonInfos) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No season history yet.</p>
<p class="text-subtext1 text-sm mt-2">This player has not participated in any seasons.</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-4 py-3 text-left text-sm font-semibold text-text">Season</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-text">League</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-text">Team</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-text">Role</th>
</tr>
</thead>
<tbody class="divide-y divide-surface1">
for _, info := range seasonInfos {
<tr class="hover:bg-surface1 transition-colors">
<td class="px-4 py-3 text-sm">
<a
href={ templ.SafeURL(fmt.Sprintf("/seasons/%s", info.Season.ShortName)) }
class="text-blue hover:text-blue/80 transition"
>
{ info.Season.Name }
</a>
</td>
<td class="px-4 py-3 text-sm text-subtext0">
{ info.League.Name }
</td>
<td class="px-4 py-3 text-sm">
<a
href={ templ.SafeURL(fmt.Sprintf(
"/seasons/%s/leagues/%s/teams/%d",
info.Season.ShortName, info.League.ShortName, info.Team.ID,
)) }
class="text-blue hover:text-blue/80 transition"
>
<div class="flex items-center gap-2">
if info.Team.Color != "" {
<div
class="w-3 h-3 rounded-full border border-surface1 shrink-0"
style={ "background-color: " + templ.SafeCSS(info.Team.Color) }
></div>
}
<span>{ info.Team.Name }</span>
</div>
</a>
</td>
<td class="px-4 py-3 text-sm text-center">
if info.IsManager {
<span class="px-2 py-0.5 bg-yellow/20 text-yellow rounded text-xs font-medium">
Manager
</span>
} else {
<span class="text-subtext1 text-xs">Player</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
}

View File

@@ -0,0 +1,130 @@
package playersview
import "git.haelnorr.com/h/oslstats/internal/db"
import "fmt"
templ PlayerStatsTab(player *db.Player, stats *db.PlayerAllTimeStats, seasons []*db.Season, teams []*db.Team, activeFilter string, activeFilterID int) {
<div class="space-y-6" data-filter-url={ fmt.Sprintf("/players/%d/stats/filter", player.ID) }>
<!-- Filter Controls -->
<div class="flex flex-col sm:flex-row gap-4">
<!-- Season Filter -->
<div class="flex-1">
<label class="block text-xs text-subtext0 uppercase font-medium mb-1">Filter by Season</label>
<select
name="season_id"
class="w-full px-3 py-2 bg-mantle border border-surface1 rounded text-text focus:border-blue focus:outline-none hover:cursor-pointer"
onchange={ handleFilterChange("season") }
>
<option value="">All Seasons</option>
for _, s := range seasons {
<option
value={ fmt.Sprint(s.ID) }
selected?={ activeFilter == "season" && activeFilterID == s.ID }
>
{ s.Name }
</option>
}
</select>
</div>
<!-- Team Filter -->
<div class="flex-1">
<label class="block text-xs text-subtext0 uppercase font-medium mb-1">Filter by Team</label>
<select
name="team_id"
class="w-full px-3 py-2 bg-mantle border border-surface1 rounded text-text focus:border-blue focus:outline-none hover:cursor-pointer"
onchange={ handleFilterChange("team") }
>
<option value="">All Teams</option>
for _, t := range teams {
<option
value={ fmt.Sprint(t.ID) }
selected?={ activeFilter == "team" && activeFilterID == t.ID }
>
{ t.Name }
</option>
}
</select>
</div>
</div>
<!-- Filter Label -->
<div class="text-sm text-subtext0">
if activeFilter == "" {
Showing <span class="text-text font-medium">All-Time</span> stats
} else if activeFilter == "season" {
Showing stats for season:
<span class="text-text font-medium">
{ getSeasonName(seasons, activeFilterID) }
</span>
} else if activeFilter == "team" {
Showing stats for team:
<span class="text-text font-medium">
{ getTeamName(teams, activeFilterID) }
</span>
}
</div>
<!-- Stats Grid -->
@playerStatsGrid(stats)
</div>
}
templ playerStatsGrid(stats *db.PlayerAllTimeStats) {
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
@statCard("Games Played", fmt.Sprint(stats.GamesPlayed), "text-blue")
@statCard("Goals", fmt.Sprint(stats.Goals), "text-green")
@statCard("Assists", fmt.Sprint(stats.Assists), "text-teal")
@statCard("Saves", fmt.Sprint(stats.Saves), "text-yellow")
@statCard("Shots", fmt.Sprint(stats.Shots), "text-peach")
@statCard("Blocks", fmt.Sprint(stats.Blocks), "text-mauve")
@statCard("Passes", fmt.Sprint(stats.Passes), "text-sky")
@statCard("Periods Played", fmt.Sprint(stats.PeriodsPlayed), "text-subtext0")
</div>
}
templ statCard(label string, value string, colorClass string) {
<div class="bg-surface0 border border-surface1 rounded-lg p-4 text-center">
<p class="text-xs text-subtext0 uppercase font-medium mb-1">{ label }</p>
<p class={ "text-2xl font-bold", colorClass }>{ value }</p>
</div>
}
script handleFilterChange(filterType string) {
var container = event.target.closest("[data-filter-url]")
if (!container) return
var baseUrl = container.getAttribute("data-filter-url")
var seasonSelect = container.querySelector("select[name='season_id']")
var teamSelect = container.querySelector("select[name='team_id']")
// Reset the other filter when one is selected
if (filterType === "season" && teamSelect) {
teamSelect.value = ""
} else if (filterType === "team" && seasonSelect) {
seasonSelect.value = ""
}
var value = event.target.value
var url = baseUrl
if (value) {
url += "?filter=" + filterType + "&filter_id=" + value
}
htmx.ajax("POST", url, {target: "#player-content", swap: "innerHTML"})
}
func getSeasonName(seasons []*db.Season, id int) string {
for _, s := range seasons {
if s.ID == id {
return s.Name
}
}
return "Unknown"
}
func getTeamName(teams []*db.Team, id int) string {
for _, t := range teams {
if t.ID == id {
return t.Name
}
}
return "Unknown"
}

View File

@@ -0,0 +1,51 @@
package playersview
import "git.haelnorr.com/h/oslstats/internal/db"
import "fmt"
templ PlayerTeamsTab(teamInfos []*db.PlayerTeamInfo) {
if len(teamInfos) == 0 {
<div class="bg-surface0 border border-surface1 rounded-lg p-8 text-center">
<p class="text-subtext0 text-lg">No team history yet.</p>
<p class="text-subtext1 text-sm mt-2">This player has not been on any teams.</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-4 py-3 text-left text-sm font-semibold text-text">Team</th>
<th class="px-4 py-3 text-right text-sm font-semibold text-text">Seasons Played</th>
</tr>
</thead>
<tbody class="divide-y divide-surface1">
for _, info := range teamInfos {
<tr class="hover:bg-surface1 transition-colors">
<td class="px-4 py-3 text-sm">
<a
href={ templ.SafeURL(fmt.Sprintf("/teams/%d", info.Team.ID)) }
class="text-blue hover:text-blue/80 transition"
>
<div class="flex items-center gap-3">
if info.Team.Color != "" {
<div
class="w-4 h-4 rounded-full border border-surface1 shrink-0"
style={ "background-color: " + templ.SafeCSS(info.Team.Color) }
></div>
}
<span>{ info.Team.Name }</span>
</div>
</a>
</td>
<td class="px-4 py-3 text-sm text-subtext0 text-right">
{ fmt.Sprint(info.SeasonsCount) }
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
}

View File

@@ -7,8 +7,6 @@ templ SlapIDSection(player *db.Player, isOwner bool) {
<div id="slap-id-section">
if player.SlapID == nil && isOwner {
@slapIDLinkPrompt(player)
} else if player.SlapID != nil {
@slapIDLinked(player)
}
</div>
}
@@ -52,26 +50,3 @@ templ slapIDLinkPrompt(player *db.Player) {
</div>
</div>
}
templ slapIDLinked(player *db.Player) {
<div class="bg-green/10 border border-green/30 rounded-lg p-4">
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-green shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
<span class="text-text">
Slapshot ID linked:
<span class="font-mono text-subtext0">
if player.SlapID != nil {
{ fmt.Sprintf("%d", *player.SlapID) }
}
</span>
</span>
</div>
</div>
}