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%
3.5 KB · 80 lines typescript
Raw Blame History
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