teecup/frontend/components/target-distance.tsx
Erol Haagenrud 9b7ff8e2ef Eclectic-format for org-individuelle turneringer (Del C) + tre UI-rettelser
Del C (ADR-068, migrasjon 072, siste del av den tredelte utvidelsen som
startet med Flaggturnering GPS/kart, se ADR-066/067): nytt format
eclectic_gross/eclectic_net/eclectic_stableford -- beste resultat per
hull på tvers av en turnerings egne runder, krever samme bane (avvist
tydelig ved rundeopprettelse ellers). Regnes ut ved lesing, ingen nye
tabeller. Bevisst avvik fra opprinnelig plan: integrert som en ny gren
i eksisterende individual-leaderboard-endepunkt fremfor et nytt eget
endepunkt -- se ADR-068 for begrunnelsen.

Tre ikke-relaterte, brukerrapporterte UI-rettelser tatt med i samme
runde: avstandsindikatoren brukte "grønn"/"Midt" i stedet for riktige
golf-uttrykk "green"/"senter", og "Oppdateres live"-badgen fjernet.
"Antall hull"-bryteren i Ny runde-veiviseren fikk samme grønne
aksent-valgt-stil som resten av samme skjerm (delt Segmented-primitiv).

Se ARCHITECTURE_DECISIONS.md (ADR-068) og CHANGELOG.md (punkt 84) for
full begrunnelse og verifiseringslogg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 11:12:25 +02:00

176 lines
5.1 KiB
TypeScript

"use client"
import { TriangleAlert, Satellite } from "lucide-react"
import { cn } from "@/lib/utils"
/**
* TargetDistance — ren visningskomponent for gjenstående avstand til green.
*
* "Dum" med vilje: den mottar ferdigregnede tall som props. Ingen GPS,
* ingen fetch, ingen kart. Følger TeeCups clubhouse-palett (grønn/oransje
* aksenter, samme korthjørner/skygge-stil som resten av scorekortet).
*/
export type TargetDistanceProps = {
distances: {
front: number // meter
middle: number
back: number
} | null // null = ingen koordinatdata for dette hullet (normal, forventet tilstand)
nearestHazard?: {
label: string // f.eks. "Bunker (front)"
distanceMeters: number
} | null
/** "compact" = badge ved siden av GIR-merket, "full" = eget panel. */
size?: "compact" | "full"
className?: string
}
/** Hindring-linje: kun synlig når det finnes en relevant hindring. Oransje varsel, ikke rødt. */
function HazardLine({
hazard,
compact,
}: {
hazard: { label: string; distanceMeters: number }
compact?: boolean
}) {
return (
<p
className={cn(
"inline-flex items-center gap-1.5 font-medium text-cup-strong",
compact ? "text-[11px]" : "text-sm",
)}
>
<TriangleAlert aria-hidden="true" className={compact ? "size-3.5 shrink-0" : "size-4 shrink-0"} />
<span className="text-pretty">
{hazard.label}: <span className="tabular-nums font-bold">{hazard.distanceMeters}</span> m
</span>
</p>
)
}
/** En avstandskolonne (Front/Midt/Bak). Alltid tekst-label + tall + enhet. */
function DistanceColumn({
label,
value,
emphasized,
compact,
}: {
label: string
value: number
emphasized?: boolean
compact?: boolean
}) {
return (
<div className="flex flex-col items-center justify-end gap-0.5">
<span
className={cn(
"font-semibold uppercase tracking-wide text-clubhouse-muted",
compact ? "text-[10px]" : "text-xs",
)}
>
{label}
</span>
<span
className={cn(
"flex items-baseline gap-0.5 leading-none tabular-nums",
emphasized ? "text-tee-strong" : "text-clubhouse-ink",
// Stort nok til å leses på avstand, i sollys — som en klokke-visning.
compact
? emphasized
? "text-3xl font-black"
: "text-xl font-bold"
: emphasized
? "text-6xl font-black"
: "text-4xl font-bold",
)}
>
{value}
<span
className={cn(
"font-semibold text-clubhouse-muted",
compact ? "text-[10px]" : "text-sm",
)}
>
m
</span>
</span>
</div>
)
}
/** Nøytral, lavmælt placeholder når banen ikke har koordinatdata. Aldri en feilmelding. */
function NoData({ compact, className }: { compact?: boolean; className?: string }) {
return (
<div
role="note"
className={cn(
"inline-flex items-center gap-2 rounded-xl border border-clubhouse-border bg-clubhouse-field text-clubhouse-muted",
compact ? "px-2.5 py-1.5 text-[11px]" : "px-4 py-3 text-sm",
className,
)}
>
<Satellite aria-hidden="true" className={compact ? "size-3.5" : "size-4"} />
<span>Avstand ikke tilgjengelig</span>
</div>
)
}
export function TargetDistance({
distances,
nearestHazard,
size = "full",
className,
}: TargetDistanceProps) {
const compact = size === "compact"
// Punkt 4: ingen data er en normal, forventet tilstand — vis en nøytral placeholder.
if (!distances) {
return <NoData compact={compact} className={className} />
}
if (compact) {
return (
<div
className={cn(
"inline-flex flex-col gap-1 rounded-xl border border-clubhouse-border bg-clubhouse-card px-3 py-2 shadow-sm",
className,
)}
>
<div className="flex items-end gap-3">
<DistanceColumn label="Front" value={distances.front} compact />
<DistanceColumn label="Senter" value={distances.middle} emphasized compact />
<DistanceColumn label="Bak" value={distances.back} compact />
</div>
{nearestHazard ? (
<div className="flex items-center justify-between gap-2">
<HazardLine hazard={nearestHazard} compact />
</div>
) : null}
</div>
)
}
return (
<section
aria-label="Avstand til green"
className={cn(
"flex w-full flex-col gap-3 rounded-2xl border border-clubhouse-border bg-clubhouse-card p-5 shadow-sm",
className,
)}
>
<h3 className="text-sm font-semibold uppercase tracking-wide text-clubhouse-muted">Til green</h3>
<div className="flex items-end justify-between gap-2">
<DistanceColumn label="Front" value={distances.front} />
<DistanceColumn label="Senter" value={distances.middle} emphasized />
<DistanceColumn label="Bak" value={distances.back} />
</div>
{nearestHazard ? (
<div className="border-t border-clubhouse-border pt-3">
<HazardLine hazard={nearestHazard} />
</div>
) : null}
</section>
)
}