teecup/frontend/components/flag-map-overview.tsx

158 lines
5.3 KiB
TypeScript
Raw Normal View History

"use client"
import { useEffect, useRef, useState } from "react"
import mapboxgl from "mapbox-gl"
import "mapbox-gl/dist/mapbox-gl.css"
export type FlagMapEntry = {
participantId: string
displayName: string
lap: number
holeNumber: number
lat: number
lng: number
onGreen: boolean
distanceToPinCm: number | null
}
export type FlagMapOverviewProps = {
flags: FlagMapEntry[]
visibleToAll: boolean
}
// ADR-067-flagg-tillegget ("Del B", 2026-08-14) -- MIDLERTIDIG, enkel
// kartkomponent inntil V0-eksporten (prompt sendt til bruker) er klar.
// Samme props-kontrakt -- når eksporten kommer, byttes kun komponentens
// INNMAT, kalleren skal ikke måtte endres. Re-initialiserer hele kartet
// når `flags` endrer referanse (enkel, men ikke det mest effisiente --
// akseptert forenkling for den midlertidige versjonen).
export function FlagMapOverview({ flags, visibleToAll }: FlagMapOverviewProps) {
const containerRef = useRef<HTMLDivElement | null>(null)
const [loaded, setLoaded] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (flags.length === 0) return
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN
const container = containerRef.current
if (!container) return
if (!token) {
setError("Kartet er ikke tilgjengelig akkurat nå.")
return
}
setLoaded(false)
setError(null)
mapboxgl.accessToken = token
const map = new mapboxgl.Map({
container,
style: "mapbox://styles/mapbox/satellite-v9",
center: [flags[0].lng, flags[0].lat],
zoom: 16,
attributionControl: false,
})
map.on("load", () => {
setLoaded(true)
if (flags.length > 1) {
const bounds = new mapboxgl.LngLatBounds([flags[0].lng, flags[0].lat], [flags[0].lng, flags[0].lat])
for (const f of flags) bounds.extend([f.lng, f.lat])
map.fitBounds(bounds, { padding: 80, maxZoom: 18, duration: 0 })
}
})
map.on("error", () => setError("Kunne ikke laste kartet."))
const markers: mapboxgl.Marker[] = []
for (const f of flags) {
const el = document.createElement("div")
el.className =
"flex size-7 items-center justify-center rounded-full border-2 border-white text-[10px] font-extrabold text-white shadow-md"
el.style.backgroundColor = f.lap > 1 ? "#7c3aed" : "#ff5a1f"
el.textContent = f.lap > 1 ? `R${f.lap}` : "⚑"
const popupHtml = `
<div style="font-size:13px; line-height:1.4;">
<strong>${f.displayName}</strong><br/>
Hull ${f.holeNumber}${f.lap > 1 ? ` · runde ${f.lap}` : ""}
${
f.onGreen && f.distanceToPinCm !== null
? `<br/>${Math.floor(f.distanceToPinCm / 100)} m ${f.distanceToPinCm % 100} cm fra hullet`
: ""
}
</div>
`
const marker = new mapboxgl.Marker({ element: el })
.setLngLat([f.lng, f.lat])
.setPopup(new mapboxgl.Popup({ offset: 16 }).setHTML(popupHtml))
.addTo(map)
markers.push(marker)
}
return () => {
markers.forEach((m) => m.remove())
map.remove()
}
}, [flags])
if (flags.length === 0) {
return (
<div className="flex h-full items-center justify-center p-6 text-center">
<p className="text-base text-muted-foreground">
{visibleToAll ? "Ingen flagg plantet ennå" : "Du har ikke plantet flagget ennå"}
</p>
</div>
)
}
return (
<div className="relative h-full w-full">
<div
ref={containerRef}
className="absolute inset-0 h-full w-full"
aria-label="Kart over plantede flagg"
role="application"
/>
{!loaded && !error ? (
<div className="absolute inset-0 flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<div className="size-8 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
<p className="text-base text-muted-foreground">Laster kart </p>
</div>
</div>
) : null}
{error ? (
<div className="absolute inset-0 flex items-center justify-center p-6">
<p role="alert" className="max-w-sm text-center text-base text-destructive">
{error}
</p>
</div>
) : null}
{!visibleToAll ? (
<div className="pointer-events-none absolute inset-x-0 top-4 flex justify-center px-4">
<p className="rounded-full bg-card/95 px-4 py-2 text-sm font-semibold text-foreground shadow-md">
Kun ditt eget flagg er synlig
</p>
</div>
) : null}
<div className="pointer-events-none absolute inset-x-0 bottom-4 flex justify-center px-4">
<div className="flex items-center gap-3 rounded-full bg-card/95 px-4 py-2 text-xs font-semibold text-foreground shadow-md">
<span className="flex items-center gap-1.5">
<span className="size-3 rounded-full" style={{ backgroundColor: "#ff5a1f" }} aria-hidden="true" />
Runde 1
</span>
<span className="flex items-center gap-1.5">
<span className="size-3 rounded-full" style={{ backgroundColor: "#7c3aed" }} aria-hidden="true" />
Runde 2+
</span>
</div>
</div>
</div>
)
}
export default FlagMapOverview