SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
5.7 KB · 139 lines tsx
Raw Blame History
1import type { FacilityFilters, FacilitySummary } from "@dci/core";2import Link from "next/link";3import { ConfidenceBadge, EstimateMark, FlagBadges, StatusBadge, TypeBadge } from "@/components/ui/badge";4import { CountryFlag } from "@/components/ui/country-flag";5import { EmptyState } from "@/components/ui/states";6import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";7import { bestMw, fmtMw } from "@/lib/format";8import { filtersToQuery } from "@/lib/query";9import { routes } from "@/lib/routes";1011export interface FacilityTableProps {12  items: FacilitySummary[];13  /** current filters → sort links */14  filters?: FacilityFilters;15  basePath?: string;16  /** hide columns for compact embeds */17  columns?: Array<"operator" | "market" | "country" | "status" | "mw" | "type" | "confidence">;18  emptyTitle?: string;19  emptyDescription?: string;20}2122const ALL: NonNullable<FacilityTableProps["columns"]> = ["operator", "market", "country", "status", "mw", "type", "confidence"];2324/**25 * Facility index table. Sortable headers link to ?sort=&order= (server-side). Under 640 px, rows26 * become stacked cards (data-label). Reused by /datacenters, /search and the entity pages.27 */28export function FacilityTable({ items, filters = {}, basePath = routes.datacenters(), columns = ALL, emptyTitle = "No facilities match these filters", emptyDescription = "Loosen a filter or clear them all." }: FacilityTableProps) {29  const has = (c: (typeof ALL)[number]) => columns.includes(c);30  const sortHref = (key: NonNullable<FacilityFilters["sort"]>) => {31    const dir = filters.sort === key ? (filters.order === "asc" ? "desc" : "asc") : key === "name" ? "asc" : "desc";32    const q = filtersToQuery({ ...filters, sort: key, order: dir, page: undefined });33    return `${basePath}${q ? `?${q}` : ""}`;34  };35  const sortState = (key: NonNullable<FacilityFilters["sort"]>) => (filters.sort === key ? (filters.order ?? "desc") : null);3637  if (!items.length) return <EmptyState title={emptyTitle} description={emptyDescription} compact />;3839  return (40    <Table mobileCollapse={{ total: items.length, label: "facilities" }}>41      <THead>42        <tr>43          <Th sort={sortState("name")} sortHref={sortHref("name")}>44            Facility45          </Th>46          {has("operator") && <Th>Operator</Th>}47          {has("market") && <Th className="hidden lg:table-cell">Market</Th>}48          {has("country") && <Th>Country</Th>}49          {has("status") && <Th>Status</Th>}50          {has("mw") && (51            <Th numeric sort={sortState("mw")} sortHref={sortHref("mw")}>52              Capacity53            </Th>54          )}55          {has("type") && <Th className="hidden md:table-cell">Type</Th>}56          {has("confidence") && (57            <Th className="hidden md:table-cell" sort={sortState("completeness")} sortHref={sortHref("completeness")}>58              Confidence59            </Th>60          )}61        </tr>62      </THead>63      <TBody>64        {items.map((f) => {65          const { mw, kind } = bestMw(f);66          return (67            <Tr key={f.id}>68              <Td primary>69                <div className="flex items-center gap-2">70                  <RowLink href={routes.facility(f.slug)}>{f.name}</RowLink>71                  <FlagBadges isAi={f.isAi} isHyperscale={f.isHyperscale} />72                </div>73                <div className="text-[11.5px] text-ink-3 sm:hidden">74                  {f.operator?.name ?? "Operator unknown"}75                  {f.city ? ` · ${f.city}` : ""}76                </div>77              </Td>78              {has("operator") && (79                <Td label="Operator" className="hidden sm:table-cell">80                  {f.operator ? (81                    <Link href={routes.operator(f.operator.slug)} className="text-ink-2 hover:text-ink hover:underline">82                      {f.operator.name}83                    </Link>84                  ) : (85                    <span className="text-ink-4">—</span>86                  )}87                </Td>88              )}89              {has("market") && (90                <Td label="Market" className="hidden lg:table-cell" muted>91                  {f.metro ? (92                    <Link href={routes.market(f.metro.slug)} className="hover:text-ink hover:underline">93                      {f.metro.name}94                    </Link>95                  ) : (96                    f.city ?? "—"97                  )}98                </Td>99              )}100              {has("country") && (101                <Td label="Country">102                  <CountryFlag iso2={f.countryIso2} withName className="max-w-[160px] text-ink-2" />103                </Td>104              )}105              {has("status") && (106                <Td label="Status">107                  <StatusBadge status={f.status} short size="xs" />108                </Td>109              )}110              {has("mw") && (111                <Td label="Capacity" numeric lead>112                  {mw === null ? (113                    <span className="text-ink-4">—</span>114                  ) : (115                    <span title={kind === "it" ? "IT capacity" : kind === "total" ? "Total power" : "Planned power"}>116                      {kind === "planned" && <EstimateMark className="mr-0.5" />}117                      {fmtMw(mw)}118                    </span>119                  )}120                </Td>121              )}122              {has("type") && (123                <Td label="Type" className="hidden md:table-cell">124                  <TypeBadge type={f.facilityType} size="xs" />125                </Td>126              )}127              {has("confidence") && (128                <Td label="Confidence" className="hidden md:table-cell">129                  <ConfidenceBadge level={f.confidence} size="xs" />130                </Td>131              )}132            </Tr>133          );134        })}135      </TBody>136    </Table>137  );138}139