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.2 KB · 56 lines tsx
Raw Blame History
1"use client";23import type { ExploreQuery, FacilityFilters } from "@dci/core";4import dynamic from "next/dynamic";5import Link from "next/link";6import { Skeleton } from "@/components/ui/skeleton";7import { fmtInt } from "@/lib/format";8import { routes } from "@/lib/routes";910const MapCanvas = dynamic(() => import("@/components/map/map-canvas").then((m) => m.MapCanvas), { ssr: false, loading: () => <Skeleton className="h-full w-full rounded-none" /> });1112/** Facility filters the /map endpoint understands, derived from the explore query. */13export function mapFiltersFor(q: ExploreQuery): FacilityFilters {14  const f: FacilityFilters = {};15  if (q.country) f.country = q.country;16  if (q.operator) f.operator = q.operator;17  if (q.metro) f.metro = q.metro;18  if (q.status) f.status = q.status;19  if (q.type) f.type = q.type;20  if (q.min_mw !== undefined) f.min_mw = q.min_mw;21  if (q.max_mw !== undefined) f.max_mw = q.max_mw;22  if (q.hyperscale) f.hyperscale = true;23  if (q.has_mw) f.has_mw = true;24  if (q.ai && q.ai !== "associated") f.ai = true;25  if (q.q) f.q = q.q;26  if (q.opened_from !== undefined) f.opened_from = q.opened_from;27  if (q.opened_to !== undefined) f.opened_to = q.opened_to;28  return f;29}3031/**32 * Map view of the explore result. The canvas fetches /map with the same filters (server-side clustering), so what it33 * draws is the filtered set, not the current table page. Projects use the projects layer.34 */35export function ExploreMap({ query, total, degraded }: { query: ExploreQuery; total: number; degraded: boolean }) {36  const projects = query.entity === "projects";37  const filters = mapFiltersFor(query);38  const extra: Record<string, string | number | boolean | undefined> = projects ? { project_status: query.status, expected_from: query.expected_after, expected_to: query.expected_before, ai_evidence: query.ai && query.ai !== "any" ? query.ai : undefined } : { location_precision: query.location_precision, ai_evidence: query.ai && query.ai !== "any" ? query.ai : undefined };39  const sp = new URLSearchParams();40  for (const [k, v] of Object.entries({ ...filters, ...extra, layer: projects ? "projects" : undefined })) if (v !== undefined && v !== "" && v !== false) sp.set(k, v === true ? "1" : String(v));41  const href = routes.map(sp.toString() || undefined);42  return (43    <div>44      <div className="relative h-[360px] w-full overflow-hidden rounded-sm border border-hair bg-surface-2 sm:h-[480px]">45        <MapCanvas className="h-full w-full" filters={filters} layer={projects ? "projects" : "facilities"} extraQuery={extra} initialView={{ lng: 10, lat: 25, zoom: 1.4 }} />46      </div>47      <p className="mt-2 text-[11.5px] text-ink-3">48        <span className="figure">{fmtInt(total)}</span> {projects ? "project" : "facility"} points carry coordinates in this result{degraded ? " — the point cap (5 000) was hit, so the map shows clusters" : ""}. Hollow rings are city-level locations; {projects ? "projects are geocoded from their metro or city, never to a parcel" : "records without coordinates are listed in the table but not drawn"}.{" "}49        <Link href={href} className="underline decoration-hair-2 hover:text-ink">50          Open in the full map →51        </Link>52      </p>53    </div>54  );55}56