208 lines
6.2 KiB
TypeScript
208 lines
6.2 KiB
TypeScript
|
|
"use client"
|
||
|
|
|
||
|
|
import { TriangleAlert, Satellite } from "lucide-react"
|
||
|
|
import { cn } from "@/lib/utils"
|
||
|
|
|
||
|
|
/**
|
||
|
|
* TargetDistance — ren visningskomponent for gjenstående avstand til grønn.
|
||
|
|
*
|
||
|
|
* "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
|
||
|
|
isLive: boolean // true = tallene oppdateres kontinuerlig fra GPS akkurat nå
|
||
|
|
/** "compact" = badge ved siden av GIR-merket, "full" = eget panel. */
|
||
|
|
size?: "compact" | "full"
|
||
|
|
className?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Liten "oppdateres live"-indikator: pulserende prikk + tekst (aldri kun farge). */
|
||
|
|
function LiveStatus({ isLive, compact }: { isLive: boolean; compact?: boolean }) {
|
||
|
|
return (
|
||
|
|
<span
|
||
|
|
className={cn(
|
||
|
|
"inline-flex items-center gap-1.5 font-medium text-clubhouse-muted",
|
||
|
|
compact ? "text-[11px]" : "text-xs",
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{isLive ? (
|
||
|
|
<>
|
||
|
|
<span aria-hidden="true" className="relative flex size-2">
|
||
|
|
<span className="absolute inline-flex size-full animate-ping rounded-full bg-tee-strong opacity-70" />
|
||
|
|
<span className="relative inline-flex size-2 rounded-full bg-tee-strong" />
|
||
|
|
</span>
|
||
|
|
{compact ? "Live" : "Oppdateres live"}
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Satellite aria-hidden="true" className="size-3.5" />
|
||
|
|
{compact ? "Sist målt" : "Ikke live — sist målte posisjon"}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</span>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 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,
|
||
|
|
isLive,
|
||
|
|
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="Midt" value={distances.middle} emphasized compact />
|
||
|
|
<DistanceColumn label="Bak" value={distances.back} compact />
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center justify-between gap-2">
|
||
|
|
<LiveStatus isLive={isLive} compact />
|
||
|
|
{nearestHazard ? <HazardLine hazard={nearestHazard} compact /> : null}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<section
|
||
|
|
aria-label="Avstand til grønn"
|
||
|
|
className={cn(
|
||
|
|
"flex w-full flex-col gap-3 rounded-2xl border border-clubhouse-border bg-clubhouse-card p-5 shadow-sm",
|
||
|
|
className,
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between gap-2">
|
||
|
|
<h3 className="text-sm font-semibold uppercase tracking-wide text-clubhouse-muted">Til grønn</h3>
|
||
|
|
<LiveStatus isLive={isLive} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-end justify-between gap-2">
|
||
|
|
<DistanceColumn label="Front" value={distances.front} />
|
||
|
|
<DistanceColumn label="Midt" 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>
|
||
|
|
)
|
||
|
|
}
|