spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { ExploreQuery } from "@dci/core";2import { CONFIDENCE_LEVELS, FACILITY_STATUSES, FACILITY_TYPES, GEO_PRECISIONS, PROJECT_CLASSES } from "@dci/core";34/** URL search params ↔ ExploreQuery (/explore). Unknown values are dropped so the API never sees an invalid key. */56export type ExploreParamsInput = Record<string, string | string[] | undefined> | URLSearchParams;78export const EXPLORE_PER_PAGE = 24;9export const EXPLORE_SORTS = ["updated", "name", "mw", "opened", "completeness", "announced", "opening"] as const;10export const EXPLORE_VIEWS = ["table", "map", "charts"] as const;11export const AI_FILTERS = ["any", "confirmed", "likely", "associated"] as const;1213function get(sp: ExploreParamsInput, key: string): string | undefined {14 if (sp instanceof URLSearchParams) return sp.get(key) ?? undefined;15 const v = sp[key];16 const s = Array.isArray(v) ? v[0] : v;17 return s === undefined || s === "" ? undefined : s;18}1920const num = (v: string | undefined): number | undefined => {21 if (v === undefined) return undefined;22 const n = Number(v);23 return Number.isFinite(n) ? n : undefined;24};25const int = (v: string | undefined): number | undefined => {26 const n = num(v);27 return n === undefined ? undefined : Math.trunc(n);28};29const bool = (v: string | undefined): boolean | undefined => (v === "1" || v === "true" ? true : v === "0" || v === "false" ? false : undefined);30const csv = (v: string | undefined, allowed: ReadonlySet<string>): string | undefined => {31 if (!v) return undefined;32 const out = v33 .split(",")34 .map((s) => s.trim())35 .filter((s) => s && allowed.has(s));36 return out.length ? out.join(",") : undefined;37};3839const STATUS_SET = new Set<string>(FACILITY_STATUSES);40const TYPE_SET = new Set<string>(FACILITY_TYPES);41const CONF_SET = new Set<string>(CONFIDENCE_LEVELS);42const PREC_SET = new Set<string>(GEO_PRECISIONS);43const CLASS_SET = new Set<string>(PROJECT_CLASSES);4445export function parseExploreQuery(sp: ExploreParamsInput): ExploreQuery {46 const q: ExploreQuery = {};47 const entity = get(sp, "entity");48 q.entity = entity === "projects" ? "projects" : "facilities";49 const status = csv(get(sp, "status"), STATUS_SET);50 if (status) q.status = status;51 const country = get(sp, "country");52 if (country) {53 const cc = country54 .toUpperCase()55 .split(",")56 .map((s) => s.trim())57 .filter((s) => /^[A-Z]{2}$/.test(s));58 if (cc.length) q.country = cc.join(",");59 }60 const metro = get(sp, "metro");61 if (metro && /^[a-z0-9-]+$/i.test(metro)) q.metro = metro;62 const operator = get(sp, "operator");63 if (operator && /^[a-z0-9-]+$/i.test(operator)) q.operator = operator;64 if (q.entity === "facilities") {65 const type = csv(get(sp, "type"), TYPE_SET);66 if (type) q.type = type;67 const of = int(get(sp, "opened_from"));68 if (of !== undefined) q.opened_from = of;69 const ot = int(get(sp, "opened_to"));70 if (ot !== undefined) q.opened_to = ot;71 const hs = bool(get(sp, "hyperscale"));72 if (hs !== undefined) q.hyperscale = hs;73 const lp = csv(get(sp, "location_precision"), PREC_SET);74 if (lp) q.location_precision = lp;75 } else {76 const eb = int(get(sp, "expected_before"));77 if (eb !== undefined) q.expected_before = eb;78 const ea = int(get(sp, "expected_after"));79 if (ea !== undefined) q.expected_after = ea;80 const as = get(sp, "announced_since");81 if (as && /^\d{4}(-\d{2}(-\d{2})?)?$/.test(as)) q.announced_since = as;82 const pc = csv(get(sp, "project_class"), CLASS_SET);83 if (pc) q.project_class = pc;84 }85 const minMw = num(get(sp, "min_mw"));86 if (minMw !== undefined && minMw >= 0) q.min_mw = minMw;87 const maxMw = num(get(sp, "max_mw"));88 if (maxMw !== undefined && maxMw >= 0) q.max_mw = maxMw;89 const ai = get(sp, "ai");90 if (ai && (AI_FILTERS as readonly string[]).includes(ai)) q.ai = ai as ExploreQuery["ai"];91 const conf = csv(get(sp, "confidence"), CONF_SET);92 if (conf) q.confidence = conf;93 const hasMw = bool(get(sp, "has_mw"));94 if (hasMw !== undefined) q.has_mw = hasMw;95 const text = get(sp, "q");96 if (text) q.q = text.slice(0, 120);97 const sort = get(sp, "sort");98 if (sort && (EXPLORE_SORTS as readonly string[]).includes(sort)) q.sort = sort;99 const order = get(sp, "order");100 if (order === "asc" || order === "desc") q.order = order;101 const page = int(get(sp, "page"));102 if (page && page > 1) q.page = page;103 const per = int(get(sp, "per_page"));104 if (per && per > 0 && per !== EXPLORE_PER_PAGE) q.per_page = Math.min(100, per);105 const view = get(sp, "view");106 if (view && (EXPLORE_VIEWS as readonly string[]).includes(view)) q.view = view as ExploreQuery["view"];107 return q;108}109110/** Serialize (defaults skipped: entity=facilities, view=table, page=1, per_page=24). */111export function exploreToParams(q: ExploreQuery, patch?: Partial<ExploreQuery> & Record<string, unknown>): URLSearchParams {112 const merged: Record<string, unknown> = { ...q, ...(patch ?? {}) };113 const sp = new URLSearchParams();114 for (const [k, v] of Object.entries(merged)) {115 if (v === undefined || v === null || v === "" || v === false) continue;116 if (k === "entity" && v === "facilities") continue;117 if (k === "view" && v === "table") continue;118 if (k === "page" && Number(v) <= 1) continue;119 if (k === "per_page" && Number(v) === EXPLORE_PER_PAGE) continue;120 sp.set(k, v === true ? "1" : String(v));121 }122 return sp;123}124125export function exploreHref(q: ExploreQuery, patch?: Partial<ExploreQuery> & Record<string, unknown>): string {126 const s = exploreToParams(q, patch).toString();127 return `/explore${s ? `?${s}` : ""}`;128}129130/** Keys that count as active filters (not paging / view / sort). */131export const EXPLORE_FILTER_KEYS: Array<keyof ExploreQuery> = ["status", "country", "metro", "operator", "type", "min_mw", "max_mw", "ai", "hyperscale", "expected_before", "expected_after", "opened_from", "opened_to", "announced_since", "confidence", "location_precision", "has_mw", "project_class", "q"];132133export function countExploreFilters(q: ExploreQuery): number {134 return EXPLORE_FILTER_KEYS.filter((k) => q[k] !== undefined && q[k] !== "" && q[k] !== false).length;135}136137/** Toggle a member in a csv key. */138export function toggleCsv(current: string | undefined, member: string): string | undefined {139 const set = new Set((current ?? "").split(",").filter(Boolean));140 if (set.has(member)) set.delete(member);141 else set.add(member);142 return set.size ? Array.from(set).join(",") : undefined;143}144