teecup/frontend/components/shot/map-point-picker.tsx
Erol Haagenrud 7e7c33a724 Fiks: kart-steget kollapset til 0px høyde (mapbox-gl.css CSS-krasj)
Produksjonsbug: "Kunne ikke laste kartet" for alle, bekreftet av bruker
etter oppfølgingsspørsmål ("alle tre" -- kart-valg, resultat-forhånds-
visning, delt bilde).

Rotårsak: mapbox-gl.css sin egen ".mapboxgl-map { position: relative }"
overstyrte Tailwind sin "absolute"-klasse på kart-beholderen (samme
klassenavn Mapbox GL JS legger til ved init, senere i CSS-cascaden).
Så snart position ikke lenger var absolute, mistet inset-0 all effekt
på størrelsen -- beholderen kollapset til height:0, kartet lastet helt
fint (style/tiles/events alle 200/204) men ble klippet vekk, usynlig.
Rettet ved å bytte til h-full w-full, som løser størrelsen via prosent-
arv uavhengig av hvilken position-verdi som vinner.

Kunne ikke fanges opp i tidligere scratch-tester siden Mapbox sin
URL-restriksjon alltid avviste localhost-scratch-opprinnelser før
kartet fikk lastet/rendret. Verifisert denne runden ved å overstyre
Referer-headeren for å simulere det ekte domenet.

Bygget i samme runde (ikke opprinnelig planlagt, brukerønske): en
client-side satellitt-forhåndsvisning i resultatsteget før lagring, med
det offentlige tokenet direkte som <img src> mot Mapbox Static Images
API.

Se CHANGELOG.md punkt 51 for full diagnose, inkl. en separat snubletråd
i mitt eget scratch-testverktøy (grep uten ^-anker matchet en
kommentarlinje) og en ekte service-worker-cache-fallgruve funnet
underveis.
2026-08-08 17:23:59 +02:00

137 lines
5.3 KiB
TypeScript

"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 and stays mounted until the sheet closes, 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 sheet-open.
*/
export function MapPointPicker({ onConfirm }: { onConfirm: (lngLat: LngLat) => void }) {
const containerRef = useRef<HTMLDivElement | null>(null)
const mapRef = useRef<mapboxgl.Map | null>(null)
const markerRef = useRef<mapboxgl.Marker | null>(null)
const [loaded, setLoaded] = useState(false)
const [point, setPoint] = useState<LngLat | null>(null)
const [error, setError] = useState<string | null>(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
}
mapboxgl.accessToken = token
const map = new mapboxgl.Map({
container,
style: "mapbox://styles/mapbox/satellite-v9",
center: [10.7522, 59.9139], // Oslo fallback; real app centers on last-known position
zoom: 16,
attributionControl: false,
})
mapRef.current = map
map.on("load", () => setLoaded(true))
map.on("error", () => setError("Kunne ikke laste kartet."))
// 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: "#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)
})
return () => {
markerRef.current?.remove()
markerRef.current = null
map.remove()
mapRef.current = null
}
}, [])
return (
<div className="flex h-full flex-col">
<div className="relative flex-1">
{/* 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. */}
<div ref={containerRef} className="absolute inset-0 h-full w-full" aria-label="Satellittkart" 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}
{loaded && !point && !error ? (
<div className="pointer-events-none absolute inset-x-0 top-4 flex justify-center px-4">
<p className="flex items-center gap-2 rounded-full bg-card/95 px-4 py-2 text-sm font-semibold text-foreground shadow-md">
<MapPin className="size-4 shrink-0" aria-hidden="true" />
Trykk kartet for å plassere punktet
</p>
</div>
) : null}
</div>
<div className="shrink-0 border-t border-border bg-background p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
<button
type="button"
disabled={!point}
onClick={() => point && onConfirm(point)}
className="inline-flex min-h-14 w-full items-center justify-center rounded-xl bg-primary px-6 text-lg font-bold text-primary-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
Bekreft punkt
</button>
</div>
</div>
)
}
export default MapPointPicker