const REMINDER_EXPIRY_MS = 72 * 60 * 60 * 1000; const DISMISSALS_STORAGE_KEY = "ptf:mission-reminder:dismissals"; export type ReminderLeader = { readonly id: number; readonly username: string; readonly displayName: string; }; export type MissionReminder = { readonly missionId: number; readonly missionName: string; readonly missionCodeName: string | null; readonly completedAt: string; readonly leaders: readonly ReminderLeader[]; }; export type ReminderEntry = MissionReminder & { readonly key: string; readonly leader: ReminderLeader; readonly expiresAt: number; }; export type Dismissals = Record; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function isReminderLeader(value: unknown): value is ReminderLeader { return ( isRecord(value) && typeof value.id === "number" && typeof value.username === "string" && typeof value.displayName === "string" ); } function isMissionReminder(value: unknown): value is MissionReminder { return ( isRecord(value) && typeof value.missionId === "number" && typeof value.missionName === "string" && (value.missionCodeName === null || typeof value.missionCodeName === "string") && typeof value.completedAt === "string" && Array.isArray(value.leaders) && value.leaders.every(isReminderLeader) ); } export function parseMissionReminders(value: unknown): readonly MissionReminder[] { if (!isRecord(value) || !Array.isArray(value.reminders)) return []; return value.reminders.filter(isMissionReminder); } export function missionReminderKey(missionId: number, leaderId: number): string { return `${missionId}:${leaderId}`; } export function buildReminderEntries( reminders: readonly MissionReminder[], now: number, ): readonly ReminderEntry[] { const entries: ReminderEntry[] = []; const seen = new Set(); for (const reminder of reminders) { const completedAt = Date.parse(reminder.completedAt); if (!Number.isFinite(completedAt)) continue; const expiresAt = completedAt + REMINDER_EXPIRY_MS; if (expiresAt <= now) continue; for (const leader of reminder.leaders) { const key = missionReminderKey(reminder.missionId, leader.id); if (seen.has(key)) continue; seen.add(key); entries.push({ ...reminder, key, leader, expiresAt }); } } return entries; } // Browser storage is read during mount and written during mount or the // explicit dismissal handler; it is never accessed during render. export function readReminderDismissals(now: number): Dismissals { if (typeof window === "undefined") return {}; try { const raw = window.localStorage.getItem(DISMISSALS_STORAGE_KEY); if (!raw) return {}; const parsed: unknown = JSON.parse(raw); if (!isRecord(parsed)) return {}; const dismissals: Dismissals = {}; for (const [key, value] of Object.entries(parsed)) { if (typeof value === "number" && Number.isFinite(value) && value > now) { dismissals[key] = value; } } return dismissals; } catch (error) { if (error instanceof Error) return {}; throw error; } } export function writeReminderDismissals(dismissals: Dismissals, now: number): void { if (typeof window === "undefined") return; const activeDismissals: Dismissals = {}; for (const [key, expiresAt] of Object.entries(dismissals)) { if (expiresAt > now) activeDismissals[key] = expiresAt; } try { window.localStorage.setItem(DISMISSALS_STORAGE_KEY, JSON.stringify(activeDismissals)); } catch (error) { if (error instanceof Error) return; throw error; } }