spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { FacilityStatus } from "@dci/core";2import Link from "next/link";3import { cn } from "@/components/ui/cn";4import { StatusDot } from "@/components/ui/status-dot";5import { fmtInt, splitMw } from "@/lib/format";6import { routes } from "@/lib/routes";78export interface FunnelInput {9 /** project counts + published MW by status */10 byStatus: Array<{ status: FacilityStatus; count: number; mw: number | null }>;11 /** facilities opened in the last 12 months (from a pulse / velocity window) — null when the API does not expose it */12 openedLast12m?: { count: number; mw: number | null } | null;13}1415const STEPS: Array<{ key: string; label: string; statuses: FacilityStatus[]; tone: FacilityStatus }> = [16 { key: "announced", label: "Announced", statuses: ["rumored", "proposed", "announced", "expansion"], tone: "announced" },17 { key: "permitting", label: "Permitting", statuses: ["permitting"], tone: "permitting" },18 { key: "approved", label: "Approved", statuses: ["approved"], tone: "approved" },19 { key: "construction", label: "Construction", statuses: ["under_construction", "partially_operational"], tone: "under_construction" },20];2122/**23 * Construction pipeline funnel (§33): ANNOUNCED → PERMITTING → APPROVED → CONSTRUCTION → OPENED, each with project24 * count and the sum of published MW (GW when ≥ 1 000). Bars are proportional to the count; MW are labelled25 * "known published MW only" because most projects do not publish a figure.26 */27export function PipelineFunnel({ data, className, compact }: { data: FunnelInput; className?: string; compact?: boolean }) {28 const agg = STEPS.map((s) => {29 const rows = data.byStatus.filter((r) => s.statuses.includes(r.status));30 return { ...s, count: rows.reduce((a, r) => a + r.count, 0), mw: rows.some((r) => r.mw !== null) ? rows.reduce((a, r) => a + (r.mw ?? 0), 0) : null };31 });32 const opened = data.openedLast12m ?? null;33 const delayed = data.byStatus.filter((r) => r.status === "delayed" || r.status === "cancelled");34 const max = Math.max(1, ...agg.map((a) => a.count), opened?.count ?? 0);35 const cells = [...agg.map((a) => ({ key: a.key, label: a.label, count: a.count, mw: a.mw, tone: a.tone as FacilityStatus, href: routes.projects(`status=${a.statuses.join(",")}`) })), { key: "opened", label: "Opened · 12 m", count: opened?.count ?? null, mw: opened?.mw ?? null, tone: "operational" as FacilityStatus, href: routes.datacenters("status=operational&sort=opened") }];36 return (37 <div className={className}>38 <ol className={cn("grid gap-x-3 gap-y-3", compact ? "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5" : "grid-cols-2 sm:grid-cols-5")}>39 {cells.map((c) => {40 const s = splitMw(c.mw);41 const pct = c.count === null ? 0 : Math.max(2, (c.count / max) * 100);42 return (43 <li key={c.key} className="min-w-0">44 <Link href={c.href} className="group block">45 <div className="label-caps mb-1 flex min-w-0 items-center gap-1.5">46 <StatusDot status={c.tone} size={7} /> <span className="truncate">{c.label}</span>47 </div>48 <div className={cn("figure font-medium leading-none tracking-[-0.02em] text-ink group-hover:text-accent", compact ? "text-[18px]" : "text-[22px] sm:text-[26px]")}>{c.count === null ? <span className="text-ink-4">—</span> : fmtInt(c.count)}</div>49 <div className="mt-1.5 h-[3px] w-full bg-surface-3">50 <div className="h-full" style={{ width: `${pct}%`, background: `var(--st-${c.tone === "under_construction" ? "construction" : c.tone === "announced" ? "planned" : c.tone === "approved" || c.tone === "permitting" ? "permitting" : "operational"})` }} />51 </div>52 <div className="figure mt-1 text-[11.5px] text-ink-3">{c.count === null ? "not tracked as projects" : s ? `${s.value} ${s.unit}` : c.count > 0 ? "no MW published" : "—"}</div>53 </Link>54 </li>55 );56 })}57 </ol>58 <div className="mt-2 flex flex-wrap items-center justify-between gap-x-4 gap-y-1 text-[11px] text-ink-4">59 <span className="label-caps text-ink-4">Known published MW only</span>60 {delayed.length > 0 && (61 <span>62 {delayed.map((d, i) => (63 <span key={d.status}>64 {i > 0 && " · "}65 <Link href={routes.projects(`status=${d.status}`)} className="hover:text-ink">66 {fmtInt(d.count)} {d.status}67 </Link>68 </span>69 ))}70 </span>71 )}72 </div>73 </div>74 );75}76