"use client"; import { Megaphone } from "lucide-react"; import { useEffect, useRef } from "react"; import { toast } from "sonner"; import type { Announcement } from "@/contracts/types/announcement.types"; import { announcements } from "@/lib/announcements/announcements-data"; import { isAnnouncementToasted, markAnnouncementRead, markAnnouncementToasted, } from "@/lib/announcements/announcements-storage"; import { getActiveAnnouncements } from "@/lib/announcements/announcements-utils"; import { isAuthenticated } from "@/lib/auth-utils"; /** Map announcement category to the Sonner toast method */ const categoryToVariant: Record = { update: "info", feature: "success", maintenance: "warning", info: "info", }; /** Show a single announcement as a toast */ function showAnnouncementToast(announcement: Announcement) { const variant = categoryToVariant[announcement.category] ?? "info"; const options = { description: truncateText(announcement.description, 120), duration: 12000, icon: , action: announcement.link ? { label: announcement.link.label, onClick: () => { if (announcement.link?.url.startsWith("http")) { window.open(announcement.link.url, "_blank"); } else if (announcement.link?.url) { window.location.href = announcement.link.url; } }, } : undefined, onDismiss: () => { markAnnouncementRead(announcement.id); }, }; toast[variant](announcement.title, options); markAnnouncementToasted(announcement.id); } /** * Global provider that shows important announcements as toast notifications. * * Place this component once at the root layout level (alongside ). * On mount, it checks for active, audience-matched, unread important * announcements that haven't been shown as toasts yet, and displays them * with a short stagger delay. */ export function AnnouncementToastProvider() { const hasChecked = useRef(false); useEffect(() => { if (hasChecked.current) return; hasChecked.current = true; const timer = setTimeout(() => { const authed = isAuthenticated(); const active = getActiveAnnouncements(announcements, authed); const importantUntoasted = active.filter( (a) => a.isImportant && !isAnnouncementToasted(a.id) ); for (let i = 0; i < importantUntoasted.length; i++) { const announcement = importantUntoasted[i]; setTimeout(() => showAnnouncementToast(announcement), i * 800); } }, 1500); return () => clearTimeout(timer); }, []); return null; } /** Truncate text to a maximum length, adding ellipsis if needed */ function truncateText(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return `${text.slice(0, maxLength).trimEnd()}...`; }