spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { FacilityFilters, SearchHit, SearchResponse } from "@dci/core";2import { Map as MapIcon, Search } from "lucide-react";3import type { Metadata } from "next";4import Link from "next/link";5import { FacilityTable } from "@/components/facility-table";6import { HeroSearch } from "@/components/home/hero";7import { Badge } from "@/components/ui/badge";8import { Button } from "@/components/ui/button";9import { Page, PageHeader, Section } from "@/components/ui/card";10import { EmptyState, OfflineNotice } from "@/components/ui/states";11import { api } from "@/lib/api";12import { fmtInt, fmtMw } from "@/lib/format";13import { SEARCH_GROUP_LABEL, STATUS_LABEL, TYPE_LABEL } from "@/lib/labels";14import { filtersToQuery } from "@/lib/query";15import { routes } from "@/lib/routes";1617export const revalidate = 120;1819export async function generateMetadata({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }): Promise<Metadata> {20 const sp = await searchParams;21 const q = typeof sp.q === "string" ? sp.q : "";22 return { title: q ? `“${q}” — search` : "Search", robots: { index: false, follow: true }, alternates: { canonical: "/search" } };23}2425const GROUPS: SearchHit["type"][] = ["facility", "operator", "country", "metro", "city", "cloud_region", "project", "ixp"];2627export default async function SearchPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {28 const sp = await searchParams;29 const q = (typeof sp.q === "string" ? sp.q : "").trim();3031 if (!q) {32 return (33 <Page>34 <PageHeader eyebrow="Search" title="Search the index" description="Data centers, operators, markets, countries, cloud regions, projects and internet exchanges. Natural filters work too: “hyperscale in Ireland”, “> 100 MW under construction”." />35 <HeroSearch />36 </Page>37 );38 }3940 const r = await api.search(q);41 const data: SearchResponse | null = r.ok ? r.data : null;42 const hits = data?.hits ?? [];43 const facilities = data?.facilities ?? null;44 const interp = data?.interpreted;4546 const interpFilters: FacilityFilters = {};47 if (interp?.countryIso2) interpFilters.country = interp.countryIso2;48 if (interp?.operator) interpFilters.operator = interp.operator;49 if (interp?.status) interpFilters.status = interp.status;50 if (interp?.minMw !== undefined) interpFilters.min_mw = interp.minMw;51 if (interp?.facilityType) interpFilters.type = interp.facilityType;52 if (interp?.text) interpFilters.q = interp.text;53 const mapQs = filtersToQuery(Object.keys(interpFilters).length ? interpFilters : { q });5455 const grouped = new Map<SearchHit["type"], SearchHit[]>();56 for (const h of hits.filter((h) => h.type !== "facility" || !facilities)) {57 if (!grouped.has(h.type)) grouped.set(h.type, []);58 grouped.get(h.type)!.push(h);59 }60 const nothing = r.ok && hits.length === 0 && (!facilities || facilities.total === 0);6162 return (63 <Page>64 <PageHeader65 eyebrow="Search"66 title={67 <>68 Results for <span className="text-ink-2">“{q}”</span>69 </>70 }71 description={r.ok ? `${fmtInt(hits.length + (facilities?.total ?? 0))} matches across the index.` : undefined}72 aside={73 <Button href={routes.map(mapQs)} size="sm">74 <MapIcon className="size-4" strokeWidth={1.75} /> Open in map75 </Button>76 }77 />78 {!r.ok && <OfflineNotice detail={r.error} className="mb-4" />}7980 {interp && Object.keys(interp).length > 0 && (81 <div className="mb-5 flex flex-wrap items-center gap-1.5 text-[12px] text-ink-3">82 <span>Interpreted as</span>83 {interp.text && <Badge tone="outline">text: {interp.text}</Badge>}84 {interp.countryIso2 && <Badge tone="outline">country: {interp.countryIso2}</Badge>}85 {interp.operator && <Badge tone="outline">operator: {interp.operator}</Badge>}86 {interp.status && <Badge tone="outline">status: {STATUS_LABEL[interp.status]}</Badge>}87 {interp.facilityType && <Badge tone="outline">type: {TYPE_LABEL[interp.facilityType]}</Badge>}88 {interp.minMw !== undefined && <Badge tone="outline">≥ {fmtMw(interp.minMw)}</Badge>}89 <Link href={routes.datacenters(filtersToQuery(interpFilters))} className="ml-1 text-ink-2 underline decoration-hair-2 hover:text-ink">90 Apply as filters91 </Link>92 </div>93 )}9495 {nothing && <EmptyState icon={<Search className="size-5" strokeWidth={1.5} />} title={`Nothing matched “${q}”`} description="Try a shorter query, an operator name, a city or a country code." action={<Button href={routes.map()}>Browse the map</Button>} />}9697 <div className="grid gap-x-8 gap-y-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">98 {(facilities || (!r.ok && true)) && (99 <Section title="Data centers" description={facilities ? `${fmtInt(facilities.total)} facilities` : undefined} className="lg:order-1">100 <FacilityTable items={facilities?.items ?? []} basePath={routes.search(q)} columns={["operator", "country", "status", "mw", "type"]} emptyTitle={r.ok ? "No facilities match" : "Facilities unavailable"} emptyDescription={r.ok ? "Try another spelling or browse the index." : "The index API is not reachable right now."} />101 {facilities && facilities.total > facilities.items.length && (102 <div className="border-t border-hair py-3 text-[12.5px] text-ink-3">103 Showing {fmtInt(facilities.items.length)} of {fmtInt(facilities.total)} —{" "}104 <Link href={routes.datacenters(filtersToQuery(Object.keys(interpFilters).length ? interpFilters : { q }))} className="text-ink-2 underline decoration-hair-2 hover:text-ink">105 see all in the index106 </Link>107 </div>108 )}109 </Section>110 )}111 <div className="flex flex-col gap-6 lg:order-2">112 {GROUPS.filter((g) => grouped.has(g)).map((g) => (113 <Section key={g} title={SEARCH_GROUP_LABEL[g] ?? g} level={3} aside={<span className="figure text-[11.5px] text-ink-3">{grouped.get(g)!.length}</span>}>114 <ul className="divide-y divide-hair">115 {grouped.get(g)!.map((h) => (116 <li key={h.id}>117 <Link href={h.href || routes.search(h.title)} className="group flex min-h-11 items-center justify-between gap-3 py-2">118 <span className="min-w-0">119 <span className="block truncate text-[13.5px] font-medium text-ink group-hover:text-accent">{h.title}</span>120 {h.subtitle && <span className="block truncate text-[11.5px] text-ink-3">{h.subtitle}</span>}121 </span>122 {h.meta && typeof h.meta.mw === "number" && <span className="figure shrink-0 text-[12px] text-ink-3">{fmtMw(h.meta.mw)}</span>}123 {h.meta && typeof h.meta.facilityCount === "number" && <span className="figure shrink-0 text-[12px] text-ink-3">{fmtInt(h.meta.facilityCount)} DC</span>}124 </Link>125 </li>126 ))}127 </ul>128 </Section>129 ))}130 {r.ok && grouped.size === 0 && !nothing && <EmptyState compact title="No entities matched" description="Only facilities matched this query." />}131 </div>132 </div>133 </Page>134 );135}136