From a493e83ae48162200817b1a6a8bcdfab18eca150 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sun, 29 Mar 2026 22:10:07 -0700 Subject: [PATCH] fix: correct BroadcastBanner and SurveyBanner API usage 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 --- web/src/components/BroadcastBanner.tsx | 67 ++++++------- web/src/components/SurveyBanner.tsx | 130 +++++++++++++++---------- 2 files changed, 105 insertions(+), 92 deletions(-) diff --git a/web/src/components/BroadcastBanner.tsx b/web/src/components/BroadcastBanner.tsx index 11e05ff..90e4c1c 100644 --- a/web/src/components/BroadcastBanner.tsx +++ b/web/src/components/BroadcastBanner.tsx @@ -1,40 +1,29 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { getBroadcastClient } from "@/lib/broadcast-client"; - -interface BroadcastMessage { - id: string; - title: string; - body?: string; - type: "info" | "warning" | "announcement"; - actionUrl?: string; - actionLabel?: string; -} +import type { InAppMessage } from "@bytelyst/broadcast-client"; export function BroadcastBanner() { - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); + const intervalRef = useRef | null>(null); + + const fetchMessages = useCallback(async () => { + try { + const { messages: list } = await getBroadcastClient().listMessages(); + setMessages(list.filter((m) => m.status !== "dismissed")); + } catch { + // non-critical + } + }, []); useEffect(() => { - const client = getBroadcastClient(); - let stop: (() => void) | undefined; - - async function init() { - try { - const list = await client.listMessages(); - setMessages(list as BroadcastMessage[]); - - stop = client.pollMessages(5 * 60_000, (updated) => { - setMessages(updated as BroadcastMessage[]); - }); - } catch { - // silent — broadcast is non-critical - } - } - - init(); - return () => stop?.(); - }, []); + fetchMessages(); + intervalRef.current = setInterval(fetchMessages, 5 * 60_000); + return () => { + if (intervalRef.current) clearInterval(intervalRef.current); + }; + }, [fetchMessages]); async function dismiss(id: string) { try { @@ -45,13 +34,13 @@ export function BroadcastBanner() { setMessages((prev) => prev.filter((m) => m.id !== id)); } - async function handleClick(msg: BroadcastMessage) { + async function handleClick(msg: InAppMessage) { try { await getBroadcastClient().trackClick(msg.id); } catch { // best-effort } - if (msg.actionUrl) window.open(msg.actionUrl, "_blank", "noopener"); + if (msg.ctaUrl) window.open(msg.ctaUrl, "_blank", "noopener"); } if (messages.length === 0) return null; @@ -67,7 +56,7 @@ export function BroadcastBanner() { alignItems: "center", justifyContent: "space-between", padding: "var(--nl-space-3) var(--nl-space-4)", - background: msg.type === "warning" ? "rgba(255,180,70,0.12)" : "rgba(100,160,255,0.10)", + 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)", }} @@ -75,15 +64,17 @@ export function BroadcastBanner() {
{msg.title} {msg.body && {msg.body}} - {msg.actionUrl && ( + {msg.ctaUrl && ( )}
- + {msg.dismissible !== false && ( + + )} ))} diff --git a/web/src/components/SurveyBanner.tsx b/web/src/components/SurveyBanner.tsx index fc9d2e4..9bf3d98 100644 --- a/web/src/components/SurveyBanner.tsx +++ b/web/src/components/SurveyBanner.tsx @@ -1,49 +1,33 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { getSurveyClient } from "@/lib/survey-client"; import { toast } from "@/lib/toast"; - -interface SurveyQuestion { - id: string; - text: string; - type: "text" | "rating" | "choice"; - options?: string[]; -} - -interface ActiveSurvey { - id: string; - title: string; - questions: SurveyQuestion[]; -} +import type { ActiveSurvey, Question, QuestionAnswer } from "@bytelyst/survey-client"; export function SurveyBanner() { const [survey, setSurvey] = useState(null); const [currentIdx, setCurrentIdx] = useState(0); - const [answers, setAnswers] = useState>({}); + const [answers, setAnswers] = useState>({}); const [started, setStarted] = useState(false); + const pollRef = useRef | null>(null); + + const fetchSurvey = useCallback(async () => { + try { + const { survey: active } = await getSurveyClient().getActiveSurvey(); + if (active) setSurvey((prev) => prev ?? active); + } catch { + // non-critical + } + }, []); useEffect(() => { - const client = getSurveyClient(); - let stop: (() => void) | undefined; - - async function init() { - try { - const active = await client.getActiveSurvey(); - if (active) setSurvey(active as ActiveSurvey); - - stop = client.pollSurveys(10 * 60_000, (s) => { - if (s && !survey) setSurvey(s as ActiveSurvey); - }); - } catch { - // silent — surveys are non-critical - } - } - - init(); - return () => stop?.(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + fetchSurvey(); + pollRef.current = setInterval(fetchSurvey, 10 * 60_000); + return () => { + if (pollRef.current) clearInterval(pollRef.current); + }; + }, [fetchSurvey]); const dismiss = useCallback(async () => { if (!survey) return; @@ -57,9 +41,28 @@ export function SurveyBanner() { setStarted(true); } - async function handleAnswer(questionId: string, value: string) { - setAnswers((prev) => ({ ...prev, [questionId]: value })); - try { await getSurveyClient().submitAnswer(survey!.id, questionId, value); } catch { /* best-effort */ } + function buildAnswer(question: Question, value: string): QuestionAnswer { + switch (question.type) { + case "rating": + case "nps": + case "scale": + return { type: "rating", value: Number(value) }; + case "single_choice": + case "dropdown": + return { type: "single_choice", optionId: value }; + case "multiple_choice": + return { type: "multiple_choice", optionIds: [value] }; + case "ranking": + return { type: "ranking", rankedOptionIds: [value] }; + default: + return { type: "text", value }; + } + } + + async function handleAnswer(question: Question, value: string) { + const answer = buildAnswer(question, value); + setAnswers((prev) => ({ ...prev, [question.id]: answer })); + try { await getSurveyClient().submitAnswer(survey!.id, question.id, answer); } catch { /* best-effort */ } } async function handleNext() { @@ -90,6 +93,25 @@ export function SurveyBanner() { const question = survey.questions[currentIdx]; if (!question) return null; + const isTextType = question.type === "text_short" || question.type === "text_long"; + const isRatingType = question.type === "rating" || question.type === "nps" || question.type === "scale"; + const isChoiceType = question.type === "single_choice" || question.type === "multiple_choice" || question.type === "dropdown"; + const currentAnswer = answers[question.id]; + const hasAnswer = !!currentAnswer; + + function getTextValue(): string { + return currentAnswer?.type === "text" ? currentAnswer.value : ""; + } + + function getRatingValue(): number | null { + return currentAnswer?.type === "rating" || currentAnswer?.type === "nps" ? currentAnswer.value : null; + } + + function getChoiceValue(): string | null { + if (currentAnswer?.type === "single_choice") return currentAnswer.optionId; + return null; + } + return (
@@ -97,27 +119,27 @@ export function SurveyBanner() { {currentIdx + 1}/{survey.questions.length}
- {question.type === "text" && ( + {isTextType && ( handleAnswer(question.id, e.target.value)} + value={getTextValue()} + onChange={(e) => handleAnswer(question, e.target.value)} /> )} - {question.type === "rating" && ( + {isRatingType && (
- {[1, 2, 3, 4, 5].map((n) => ( + {Array.from({ length: (question.maxValue ?? 5) - (question.minValue ?? 1) + 1 }, (_, i) => (question.minValue ?? 1) + i).map((n) => (
)} - {question.type === "choice" && question.options && ( + {isChoiceType && question.options && (
{question.options.map((opt) => ( ))}
@@ -151,7 +173,7 @@ export function SurveyBanner() {