'use client'; import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import type { ChatMessage } from '@shared/chat'; const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? 'http://localhost:4100'; const POLL_INTERVAL = 2500; // Global singleton to prevent multiple SSE connections across all renders/remounts let globalSSEConnection: EventSource | null = null; let globalConnectionListeners: Set<(payload: any) => void> = new Set(); export default function DashboardPage() { const { messages, refresh, error: pollError } = useChatMessages(); const { selection, status: overlayStatus } = useOverlaySelection(); const { connected, liveId, connect, disconnect, connecting } = useConnection(); const handleSelect = useCallback( async (message: ChatMessage) => { try { await fetch(`${BACKEND_URL}/overlay/selection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: message.id }) }); } catch (error) { console.error('Failed to select message', error); } }, [] ); const handleClear = useCallback(async () => { try { await fetch(`${BACKEND_URL}/overlay/selection`, { method: 'DELETE' }); } catch (error) { console.error('Failed to clear selection', error); } }, []); useEffect(() => { const timer = setInterval(() => { refresh(); }, POLL_INTERVAL); return () => clearInterval(timer); }, [refresh]); const statusHint = useMemo(() => { if (pollError) return 'Backend unreachable'; if (overlayStatus === 'connecting') return 'Connecting to overlay…'; if (overlayStatus === 'error') return 'Overlay stream disconnected'; return 'Live'; }, [pollError, overlayStatus]); const superChats = useMemo(() => messages.filter(m => m.superChat), [messages]); const newMembers = useMemo(() => messages.filter(m => m.membershipGift), [messages]); const regularMessages = useMemo(() => messages.filter(m => !m.superChat && !m.membershipGift), [messages]); return (

🎬 Live Chat Monitor

Select a message to display on your OBS overlay

{statusHint} {messages.length} messages
{/* Main Chat Column */}

💬 Live Chat

{selection && ( )}
{regularMessages.map((message) => ( handleSelect(message)} /> ))} {regularMessages.length === 0 && (

⏳ Waiting for chat messages...

)}
{/* Right Split Column */}
{/* Super Chats - Top Half */}

💰 Super Chats

{superChats.length}
{superChats.map((message) => ( handleSelect(message)} /> ))} {superChats.length === 0 && (

No super chats yet

)}
{/* New Members - Bottom Half */}

⭐ New Members

{newMembers.length}
{newMembers.map((message) => ( handleSelect(message)} /> ))} {newMembers.length === 0 && (

No new members yet

)}
); } function useChatMessages() { const [messages, setMessages] = useState([]); const [error, setError] = useState(null); const refresh = useCallback(async () => { try { const response = await fetch(`${BACKEND_URL}/chat/messages`); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const data = await response.json(); setMessages(Array.isArray(data.messages) ? data.messages : []); setError(null); } catch (err) { setError(err as Error); } }, []); useEffect(() => { refresh(); }, [refresh]); return { messages, refresh, error }; } type OverlayStatus = 'connecting' | 'live' | 'error'; type SelectionPayload = { message: ChatMessage | null; }; function useOverlaySelection() { const [selection, setSelection] = useState(null); const [status, setStatus] = useState('connecting'); const listenerRef = useRef<((payload: any) => void) | null>(null); useEffect(() => { // Create global connection if it doesn't exist if (!globalSSEConnection) { console.log('[Dashboard] Creating GLOBAL SSE connection'); globalSSEConnection = new EventSource(`${BACKEND_URL}/overlay/stream`); globalSSEConnection.addEventListener('selection', ((event: MessageEvent) => { try { const payload: SelectionPayload = JSON.parse(event.data); // Broadcast to all listeners globalConnectionListeners.forEach(listener => listener(payload)); } catch (error) { console.error('Failed to parse selection payload', error); } }) as EventListener); globalSSEConnection.addEventListener('heartbeat', () => { // Heartbeat - connection is alive }); globalSSEConnection.onerror = () => { console.error('[Dashboard] SSE error'); }; } // Register this component's listener const myListener = (payload: SelectionPayload) => { setSelection(payload.message); setStatus('live'); }; listenerRef.current = myListener; globalConnectionListeners.add(myListener); return () => { // Unregister this component's listener if (listenerRef.current) { globalConnectionListeners.delete(listenerRef.current); } // Don't close global connection - other components might use it }; }, []); return { selection, status }; } function useConnection() { const [connected, setConnected] = useState(false); const [liveId, setLiveId] = useState(null); const [connecting, setConnecting] = useState(false); const checkStatus = useCallback(async () => { try { const response = await fetch(`${BACKEND_URL}/health`); const data = await response.json(); setConnected(data.connected); setLiveId(data.liveId); } catch (error) { console.error('Failed to check connection status', error); } }, []); useEffect(() => { checkStatus(); const interval = setInterval(checkStatus, 5000); return () => clearInterval(interval); }, [checkStatus]); const connect = useCallback(async (liveId: string) => { setConnecting(true); try { const response = await fetch(`${BACKEND_URL}/chat/connect`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ liveId }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to connect'); } await checkStatus(); } catch (error) { console.error('Failed to connect', error); alert('Failed to connect to YouTube Live chat. Please check the Live ID.'); } finally { setConnecting(false); } }, [checkStatus]); const disconnect = useCallback(async () => { try { await fetch(`${BACKEND_URL}/chat/disconnect`, { method: 'POST' }); await checkStatus(); } catch (error) { console.error('Failed to disconnect', error); } }, [checkStatus]); return { connected, liveId, connect, disconnect, connecting }; } type ConnectionControlProps = { connected: boolean; liveId: string | null; connecting: boolean; onConnect: (liveId: string) => void; onDisconnect: () => void; }; function ConnectionControl({ connected, liveId, connecting, onConnect, onDisconnect }: ConnectionControlProps) { const [inputValue, setInputValue] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (inputValue.trim()) { onConnect(inputValue.trim()); } }; return (
{connected ? (
🟢 Connected Live ID: {liveId}
) : (
setInputValue(e.target.value)} placeholder="e.g., dQw4w9WgXcQ or https://youtube.com/watch?v=..." disabled={connecting} />
)}
); } type ChatItemProps = { message: ChatMessage; isSelected: boolean; onSelect: () => void; }; function ChatItem({ message, isSelected, onSelect }: ChatItemProps) { return ( ); } function MemberItem({ message, isSelected, onSelect }: ChatItemProps) { return ( ); }