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, useState } from "react"; | |
| import { getMapData } from "@/lib/api"; | |
| import { Loader2, MapPin, Users } from "lucide-react"; | |
| import dynamic from "next/dynamic"; | |
| // Leaflet must be dynamically imported (no SSR) | |
| const MapComponent = dynamic(() => import("@/components/MapComponent"), { | |
| ssr: false, | |
| loading: () => ( | |
| <div className="flex items-center justify-center h-full"> | |
| <Loader2 className="w-6 h-6 animate-spin text-indigo-500" /> | |
| </div> | |
| ), | |
| }); | |
| export default function MapPage() { | |
| const [mapData, setMapData] = useState<{ sites: any[]; patient_clusters: any[] } | null>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [selectedSite, setSelectedSite] = useState<any | null>(null); | |
| useEffect(() => { | |
| getMapData().then((d) => { setMapData(d); setLoading(false); }).catch(() => setLoading(false)); | |
| }, []); | |
| return ( | |
| <div className="p-6 h-full flex flex-col"> | |
| <div className="mb-4"> | |
| <h1 className="text-2xl font-bold text-slate-900 mb-1">Study Site Map</h1> | |
| <p className="text-slate-500 text-sm">Interactive map of clinical trial sites and patient density clusters</p> | |
| </div> | |
| <div className="flex gap-4 flex-1 min-h-0"> | |
| <div className="flex-1 bg-white rounded-xl border border-slate-200 overflow-hidden min-h-0"> | |
| {loading ? ( | |
| <div className="flex items-center justify-center h-full"> | |
| <Loader2 className="w-6 h-6 animate-spin text-indigo-500" /> | |
| </div> | |
| ) : mapData ? ( | |
| <MapComponent | |
| sites={mapData.sites} | |
| clusters={mapData.patient_clusters} | |
| onSiteClick={setSelectedSite} | |
| /> | |
| ) : ( | |
| <div className="flex items-center justify-center h-full text-slate-400 text-sm"> | |
| Failed to load map data | |
| </div> | |
| )} | |
| </div> | |
| <div className="w-64 shrink-0 space-y-3 overflow-y-auto"> | |
| {selectedSite && ( | |
| <div className="bg-white rounded-xl border border-indigo-200 p-4"> | |
| <h3 className="font-semibold text-slate-900 text-sm mb-2">{selectedSite.name}</h3> | |
| <p className="text-xs text-slate-500 mb-3">{selectedSite.city}, {selectedSite.state}</p> | |
| <div className="space-y-2 text-xs"> | |
| <div className="flex justify-between"><span className="text-slate-500">Active Trials</span><span className="font-semibold text-slate-800">{selectedSite.trials}</span></div> | |
| <div className="flex justify-between"><span className="text-slate-500">Enrolled</span><span className="font-semibold text-slate-800">{selectedSite.enrolled}</span></div> | |
| <div className="flex justify-between"><span className="text-slate-500">Capacity</span><span className="font-semibold text-slate-800">{selectedSite.capacity}</span></div> | |
| <div className="mt-2"> | |
| <div className="flex justify-between text-slate-500 mb-1"><span>Fill Rate</span><span>{Math.round(selectedSite.enrolled / selectedSite.capacity * 100)}%</span></div> | |
| <div className="h-2 bg-slate-100 rounded-full overflow-hidden"> | |
| <div className="h-full bg-indigo-500 rounded-full" style={{ width: `${selectedSite.enrolled / selectedSite.capacity * 100}%` }} /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="bg-white rounded-xl border border-slate-200 p-4"> | |
| <h3 className="text-xs font-semibold text-slate-600 mb-3">Legend</h3> | |
| <div className="space-y-2 text-xs"> | |
| <div className="flex items-center gap-2"> | |
| <MapPin className="w-4 h-4 text-indigo-600" /> | |
| <span className="text-slate-600">Study sites (click for details)</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className="w-4 h-4 rounded-full bg-indigo-200 border border-indigo-400" /> | |
| <span className="text-slate-600">Patient density clusters</span> | |
| </div> | |
| </div> | |
| </div> | |
| {mapData?.sites && ( | |
| <div className="bg-white rounded-xl border border-slate-200 p-4"> | |
| <h3 className="text-xs font-semibold text-slate-600 mb-2">All Sites</h3> | |
| <div className="space-y-2"> | |
| {mapData.sites.map((site, i) => ( | |
| <button | |
| key={i} | |
| onClick={() => setSelectedSite(site)} | |
| className="w-full text-left text-xs hover:bg-slate-50 rounded-lg px-2 py-1.5 transition-colors" | |
| > | |
| <div className="font-medium text-slate-700 truncate">{site.name}</div> | |
| <div className="text-slate-400">{site.city}, {site.state} · {site.trials} trials</div> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |