spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1"use client";23import type { DownloadDataset } from "@dci/core";4import { EVENT_TYPES, FACILITY_STATUSES, FACILITY_TYPES, PIPELINE_STATUSES } from "@dci/core";5import { Check, Copy, Download } from "lucide-react";6import { useMemo, useState } from "react";7import { Button } from "@/components/ui/button";8import { cn } from "@/components/ui/cn";9import { SegmentedControl } from "@/components/ui/segmented";10import { clientApiBase } from "@/lib/api";11import { EVENT_LABEL, STATUS_LABEL, TYPE_LABEL } from "@/lib/labels";1213const inputCls = "h-10 w-full min-w-0 rounded-sm border border-hair-2 bg-surface px-2.5 text-[13px] text-ink outline-none focus:border-ink sm:h-9";14const FORMAT_LABEL: Record<DownloadDataset["formats"][number], string> = { csv: "CSV", json: "JSON", geojson: "GeoJSON" };1516type Values = Record<string, string | boolean>;1718function Field({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) {19 return (20 <label className={cn("block min-w-0", className)}>21 <span className="label-caps mb-1 block">{label}</span>22 {children}23 </label>24 );25}2627/** Builds the /api/v1/download/:key URL from the dataset's own filter list; nothing else is offered. */28export function DatasetForm({ dataset }: { dataset: DownloadDataset }) {29 const [format, setFormat] = useState<DownloadDataset["formats"][number]>(dataset.formats[0] ?? "csv");30 const [values, setValues] = useState<Values>({});31 const [copied, setCopied] = useState(false);32 const base = useMemo(() => (process.env.NEXT_PUBLIC_API_URL ? `${process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, "")}/api/v1` : clientApiBase()), []);3334 const url = useMemo(() => {35 const sp = new URLSearchParams();36 if (format !== "csv") sp.set("format", format);37 for (const f of dataset.filters) {38 const v = values[f];39 if (v === undefined || v === "" || v === false) continue;40 sp.set(f, v === true ? "1" : String(v));41 }42 const qs = sp.toString();43 return `${base}/download/${dataset.key}${qs ? `?${qs}` : ""}`;44 }, [base, dataset, format, values]);4546 const set = (k: string, v: string | boolean) => setValues((p) => ({ ...p, [k]: v }));47 const copy = async () => {48 try {49 await navigator.clipboard.writeText(url);50 setCopied(true);51 setTimeout(() => setCopied(false), 1800);52 } catch {53 window.prompt("Copy the download URL", url);54 }55 };56 const has = (f: string) => dataset.filters.includes(f);5758 return (59 <div className="flex flex-col gap-3">60 {dataset.filters.length > 0 ? (61 <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">62 {has("country") && (63 <Field label="Country (ISO2, comma list)">64 <input value={(values.country as string) ?? ""} onChange={(e) => set("country", e.target.value.toUpperCase().replace(/[^A-Z,]/g, ""))} placeholder="US,CA" className={cn(inputCls, "figure")} />65 </Field>66 )}67 {has("status") && (68 <Field label="Status">69 <select value={(values.status as string) ?? ""} onChange={(e) => set("status", e.target.value)} className={inputCls}>70 <option value="">Any status</option>71 {FACILITY_STATUSES.map((s) => (72 <option key={s} value={s}>73 {STATUS_LABEL[s]}74 </option>75 ))}76 </select>77 </Field>78 )}79 {has("project_status") && (80 <Field label="Project status">81 <select value={(values.project_status as string) ?? ""} onChange={(e) => set("project_status", e.target.value)} className={inputCls}>82 <option value="">Any status</option>83 {[...PIPELINE_STATUSES, "operational", "expansion", "cancelled"].map((s) => (84 <option key={s} value={s}>85 {STATUS_LABEL[s as keyof typeof STATUS_LABEL]}86 </option>87 ))}88 </select>89 </Field>90 )}91 {has("type") && (92 <Field label="Facility type">93 <select value={(values.type as string) ?? ""} onChange={(e) => set("type", e.target.value)} className={inputCls}>94 <option value="">Any type</option>95 {FACILITY_TYPES.map((t) => (96 <option key={t} value={t}>97 {TYPE_LABEL[t]}98 </option>99 ))}100 </select>101 </Field>102 )}103 {has("event_type") && (104 <Field label="Event type">105 <select value={(values.event_type as string) ?? ""} onChange={(e) => set("event_type", e.target.value)} className={inputCls}>106 <option value="">Any type</option>107 {EVENT_TYPES.map((t) => (108 <option key={t} value={t}>109 {EVENT_LABEL[t]}110 </option>111 ))}112 </select>113 </Field>114 )}115 {has("operator") && (116 <Field label="Operator slug">117 <input value={(values.operator as string) ?? ""} onChange={(e) => set("operator", e.target.value.trim())} placeholder="equinix" className={cn(inputCls, "figure")} />118 </Field>119 )}120 {has("metro") && (121 <Field label="Market slug">122 <input value={(values.metro as string) ?? ""} onChange={(e) => set("metro", e.target.value.trim())} placeholder="northern-virginia" className={cn(inputCls, "figure")} />123 </Field>124 )}125 {has("min_mw") && (126 <Field label="Min MW">127 <input type="number" min={0} inputMode="decimal" value={(values.min_mw as string) ?? ""} onChange={(e) => set("min_mw", e.target.value)} className={cn(inputCls, "figure")} />128 </Field>129 )}130 {has("max_mw") && (131 <Field label="Max MW">132 <input type="number" min={0} inputMode="decimal" value={(values.max_mw as string) ?? ""} onChange={(e) => set("max_mw", e.target.value)} className={cn(inputCls, "figure")} />133 </Field>134 )}135 {has("expected_from") && (136 <Field label="Expected from (year)">137 <input type="number" min={2000} max={2100} inputMode="numeric" value={(values.expected_from as string) ?? ""} onChange={(e) => set("expected_from", e.target.value)} className={cn(inputCls, "figure")} />138 </Field>139 )}140 {has("expected_to") && (141 <Field label="Expected to (year)">142 <input type="number" min={2000} max={2100} inputMode="numeric" value={(values.expected_to as string) ?? ""} onChange={(e) => set("expected_to", e.target.value)} className={cn(inputCls, "figure")} />143 </Field>144 )}145 {has("since") && (146 <Field label="Since (date)">147 <input type="date" value={(values.since as string) ?? ""} onChange={(e) => set("since", e.target.value)} className={cn(inputCls, "figure")} />148 </Field>149 )}150 {has("until") && (151 <Field label="Until (date)">152 <input type="date" value={(values.until as string) ?? ""} onChange={(e) => set("until", e.target.value)} className={cn(inputCls, "figure")} />153 </Field>154 )}155 {has("min_significance") && (156 <Field label="Min significance (0–100)">157 <input type="number" min={0} max={100} inputMode="numeric" value={(values.min_significance as string) ?? ""} onChange={(e) => set("min_significance", e.target.value)} className={cn(inputCls, "figure")} />158 </Field>159 )}160 {(has("ai") || has("hyperscale") || has("has_mw")) && (161 <div className="flex flex-wrap items-end gap-3 sm:col-span-2 lg:col-span-3">162 {has("ai") && (163 <label className="inline-flex min-h-10 items-center gap-2 text-[13px] text-ink-2">164 <input type="checkbox" checked={values.ai === true} onChange={(e) => set("ai", e.target.checked)} className="size-5 accent-[var(--accent)]" /> AI / HPC only165 </label>166 )}167 {has("hyperscale") && (168 <label className="inline-flex min-h-10 items-center gap-2 text-[13px] text-ink-2">169 <input type="checkbox" checked={values.hyperscale === true} onChange={(e) => set("hyperscale", e.target.checked)} className="size-5 accent-[var(--accent)]" /> Hyperscale only170 </label>171 )}172 {has("has_mw") && (173 <label className="inline-flex min-h-10 items-center gap-2 text-[13px] text-ink-2">174 <input type="checkbox" checked={values.has_mw === true} onChange={(e) => set("has_mw", e.target.checked)} className="size-5 accent-[var(--accent)]" /> Known MW only175 </label>176 )}177 </div>178 )}179 </div>180 ) : (181 <p className="text-[12.5px] text-ink-3">This dataset has no filters — it is downloaded whole.</p>182 )}183184 <div className="flex flex-wrap items-center gap-2">185 <SegmentedControl label="Format" value={format} onChange={(v) => setFormat(v)} options={dataset.formats.map((f) => ({ value: f, label: FORMAT_LABEL[f] }))} />186 <span className="text-[11.5px] text-ink-4">{format === "geojson" ? "FeatureCollection; rows without coordinates are omitted." : format === "json" ? "{ data: rows[], meta } — meta lists excluded sources." : "UTF-8, header row, one record per line."}</span>187 </div>188189 <div className="flex flex-col gap-2 sm:flex-row sm:items-stretch">190 <div className="figure min-w-0 flex-1 overflow-x-auto whitespace-nowrap rounded-sm border border-hair bg-surface-2 px-2.5 py-2 text-[12px] text-ink-2" aria-label="Download URL">191 {url}192 </div>193 <div className="flex gap-2">194 <Button size="sm" variant="secondary" onClick={() => void copy()} aria-label="Copy download URL">195 {copied ? <Check className="size-3.5 text-ok" aria-hidden /> : <Copy className="size-3.5" aria-hidden />} {copied ? "Copied" : "Copy"}196 </Button>197 <Button size="sm" variant="primary" href={url} external>198 <Download className="size-3.5" aria-hidden /> Download {FORMAT_LABEL[format]}199 </Button>200 </div>201 </div>202 </div>203 );204}205