import { useEffect, useState } from "react"; interface TextState { showing: boolean; title: string; } interface ImagePopupState { showing: boolean; image_url: string; caption: string; } interface DisplayOptions { font_size: number; text_color: string; } interface DataCardConfig { url: string; refresh_interval: number; display_options: DisplayOptions; take?: string; additional_headers?: Record; } interface DataCard { id: string; type: "custom_json"; name: string; config: DataCardConfig; } // ─── Path evaluator for "$out.foo.bar[0].baz" ──────────────────────────────── function evaluatePath(data: unknown, path: string): unknown { if (!path || !path.startsWith("$out")) return data; const expr = path.slice(4); // strip "$out" const tokens: string[] = []; const regex = /\.([a-zA-Z_][a-zA-Z0-9_]*)|\[(\d+)\]/g; let match: RegExpExecArray | null; while ((match = regex.exec(expr)) !== null) { tokens.push(match[1] ?? match[2]); } let current: unknown = data; for (const token of tokens) { if (current == null) return null; const idx = parseInt(token, 10); if (!isNaN(idx) && Array.isArray(current)) { current = current[idx]; } else { current = (current as Record)[token]; } } return current; } // ─── Data Card Widget ───────────────────────────────────────────────────────── function DataCardWidget({ card }: { card: DataCard }) { const [value, setValue] = useState("…"); const [lastUpdated, setLastUpdated] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetchData = () => { const headers: Record = { Accept: "application/json", ...(card.config.additional_headers ?? {}), }; fetch(card.config.url, { headers }) .then((r) => r.json()) .then((data) => { const result = card.config.take ? evaluatePath(data, card.config.take) : data; const display = result === null || result === undefined ? "(null)" : typeof result === "object" ? JSON.stringify(result, null, 2) : String(result); setValue(display); setLastUpdated(new Date()); setError(null); }) .catch((err) => { setError(String(err)); }); }; fetchData(); const ms = Math.max(5000, (card.config.refresh_interval ?? 60) * 1000); const interval = setInterval(fetchData, ms); return () => clearInterval(interval); }, [card.id, card.config.url, card.config.refresh_interval, card.config.take]); const fontSize = card.config.display_options?.font_size ?? 16; const textColor = card.config.display_options?.text_color ?? "#ffffff"; return (
{card.name} {error ? ( ⚠ {error} ) : ( {value} )} {lastUpdated && ( {lastUpdated.toLocaleTimeString()} )}
); } function App() { const [screenStatus, setScreenStatus] = useState< "notfullscreen" | "fullscreen" >("notfullscreen"); const [textState, setTextState] = useState({ showing: false, title: "" }); const [imagePopup, setImagePopup] = useState({ showing: false, image_url: "", caption: "" }); const [dataCards, setDataCards] = useState([]); useEffect(() => { const handleFullscreenChange = () => { if (!document.fullscreenElement) { setScreenStatus("notfullscreen"); } else { setScreenStatus("fullscreen"); } }; document.addEventListener("fullscreenchange", handleFullscreenChange); return () => { document.removeEventListener("fullscreenchange", handleFullscreenChange); }; }, []); useEffect(() => { if (screenStatus === "fullscreen") { const handlePullState = () => { fetch( "https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329/pull_full", ) .then((response) => response.json()) .then((data) => { const state = data.state ?? {}; setTextState(state.text ?? { showing: false, title: "" }); setImagePopup(state.image_popup ?? { showing: false, image_url: "", caption: "" }); setDataCards(state.data_cards ?? []); }) .catch((error) => { console.error("Error pulling state:", error); }); }; handlePullState(); const interval = setInterval(handlePullState, 5000); return () => clearInterval(interval); } }, [screenStatus]); return ( <> {screenStatus === "notfullscreen" ? (

TV View

Enter fullscreen mode to start displaying content.

) : (
{/* Text popup modal */} {textState.showing && (

{textState.title}

)} {/* Image popup modal */} {imagePopup.showing && imagePopup.image_url && (
{imagePopup.caption && (

{imagePopup.caption}

)} TV display
)} {/* Background idle state */} {!textState.showing && !imagePopup.showing && dataCards.length === 0 && (

Waiting for content . . .

)} {/* Data cards — persistent widgets at the bottom */} {dataCards.length > 0 && (
{dataCards.map((card) => ( ))}
)}
)} ); } export default App;