added team overview
This commit is contained in:
@@ -42,13 +42,22 @@ templ SeasonLeagueTeamDetailPage(twr *db.TeamWithRoster, fixtures []*db.Fixture,
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 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 -->
|
||||
|
||||
81
internal/view/teamsview/detail_page.templ
Normal file
81
internal/view/teamsview/detail_page.templ
Normal file
@@ -0,0 +1,81 @@
|
||||
package teamsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||
import "fmt"
|
||||
|
||||
templ DetailPage(team *db.Team, seasonInfos []*db.TeamSeasonInfo, playerStats []*db.TeamAllTimePlayerStats, activeTab string) {
|
||||
@baseview.Layout(team.Name) {
|
||||
<div class="max-w-screen-xl 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 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="/teams"
|
||||
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>
|
||||
<!-- Tab Navigation -->
|
||||
<nav class="bg-surface0 border-b border-surface1">
|
||||
<ul class="flex flex-wrap">
|
||||
@teamDetailTab("seasons", "Seasons", activeTab, team)
|
||||
@teamDetailTab("stats", "Player Stats", activeTab, team)
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- Tab Content -->
|
||||
<div class="mt-6">
|
||||
if activeTab == "seasons" {
|
||||
@TeamDetailSeasons(team, seasonInfos)
|
||||
} else if activeTab == "stats" {
|
||||
@TeamDetailPlayerStats(playerStats)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ teamDetailTab(section string, label string, activeTab string, team *db.Team) {
|
||||
{{
|
||||
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("/teams/%d", team.ID)
|
||||
if section != "seasons" {
|
||||
url = fmt.Sprintf("/teams/%d?tab=%s", team.ID, section)
|
||||
}
|
||||
}}
|
||||
<li class="inline-block">
|
||||
<a
|
||||
href={ templ.SafeURL(url) }
|
||||
class={ baseClasses, templ.KV(activeClasses, isActive), templ.KV(inactiveClasses, !isActive) }
|
||||
>
|
||||
{ label }
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
127
internal/view/teamsview/detail_player_stats.templ
Normal file
127
internal/view/teamsview/detail_player_stats.templ
Normal file
@@ -0,0 +1,127 @@
|
||||
package teamsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "fmt"
|
||||
import "sort"
|
||||
|
||||
templ TeamDetailPlayerStats(playerStats []*db.TeamAllTimePlayerStats) {
|
||||
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 x-data="{ activeView: 'goals' }">
|
||||
<!-- Sub-view Tabs -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<button
|
||||
@click="activeView = 'goals'"
|
||||
:class="activeView === 'goals'
|
||||
? 'bg-blue text-mantle'
|
||||
: 'bg-surface0 border border-surface1 text-subtext0 hover:text-text hover:bg-surface1'"
|
||||
class="px-4 py-2 rounded-lg font-medium text-sm transition hover:cursor-pointer"
|
||||
>
|
||||
Goals
|
||||
</button>
|
||||
<button
|
||||
@click="activeView = 'assists'"
|
||||
:class="activeView === 'assists'
|
||||
? 'bg-blue text-mantle'
|
||||
: 'bg-surface0 border border-surface1 text-subtext0 hover:text-text hover:bg-surface1'"
|
||||
class="px-4 py-2 rounded-lg font-medium text-sm transition hover:cursor-pointer"
|
||||
>
|
||||
Assists
|
||||
</button>
|
||||
<button
|
||||
@click="activeView = 'saves'"
|
||||
:class="activeView === 'saves'
|
||||
? 'bg-blue text-mantle'
|
||||
: 'bg-surface0 border border-surface1 text-subtext0 hover:text-text hover:bg-surface1'"
|
||||
class="px-4 py-2 rounded-lg font-medium text-sm transition hover:cursor-pointer"
|
||||
>
|
||||
Saves
|
||||
</button>
|
||||
</div>
|
||||
<!-- Goals View -->
|
||||
<div x-show="activeView === 'goals'">
|
||||
@playerStatsTable(playerStats, "goals")
|
||||
</div>
|
||||
<!-- Assists View -->
|
||||
<div x-show="activeView === 'assists'" style="display: none;">
|
||||
@playerStatsTable(playerStats, "assists")
|
||||
</div>
|
||||
<!-- Saves View -->
|
||||
<div x-show="activeView === 'saves'" style="display: none;">
|
||||
@playerStatsTable(playerStats, "saves")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ playerStatsTable(playerStats []*db.TeamAllTimePlayerStats, statType string) {
|
||||
{{
|
||||
// Make a copy so sorting doesn't affect other views
|
||||
sorted := make([]*db.TeamAllTimePlayerStats, len(playerStats))
|
||||
copy(sorted, playerStats)
|
||||
|
||||
switch statType {
|
||||
case "goals":
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].Goals > sorted[j].Goals
|
||||
})
|
||||
case "assists":
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].Assists > sorted[j].Assists
|
||||
})
|
||||
case "saves":
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].Saves > sorted[j].Saves
|
||||
})
|
||||
}
|
||||
|
||||
statLabel := "Goals"
|
||||
statShort := "G"
|
||||
if statType == "assists" {
|
||||
statLabel = "Assists"
|
||||
statShort = "A"
|
||||
} else if statType == "saves" {
|
||||
statLabel = "Saves"
|
||||
statShort = "SV"
|
||||
}
|
||||
_ = statLabel
|
||||
}}
|
||||
<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-center text-xs font-semibold text-subtext0 w-10">#</th>
|
||||
<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="Seasons Played">SZN</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-blue" title={ statLabel }>{ statShort }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-surface1">
|
||||
for i, ps := range sorted {
|
||||
<tr class="hover:bg-surface1 transition-colors">
|
||||
<td class="px-3 py-2 text-center text-sm font-medium text-subtext0">
|
||||
{ fmt.Sprint(i + 1) }
|
||||
</td>
|
||||
<td class="px-3 py-2 text-sm text-text font-medium">{ ps.PlayerName }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.SeasonsPlayed) }</td>
|
||||
<td class="px-2 py-2 text-center text-sm text-subtext0">{ fmt.Sprint(ps.PeriodsPlayed) }</td>
|
||||
if statType == "goals" {
|
||||
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(ps.Goals) }</td>
|
||||
} else if statType == "assists" {
|
||||
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(ps.Assists) }</td>
|
||||
} else {
|
||||
<td class="px-2 py-2 text-center text-sm font-bold text-blue">{ fmt.Sprint(ps.Saves) }</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
86
internal/view/teamsview/detail_seasons.templ
Normal file
86
internal/view/teamsview/detail_seasons.templ
Normal file
@@ -0,0 +1,86 @@
|
||||
package teamsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/seasonsview"
|
||||
import "fmt"
|
||||
|
||||
templ TeamDetailSeasons(team *db.Team, seasonInfos []*db.TeamSeasonInfo) {
|
||||
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 team has not participated in any seasons.</p>
|
||||
</div>
|
||||
} else {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
for _, info := range seasonInfos {
|
||||
@teamSeasonCard(team, info)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ teamSeasonCard(team *db.Team, info *db.TeamSeasonInfo) {
|
||||
{{
|
||||
detailURL := fmt.Sprintf(
|
||||
"/seasons/%s/leagues/%s/teams/%d",
|
||||
info.Season.ShortName, info.League.ShortName, team.ID,
|
||||
)
|
||||
}}
|
||||
<a
|
||||
href={ templ.SafeURL(detailURL) }
|
||||
class="bg-mantle border border-surface1 rounded-lg overflow-hidden
|
||||
hover:bg-surface0 transition hover:cursor-pointer block"
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-lg font-bold text-text">{ info.Season.Name }</h3>
|
||||
<span class="text-subtext0 text-sm">—</span>
|
||||
<span class="text-subtext0 text-sm">{ info.League.Name }</span>
|
||||
</div>
|
||||
@seasonsview.StatusBadge(info.Season, true, true)
|
||||
</div>
|
||||
<!-- Card Body -->
|
||||
<div class="p-4">
|
||||
<!-- Position & Points Row -->
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Position Badge -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-xs text-subtext0 uppercase font-medium">Position</span>
|
||||
<span class="text-2xl font-bold text-text">
|
||||
{ fmt.Sprint(info.Position) }
|
||||
</span>
|
||||
<span class="text-sm text-subtext0">
|
||||
/ { fmt.Sprint(info.TotalTeams) }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Points -->
|
||||
<div class="text-right">
|
||||
<span class="text-xs text-subtext0 uppercase font-medium">Points</span>
|
||||
<p class="text-2xl font-bold text-blue">{ fmt.Sprint(info.Record.Points) }</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Record Row -->
|
||||
<div class="grid grid-cols-4 gap-2 text-center">
|
||||
<div class="bg-surface0 rounded px-2 py-1.5">
|
||||
<p class="text-xs text-subtext0 font-medium">W</p>
|
||||
<p class="text-sm font-bold text-green">{ fmt.Sprint(info.Record.Wins) }</p>
|
||||
</div>
|
||||
<div class="bg-surface0 rounded px-2 py-1.5">
|
||||
<p class="text-xs text-subtext0 font-medium">OTW</p>
|
||||
<p class="text-sm font-bold text-teal">{ fmt.Sprint(info.Record.OvertimeWins) }</p>
|
||||
</div>
|
||||
<div class="bg-surface0 rounded px-2 py-1.5">
|
||||
<p class="text-xs text-subtext0 font-medium">OTL</p>
|
||||
<p class="text-sm font-bold text-peach">{ fmt.Sprint(info.Record.OvertimeLosses) }</p>
|
||||
</div>
|
||||
<div class="bg-surface0 rounded px-2 py-1.5">
|
||||
<p class="text-xs text-subtext0 font-medium">L</p>
|
||||
<p class="text-sm font-bold text-red">{ fmt.Sprint(info.Record.Losses) }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import "git.haelnorr.com/h/oslstats/internal/view/sort"
|
||||
import "git.haelnorr.com/h/oslstats/internal/contexts"
|
||||
import "git.haelnorr.com/h/oslstats/internal/permissions"
|
||||
import "github.com/uptrace/bun"
|
||||
import "fmt"
|
||||
|
||||
templ ListPage(teams *db.List[db.Team]) {
|
||||
@baseview.Layout("Teams") {
|
||||
@@ -80,8 +81,10 @@ templ TeamsList(teams *db.List[db.Team]) {
|
||||
<!-- Card grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
for _, t := range teams.Items {
|
||||
<div
|
||||
class="bg-mantle border border-surface1 rounded-lg p-6 hover:bg-surface0 transition-colors flex flex-col"
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/teams/%d", t.ID)) }
|
||||
class="bg-mantle border border-surface1 rounded-lg p-6 hover:bg-surface0
|
||||
transition-colors flex flex-col hover:cursor-pointer"
|
||||
>
|
||||
<!-- Header: Name with color indicator -->
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
@@ -102,7 +105,7 @@ templ TeamsList(teams *db.List[db.Team]) {
|
||||
{ t.AltShortName }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<!-- Pagination controls -->
|
||||
|
||||
Reference in New Issue
Block a user