"use client"; import type { FacilityFilters, GeoPrecision } from "@dci/core"; import dynamic from "next/dynamic"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { cn } from "@/components/ui/cn"; import { Skeleton } from "@/components/ui/skeleton"; import { isImprecise, PRECISION_LABEL } from "@/lib/labels"; import { filtersToQuery } from "@/lib/query"; import { routes } from "@/lib/routes"; const MapCanvas = dynamic(() => import("@/components/map/map-canvas").then((m) => m.MapCanvas), { ssr: false, loading: () => }); export interface EntityMapProps { /** map centre; falls back to the world view */ center?: { lat: number; lng: number } | null; zoom?: number; /** facility filters applied to /map (operator, country, status…) */ filters?: FacilityFilters; /** draws a hollow precision ring at the centre when the point is city/metro/approximate */ precision?: GeoPrecision; /** height classes (mobile → desktop) */ className?: string; /** label for the “open in map” affordance */ openLabel?: string; /** hide the open-map link (e.g. facility without exact geo) */ noLink?: boolean; } /** Zoom that fits a point given its geo precision. */ export function zoomForPrecision(p: GeoPrecision | undefined): number { switch (p) { case "exact": case "parcel": return 13.5; case "street": return 12.5; case "approximate": return 10.5; case "city": return 9.5; case "metro": return 8.5; default: return 9; } } /** * Lazy, non-interactive map preview for entity pages (facility point, operator footprint, country / metro view). * Rendered only when scrolled near the viewport; the whole surface links to the interactive map with the same view. */ export function EntityMap({ center, zoom, filters, precision, className, openLabel = "Open in map", noLink }: EntityMapProps) { const ref = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; if (typeof IntersectionObserver === "undefined") { const t = setTimeout(() => setVisible(true), 0); return () => clearTimeout(t); } const io = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) { setVisible(true); io.disconnect(); } }, { rootMargin: "240px" }, ); io.observe(el); return () => io.disconnect(); }, []); const view = center ? { lng: center.lng, lat: center.lat, zoom: zoom ?? zoomForPrecision(precision) } : undefined; const hollow = precision ? isImprecise(precision) : false; const qs = [filtersToQuery(filters ?? {}), view ? `lng=${view.lng.toFixed(4)}&lat=${view.lat.toFixed(4)}&z=${view.zoom}` : ""].filter(Boolean).join("&"); const href = routes.map(qs || undefined); return (
{visible ? : } {center && precision && (
{hollow ? ( ) : ( )}
)} {!noLink && ( {openLabel} → )}
); }