'use client'; /** * MapView — fetches /api/map and renders its location-level markers with * themed popups through LeafletMap, which owns the Leaflet setup. */ import { useEffect, useMemo, useState } from 'react'; import { useDictionary } from './I18nProvider'; import { LeafletMap, escapeHtml, type LeafletMarker } from './LeafletMap'; interface MapLocationPublic { city: string; country: string; lat: number; lng: number; photoCount: number; coverUrl: string; albums: { name: string; url: string }[]; } export function MapView() { const t = useDictionary(); const [locations, setLocations] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function load() { const res = await fetch('/api/map'); if (!res.ok) { let detail = ''; try { detail = await res.text(); } catch { /* ignore */ } console.error('[Map] API error', res.status, detail); if (!cancelled) { setError(t.map.loadFailed(res.status)); setLoading(false); } return; } const data: MapLocationPublic[] = await res.json(); if (!cancelled) setLocations(data); } load().catch((err) => { console.error('[Map] Init error:', err); if (!cancelled) { setError(t.map.initFailed); setLoading(false); } }); return () => { cancelled = true; }; }, [t]); const markers = useMemo( () => (locations ?? []).map((loc) => { const albumLinks = loc.albums .map( (a) => `${escapeHtml(a.name)} →`, ) .join(''); /* * An album set to `location: country` has no city name to show — the * API withholds it rather than naming a village inside a 100 km cell * (#469). The country then carries the heading on its own. */ const heading = loc.city || loc.country; const subheading = loc.city ? loc.country : ''; return { lat: loc.lat, lng: loc.lng, html: `
${loc.photoCount}
`, popupHtml: `
${escapeHtml(heading)}

${escapeHtml(heading)}

${subheading ? `

${escapeHtml(subheading)}

` : ''}

${escapeHtml(t.common.photos(loc.photoCount))}

${albumLinks}
`, }; }), [locations, t], ); if (error) { return (

{error}

); } return (
{loading && (

{t.map.loading}

)} {locations ? ( setLoading(false)} onError={() => { setError(t.map.initFailed); setLoading(false); }} /> ) : (
)}
); }