← back to Homesonspec
apps/web/src/app/map/MapCanvas.tsx
75 lines
"use client";
import { useEffect, useRef } from "react";
import { MapContainer, TileLayer, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
export interface CanvasMarker {
i: string;
la: number;
lo: number;
color: string;
title: string;
sub: string;
}
/**
* Imperative canvas layer — draws thousands of circle markers in a single
* canvas pass instead of mounting one React element per point. Rebuilds when
* the filtered marker set changes; fits bounds only on the first non-empty set.
*/
function CircleLayer({ markers }: { markers: CanvasMarker[] }) {
const map = useMap();
const rendererRef = useRef<L.Canvas | null>(null);
const fittedRef = useRef(false);
useEffect(() => {
if (!rendererRef.current) rendererRef.current = L.canvas({ padding: 0.5 });
const renderer = rendererRef.current;
const group = L.layerGroup().addTo(map);
for (const m of markers) {
L.circleMarker([m.la, m.lo], {
renderer,
radius: 5,
weight: 1,
color: "#ffffff",
fillColor: m.color,
fillOpacity: 0.85,
})
.bindPopup(
`<div style="min-width:150px"><strong>${m.title}</strong><br>${m.sub}<br>` +
`<a href="/homes/${m.i}" style="color:#0f766e;font-weight:600">View home →</a></div>`,
)
.addTo(group);
}
if (!fittedRef.current && markers.length > 0) {
map.fitBounds(
L.latLngBounds(markers.map((m) => [m.la, m.lo] as [number, number])).pad(0.1),
{ maxZoom: 11 },
);
fittedRef.current = true;
}
return () => {
map.removeLayer(group);
};
}, [map, markers]);
return null;
}
export default function MapCanvas({ markers }: { markers: CanvasMarker[] }) {
return (
<MapContainer center={[39.5, -98.35]} zoom={4} preferCanvas style={{ width: "100%", height: "100%" }}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<CircleLayer markers={markers} />
</MapContainer>
);
}