import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useLiveBlogStream } from '@/hooks/useLiveBlogStream'; import { useLiveBlog, useLiveBlogUpdates } from '@/queries/live-blogs'; import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { cn } from '@/lib/utils'; interface LiveBlogViewerProps { slug: string; className?: string; } export function LiveBlogViewer({ slug, className }: LiveBlogViewerProps) { const [autoScroll, setAutoScroll] = useState(true); const [newUpdatesCount, setNewUpdatesCount] = useState(0); const [isScrolledUp, setIsScrolledUp] = useState(false); const updatesContainerRef = useRef(null); const lastUpdateCountRef = useRef(0); const { data: liveBlog, isLoading: blogLoading, error: blogError } = useLiveBlog(slug); const { data: updatesData, isLoading: updatesLoading, refetch: refetchUpdates } = useLiveBlogUpdates(liveBlog?.id || '', 1, 100); const { isConnected, lastEvent, connectionError, reconnectAttempts, connect } = useLiveBlogStream(liveBlog?.id || ''); const scrollToBottom = useCallback(() => { if (updatesContainerRef.current && autoScroll) { const container = updatesContainerRef.current; container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); } }, [autoScroll]); const handleScroll = useCallback(() => { if (!updatesContainerRef.current) return; const container = updatesContainerRef.current; const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50; setIsScrolledUp(!isAtBottom); if (isAtBottom) { setNewUpdatesCount(0); } }, []); const handleAutoScrollToggle = useCallback(() => { setAutoScroll(!autoScroll); if (!autoScroll) { scrollToBottom(); setNewUpdatesCount(0); } }, [autoScroll, scrollToBottom]); const showNewUpdates = useCallback(() => { setNewUpdatesCount(0); scrollToBottom(); setIsScrolledUp(false); }, [scrollToBottom]); // Handle new updates from SSE useEffect(() => { if (lastEvent?.type === 'update') { const handleUpdate = () => { refetchUpdates(); if (autoScroll && !isScrolledUp) { // Add a small delay to ensure the update is in the DOM setTimeout(() => { scrollToBottom(); }, 100); } else if (!autoScroll || isScrolledUp) { setNewUpdatesCount(prev => prev + 1); } }; handleUpdate(); } }, [lastEvent, autoScroll, isScrolledUp, refetchUpdates, scrollToBottom]); // Initial scroll to bottom when data loads useEffect(() => { if (updatesData?.data && updatesData.data.length > 0) { const currentUpdateCount = updatesData.data.length; // Only auto-scroll if this is the initial load or user is at bottom if (currentUpdateCount > lastUpdateCountRef.current && !isScrolledUp) { setTimeout(scrollToBottom, 100); } lastUpdateCountRef.current = currentUpdateCount; } }, [updatesData, scrollToBottom, isScrolledUp]); if (blogLoading) { return (
); } if (blogError || !liveBlog) { return (

Failed to load live blog

{blogError instanceof Error ? blogError.message : 'Unknown error occurred'}

); } const updates = updatesData?.data || []; const isLive = liveBlog.status === 'live'; return (
{liveBlog.title} {liveBlog.description && (

{liveBlog.description}

)}
{isLive ? 'Live' : 'Ended'}
{isLive && (
{isConnected ? 'Connected' : `Reconnecting... (${reconnectAttempts})`}
)} {liveBlog.viewCount} views {updates.length} updates
{!isConnected && ( )}
{connectionError && (

{connectionError}

)}
{updates.length === 0 && !updatesLoading ? (

No updates yet. Check back soon!

) : (
{updates.map((update) => ( ))} {updatesLoading && (
)}
)}
{/* New updates indicator */} {isScrolledUp && newUpdatesCount > 0 && (
{newUpdatesCount} new {newUpdatesCount === 1 ? 'update' : 'updates'}
)}
); } interface LiveBlogUpdateProps { update: ApiLiveBlogUpdate; } function LiveBlogUpdate({ update }: LiveBlogUpdateProps) { const isPinned = update.isPinned; return (
{isPinned && (
Pinned
)}
{update.author && ( <> {update.author.avatar && ( {update.author.name} )} {update.author.name} )}
{new Date(update.createdAt).toLocaleTimeString()}
{update.content}
); }