Fikser to ting rapportert fra ekte produksjonsskjermbilde: (1) kun nærmeste hindring ble vist -- target-distance.tsx viser nå ALLE hindringer, sortert nærmest først, uavhengig av om et diagram kan tegnes. (2) Nytt hull-diagram (hole-diagram-view.tsx) plasserer hindringer og spiller i 2D langs en fast tee->green-akse, tee nederst/ green øverst konsistent uansett gangretning. Ny geometri-modul frontend/lib/hole-geometry.ts (projectOntoAxis, bygget på eksisterende haversineMeters/bearingDegrees) med 10 enhetstester som beviser matematikken riktig før noe visuelt ble bygget. Verifisert i scratch mot presist kjente syntetiske koordinater -- alle tall og posisjoner stemte eksakt med håndregnede fasitverdier. Midlertidig hånd-kodet visning, venter på V0-eksport for den polerte versjonen (samme mønster som FlagPlantSheet/PlayerImportPanel). Ingen migrasjon, ingen backend-endring. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
192 lines
5.5 KiB
TypeScript
192 lines
5.5 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)
|
|
/** ALLE hindringer på hullet (ikke bare nærmeste, se ADR-083), sortert
|
|
* nærmest spilleren først -- kalleren styrer rekkefølgen. */
|
|
hazards?: {
|
|
label: string // f.eks. "Bunker (front)"
|
|
distanceMeters: number
|
|
}[]
|
|
/** "compact" = badge ved siden av GIR-merket, "full" = eget panel. */
|
|
size?: "compact" | "full"
|
|
className?: string
|
|
}
|
|
|
|
/** Én hindring-linje. 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>
|
|
)
|
|
}
|
|
|
|
/** Alle hindringer, stablet -- erstatter den gamle "kun nærmeste"-visningen. */
|
|
function HazardList({
|
|
hazards,
|
|
compact,
|
|
}: {
|
|
hazards: { label: string; distanceMeters: number }[]
|
|
compact?: boolean
|
|
}) {
|
|
if (hazards.length === 0) return null
|
|
return (
|
|
<div className={cn("flex flex-col", compact ? "gap-0.5" : "gap-1")}>
|
|
{hazards.map((h, i) => (
|
|
<HazardLine key={`${h.label}-${i}`} hazard={h} compact={compact} />
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** 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,
|
|
hazards = [],
|
|
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>
|
|
<HazardList hazards={hazards} compact />
|
|
</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>
|
|
|
|
{hazards.length > 0 && (
|
|
<div className="border-t border-clubhouse-border pt-3">
|
|
<HazardList hazards={hazards} />
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|