spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1/** URL-state sorting for server-rendered index tables (operators, countries, markets, projects, cloud regions…). */23export type SearchParams = Record<string, string | string[] | undefined>;45export function spGet(sp: SearchParams, key: string): string | undefined {6 const v = sp[key];7 const s = Array.isArray(v) ? v[0] : v;8 return s === undefined || s === "" ? undefined : s;9}1011export function spInt(sp: SearchParams, key: string, fallback = 1): number {12 const n = Number(spGet(sp, key));13 return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;14}1516/** Build an href for the same page with params merged (undefined removes; `page` reset unless kept). */17export function hrefWith(base: string, sp: SearchParams, patch: Record<string, string | number | undefined | null>, opts: { keepPage?: boolean; anchor?: string } = {}): string {18 const out = new URLSearchParams();19 for (const [k, v] of Object.entries(sp)) {20 if (v === undefined) continue;21 if (k === "page" && !opts.keepPage) continue;22 out.set(k, Array.isArray(v) ? v[0] ?? "" : v);23 }24 for (const [k, v] of Object.entries(patch)) {25 if (v === undefined || v === null || v === "") out.delete(k);26 else out.set(k, String(v));27 }28 const qs = out.toString();29 return `${base}${qs ? `?${qs}` : ""}${opts.anchor ? `#${opts.anchor}` : ""}`;30}3132export interface SortState<K extends string> {33 key: K;34 order: "asc" | "desc";35}3637/** Read ?sort=&order= against an allow-list. */38export function parseSort<K extends string>(sp: SearchParams, allowed: readonly K[], fallback: SortState<K>): SortState<K> {39 const s = spGet(sp, "sort");40 const o = spGet(sp, "order");41 const key = (allowed as readonly string[]).includes(s ?? "") ? (s as K) : fallback.key;42 const order = o === "asc" || o === "desc" ? o : key === fallback.key ? fallback.order : defaultOrderFor(key);43 return { key, order };44}4546function defaultOrderFor(key: string): "asc" | "desc" {47 return key === "name" || key === "code" || key === "provider" || key === "country" || key === "region" ? "asc" : "desc";48}4950/** Props for <Th sort sortHref> given the current state. */51export function sortTh<K extends string>(base: string, sp: SearchParams, state: SortState<K>, key: K): { sort: "asc" | "desc" | null; sortHref: string } {52 const active = state.key === key;53 const next = active ? (state.order === "asc" ? "desc" : "asc") : defaultOrderFor(key);54 return { sort: active ? state.order : null, sortHref: hrefWith(base, sp, { sort: key, order: next }) };55}5657/** Generic in-memory comparator honouring nulls last. */58export function compareBy<T>(get: (t: T) => number | string | null | undefined, order: "asc" | "desc") {59 return (a: T, b: T): number => {60 const va = get(a);61 const vb = get(b);62 const na = va === null || va === undefined || va === "";63 const nb = vb === null || vb === undefined || vb === "";64 if (na && nb) return 0;65 if (na) return 1;66 if (nb) return -1;67 let c: number;68 if (typeof va === "number" && typeof vb === "number") c = va - vb;69 else c = String(va).localeCompare(String(vb), "en", { sensitivity: "base", numeric: true });70 return order === "asc" ? c : -c;71 };72}7374/** Slice for client-agnostic pagination of an in-memory list. */75export function paginate<T>(items: T[], page: number, perPage: number): { items: T[]; page: number; pages: number } {76 const pages = Math.max(1, Math.ceil(items.length / perPage));77 const p = Math.min(Math.max(1, page), pages);78 return { items: items.slice((p - 1) * perPage, p * perPage), page: p, pages };79}80