spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
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