import type { FacilityStatus } from "@dci/core"; import { Activity, Columns3, Map as MapIcon, Table2 } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; import { PipelineByYear } from "@/components/home/charts"; import { PipelineFunnel } from "@/components/home/pipeline-funnel"; import { AiMark, ConfidenceBadge, EstimateMark, PrecisionBadge, ScopeBadge, StatusBadge } from "@/components/ui/badge"; import { Page, PageHeader } from "@/components/ui/card"; import { BarList } from "@/components/ui/charts"; import { CountryFlag } from "@/components/ui/country-flag"; import { FilterChips, type Chip } from "@/components/ui/filter-chips"; import { Pagination } from "@/components/ui/pagination"; import { SegmentedControl } from "@/components/ui/segmented"; import { EmptyState, OfflineNotice } from "@/components/ui/states"; import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table"; import { api } from "@/lib/api"; import { fmtDate, fmtInt, fmtMw, fmtUsd, placeLine } from "@/lib/format"; import { fmtPartialDateSafe } from "@/lib/format-project"; import { STATUS_SHORT, isSiteScopeLabel } from "@/lib/labels"; import { routes } from "@/lib/routes"; import { pageMetadata } from "@/lib/seo"; import { spGet, spInt, type SearchParams } from "@/lib/sort"; import { ProjectFilters } from "../filters"; import { ProjectsMapView } from "../projects-map"; import { PROJECT_CLASS_OPTIONS, PROJECT_VIEWS, projectQueryString, type ProjectQuery, type ProjectView } from "../query"; import { ProjectsKanban, ProjectsTimeline, investmentCell } from "../views"; export const revalidate = 120; export const metadata: Metadata = pageMetadata({ title: "Data center projects & construction pipeline", description: "Announced, permitted, approved and under-construction data center projects worldwide — lifecycle stages, planned MW (published figures only), investment and expected opening. Table, map, timeline and kanban views.", path: routes.projects(), }); const PER_PAGE = 50; const SORTS = ["updated", "mw", "announced", "opening"] as const; function parseQuery(sp: SearchParams): ProjectQuery { const q: ProjectQuery = {}; const view = spGet(sp, "view"); if (view && (PROJECT_VIEWS as readonly string[]).includes(view)) q.view = view as ProjectView; const s = spGet(sp, "status"); if (s) q.status = s; const c = spGet(sp, "country"); if (c) q.country = c.toUpperCase(); const o = spGet(sp, "operator"); if (o) q.operator = o; const mw = Number(spGet(sp, "min_mw")); if (Number.isFinite(mw) && mw > 0) q.min_mw = mw; const ai = spGet(sp, "ai"); if (ai === "1" || ai === "true") q.ai = true; const cls = spGet(sp, "class"); if (cls) q.project_class = cls; const ef = Number(spGet(sp, "expected_from")); if (Number.isFinite(ef) && ef > 1990) q.expected_from = ef; const et = Number(spGet(sp, "expected_to")); if (Number.isFinite(et) && et > 1990) q.expected_to = et; const text = spGet(sp, "q"); if (text) q.q = text; const sort = spGet(sp, "sort"); if (sort && (SORTS as readonly string[]).includes(sort)) q.sort = sort as ProjectQuery["sort"]; const order = spGet(sp, "order"); if (order === "asc" || order === "desc") q.order = order; q.page = spInt(sp, "page"); return q; } export default async function ProjectsPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const query = parseQuery(sp); const view: ProjectView = query.view ?? "table"; const page = query.page ?? 1; const perPage = view === "table" ? PER_PAGE : 100; const [pipe, list, countriesRes] = await Promise.all([ api.projectPipeline(), api.projects({ status: query.status, country: query.country, operator: query.operator, min_mw: query.min_mw, ai: query.ai, project_class: query.project_class, expected_from: query.expected_from, expected_to: query.expected_to, q: query.q, sort: query.sort ?? (view === "timeline" ? "opening" : "updated"), order: query.order ?? (view === "timeline" ? "asc" : undefined), page, per_page: perPage }), api.countries(), ]); const p = pipe.ok ? pipe.data : null; const countries = countriesRes.items.map((c) => ({ iso2: c.iso2, name: c.name })); const href = (patch: Partial) => routes.projects(projectQueryString({ ...query, page: undefined, ...patch })); const sortHref = (key: (typeof SORTS)[number]) => { const active = (query.sort ?? "updated") === key; const next = active ? (query.order === "asc" ? "desc" : "asc") : key === "opening" || key === "announced" ? "asc" : "desc"; return href({ sort: key, order: next }); }; const sortState = (key: (typeof SORTS)[number]) => ((query.sort ?? "updated") === key ? (query.order ?? (key === "opening" || key === "announced" ? "asc" : "desc")) : null); const byStatus: Partial> = {}; for (const s of p?.byStatus ?? []) byStatus[s.status] = (byStatus[s.status] ?? 0) + s.count; const byYear = (p?.byYear ?? []).map((y) => ({ year: y.year, count: y.count, mw: y.mw })); const chips: Chip[] = []; for (const s of (query.status ?? "").split(",").filter(Boolean)) chips.push({ key: `status-${s}`, label: "Stage", value: STATUS_SHORT[s as FacilityStatus] ?? s, removeHref: href({ status: (query.status ?? "").split(",").filter((x) => x && x !== s).join(",") || undefined }) }); if (query.country) chips.push({ key: "country", label: "Country", value: countries.find((c) => c.iso2 === query.country)?.name ?? query.country, removeHref: href({ country: undefined }) }); if (query.operator) chips.push({ key: "operator", label: "Operator", value: query.operator, removeHref: href({ operator: undefined }) }); if (query.min_mw) chips.push({ key: "min_mw", label: "Planned", value: `≥ ${fmtMw(query.min_mw)}`, removeHref: href({ min_mw: undefined }) }); if (query.ai) chips.push({ key: "ai", label: "AI", value: "AI infrastructure", removeHref: href({ ai: undefined }) }); for (const c of (query.project_class ?? "").split(",").filter(Boolean)) chips.push({ key: `class-${c}`, label: "Class", value: PROJECT_CLASS_OPTIONS.find((o) => o.v === c)?.label ?? c, removeHref: href({ project_class: (query.project_class ?? "").split(",").filter((x) => x && x !== c).join(",") || undefined }) }); if (query.expected_from || query.expected_to) chips.push({ key: "expected", label: "Expected", value: `${query.expected_from ?? "…"}–${query.expected_to ?? "…"}`, removeHref: href({ expected_from: undefined, expected_to: undefined }) }); if (query.q) chips.push({ key: "q", label: "Search", value: query.q, removeHref: href({ q: undefined }) }); const views: Array<{ value: ProjectView; label: React.ReactNode; title: string }> = [ { value: "table", label: <> Table, title: "Sortable table" }, { value: "map", label: <> Map, title: "Located projects on the map" }, { value: "timeline", label: <> Timeline, title: "By expected opening year" }, { value: "kanban", label: <> Kanban, title: "Columns by lifecycle stage" }, ]; return ( ({ value: v.value, label: v.label, title: v.title, href: href({ view: v.value === "table" ? undefined : v.value }) }))} />} /> {!pipe.ok && } {/* funnel */}
Investment announced
{p?.totals.investmentUsd ? fmtUsd(p.totals.investmentUsd) : "—"}

{p?.totals.investmentUsd ? "where disclosed, site-scoped only" : "not disclosed"} · {p ? `${fmtInt(p.totals.ai)} AI projects` : ""}

By expected year
{byYear.length ? : }
Top countries
{p?.byCountry.length ? ( ({ label: , value: c.count, href: href({ country: c.countryIso2 }), secondary: c.mw !== null ? `≈ ${fmtMw(c.mw)}` : undefined }))} /> ) : ( )}
{list.items.length === 0 ? ( ) : view === "map" ? ( ) : view === "timeline" ? ( ) : view === "kanban" ? ( ) : ( {list.items.map((pr) => { const site = pr.capacityScope == null || isSiteScopeLabel(pr.capacityScope); return ( ); })}
Project Operator Location Stage Planned MW Investment Announced Expected Confidence
{pr.name} {pr.isAi && }
{pr.operator?.name ?? "Operator unknown"}
{pr.operator ? ( {pr.operator.name} ) : ( — )} {pr.metro ? {pr.metro.name} : placeLine(pr)} {pr.geoPrecision && pr.geoPrecision !== "exact" && pr.geoPrecision !== "parcel" && } {pr.plannedMw === null ? ( — ) : site ? ( {fmtMw(pr.plannedMw)} ) : ( — )} {investmentCell(pr) === "—" ? — : investmentCell(pr)} {fmtDate(pr.announcedOn)} {pr.expectedOpening ? fmtPartialDateSafe(pr.expectedOpening) : TBD}
)}
routes.projects(projectQueryString({ ...query, page: pg }))} label="projects" className="mt-3" />

Projects are reconciled from operator press releases, planning filings and news through a lifecycle state machine; status changes are versioned and appear in the{" "} live feed . Locations are shown at the precision published (a city-level ring is never a site).

); }