learning_ai_notes/web/src/components/BroadcastBanner.tsx

83 lines
3.0 KiB
TypeScript

"use client";
import { useEffect, useState, useCallback, useRef } from "react";
import { getBroadcastClient } from "@/lib/broadcast-client";
import type { InAppMessage } from "@bytelyst/broadcast-client";
export function BroadcastBanner() {
const [messages, setMessages] = useState<InAppMessage[]>([]);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fetchMessages = useCallback(async () => {
try {
const { messages: list } = await getBroadcastClient().listMessages();
setMessages(list.filter((m) => m.status !== "dismissed"));
} catch {
// non-critical
}
}, []);
useEffect(() => {
fetchMessages();
intervalRef.current = setInterval(fetchMessages, 5 * 60_000);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [fetchMessages]);
async function dismiss(id: string) {
try {
await getBroadcastClient().markDismissed(id);
} catch {
// best-effort
}
setMessages((prev) => prev.filter((m) => m.id !== id));
}
async function handleClick(msg: InAppMessage) {
try {
await getBroadcastClient().trackClick(msg.id);
} catch {
// best-effort
}
if (msg.ctaUrl) window.open(msg.ctaUrl, "_blank", "noopener");
}
if (messages.length === 0) return null;
return (
<div style={{ display: "grid", gap: "var(--nl-space-2)", padding: "var(--nl-space-3) var(--nl-space-4)" }}>
{messages.map((msg) => (
<div
key={msg.id}
role="status"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "var(--nl-space-3) var(--nl-space-4)",
background: msg.priority === "high" || msg.priority === "urgent" ? "var(--nl-warning-muted)" : "var(--nl-info-muted)",
borderRadius: "var(--nl-radius-sm)",
fontSize: "var(--nl-fs-sm)",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "var(--nl-space-3)", flex: 1, minWidth: 0 }}>
<strong style={{ whiteSpace: "nowrap" }}>{msg.title}</strong>
{msg.body && <span style={{ color: "var(--nl-text-secondary)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{msg.body}</span>}
{msg.ctaUrl && (
<button onClick={() => handleClick(msg)} style={{ background: "none", border: "none", color: "var(--nl-accent-primary)", cursor: "pointer", fontWeight: 600, whiteSpace: "nowrap" }}>
{msg.ctaText ?? "Learn more"}
</button>
)}
</div>
{msg.dismissible !== false && (
<button onClick={() => dismiss(msg.id)} aria-label="Dismiss" style={{ background: "none", border: "none", color: "var(--nl-text-secondary)", cursor: "pointer", fontSize: 16, lineHeight: 1, padding: "0 0 0 var(--nl-space-3)" }}>
&times;
</button>
)}
</div>
))}
</div>
);
}