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%
7.6 KB · 170 lines typescript
Raw Blame History
1import type { FacilityFilters, FacilityStatus, FacilityType } from "@dci/core";2import { FACILITY_STATUSES, FACILITY_TYPES } from "@dci/core";34/** URL search params ↔ FacilityFilters (shared by /datacenters, /map and /search). */56export type SearchParamsInput = Record<string, string | string[] | undefined> | URLSearchParams;78function get(sp: SearchParamsInput, key: string): string | undefined {9  if (sp instanceof URLSearchParams) return sp.get(key) ?? undefined;10  const v = sp[key];11  return Array.isArray(v) ? v[0] : v;12}1314const num = (v: string | undefined): number | undefined => {15  if (v === undefined || v === "") return undefined;16  const n = Number(v);17  return Number.isFinite(n) ? n : undefined;18};19const bool = (v: string | undefined): boolean | undefined => (v === "1" || v === "true" ? true : v === "0" || v === "false" ? false : undefined);2021const STATUS_SET = new Set<string>(FACILITY_STATUSES);22const TYPE_SET = new Set<string>(FACILITY_TYPES);2324export function csv<T extends string>(v: string | undefined, allowed?: Set<string>): T[] {25  if (!v) return [];26  return v27    .split(",")28    .map((s) => s.trim())29    .filter((s) => s && (!allowed || allowed.has(s))) as T[];30}3132export function parseFilters(sp: SearchParamsInput): FacilityFilters {33  const f: FacilityFilters = {};34  const q = get(sp, "q");35  if (q) f.q = q;36  const country = get(sp, "country");37  if (country) f.country = country.toUpperCase();38  const region = get(sp, "region");39  if (region) f.region = region;40  const metro = get(sp, "metro");41  if (metro) f.metro = metro;42  const operator = get(sp, "operator");43  if (operator) f.operator = operator;44  const status = csv<FacilityStatus>(get(sp, "status"), STATUS_SET);45  if (status.length) f.status = status.join(",");46  const type = csv<FacilityType>(get(sp, "type"), TYPE_SET);47  if (type.length) f.type = type.join(",");48  const minMw = num(get(sp, "min_mw"));49  if (minMw !== undefined) f.min_mw = minMw;50  const maxMw = num(get(sp, "max_mw"));51  if (maxMw !== undefined) f.max_mw = maxMw;52  const of = num(get(sp, "opened_from"));53  if (of !== undefined) f.opened_from = of;54  const ot = num(get(sp, "opened_to"));55  if (ot !== undefined) f.opened_to = ot;56  const pf = num(get(sp, "planned_from"));57  if (pf !== undefined) f.planned_from = pf;58  const pt = num(get(sp, "planned_to"));59  if (pt !== undefined) f.planned_to = pt;60  const cloud = get(sp, "cloud");61  if (cloud) f.cloud = cloud;62  for (const k of ["hyperscale", "colocation", "ai", "renewable", "has_mw", "has_geo"] as const) {63    const b = bool(get(sp, k));64    if (b !== undefined) f[k] = b;65  }66  const conf = get(sp, "confidence");67  if (conf) f.confidence = conf;68  const sort = get(sp, "sort");69  if (sort && ["name", "mw", "updated", "opened", "completeness"].includes(sort)) f.sort = sort as FacilityFilters["sort"];70  const order = get(sp, "order");71  if (order === "asc" || order === "desc") f.order = order;72  const page = num(get(sp, "page"));73  if (page && page > 0) f.page = Math.floor(page);74  const per = num(get(sp, "per_page"));75  if (per && per > 0) f.per_page = Math.min(200, Math.floor(per));76  return f;77}7879/** Serialize filters into URLSearchParams (skips undefined / defaults). */80export function filtersToParams(f: FacilityFilters, extra?: Record<string, string | number | undefined>): URLSearchParams {81  const sp = new URLSearchParams();82  const entries = Object.entries(f) as Array<[string, unknown]>;83  for (const [k, v] of entries) {84    if (v === undefined || v === null || v === "" || v === false) continue;85    if (k === "page" && v === 1) continue;86    sp.set(k, typeof v === "boolean" ? "1" : String(v));87  }88  if (extra) for (const [k, v] of Object.entries(extra)) if (v !== undefined && v !== "") sp.set(k, String(v));89  return sp;90}9192export function filtersToQuery(f: FacilityFilters, extra?: Record<string, string | number | undefined>): string {93  return filtersToParams(f, extra).toString();94}9596/** Human-readable chips for the active filter set. */97export interface FilterChipDesc {98  key: keyof FacilityFilters;99  label: string;100  value: string;101  /** removing this chip: the full value (csv) or a single member */102  member?: string;103}104105export function describeFilters(f: FacilityFilters, labels: { status: Record<string, string>; type: Record<string, string> }): FilterChipDesc[] {106  const chips: FilterChipDesc[] = [];107  if (f.q) chips.push({ key: "q", label: "Search", value: f.q });108  for (const s of csv(f.status)) chips.push({ key: "status", label: "Status", value: labels.status[s] ?? s, member: s });109  for (const t of csv(f.type)) chips.push({ key: "type", label: "Type", value: labels.type[t] ?? t, member: t });110  if (f.operator) chips.push({ key: "operator", label: "Operator", value: f.operator });111  if (f.country) chips.push({ key: "country", label: "Country", value: f.country });112  if (f.metro) chips.push({ key: "metro", label: "Market", value: f.metro });113  if (f.region) chips.push({ key: "region", label: "Region", value: f.region });114  if (f.min_mw !== undefined || f.max_mw !== undefined) {115    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`;116    chips.push({ key: "min_mw", label: "Capacity", value: v });117  }118  if (f.opened_from !== undefined || f.opened_to !== undefined) {119    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}`;120    chips.push({ key: "opened_from", label: "Opened", value: v });121  }122  if (f.planned_from !== undefined || f.planned_to !== undefined) {123    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}`;124    chips.push({ key: "planned_from", label: "Expected", value: v });125  }126  if (f.cloud) chips.push({ key: "cloud", label: "Cloud", value: f.cloud });127  if (f.ai) chips.push({ key: "ai", label: "AI", value: "AI infrastructure" });128  if (f.hyperscale) chips.push({ key: "hyperscale", label: "Hyperscale", value: "Hyperscale" });129  if (f.colocation) chips.push({ key: "colocation", label: "Colocation", value: "Colocation" });130  if (f.renewable) chips.push({ key: "renewable", label: "Renewable", value: "Renewable claim" });131  if (f.has_mw) chips.push({ key: "has_mw", label: "Data", value: "Known MW" });132  if (f.has_geo) chips.push({ key: "has_geo", label: "Data", value: "Geolocated" });133  if (f.confidence) chips.push({ key: "confidence", label: "Confidence", value: f.confidence });134  return chips;135}136137/** Remove one chip (or one csv member) from filters, resetting pagination. */138export function removeFilter(f: FacilityFilters, chip: FilterChipDesc): FacilityFilters {139  const next: FacilityFilters = { ...f };140  delete next.page;141  if (chip.member && (chip.key === "status" || chip.key === "type")) {142    const rest = csv(next[chip.key]).filter((x) => x !== chip.member);143    if (rest.length) next[chip.key] = rest.join(",");144    else delete next[chip.key];145    return next;146  }147  if (chip.key === "min_mw") {148    delete next.min_mw;149    delete next.max_mw;150    return next;151  }152  if (chip.key === "opened_from") {153    delete next.opened_from;154    delete next.opened_to;155    return next;156  }157  if (chip.key === "planned_from") {158    delete next.planned_from;159    delete next.planned_to;160    return next;161  }162  delete next[chip.key];163  return next;164}165166export function countActiveFilters(f: FacilityFilters): number {167  const ignore = new Set(["sort", "order", "page", "per_page"]);168  return Object.entries(f).filter(([k, v]) => !ignore.has(k) && v !== undefined && v !== "" && v !== false).length;169}170