Spaces:
Sleeping
Sleeping
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: '© <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%" }} />; | |
| } | |