import { SeasonTheme } from './types'; /** * Single source of truth for when each seasonal theme auto-activates. * * Both `getActiveSeason` (the runtime "Auto" selector) and the settings UI read * this list, so the date windows shown to the user can never drift from the * dates actually used. Order matters: it is the activation PRIORITY — the first * entry whose window matches wins (e.g. Deep Space outranks Autumn in their * early-October overlap). */ export type SeasonScheduleEntry = { theme: SeasonTheme; /** Human-readable activation window for display in settings. */ dateRange: string; /** Whether this theme is active on the given month (1-12) and day (1-31). */ matches: (month: number, day: number) => boolean; }; export const SEASON_SCHEDULE: SeasonScheduleEntry[] = [ { theme: 'newyear', dateRange: 'Dec 31 – Jan 2', matches: (m, d) => (m === 12 && d === 31) || (m === 1 && d <= 2), }, { theme: 'valentines', dateRange: 'Feb 10 – 15', matches: (m, d) => m === 2 && d >= 10 && d <= 15, }, { theme: 'stpatricks', dateRange: 'Mar 15 – 18', matches: (m, d) => m === 3 && d >= 15 && d <= 18, }, { theme: 'aprilfools', dateRange: 'Apr 1', matches: (m, d) => m === 4 && d === 1, }, { theme: 'earthday', dateRange: 'Apr 20 – 23', matches: (m, d) => m === 4 && d >= 20 && d <= 23, }, { theme: 'lunar', dateRange: 'Jan 22 – Feb 5', matches: (m, d) => (m === 1 && d >= 22) || (m === 2 && d <= 5), }, { theme: 'arcade', dateRange: 'Sep 12', matches: (m, d) => m === 9 && d === 12, }, { theme: 'deepspace', dateRange: 'Oct 4 – 10', matches: (m, d) => m === 10 && d >= 4 && d <= 10, }, { theme: 'halloween', dateRange: 'Oct 15 – Nov 1', matches: (m, d) => (m === 10 && d >= 15) || (m === 11 && d === 1), }, { theme: 'christmas', dateRange: 'Dec 10 – 30', matches: (m, d) => m === 12 && d >= 10, }, { theme: 'autumn', dateRange: 'Sep 21 – Oct 14', matches: (m, d) => (m === 9 && d >= 21) || (m === 10 && d <= 14), }, ]; /** Map of theme → human-readable activation window (for settings captions). */ export const SEASON_DATE_RANGES: Record = SEASON_SCHEDULE.reduce( (acc, entry) => { acc[entry.theme] = entry.dateRange; return acc; }, {} as Record, ); /** * The seasonal theme that should be active on `now`, or null if none. First * matching entry in SEASON_SCHEDULE priority order wins. */ export function getActiveSeason(now: Date): SeasonTheme | null { const month = now.getMonth() + 1; // 1-12 const day = now.getDate(); return SEASON_SCHEDULE.find((entry) => entry.matches(month, day))?.theme ?? null; }