export const matchPageSize = 25; export const matchSorts = ['started', 'round', 'duration', 'players'] as const; export type MatchSort = (typeof matchSorts)[number]; export interface MatchHistoryQuery { game: string; map: string; q: string; days: number; status: '' | 'active' | 'completed' | 'interrupted' | 'end-report-missing'; sort: MatchSort; direction: 'asc' | 'desc'; page: number; archive: boolean; } export const matchQueryKeys = [ 'game', 'map', 'q', 'days', 'status', 'sort', 'direction', 'page', 'archive' ] as const; export function readMatchQuery(params: URLSearchParams): MatchHistoryQuery { const game = params.get('game') ?? ''; const map = params.get('map') ?? ''; const q = (params.get('q') ?? '').trim(); const days = Number(params.get('days') || 0); const status = params.get('status') || ''; const sort = params.get('sort') || 'started'; const direction = params.get('direction') || 'desc'; const page = Number(params.get('page') || 1); const archive = params.get('archive') ?? ''; if ( !['', 't5', 't6'].includes(game) || map.length > 1024 || q.length > 100 || ![0, 7, 30, 90].includes(days) || !['', 'active', 'completed', 'interrupted', 'end-report-missing'].includes(status) || !matchSorts.includes(sort as MatchSort) || !['asc', 'desc'].includes(direction) || !Number.isSafeInteger(page) || page < 1 || page > 1000000 || !['', '0', '1'].includes(archive) ) throw new Error('Invalid match history filters.'); return { game, map, q, days, status: status as MatchHistoryQuery['status'], sort: sort as MatchSort, direction: direction as 'asc' | 'desc', page, archive: archive === '1' }; } /** Namespaced filters also work inside an instance's Matches tab. */ export function matchApiParams(url: URL): URLSearchParams { const params = new URLSearchParams(); for (const key of matchQueryKeys) { const value = url.searchParams.get(`match${key[0].toUpperCase()}${key.slice(1)}`); if (value) params.set(key, value); } return params; } export function matchHistoryHref( url: URL, changes: Partial> ): string { const next = new URL(url); next.searchParams.delete('matches'); // Retire the former growing-list limit. next.searchParams.delete('matchPage'); for (const [key, value] of Object.entries(changes)) { const name = `match${key[0].toUpperCase()}${key.slice(1)}`; if (value) next.searchParams.set(name, value); else next.searchParams.delete(name); } return next.pathname + next.search; }