'use client'; /** * MapView — client component that initializes Leaflet, fetches /api/map, * and renders location-level markers with themed popups. */ import { useEffect, useRef, useState } from 'react'; interface MapLocationPublic { city: string; country: string; lat: number; lng: number; photoCount: number; coverUrl: string; albums: { name: string; url: string }[]; } export function MapView() { const containerRef = useRef(null); const mapRef = useRef(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function init() { // Dynamically import Leaflet (client-only) const L = (await import('leaflet')).default; if (cancelled || !containerRef.current || mapRef.current) return; // Fetch map data const res = await fetch('/api/map'); if (!res.ok) { setError('Failed to load map data'); setLoading(false); return; } const locations: MapLocationPublic[] = await res.json(); if (cancelled) return; // Initialize map const map = L.map(containerRef.current, { zoomControl: true, attributionControl: true, }); mapRef.current = map; // Dark-themed tiles (CartoDB Dark Matter) L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OSM © CARTO', subdomains: 'abcd', maxZoom: 19, }).addTo(map); if (locations.length === 0) { // No geotagged photos — show world view map.setView([20, 0], 2); setLoading(false); return; } // Create markers with custom HTML icons const markers: L.Marker[] = []; for (const loc of locations) { const icon = L.divIcon({ className: '', html: `
${loc.photoCount}
`, iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -20], }); const albumLinks = loc.albums .map((a) => `${a.name} →`) .join(''); const popupHtml = `
${loc.city}

${loc.city}

${loc.country}

${loc.photoCount} photo${loc.photoCount === 1 ? '' : 's'}

${albumLinks}
`; const marker = L.marker([loc.lat, loc.lng], { icon }) .addTo(map) .bindPopup(popupHtml, { maxWidth: 260, minWidth: 200 }); markers.push(marker); } // Fit bounds to show all markers if (markers.length > 0) { const group = L.featureGroup(markers); map.fitBounds(group.getBounds().pad(0.15)); } // Force Leaflet to recalculate tile grid setTimeout(() => map.invalidateSize(), 100); setLoading(false); } init().catch((err) => { console.error('[Map] Init error:', err); if (!cancelled) { setError('Failed to initialize map'); setLoading(false); } }); return () => { cancelled = true; if (mapRef.current) { (mapRef.current as L.Map).remove(); mapRef.current = null; } }; }, []); if (error) { return (

{error}

); } return (
{loading && (

Loading map…

)}
); }