import type { FacilityFilters, FacilityStatus, FacilityType } from "@dci/core"; import { FACILITY_STATUSES, FACILITY_TYPES } from "@dci/core"; /** URL search params ↔ FacilityFilters (shared by /datacenters, /map and /search). */ export type SearchParamsInput = Record | URLSearchParams; function get(sp: SearchParamsInput, key: string): string | undefined { if (sp instanceof URLSearchParams) return sp.get(key) ?? undefined; const v = sp[key]; return Array.isArray(v) ? v[0] : v; } const num = (v: string | undefined): number | undefined => { if (v === undefined || v === "") return undefined; const n = Number(v); return Number.isFinite(n) ? n : undefined; }; const bool = (v: string | undefined): boolean | undefined => (v === "1" || v === "true" ? true : v === "0" || v === "false" ? false : undefined); const STATUS_SET = new Set(FACILITY_STATUSES); const TYPE_SET = new Set(FACILITY_TYPES); export function csv(v: string | undefined, allowed?: Set): T[] { if (!v) return []; return v .split(",") .map((s) => s.trim()) .filter((s) => s && (!allowed || allowed.has(s))) as T[]; } export function parseFilters(sp: SearchParamsInput): FacilityFilters { const f: FacilityFilters = {}; const q = get(sp, "q"); if (q) f.q = q; const country = get(sp, "country"); if (country) f.country = country.toUpperCase(); const region = get(sp, "region"); if (region) f.region = region; const metro = get(sp, "metro"); if (metro) f.metro = metro; const operator = get(sp, "operator"); if (operator) f.operator = operator; const status = csv(get(sp, "status"), STATUS_SET); if (status.length) f.status = status.join(","); const type = csv(get(sp, "type"), TYPE_SET); if (type.length) f.type = type.join(","); const minMw = num(get(sp, "min_mw")); if (minMw !== undefined) f.min_mw = minMw; const maxMw = num(get(sp, "max_mw")); if (maxMw !== undefined) f.max_mw = maxMw; const of = num(get(sp, "opened_from")); if (of !== undefined) f.opened_from = of; const ot = num(get(sp, "opened_to")); if (ot !== undefined) f.opened_to = ot; const pf = num(get(sp, "planned_from")); if (pf !== undefined) f.planned_from = pf; const pt = num(get(sp, "planned_to")); if (pt !== undefined) f.planned_to = pt; const cloud = get(sp, "cloud"); if (cloud) f.cloud = cloud; for (const k of ["hyperscale", "colocation", "ai", "renewable", "has_mw", "has_geo"] as const) { const b = bool(get(sp, k)); if (b !== undefined) f[k] = b; } const conf = get(sp, "confidence"); if (conf) f.confidence = conf; const sort = get(sp, "sort"); if (sort && ["name", "mw", "updated", "opened", "completeness"].includes(sort)) f.sort = sort as FacilityFilters["sort"]; const order = get(sp, "order"); if (order === "asc" || order === "desc") f.order = order; const page = num(get(sp, "page")); if (page && page > 0) f.page = Math.floor(page); const per = num(get(sp, "per_page")); if (per && per > 0) f.per_page = Math.min(200, Math.floor(per)); return f; } /** Serialize filters into URLSearchParams (skips undefined / defaults). */ export function filtersToParams(f: FacilityFilters, extra?: Record): URLSearchParams { const sp = new URLSearchParams(); const entries = Object.entries(f) as Array<[string, unknown]>; for (const [k, v] of entries) { if (v === undefined || v === null || v === "" || v === false) continue; if (k === "page" && v === 1) continue; sp.set(k, typeof v === "boolean" ? "1" : String(v)); } if (extra) for (const [k, v] of Object.entries(extra)) if (v !== undefined && v !== "") sp.set(k, String(v)); return sp; } export function filtersToQuery(f: FacilityFilters, extra?: Record): string { return filtersToParams(f, extra).toString(); } /** Human-readable chips for the active filter set. */ export interface FilterChipDesc { key: keyof FacilityFilters; label: string; value: string; /** removing this chip: the full value (csv) or a single member */ member?: string; } export function describeFilters(f: FacilityFilters, labels: { status: Record; type: Record }): FilterChipDesc[] { const chips: FilterChipDesc[] = []; if (f.q) chips.push({ key: "q", label: "Search", value: f.q }); for (const s of csv(f.status)) chips.push({ key: "status", label: "Status", value: labels.status[s] ?? s, member: s }); for (const t of csv(f.type)) chips.push({ key: "type", label: "Type", value: labels.type[t] ?? t, member: t }); if (f.operator) chips.push({ key: "operator", label: "Operator", value: f.operator }); if (f.country) chips.push({ key: "country", label: "Country", value: f.country }); if (f.metro) chips.push({ key: "metro", label: "Market", value: f.metro }); if (f.region) chips.push({ key: "region", label: "Region", value: f.region }); if (f.min_mw !== undefined || f.max_mw !== undefined) { const v = f.min_mw !== undefined && f.max_mw !== undefined ? `${f.min_mw}–${f.max_mw} MW` : f.min_mw !== undefined ? `≥ ${f.min_mw} MW` : `≤ ${f.max_mw} MW`; chips.push({ key: "min_mw", label: "Capacity", value: v }); } if (f.opened_from !== undefined || f.opened_to !== undefined) { const v = f.opened_from !== undefined && f.opened_to !== undefined ? `${f.opened_from}–${f.opened_to}` : f.opened_from !== undefined ? `≥ ${f.opened_from}` : `≤ ${f.opened_to}`; chips.push({ key: "opened_from", label: "Opened", value: v }); } if (f.planned_from !== undefined || f.planned_to !== undefined) { const v = f.planned_from !== undefined && f.planned_to !== undefined ? `${f.planned_from}–${f.planned_to}` : f.planned_from !== undefined ? `≥ ${f.planned_from}` : `≤ ${f.planned_to}`; chips.push({ key: "planned_from", label: "Expected", value: v }); } if (f.cloud) chips.push({ key: "cloud", label: "Cloud", value: f.cloud }); if (f.ai) chips.push({ key: "ai", label: "AI", value: "AI infrastructure" }); if (f.hyperscale) chips.push({ key: "hyperscale", label: "Hyperscale", value: "Hyperscale" }); if (f.colocation) chips.push({ key: "colocation", label: "Colocation", value: "Colocation" }); if (f.renewable) chips.push({ key: "renewable", label: "Renewable", value: "Renewable claim" }); if (f.has_mw) chips.push({ key: "has_mw", label: "Data", value: "Known MW" }); if (f.has_geo) chips.push({ key: "has_geo", label: "Data", value: "Geolocated" }); if (f.confidence) chips.push({ key: "confidence", label: "Confidence", value: f.confidence }); return chips; } /** Remove one chip (or one csv member) from filters, resetting pagination. */ export function removeFilter(f: FacilityFilters, chip: FilterChipDesc): FacilityFilters { const next: FacilityFilters = { ...f }; delete next.page; if (chip.member && (chip.key === "status" || chip.key === "type")) { const rest = csv(next[chip.key]).filter((x) => x !== chip.member); if (rest.length) next[chip.key] = rest.join(","); else delete next[chip.key]; return next; } if (chip.key === "min_mw") { delete next.min_mw; delete next.max_mw; return next; } if (chip.key === "opened_from") { delete next.opened_from; delete next.opened_to; return next; } if (chip.key === "planned_from") { delete next.planned_from; delete next.planned_to; return next; } delete next[chip.key]; return next; } export function countActiveFilters(f: FacilityFilters): number { const ignore = new Set(["sort", "order", "page", "per_page"]); return Object.entries(f).filter(([k, v]) => !ignore.has(k) && v !== undefined && v !== "" && v !== false).length; }