SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
4.2 KB · 104 lines tsx
Raw Blame History
1"use client";23import type { FacilityFilters, GeoPrecision } from "@dci/core";4import dynamic from "next/dynamic";5import Link from "next/link";6import { useEffect, useRef, useState } from "react";7import { cn } from "@/components/ui/cn";8import { Skeleton } from "@/components/ui/skeleton";9import { isImprecise, PRECISION_LABEL } from "@/lib/labels";10import { filtersToQuery } from "@/lib/query";11import { routes } from "@/lib/routes";1213const MapCanvas = dynamic(() => import("@/components/map/map-canvas").then((m) => m.MapCanvas), { ssr: false, loading: () => <Skeleton className="h-full w-full rounded-none" /> });1415export interface EntityMapProps {16  /** map centre; falls back to the world view */17  center?: { lat: number; lng: number } | null;18  zoom?: number;19  /** facility filters applied to /map (operator, country, status…) */20  filters?: FacilityFilters;21  /** draws a hollow precision ring at the centre when the point is city/metro/approximate */22  precision?: GeoPrecision;23  /** height classes (mobile → desktop) */24  className?: string;25  /** label for the “open in map” affordance */26  openLabel?: string;27  /** hide the open-map link (e.g. facility without exact geo) */28  noLink?: boolean;29}3031/** Zoom that fits a point given its geo precision. */32export function zoomForPrecision(p: GeoPrecision | undefined): number {33  switch (p) {34    case "exact":35    case "parcel":36      return 13.5;37    case "street":38      return 12.5;39    case "approximate":40      return 10.5;41    case "city":42      return 9.5;43    case "metro":44      return 8.5;45    default:46      return 9;47  }48}4950/**51 * Lazy, non-interactive map preview for entity pages (facility point, operator footprint, country / metro view).52 * Rendered only when scrolled near the viewport; the whole surface links to the interactive map with the same view.53 */54export function EntityMap({ center, zoom, filters, precision, className, openLabel = "Open in map", noLink }: EntityMapProps) {55  const ref = useRef<HTMLDivElement>(null);56  const [visible, setVisible] = useState(false);57  useEffect(() => {58    const el = ref.current;59    if (!el) return;60    if (typeof IntersectionObserver === "undefined") {61      const t = setTimeout(() => setVisible(true), 0);62      return () => clearTimeout(t);63    }64    const io = new IntersectionObserver(65      (entries) => {66        if (entries.some((e) => e.isIntersecting)) {67          setVisible(true);68          io.disconnect();69        }70      },71      { rootMargin: "240px" },72    );73    io.observe(el);74    return () => io.disconnect();75  }, []);7677  const view = center ? { lng: center.lng, lat: center.lat, zoom: zoom ?? zoomForPrecision(precision) } : undefined;78  const hollow = precision ? isImprecise(precision) : false;79  const qs = [filtersToQuery(filters ?? {}), view ? `lng=${view.lng.toFixed(4)}&lat=${view.lat.toFixed(4)}&z=${view.zoom}` : ""].filter(Boolean).join("&");80  const href = routes.map(qs || undefined);8182  return (83    <div ref={ref} className={cn("relative w-full overflow-hidden rounded-sm border border-hair bg-surface-2", className ?? "h-[220px] sm:h-[320px]")}>84      {visible ? <MapCanvas preview initialView={view} filters={filters} className="h-full w-full" /> : <Skeleton className="h-full w-full rounded-none" />}85      {center && precision && (86        <div aria-hidden className="pointer-events-none absolute left-1/2 top-1/2 z-[2] -translate-x-1/2 -translate-y-1/2">87          {hollow ? (88            <span className="block size-16 rounded-full border-2 border-dashed border-accent/70 bg-accent/5 sm:size-20" title={precision ? PRECISION_LABEL[precision] : undefined} />89          ) : (90            <span className="block size-3 rounded-full border-2 border-surface bg-accent ring-1 ring-accent/40" />91          )}92        </div>93      )}94      {!noLink && (95        <Link href={href} aria-label={openLabel} className="absolute inset-0 z-[3] flex items-end justify-end p-2 outline-none focus-visible:ring-2 focus-visible:ring-accent sm:p-3">96          <span className="inline-flex h-8 items-center gap-1.5 rounded-sm border border-hair-2 bg-surface px-2.5 text-[12px] font-medium text-ink">97            {openLabel} <span aria-hidden>→</span>98          </span>99        </Link>100      )}101    </div>102  );103}104