Files
school_compare/nextjs-app/components/LeafletHeroMapInner.tsx
T

96 lines
3.2 KiB
TypeScript
Raw Normal View History

/**
* LeafletHeroMapInner
* Minimal single-school map for the detail hero. Renders as a static preview
* (all interaction disabled so it never traps page scroll); when `interactive`
* flips true — i.e. the hero map has gone fullscreen — pan/zoom are enabled and
* a zoom control appears.
*/
'use client';
import { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
interface LeafletHeroMapInnerProps {
lat: number;
lng: number;
interactive: boolean;
}
// Coral teardrop pin with a soft halo — inline styles only, so it renders
// reliably inside Leaflet's divIcon HTML string (no CSS-module dependency).
const PIN_HTML = `
<div style="position:relative;width:26px;height:26px">
<span style="position:absolute;left:50%;top:74%;width:32px;height:32px;transform:translate(-50%,-50%);border-radius:50%;background:rgba(224,114,86,.25)"></span>
<svg width="26" height="34" viewBox="0 0 26 34" style="filter:drop-shadow(0 3px 4px rgba(0,0,0,.35))">
<path d="M13 0C5.8 0 0 5.8 0 13c0 9.2 13 21 13 21s13-11.8 13-21C26 5.8 20.2 0 13 0Z" fill="#e07256"/>
<circle cx="13" cy="13" r="5" fill="#fff"/>
</svg>
</div>`;
export default function LeafletHeroMapInner({ lat, lng, interactive }: LeafletHeroMapInnerProps) {
const elRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<L.Map | null>(null);
const zoomCtrlRef = useRef<L.Control.Zoom | null>(null);
// Create the map once.
useEffect(() => {
if (!elRef.current || mapRef.current) return;
const map = L.map(elRef.current, {
zoomControl: false,
attributionControl: true,
dragging: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
keyboard: false,
touchZoom: false,
}).setView([lat, lng], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
}).addTo(map);
L.marker([lat, lng], {
icon: L.divIcon({ className: '', iconSize: [26, 34], iconAnchor: [13, 30], html: PIN_HTML }),
keyboard: false,
}).addTo(map);
mapRef.current = map;
setTimeout(() => map.invalidateSize(), 60);
return () => {
map.remove();
mapRef.current = null;
zoomCtrlRef.current = null;
};
}, [lat, lng]);
// Toggle interaction (and the zoom control) when entering/leaving fullscreen,
// and re-measure since the container size changes.
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const handlers = [
map.dragging, map.scrollWheelZoom, map.doubleClickZoom, map.boxZoom, map.keyboard, map.touchZoom,
];
handlers.forEach((h) => { if (h) { interactive ? h.enable() : h.disable(); } });
if (interactive && !zoomCtrlRef.current) {
zoomCtrlRef.current = L.control.zoom({ position: 'topleft' });
zoomCtrlRef.current.addTo(map);
} else if (!interactive && zoomCtrlRef.current) {
zoomCtrlRef.current.remove();
zoomCtrlRef.current = null;
}
if (!interactive) map.setView([lat, lng], 15);
setTimeout(() => map.invalidateSize(), 80);
}, [interactive, lat, lng]);
return <div ref={elRef} style={{ width: '100%', height: '100%' }} />;
}