File size: 2,860 Bytes
59abb4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
"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%" }} />;
}