SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
5 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
3.0 KB · 51 lines tsx
Raw Blame History
1"use client";23import type { MapPoint, MapResponse, ProjectSummary } from "@dci/core";4import dynamic from "next/dynamic";5import Link from "next/link";6import { useCallback, useState } from "react";7import { StatusBadge } from "@/components/ui/badge";8import { Button } from "@/components/ui/button";9import { Skeleton } from "@/components/ui/skeleton";10import { fmtInt, fmtMw } from "@/lib/format";11import { PRECISION_LABEL } from "@/lib/labels";12import { routes } from "@/lib/routes";1314const MapCanvas = dynamic(() => import("@/components/map/map-canvas").then((m) => m.MapCanvas), { ssr: false, loading: () => <Skeleton className="h-full w-full rounded-none" /> });1516/**17 * Projects MAP view: the shared canvas in `layer=projects` mode (square stage-coloured markers, hollow ring for18 * city-level points). Projects without published coordinates cannot be drawn — the caption says how many are missing19 * instead of guessing a location.20 */21export function ProjectsMapView({ items, total, projectStatus }: { items: ProjectSummary[]; total: number; projectStatus?: string }) {22  const [data, setData] = useState<MapResponse | null>(null);23  const [sel, setSel] = useState<MapPoint | null>(null);24  const onData = useCallback((r: MapResponse | null) => setData(r), []);25  const located = items.filter((p) => p.lat !== null && p.lng !== null).length;26  return (27    <div className="flex flex-col gap-2">28      <div className="relative h-[52vh] min-h-[320px] w-full overflow-hidden rounded-sm border border-hair bg-surface-2">29        <MapCanvas layer="projects" extraQuery={{ project_status: projectStatus }} onData={onData} onSelect={setSel} className="h-full w-full" />30        {sel && (31          <div className="absolute bottom-3 left-3 right-3 z-[5] flex flex-wrap items-center gap-2 rounded-sm border border-hair bg-surface/95 px-3 py-2 text-[12.5px] backdrop-blur sm:right-auto sm:max-w-[420px]">32            <StatusBadge status={sel.s} short size="xs" />33            <span className="min-w-0 flex-1 truncate font-medium text-ink">{sel.n}</span>34            <span className="figure text-ink-2">{sel.mw != null ? `≈ ${fmtMw(sel.mw)}` : "—"}</span>35            <span className="text-[11px] text-ink-3">{PRECISION_LABEL[sel.p]}</span>36            <Button href={sel.k === "project" ? routes.project(sel.slug) : routes.facility(sel.slug)} size="xs" variant="primary">37              Open38            </Button>39          </div>40        )}41      </div>42      <p className="text-[11.5px] text-ink-3">43        {data ? <>{fmtInt(data.total)} located {data.total === 1 ? "project" : "projects"} drawn</> : "Loading…"} · {fmtInt(located)} of the {fmtInt(items.length)} projects on this page have published coordinates{total > items.length ? ` (${fmtInt(total)} match in total)` : ""}. Square markers are coloured by stage; a hollow ring means a city-level location, never a site.{" "}44        <Link href={routes.map("layer=projects")} className="underline decoration-hair-2 hover:text-ink">45          Open in the full map46        </Link>47      </p>48    </div>49  );50}51