import { useCallback, useEffect, useState } from "react"; import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useNavigate } from "react-router-dom"; import { useClientNotifications } from "@/contexts/ClientNotificationContext"; import { resolveNotificationLink } from "@/components/generic/notificationDisplay"; import { getTierColor, getContrastText } from "@/utils/tierColors"; import AnnouncementCarouselDialog from "@/components/generic/AnnouncementCarouselDialog"; const ROTATE_INTERVAL_MS = 6000; // resolveNotificationLink already gives explicit link_url (the admin "On // Open" section) precedence over the type-based fallbacks, for broadcasts // sent before that field existed. function resolveClickAction(stickyAnnouncement) { return resolveNotificationLink(stickyAnnouncement.type, stickyAnnouncement.data); } export default function StickyAnnouncementBar() { const navigate = useNavigate(); const { stickyAnnouncements, bannerImage, markSeen } = useClientNotifications(); const [activeIndex, setActiveIndex] = useState(0); const [detailsOpen, setDetailsOpen] = useState(false); const count = stickyAnnouncements.length; // Derived rather than clamped via effect — safe the instant the array // shrinks (e.g. after a dismiss), no render with a stale out-of-range index. const safeIndex = count ? Math.min(activeIndex, count - 1) : 0; // Auto-rotate through active announcements while more than one is live. useEffect(() => { if (count <= 1) return; const id = setInterval(() => { setActiveIndex((i) => (i + 1) % count); }, ROTATE_INTERVAL_MS); return () => clearInterval(id); }, [count]); const current = stickyAnnouncements[safeIndex]; const onDismiss = useCallback(async () => { if (!current) return; await markSeen(current.notification_id); }, [current, markSeen]); // Opening the dialog must NOT mark it seen — markSeen removes the row from // stickyAnnouncements, which would unmount this component (dialog included) // before it ever shows. const onClickBanner = useCallback(() => { if (!current) return; setDetailsOpen(true); }, [current]); // Closing the details dialog (X, Escape, overlay click — any reason) // dismisses whichever announcement was being viewed at the time. This is // the only dismiss path when multiple are active (no per-item X on the bar // itself — see the count > 1 branch below). const onDialogOpenChange = useCallback((open) => { setDetailsOpen(open); if (!open) void onDismiss(); }, [onDismiss]); if (!current) return null; const swatch = getTierColor(current.color || "indigo").swatch; const textColor = getContrastText(swatch, current.color || "indigo"); const clickAction = resolveClickAction(current); return ( <>
{current.title || "Announcement"}
{clickAction && ( )}