CTA / frontend /src /components /MapComponent.tsx
TheQuantEd's picture
Initial deployment: ClinicalMatch AI v2.0 — FHIR R4 · MCP (9 tools) · A2A workflow · SHARP compliance · 100k synthetic patients · Neo4j graph · GraphRAG chatbot
59abb4f
"use client";
import { useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
interface Props {
sites: any[];
clusters: any[];
onSiteClick: (site: any) => void;
}
export default function MapComponent({ sites, clusters, onSiteClick }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<L.Map | null>(null);
const onSiteClickRef = useRef(onSiteClick);
onSiteClickRef.current = onSiteClick;
useEffect(() => {
if (!containerRef.current) return;
// Destroy any pre-existing Leaflet instance on this element
if ((containerRef.current as any)._leaflet_id) {
(containerRef.current as any)._leaflet_id = null;
}
if (mapRef.current) {
mapRef.current.remove();
mapRef.current = null;
}
const map = L.map(containerRef.current, { center: [39.5, -98.35], zoom: 4 });
mapRef.current = map;
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map);
const siteIcon = L.divIcon({
className: "",
html: `<div style="width:28px;height:28px;background:#4f46e5;border-radius:50% 50% 50% 0;transform:rotate(-45deg);border:3px solid white;box-shadow:0 2px 6px rgba(0,0,0,.3)"></div>`,
iconSize: [28, 28],
iconAnchor: [14, 28],
popupAnchor: [0, -30],
});
const validSites = sites.filter((s) => s.lat && s.lon);
const validClusters = clusters.filter((c) => c.lat && c.lon);
validClusters.forEach((cluster) => {
L.circle([cluster.lat, cluster.lon], {
radius: cluster.count * 800,
color: "#6366f1",
fillColor: "#818cf8",
fillOpacity: 0.25,
weight: 1,
})
.bindPopup(`<div class="text-sm font-semibold">${cluster.city}</div><div class="text-xs text-gray-600">${cluster.count} potential patients</div>`)
.addTo(map);
});
validSites.forEach((site) => {
L.marker([site.lat, site.lon], { icon: siteIcon })
.bindPopup(
`<div class="text-sm font-semibold">${site.name}</div>` +
`<div class="text-xs text-gray-500">${site.city}, ${site.state}</div>` +
`<div class="text-xs mt-1">${site.trials} active trials · ${site.enrolled}/${site.capacity} enrolled</div>`
)
.on("click", () => onSiteClickRef.current(site))
.addTo(map);
});
if (validSites.length > 0) {
const bounds = L.latLngBounds(validSites.map((s) => [s.lat, s.lon]));
map.fitBounds(bounds, { padding: [40, 40] });
}
return () => {
map.remove();
mapRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div ref={containerRef} style={{ height: "100%", width: "100%" }} />;
}