"use client" import { useEffect, useRef, useState } from "react" import mapboxgl from "mapbox-gl" import "mapbox-gl/dist/mapbox-gl.css" import { MapPin } from "lucide-react" type LngLat = { lng: number; lat: number } /** * MapPointPicker * * Rendered ONLY when the user chooses "Velg punkt på kart", and always via * next/dynamic(..., { ssr: false }) from the parent. It mounts once per * sheet-open (once per step, since start and end use separate mounted * instances) and stays mounted until that step is left, so the Mapbox map is * created exactly once (empty-deps useEffect) — taps, pans and marker drags * never re-initialise it. This keeps Mapbox map loads (which are billed) to a * single load per step-open. */ export function MapPointPicker({ onConfirm, referencePoint, referenceLabel = "Utslag", }: { onConfirm: (lngLat: LngLat) => void /** * Satt KUN på ballposisjon-steget (ADR-048-tillegg 2026-08-08): et fast, * ikke-flyttbart referansepunkt (utslagspunktet) vises på kartet, og * kartet forsøker å vise BÅDE dette og brukerens nåværende posisjon i * samme bilde -- brukerønske: "slagpunktet må være en del av det jeg ser". */ referencePoint?: LngLat referenceLabel?: string }) { const containerRef = useRef(null) const mapRef = useRef(null) const markerRef = useRef(null) const referenceMarkerRef = useRef(null) const [loaded, setLoaded] = useState(false) const [point, setPoint] = useState(null) const [error, setError] = useState(null) useEffect(() => { const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN const container = containerRef.current if (!container) return if (!token) { setError("Kartet er ikke tilgjengelig akkurat nå. Bruk «Min posisjon nå» i stedet.") return } let cancelled = false // Reell bug funnet 2026-08-08: kartet sentrerte ALLTID på en hardkodet // Oslo-koordinat -- en kommentar her hevdet "real app centers on // last-known position", men den logikken fantes aldri, kun påstanden. // Henter nå brukerens faktiske GPS-posisjon FØR kartet initialiseres // (kun ETT kall, samme som "Min posisjon nå"-veien -- ingen løpende // watchPosition), med Oslo som fallback KUN hvis posisjon ikke kan // hentes (avslått tillatelse, tidsavbrudd, ingen støtte). // fitTo: satt når vi HAR en brukerposisjon OG et referansepunkt -- // kartet zoomer/panorerer da til å vise begge (padding rundt), i stedet // for bare å sentrere på ett av dem, slik at referansepunktet garantert // er synlig med det samme. function initMap(center: [number, number], fitTo?: [[number, number], [number, number]]) { if (cancelled || !container) return mapboxgl.accessToken = token as string const map = new mapboxgl.Map({ container, style: "mapbox://styles/mapbox/satellite-v9", center, zoom: referencePoint ? 17 : 16, attributionControl: false, }) mapRef.current = map map.on("load", () => { setLoaded(true) if (fitTo) { const bounds = new mapboxgl.LngLatBounds(fitTo[0], fitTo[0]) bounds.extend(fitTo[1]) map.fitBounds(bounds, { padding: 80, maxZoom: 18, duration: 0 }) } }) map.on("error", () => setError("Kunne ikke laste kartet.")) // Fast referansemarkør (utslagspunktet) -- kan ikke flyttes, kun til // orientering. Egen farge (samme "pin-a"-oransje som brukes i // delings-bildet) for å skille tydelig fra punktet som plasseres. if (referencePoint) { referenceMarkerRef.current = new mapboxgl.Marker({ color: "#ff5a1f" }) .setLngLat([referencePoint.lng, referencePoint.lat]) .addTo(map) } // Tap to place/move a single draggable marker — never re-inits the map. map.on("click", (e) => { const lngLat = { lng: e.lngLat.lng, lat: e.lngLat.lat } if (!markerRef.current) { const marker = new mapboxgl.Marker({ draggable: true, color: referencePoint ? "#2f7a3f" : "#d2551a" }) .setLngLat(e.lngLat) .addTo(map) marker.on("dragend", () => { const p = marker.getLngLat() setPoint({ lng: p.lng, lat: p.lat }) }) markerRef.current = marker } else { markerRef.current.setLngLat(e.lngLat) } setPoint(lngLat) }) } const OSLO_FALLBACK: [number, number] = [10.7522, 59.9139] const fallbackCenter: [number, number] = referencePoint ? [referencePoint.lng, referencePoint.lat] : OSLO_FALLBACK if (typeof navigator !== "undefined" && navigator.geolocation) { navigator.geolocation.getCurrentPosition( (pos) => { const here: [number, number] = [pos.coords.longitude, pos.coords.latitude] if (referencePoint) { initMap(here, [here, [referencePoint.lng, referencePoint.lat]]) } else { initMap(here) } }, () => initMap(fallbackCenter), { enableHighAccuracy: true, timeout: 8000 }, ) } else { initMap(fallbackCenter) } return () => { cancelled = true referenceMarkerRef.current?.remove() referenceMarkerRef.current = null markerRef.current?.remove() markerRef.current = null mapRef.current?.remove() mapRef.current = null } // eslint-disable-next-line react-hooks/exhaustive-deps -- montert én gang per steg, se komponent-docstring }, []) return (
{/* Map canvas host — kept mounted for the map's whole lifetime. `h-full w-full`, IKKE `absolute inset-0`: mapbox-gl.css sin egen `.mapboxgl-map { position: relative }`-regel overstyrer en `absolute`-Tailwind-klasse her (samme klasse-navn, senere i cascade siden mapbox-gl.css importeres i denne komponentfilen) -- når posisjonen dermed blir `relative` i stedet for `absolute`, mister `inset-0` all effekt på størrelsen (den styrer kun posisjon/størrelse for absolutt/fixed-plasserte elementer), og beholderen kollapser til 0 høyde uansett hvilken posisjonsverdi som vinner kappløpet. `h-full`/`w-full` løser størrelsen via prosent-arv i stedet, som fungerer likt for både `relative` og `absolute`. Reell bug funnet 2026-08-08, se CHANGELOG.md. */}
{!loaded && !error ? (

Laster kart …

) : null} {error ? (

{error}

) : null} {referencePoint ? (
) : null} {loaded && !point && !error ? (

) : null}
) } export default MapPointPicker