mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 08:46:22 +02:00
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
"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<string, "info" | "warning" | "success"> = {
|
|
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: <Megaphone className="h-4 w-4" />,
|
|
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 <Toaster />).
|
|
* 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()}...`;
|
|
}
|