2026-02-02 20:34:35 +00:00
|
|
|
/**
|
|
|
|
|
* LeafletMapInner Component
|
|
|
|
|
* Internal Leaflet map implementation (client-side only)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
|
import L from 'leaflet';
|
|
|
|
|
import 'leaflet/dist/leaflet.css';
|
|
|
|
|
import type { School } from '@/lib/types';
|
2026-03-29 12:41:28 +01:00
|
|
|
import { schoolUrl } from '@/lib/utils';
|
2026-02-02 20:34:35 +00:00
|
|
|
|
|
|
|
|
// Fix for default marker icons in Next.js
|
|
|
|
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
|
|
|
|
L.Icon.Default.mergeOptions({
|
|
|
|
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
|
|
|
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
|
|
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
interface LeafletMapInnerProps {
|
|
|
|
|
schools: School[];
|
|
|
|
|
center: [number, number];
|
|
|
|
|
zoom: number;
|
|
|
|
|
onMarkerClick?: (school: School) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function LeafletMapInner({ schools, center, zoom, onMarkerClick }: LeafletMapInnerProps) {
|
|
|
|
|
const mapRef = useRef<L.Map | null>(null);
|
|
|
|
|
const mapContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!mapContainerRef.current) return;
|
|
|
|
|
|
|
|
|
|
// Initialize map
|
|
|
|
|
if (!mapRef.current) {
|
|
|
|
|
mapRef.current = L.map(mapContainerRef.current).setView(center, zoom);
|
|
|
|
|
|
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
|
|
|
maxZoom: 19,
|
|
|
|
|
}).addTo(mapRef.current);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear existing markers
|
|
|
|
|
mapRef.current.eachLayer((layer) => {
|
|
|
|
|
if (layer instanceof L.Marker) {
|
|
|
|
|
mapRef.current!.removeLayer(layer);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add markers for schools
|
|
|
|
|
schools.forEach((school) => {
|
|
|
|
|
if (school.latitude && school.longitude && mapRef.current) {
|
|
|
|
|
const marker = L.marker([school.latitude, school.longitude]).addTo(mapRef.current);
|
|
|
|
|
|
|
|
|
|
// Create popup content
|
|
|
|
|
const popupContent = `
|
|
|
|
|
<div style="min-width: 200px;">
|
|
|
|
|
<strong style="font-size: 14px; display: block; margin-bottom: 8px;">${school.school_name}</strong>
|
2026-02-02 22:34:14 +00:00
|
|
|
${school.local_authority ? `<div style="font-size: 12px; color: #666; margin-bottom: 4px;">${school.local_authority}</div>` : ''}
|
|
|
|
|
${school.school_type ? `<div style="font-size: 12px; color: #666; margin-bottom: 8px;">${school.school_type}</div>` : ''}
|
2026-03-29 12:41:28 +01:00
|
|
|
<a href="${schoolUrl(school.urn, school.school_name)}" style="display: inline-block; margin-top: 8px; padding: 6px 12px; background: #e07256; color: white; text-decoration: none; border-radius: 4px; font-size: 12px;">View Details</a>
|
2026-02-02 20:34:35 +00:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
marker.bindPopup(popupContent);
|
|
|
|
|
|
|
|
|
|
if (onMarkerClick) {
|
|
|
|
|
marker.on('click', () => onMarkerClick(school));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update map view
|
|
|
|
|
if (schools.length > 1) {
|
|
|
|
|
const bounds = L.latLngBounds(
|
|
|
|
|
schools
|
|
|
|
|
.filter(s => s.latitude && s.longitude)
|
|
|
|
|
.map(s => [s.latitude!, s.longitude!] as [number, number])
|
|
|
|
|
);
|
|
|
|
|
mapRef.current.fitBounds(bounds, { padding: [50, 50] });
|
|
|
|
|
} else {
|
|
|
|
|
mapRef.current.setView(center, zoom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
return () => {
|
|
|
|
|
// Don't destroy map on every update, just clean markers
|
|
|
|
|
};
|
|
|
|
|
}, [schools, center, zoom, onMarkerClick]);
|
|
|
|
|
|
|
|
|
|
// Cleanup map on unmount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (mapRef.current) {
|
|
|
|
|
mapRef.current.remove();
|
|
|
|
|
mapRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return <div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />;
|
|
|
|
|
}
|