Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
// Horisontalt scorekort for en frittstående runde (ADR-033) -- egen,
|
|
|
|
|
|
// dedikert side (ikke en del av round-stats.tsx sin aggregerte statistikk).
|
|
|
|
|
|
// Presentasjon fra V0 (zip 17), datalag skrevet om fra mock til ekte fetch
|
|
|
|
|
|
// mot samme `/rounds/{id}/participants/{id}/holes`-endepunkt som
|
|
|
|
|
|
// round-detail.tsx og round-stats.tsx allerede bruker.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Bevisst forskjell fra V0s eksport: V0s egen `strokesReceived()` var en
|
|
|
|
|
|
// generisk modulo-formel -- byttet ut med backend sin ALLEREDE beregnede
|
|
|
|
|
|
// `strokes_received` per hull (samme `allocate_strokes_by_index()`-
|
|
|
|
|
|
// algoritme som resten av appen), for å unngå to ulike implementasjoner av
|
|
|
|
|
|
// samme HCP-slagfordeling.
|
|
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState } from "react"
|
|
|
|
|
|
import Link from "next/link"
|
|
|
|
|
|
import { ArrowLeft } from "lucide-react"
|
|
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
|
|
// --- API-typer ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
type ApiParticipant = {
|
|
|
|
|
|
id: string
|
2026-07-26 08:22:41 +02:00
|
|
|
|
user_id: string | null
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
guest_name: string | null
|
2026-07-26 08:22:41 +02:00
|
|
|
|
display_name: string
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
is_owner: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiRound = {
|
2026-07-25 06:29:09 +02:00
|
|
|
|
name: string | null
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
id: string
|
|
|
|
|
|
course_name_snapshot: string
|
|
|
|
|
|
tee_name_snapshot: string
|
|
|
|
|
|
played_at: string
|
|
|
|
|
|
start_hole: number
|
|
|
|
|
|
holes_planned: number
|
|
|
|
|
|
participants: ApiParticipant[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiHole = {
|
|
|
|
|
|
hole_number: number
|
|
|
|
|
|
par: number
|
|
|
|
|
|
stroke_index: number
|
|
|
|
|
|
played: boolean
|
|
|
|
|
|
score: number | null
|
|
|
|
|
|
strokes_received: number | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dateFormatter = new Intl.DateTimeFormat("no-NO", { day: "numeric", month: "long", year: "numeric" })
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
// Viewer-relativt (ADR-036 fase 3-utvidelsen, 2026-07-26) -- se samme
|
|
|
|
|
|
// begrunnelse i round-detail.tsx sin playerLabel().
|
|
|
|
|
|
function participantLabel(p: ApiParticipant, viewerId: string | null): string {
|
|
|
|
|
|
return viewerId !== null && p.user_id === viewerId ? "Deg" : p.display_name
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Scoring helpers -------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function netScore(h: ApiHole): number | null {
|
|
|
|
|
|
return h.played && h.score !== null && h.strokes_received !== null ? h.score - h.strokes_received : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stableford: 2 pts for net par, +/- 1 per stroke, floored at 0.
|
|
|
|
|
|
function stablefordPoints(h: ApiHole): number | null {
|
|
|
|
|
|
const net = netScore(h)
|
|
|
|
|
|
if (net === null) return null
|
|
|
|
|
|
return Math.max(0, h.par - net + 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ScoreClass = "eagle" | "birdie" | "par" | "bogey" | "double"
|
|
|
|
|
|
|
|
|
|
|
|
function classify(gross: number, par: number): ScoreClass {
|
|
|
|
|
|
const diff = gross - par
|
|
|
|
|
|
if (diff <= -2) return "eagle"
|
|
|
|
|
|
if (diff === -1) return "birdie"
|
|
|
|
|
|
if (diff === 0) return "par"
|
|
|
|
|
|
if (diff === 1) return "bogey"
|
|
|
|
|
|
return "double"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sum(values: number[]): number {
|
|
|
|
|
|
return values.reduce((a, b) => a + b, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function signedToPar(score: number, par: number): string {
|
|
|
|
|
|
const diff = score - par
|
|
|
|
|
|
if (diff === 0) return "E"
|
|
|
|
|
|
return diff > 0 ? `+${diff}` : `−${Math.abs(diff)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Score mark (the visual language for the Score row) --------------------
|
|
|
|
|
|
// Shape encodes direction (circle = under par, square = over par) so meaning
|
|
|
|
|
|
// survives without color; fill encodes magnitude (filled = 2+ strokes off).
|
|
|
|
|
|
|
|
|
|
|
|
function ScoreMark({ gross, par }: { gross: number | null; par: number }) {
|
|
|
|
|
|
const shared =
|
|
|
|
|
|
"flex size-6 items-center justify-center text-sm font-extrabold tabular-nums sm:size-7"
|
|
|
|
|
|
if (gross === null) {
|
|
|
|
|
|
return <span className={cn(shared, "text-muted-foreground")}>{"–"}</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
const kind = classify(gross, par)
|
|
|
|
|
|
if (kind === "eagle") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className={cn(shared, "rounded-full bg-primary text-primary-foreground")}>{gross}</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === "birdie") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className={cn(shared, "rounded-full border-2 border-primary bg-primary/10 text-primary")}>
|
|
|
|
|
|
{gross}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === "bogey") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
shared,
|
|
|
|
|
|
"rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-brand-orange",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{gross}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === "double") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className={cn(shared, "rounded-[4px] bg-brand-orange text-brand-orange-foreground")}>
|
|
|
|
|
|
{gross}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
// par
|
|
|
|
|
|
return <span className={cn(shared, "font-bold text-foreground")}>{gross}</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- One nine-hole block (a real table for row/column semantics) -----------
|
|
|
|
|
|
|
|
|
|
|
|
function ScoreBlock({
|
|
|
|
|
|
holes,
|
|
|
|
|
|
summaryLabel,
|
|
|
|
|
|
showStableford,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
holes: ApiHole[]
|
|
|
|
|
|
summaryLabel: string
|
|
|
|
|
|
showStableford: boolean
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const parSum = sum(holes.map((h) => h.par))
|
|
|
|
|
|
const playedHoles = holes.filter((h) => h.played && h.score !== null)
|
|
|
|
|
|
const grossSum = playedHoles.length > 0 ? sum(playedHoles.map((h) => h.score as number)) : null
|
|
|
|
|
|
const nettoValues = holes.map(netScore).filter((n): n is number => n !== null)
|
|
|
|
|
|
const nettoSum = nettoValues.length > 0 ? sum(nettoValues) : null
|
|
|
|
|
|
const pointsValues = holes.map(stablefordPoints).filter((n): n is number => n !== null)
|
|
|
|
|
|
const pointsSum = pointsValues.length > 0 ? sum(pointsValues) : null
|
|
|
|
|
|
|
|
|
|
|
|
const cell = "border border-border text-center tabular-nums"
|
|
|
|
|
|
const sumCell = "border border-border bg-muted text-center font-extrabold tabular-nums"
|
|
|
|
|
|
const rowLabel =
|
|
|
|
|
|
"border border-border px-1 text-left text-xs font-bold text-muted-foreground"
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<table className="w-full table-fixed border-collapse">
|
|
|
|
|
|
<caption className="sr-only">
|
|
|
|
|
|
Scorekort hull {holes[0].hole_number} til {holes[holes.length - 1].hole_number}
|
|
|
|
|
|
</caption>
|
|
|
|
|
|
<colgroup>
|
|
|
|
|
|
<col className="w-9" />
|
|
|
|
|
|
{holes.map((h) => (
|
|
|
|
|
|
<col key={h.hole_number} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
<col className="w-[11%]" />
|
|
|
|
|
|
</colgroup>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th scope="col" className="border border-border bg-muted px-1 py-1 text-left">
|
|
|
|
|
|
<span className="sr-only">Rad</span>
|
|
|
|
|
|
<span aria-hidden="true" className="text-xs font-extrabold text-muted-foreground">
|
|
|
|
|
|
Hull
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holes.map((h) => (
|
|
|
|
|
|
<th
|
|
|
|
|
|
key={h.hole_number}
|
|
|
|
|
|
scope="col"
|
|
|
|
|
|
className="border border-border bg-foreground py-1 text-center text-xs font-extrabold tabular-nums text-background"
|
|
|
|
|
|
>
|
|
|
|
|
|
{h.hole_number}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<th
|
|
|
|
|
|
scope="col"
|
|
|
|
|
|
className="border border-border bg-primary py-1 text-center text-xs font-extrabold uppercase text-primary-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{summaryLabel}
|
|
|
|
|
|
</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{/* Hcp (stroke index) */}
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th scope="row" className={rowLabel}>
|
|
|
|
|
|
Hcp
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holes.map((h) => (
|
|
|
|
|
|
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
|
|
|
|
|
{h.stroke_index}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<td className={cn(sumCell, "py-1 text-xs text-muted-foreground")} aria-hidden="true">
|
|
|
|
|
|
{"–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
{/* Par */}
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th scope="row" className={rowLabel}>
|
|
|
|
|
|
Par
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holes.map((h) => (
|
|
|
|
|
|
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
|
|
|
|
|
{h.par}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{parSum}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
{/* Score — the hero row: highlighted band + shape marks */}
|
|
|
|
|
|
<tr className="bg-primary/5">
|
|
|
|
|
|
<th scope="row" className={cn(rowLabel, "text-sm text-foreground")}>
|
|
|
|
|
|
Score
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holes.map((h) => (
|
|
|
|
|
|
<td key={h.hole_number} className={cn(cell, "px-0.5 py-1")}>
|
|
|
|
|
|
<span className="flex items-center justify-center">
|
|
|
|
|
|
<ScoreMark gross={h.played ? h.score : null} par={h.par} />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<td className={cn(sumCell, "py-1 text-base text-foreground")}>{grossSum ?? "–"}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
{/* Netto */}
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th scope="row" className={cn(rowLabel, "text-foreground")}>
|
|
|
|
|
|
Netto
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holes.map((h) => (
|
|
|
|
|
|
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-bold text-foreground")}>
|
|
|
|
|
|
{netScore(h) ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{nettoSum ?? "–"}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
{/* Stableford (only when relevant) */}
|
|
|
|
|
|
{showStableford && (
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th scope="row" className={rowLabel} title="Stableford-poeng">
|
|
|
|
|
|
Poeng
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holes.map((h) => (
|
|
|
|
|
|
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
|
|
|
|
|
{stablefordPoints(h) ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{pointsSum ?? "–"}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Legend ----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function Legend() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-2xl border border-border bg-card px-4 py-3"
|
|
|
|
|
|
aria-label="Symbolforklaring"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
|
<span className="flex size-6 items-center justify-center rounded-full border-2 border-primary bg-primary/10 text-xs font-extrabold text-primary">
|
|
|
|
|
|
3
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Under par (sirkel)</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
|
<span className="flex size-6 items-center justify-center rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-xs font-extrabold text-brand-orange">
|
|
|
|
|
|
5
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Over par (firkant)</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-sm font-medium text-muted-foreground">Fylt symbol = 2 slag eller mer</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Combined total --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function TotalTile({ label, value, tone }: { label: string; value: string; tone?: "orange" | "green" }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col items-center gap-0.5 rounded-2xl border border-border bg-card px-2 py-3">
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"text-2xl font-extrabold tabular-nums",
|
|
|
|
|
|
tone === "orange" ? "text-brand-orange" : tone === "green" ? "text-primary" : "text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{value}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Screen ----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export function RoundScorecard({ roundId }: { roundId: string }) {
|
|
|
|
|
|
const [round, setRound] = useState<ApiRound | null>(null)
|
|
|
|
|
|
const [participantId, setParticipantId] = useState<string | null>(null)
|
|
|
|
|
|
const [holes, setHoles] = useState<ApiHole[] | null>(null)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
const [viewerId, setViewerId] = useState<string | null>(null)
|
2026-07-26 08:49:37 +02:00
|
|
|
|
// Sanntid (2026-07-26, oppfølging av ADR-027): et rent "noe endret seg"-
|
|
|
|
|
|
// signal over WebSocket -- bumper denne, som er med i begge fetch-
|
|
|
|
|
|
// effektene under, slik at de henter på nytt akkurat som ved
|
|
|
|
|
|
// førstegangslasting (samme mønster som public-live.tsx).
|
|
|
|
|
|
const [refreshKey, setRefreshKey] = useState(0)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
fetch("/auth/me", { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : null))
|
|
|
|
|
|
.then((data: { id: string } | null) => {
|
|
|
|
|
|
if (!cancelled && data) setViewerId(data.id)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
2026-07-26 08:49:37 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
|
|
|
|
|
const socket = new WebSocket(`${protocol}//${window.location.host}/ws/rounds/${roundId}/live`)
|
|
|
|
|
|
socket.onmessage = () => setRefreshKey((k) => k + 1)
|
|
|
|
|
|
return () => socket.close()
|
|
|
|
|
|
}, [roundId])
|
|
|
|
|
|
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}`, { credentials: "include" })
|
|
|
|
|
|
if (res.status === 403 || res.status === 404) {
|
|
|
|
|
|
if (!cancelled) setError("Denne runden finnes ikke, eller du har ikke tilgang til den.")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
if (!cancelled) setError("Klarte ikke å hente runden. Prøv igjen om litt.")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const data: ApiRound = await res.json()
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
setRound(data)
|
2026-07-26 08:22:41 +02:00
|
|
|
|
// Standardvalg: SIN EGEN rad hvis man er en lenket medspiller,
|
|
|
|
|
|
// ellers eieren (samme som før ADR-036 fase 3-utvidelsen).
|
|
|
|
|
|
setParticipantId(
|
|
|
|
|
|
(prev) =>
|
|
|
|
|
|
prev ??
|
|
|
|
|
|
(viewerId && data.participants.find((p) => p.user_id === viewerId)?.id) ??
|
|
|
|
|
|
data.participants.find((p) => p.is_owner)?.id ??
|
|
|
|
|
|
data.participants[0]?.id ??
|
|
|
|
|
|
null,
|
|
|
|
|
|
)
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
void load()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
2026-07-26 08:49:37 +02:00
|
|
|
|
}, [roundId, viewerId, refreshKey])
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!participantId) return
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
async function loadHoles() {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/participants/${participantId}/holes`, { credentials: "include" })
|
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
const data: ApiHole[] = await res.json()
|
|
|
|
|
|
if (!cancelled) setHoles(data)
|
|
|
|
|
|
}
|
|
|
|
|
|
void loadHoles()
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
2026-07-26 08:49:37 +02:00
|
|
|
|
}, [roundId, participantId, refreshKey])
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex min-h-dvh flex-col items-center justify-center gap-4 bg-background px-5 text-center">
|
|
|
|
|
|
<p className="text-base font-medium text-destructive">{error}</p>
|
|
|
|
|
|
<Link href="/my-rounds" className="text-base font-semibold text-primary underline underline-offset-2">
|
|
|
|
|
|
Tilbake til egne runder
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!round || !holes) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex min-h-dvh flex-col items-center justify-center bg-background">
|
|
|
|
|
|
<div aria-hidden="true" className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Samme sirkulære start_hole-rekkefølge som round-detail.tsx/round-stats.tsx
|
|
|
|
|
|
// -- "Ut"/"Inn" er dermed første/andre halvdel AV SPILLEREKKEFØLGEN, ikke
|
|
|
|
|
|
// nødvendigvis fysisk hull 1-9/10-18 (relevant kun for en 18-hulls runde
|
|
|
|
|
|
// med et annet starthull enn 1, uvanlig men strukturelt tillatt).
|
|
|
|
|
|
const holeOrder = Array.from(
|
|
|
|
|
|
{ length: round.holes_planned },
|
|
|
|
|
|
(_, i) => ((round.start_hole - 1 + i) % 18) + 1,
|
|
|
|
|
|
)
|
|
|
|
|
|
const orderedHoles = holeOrder
|
|
|
|
|
|
.map((n) => holes.find((h) => h.hole_number === n))
|
|
|
|
|
|
.filter((h): h is ApiHole => h !== undefined)
|
|
|
|
|
|
const front = orderedHoles.slice(0, 9)
|
|
|
|
|
|
const back = orderedHoles.slice(9, 18)
|
|
|
|
|
|
|
|
|
|
|
|
const showStableford = holes.some((h) => h.strokes_received !== null)
|
|
|
|
|
|
const playedHoles = orderedHoles.filter((h) => h.played && h.score !== null)
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
// Full rundens par (statisk referanse, f.eks. "Par 72") -- ALDRI brukt til
|
|
|
|
|
|
// en til-par-utregning, kun til "Par"-feltet nederst som viser hele
|
|
|
|
|
|
// runden uansett hvor langt man har kommet.
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
const totalPar = sum(orderedHoles.map((h) => h.par))
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
// Til-par MÅ regnes mot paret for kun de SPILTE hullene -- ellers blir
|
|
|
|
|
|
// "til par" meningsløst midt i runden (f.eks. 19 slag på 4 hull ville vist
|
|
|
|
|
|
// "−53 til par" mot en 72-par bane i stedet for korrekt "+2", funnet ved
|
|
|
|
|
|
// faktisk nettleser-testing 2026-07-27).
|
|
|
|
|
|
const playedPar = playedHoles.length > 0 ? sum(playedHoles.map((h) => h.par)) : 0
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
const totalGross = playedHoles.length > 0 ? sum(playedHoles.map((h) => h.score as number)) : null
|
|
|
|
|
|
const totalPointsValues = orderedHoles.map(stablefordPoints).filter((n): n is number => n !== null)
|
|
|
|
|
|
const totalPoints = totalPointsValues.length > 0 ? sum(totalPointsValues) : null
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-dvh bg-background">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<header className="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur">
|
|
|
|
|
|
<div className="mx-auto flex min-h-14 max-w-xl items-center gap-2 px-4 py-2">
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={`/my-rounds/${roundId}`}
|
|
|
|
|
|
className="flex min-h-11 items-center gap-1.5 rounded-xl pr-3 text-base font-bold text-foreground transition-colors hover:text-primary"
|
|
|
|
|
|
>
|
|
|
|
|
|
<ArrowLeft aria-hidden="true" className="size-6" />
|
|
|
|
|
|
Scorekort
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main className="mx-auto flex max-w-xl flex-col gap-4 px-4 py-5 pb-16">
|
|
|
|
|
|
{/* Round summary strip */}
|
|
|
|
|
|
<div className="flex flex-wrap items-baseline justify-between gap-2 rounded-3xl border border-border bg-card p-4 shadow-sm shadow-black/5">
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<span className="text-xl font-extrabold tracking-tight text-foreground text-balance">
|
2026-07-25 06:29:09 +02:00
|
|
|
|
{round.name?.trim() || round.course_name_snapshot}
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-sm font-semibold text-muted-foreground">
|
2026-07-25 06:29:09 +02:00
|
|
|
|
{round.name?.trim() ? `${round.course_name_snapshot} · ` : ""}
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
{round.tee_name_snapshot} {"·"} {round.holes_planned} hull {"·"}{" "}
|
|
|
|
|
|
{dateFormatter.format(new Date(round.played_at))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{totalGross !== null && (
|
|
|
|
|
|
<div className="flex flex-col items-end">
|
|
|
|
|
|
<span className="text-3xl font-extrabold tabular-nums text-foreground">{totalGross}</span>
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
<span className="text-sm font-bold text-brand-orange">{signedToPar(totalGross, playedPar)} til par</span>
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Spillervelger, kun når runden har flere deltakere */}
|
|
|
|
|
|
{round.participants.length > 1 && (
|
|
|
|
|
|
<div role="tablist" aria-label="Velg spiller" className="-mx-4 overflow-x-auto px-4">
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
{round.participants.map((p) => {
|
|
|
|
|
|
const active = p.id === participantId
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={p.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
role="tab"
|
|
|
|
|
|
aria-selected={active}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setHoles(null)
|
|
|
|
|
|
setParticipantId(p.id)
|
|
|
|
|
|
}}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 shrink-0 items-center rounded-2xl border px-4 text-base font-bold transition-colors",
|
|
|
|
|
|
active
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-card text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-07-26 08:22:41 +02:00
|
|
|
|
{participantLabel(p, viewerId)}
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Legend */}
|
|
|
|
|
|
<Legend />
|
|
|
|
|
|
|
|
|
|
|
|
{/* Scorecard blocks */}
|
|
|
|
|
|
<div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-sm shadow-black/5 sm:p-4">
|
|
|
|
|
|
{front.length > 0 && (
|
|
|
|
|
|
<ScoreBlock holes={front} summaryLabel={back.length === 0 ? "Tot" : "Ut"} showStableford={showStableford} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
{back.length > 0 && <ScoreBlock holes={back} summaryLabel="Inn" showStableford={showStableford} />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Combined total */}
|
|
|
|
|
|
<div className={cn("grid gap-2", showStableford ? "grid-cols-4" : "grid-cols-3")}>
|
|
|
|
|
|
<TotalTile label="Par" value={String(totalPar)} />
|
|
|
|
|
|
<TotalTile label="Score" value={totalGross !== null ? String(totalGross) : "–"} />
|
|
|
|
|
|
<TotalTile
|
|
|
|
|
|
label="Til par"
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
value={totalGross !== null ? signedToPar(totalGross, playedPar) : "–"}
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
tone="orange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{showStableford && <TotalTile label="Poeng" value={totalPoints !== null ? String(totalPoints) : "–"} tone="green" />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|