BroadcastBanner: use {messages} destructure from listMessages(),
replace broken pollMessages callback with manual setInterval refetch,
use real InAppMessage type from @bytelyst/broadcast-client.
SurveyBanner: destructure {survey} from getActiveSurvey(), replace
broken pollSurveys callback with manual setInterval, pass typed
QuestionAnswer objects to submitAnswer() instead of raw strings,
align question type rendering with actual API types (single_choice,
rating, nps, text_short, text_long, etc).
Made-with: Cursor
83 lines
3.0 KiB
TypeScript
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" ? "rgba(255,180,70,0.12)" : "rgba(100,160,255,0.10)",
|
|
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)" }}>
|
|
×
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|