import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ExternalLink, Megaphone, X } from 'lucide-react'; import type { Announcement, AnnouncementLevel } from '../api/client'; import { announcementText, isAllowedAnnouncementLink } from '../hooks/useAnnouncements'; import { formatDateOnly } from '../utils/date'; const LEVEL_CHIP: Record = { info: 'bg-sky-100 text-sky-800 dark:bg-sky-500/20 dark:text-sky-300', important: 'bg-amber-100 text-amber-800 dark:bg-amber-500/20 dark:text-amber-300', critical: 'bg-red-100 text-red-800 dark:bg-red-500/20 dark:text-red-300', }; interface AnnouncementsPanelProps { open: boolean; onClose: () => void; announcements: Announcement[]; markRead: (id: string) => void; } /** * Slide-over list of announcements from the Bambuddy maintainers. * * Opening it marks everything in it read -- the dot and the banner go -- but the * ones that were unread keep a "New" chip until it closes, so the reader can * still tell what they came for. */ export function AnnouncementsPanel({ open, onClose, announcements, markRead }: AnnouncementsPanelProps) { const { t, i18n } = useTranslation(); const [newIds, setNewIds] = useState>(() => new Set()); const closeRef = useRef(null); // The list at the moment of opening; later refetches don't re-mark anything. const latest = useRef({ announcements, markRead }); latest.current = { announcements, markRead }; useEffect(() => { if (!open) return; const unreadIds = latest.current.announcements.filter((a) => !a.read).map((a) => a.id); setNewIds(new Set(unreadIds)); unreadIds.forEach((id) => latest.current.markRead(id)); closeRef.current?.focus(); }, [open]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, onClose]); if (!open) return null; return ( <>