Sorterbare kolonner i Spillere-tabellen (turneringsoppsett)
Klikk header sykler stigende/synkende/opprinnelig rekkefølge for Spiller/Hcp/Klasse/Statistikknivå/Status/Kjønn/Alder. Runde-kolonnene og handlingskolonnen forblir usorterbare. V0-mønster integrert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
544c952cab
commit
6603e94fd3
1 changed files with 124 additions and 26 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react"
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { ExternalLink, Loader2, Trash2, TriangleAlert } from "lucide-react"
|
import { ChevronDown, ChevronUp, ExternalLink, Loader2, Trash2, TriangleAlert } from "lucide-react"
|
||||||
|
|
||||||
export type TournamentPlayerRow = {
|
export type TournamentPlayerRow = {
|
||||||
participantId: string
|
participantId: string
|
||||||
|
|
@ -90,6 +90,106 @@ function computeAge(birthDate: string): number | null {
|
||||||
return age
|
return age
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sortering (2026-08-20) ──────────────────────────────────────────────
|
||||||
|
// Kun disse syv kolonnene er sorterbare -- runde-kolonnene (deltar+utslag)
|
||||||
|
// og handlingskolonnen er det bevisst IKKE, se V0-prompten.
|
||||||
|
type SortKey = "name" | "hcp" | "className" | "statLevel" | "status" | "gender" | "age"
|
||||||
|
type SortDir = "asc" | "desc"
|
||||||
|
type SortState = { key: SortKey; dir: SortDir } | null
|
||||||
|
|
||||||
|
const STAT_LEVEL_RANK: Record<TournamentPlayerRow["statLevel"], number> = {
|
||||||
|
strokes_only: 0,
|
||||||
|
strokes_and_putts: 1,
|
||||||
|
full: 2,
|
||||||
|
}
|
||||||
|
const STATUS_RANK: Record<TournamentPlayerRow["status"], number> = {
|
||||||
|
active: 0,
|
||||||
|
dsq: 1,
|
||||||
|
rtd: 2,
|
||||||
|
dnf: 3,
|
||||||
|
dns: 4,
|
||||||
|
}
|
||||||
|
const GENDER_SORT_LABEL: Record<"m" | "f" | "x", string> = { f: "Kvinne", m: "Mann", x: "Annet" }
|
||||||
|
|
||||||
|
// Sammenlignbar verdi per sorteringskolonne. Null sorteres alltid sist
|
||||||
|
// (håndtert i compareRows), uansett retning.
|
||||||
|
function sortValue(p: TournamentPlayerRow, key: SortKey, classNameById: Record<string, string>): string | number | null {
|
||||||
|
switch (key) {
|
||||||
|
case "name":
|
||||||
|
return p.playerName.toLocaleLowerCase("nb")
|
||||||
|
case "hcp":
|
||||||
|
return p.handicapSnapshot
|
||||||
|
case "className": {
|
||||||
|
const name = p.classId ? classNameById[p.classId] : undefined
|
||||||
|
return name ? name.toLocaleLowerCase("nb") : null
|
||||||
|
}
|
||||||
|
case "statLevel":
|
||||||
|
return STAT_LEVEL_RANK[p.statLevel]
|
||||||
|
case "status":
|
||||||
|
return STATUS_RANK[p.status]
|
||||||
|
case "gender":
|
||||||
|
return p.gender ? GENDER_SORT_LABEL[p.gender].toLocaleLowerCase("nb") : null
|
||||||
|
case "age":
|
||||||
|
return p.birthDate ? computeAge(p.birthDate) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareRows(
|
||||||
|
a: TournamentPlayerRow,
|
||||||
|
b: TournamentPlayerRow,
|
||||||
|
key: SortKey,
|
||||||
|
dir: SortDir,
|
||||||
|
classNameById: Record<string, string>,
|
||||||
|
): number {
|
||||||
|
const av = sortValue(a, key, classNameById)
|
||||||
|
const bv = sortValue(b, key, classNameById)
|
||||||
|
if (av === null && bv === null) return 0
|
||||||
|
if (av === null) return 1
|
||||||
|
if (bv === null) return -1
|
||||||
|
const res = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv), "nb")
|
||||||
|
return dir === "asc" ? res : -res
|
||||||
|
}
|
||||||
|
|
||||||
|
function SortableHeader({
|
||||||
|
sortKey,
|
||||||
|
label,
|
||||||
|
sort,
|
||||||
|
onSort,
|
||||||
|
align,
|
||||||
|
sticky,
|
||||||
|
}: {
|
||||||
|
sortKey: SortKey
|
||||||
|
label: string
|
||||||
|
sort: SortState
|
||||||
|
onSort: (key: SortKey) => void
|
||||||
|
align?: "right"
|
||||||
|
sticky?: boolean
|
||||||
|
}) {
|
||||||
|
const active = sort?.key === sortKey
|
||||||
|
const dir = active ? sort.dir : null
|
||||||
|
const ariaSort = active ? (dir === "asc" ? "ascending" : "descending") : "none"
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
aria-sort={ariaSort}
|
||||||
|
className={`border-b border-r border-border bg-muted p-0 ${sticky ? "sticky left-0 top-0 z-30" : "sticky top-0 z-20"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSort(sortKey)}
|
||||||
|
aria-label={`Sorter etter ${label}${active ? (dir === "asc" ? ", stigende" : ", synkende") : ""}`}
|
||||||
|
className={`flex h-full min-h-9 w-full items-center gap-1 px-2 py-2 text-[11px] font-bold uppercase tracking-wide transition-colors hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring ${
|
||||||
|
active ? "text-primary" : "text-muted-foreground"
|
||||||
|
} ${align === "right" ? "justify-end text-right" : "justify-start text-left"}`}
|
||||||
|
>
|
||||||
|
<span className="truncate">{label}</span>
|
||||||
|
{active && dir === "asc" && <ChevronUp aria-hidden="true" className="size-3.5 shrink-0" />}
|
||||||
|
{active && dir === "desc" && <ChevronDown aria-hidden="true" className="size-3.5 shrink-0" />}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function TournamentPlayersTable({
|
export function TournamentPlayersTable({
|
||||||
players,
|
players,
|
||||||
classes,
|
classes,
|
||||||
|
|
@ -122,6 +222,21 @@ export function TournamentPlayersTable({
|
||||||
setDefaultTees(next)
|
setDefaultTees(next)
|
||||||
}, [rounds])
|
}, [rounds])
|
||||||
|
|
||||||
|
// Sortering: stigende -> synkende -> tilbake til opprinnelig rekkefølge.
|
||||||
|
const [sort, setSort] = useState<SortState>(null)
|
||||||
|
const cycleSort = (key: SortKey) => {
|
||||||
|
setSort((prev) => {
|
||||||
|
if (!prev || prev.key !== key) return { key, dir: "asc" }
|
||||||
|
if (prev.dir === "asc") return { key, dir: "desc" }
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const classNameById = useMemo(() => Object.fromEntries(classes.map((c) => [c.id, c.name])), [classes])
|
||||||
|
const sortedPlayers = useMemo(() => {
|
||||||
|
if (!sort) return players
|
||||||
|
return [...players].sort((a, b) => compareRows(a, b, sort.key, sort.dir, classNameById))
|
||||||
|
}, [players, sort, classNameById])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
|
|
@ -162,32 +277,15 @@ export function TournamentPlayersTable({
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<SortableHeader sortKey="name" label="Spiller" sort={sort} onSort={cycleSort} sticky />
|
||||||
scope="col"
|
<SortableHeader sortKey="hcp" label="Hcp" sort={sort} onSort={cycleSort} align="right" />
|
||||||
className={`${TH} sticky left-0 top-0 z-30`}
|
|
||||||
>
|
|
||||||
Spiller
|
|
||||||
</th>
|
|
||||||
<th scope="col" className={`${TH} sticky top-0 z-20 text-right`}>
|
|
||||||
Hcp
|
|
||||||
</th>
|
|
||||||
{showClassColumn && (
|
{showClassColumn && (
|
||||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
<SortableHeader sortKey="className" label="Klasse" sort={sort} onSort={cycleSort} />
|
||||||
Klasse
|
|
||||||
</th>
|
|
||||||
)}
|
)}
|
||||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
<SortableHeader sortKey="statLevel" label="Statistikknivå" sort={sort} onSort={cycleSort} />
|
||||||
Statistikknivå
|
<SortableHeader sortKey="status" label="Status" sort={sort} onSort={cycleSort} />
|
||||||
</th>
|
<SortableHeader sortKey="gender" label="Kjønn" sort={sort} onSort={cycleSort} />
|
||||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
<SortableHeader sortKey="age" label="Alder" sort={sort} onSort={cycleSort} />
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
|
||||||
Kjønn
|
|
||||||
</th>
|
|
||||||
<th scope="col" className={`${TH} sticky top-0 z-20`}>
|
|
||||||
Alder
|
|
||||||
</th>
|
|
||||||
{rounds.map((round) => {
|
{rounds.map((round) => {
|
||||||
const busy = busyRoundIds.includes(round.id)
|
const busy = busyRoundIds.includes(round.id)
|
||||||
const assignedCount = players.filter(
|
const assignedCount = players.filter(
|
||||||
|
|
@ -246,7 +344,7 @@ export function TournamentPlayersTable({
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{players.map((player) => (
|
{sortedPlayers.map((player) => (
|
||||||
<PlayerRow
|
<PlayerRow
|
||||||
key={player.participantId}
|
key={player.participantId}
|
||||||
player={player}
|
player={player}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue