export const stages = [ { value: 'story', label: 'The adventure', description: 'Catch as you go, earn badges and become Champion.' }, { value: 'postgame', label: 'Beyond the credits', description: 'Explore the postgame and finish what you can in this game.' }, { value: 'collection', label: 'Complete the collection', description: 'Trades, transfers and event Pokémon.' } ] as const; export type Stage = (typeof stages)[number]['value']; export const obtainedMethods = [ 'caught', 'starter', 'gift', 'evolved', 'bred', 'trade', 'transfer', 'event' ] as const; export const isStage = (value: string): value is Stage => stages.some((s) => s.value === value); export function boundedInteger(value: FormDataEntryValue | null, min: number, max: number) { if (typeof value !== 'string' || !/^\d+$/.test(value)) return null; const number = Number(value); return Number.isSafeInteger(number) && number >= min && number <= max ? number : null; } export function validDate(value: string) { return ( /^\d{4}-\d{2}-\d{2}$/.test(value) && Number(value.slice(0, 4)) > 0 && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value ); } export function firstCatches(catches: T[]) { const first = new Map(); for (const caught of [...catches].sort((a, b) => a.id - b.id)) if (!first.has(caught.pokemonId)) first.set(caught.pokemonId, caught); return first; } export function allowedOsuUrl(value: string, kind: 'beatmap' | 'score') { if (!value) return true; try { const url = new URL(value); return ( url.protocol === 'https:' && url.hostname === 'osu.ppy.sh' && !url.username && !url.password && !url.port && (kind === 'beatmap' ? /^\/(beatmaps|b|beatmapsets)\/\d+\/?$/.test(url.pathname) : /^\/scores\/(?:osu\/|taiko\/|fruits\/|mania\/)?\d+\/?$/.test(url.pathname)) ); } catch { return false; } }