'use client' import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { getSite, type Site } from '@/lib/api/sites' import { getRealtimeVisitors, getSessionDetails, type Visitor, type SessionEvent } from '@/lib/api/realtime' import { toast } from 'sonner' import LoadingOverlay from '@/components/LoadingOverlay' function formatTimeAgo(dateString: string) { const date = new Date(dateString) const now = new Date() const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000) if (diffInSeconds < 60) return 'just now' if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago` if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago` return `${Math.floor(diffInSeconds / 86400)}d ago` } export default function RealtimePage() { const params = useParams() const router = useRouter() const siteId = params.id as string const [site, setSite] = useState(null) const [visitors, setVisitors] = useState([]) const [selectedVisitor, setSelectedVisitor] = useState(null) const [sessionEvents, setSessionEvents] = useState([]) const [loading, setLoading] = useState(true) const [loadingEvents, setLoadingEvents] = useState(false) // Load site info and initial visitors useEffect(() => { const init = async () => { try { const [siteData, visitorsData] = await Promise.all([ getSite(siteId), getRealtimeVisitors(siteId) ]) setSite(siteData) setVisitors(visitorsData || []) // Select first visitor if available if (visitorsData && visitorsData.length > 0) { handleSelectVisitor(visitorsData[0]) } } catch (error: any) { toast.error('Failed to load data') } finally { setLoading(false) } } init() }, [siteId]) // Poll for updates useEffect(() => { const interval = setInterval(async () => { try { const data = await getRealtimeVisitors(siteId) setVisitors(data || []) // Update selected visitor reference if they are still in the list if (selectedVisitor) { const updatedVisitor = data?.find(v => v.session_id === selectedVisitor.session_id) if (updatedVisitor) { // Don't overwrite the selectedVisitor state directly to avoid flickering details // But we could update "last seen" indicators if we wanted } } } catch (e) { // Silent fail } }, 5000) return () => clearInterval(interval) }, [siteId, selectedVisitor]) const handleSelectVisitor = async (visitor: Visitor) => { setSelectedVisitor(visitor) setLoadingEvents(true) try { const events = await getSessionDetails(siteId, visitor.session_id) setSessionEvents(events || []) } catch (error) { toast.error('Failed to load session details') } finally { setLoadingEvents(false) } } if (loading) return if (!site) return
Site not found
return (

Realtime Visitors {visitors.length} active now

{/* Visitors List */}

Active Sessions

{visitors.length === 0 ? (
No active visitors right now.
) : (
{visitors.map((visitor) => ( ))}
)}
{/* Session Details */}

{selectedVisitor ? 'Session Journey' : 'Select a visitor'}

{selectedVisitor && ( ID: {selectedVisitor.session_id.substring(0, 8)}... )}
{!selectedVisitor ? (
Select a visitor on the left to see their activity.
) : loadingEvents ? (
) : (
{sessionEvents.map((event, idx) => (
Visited {event.path} {new Date(event.timestamp).toLocaleTimeString()}
{event.referrer && (
Referrer: {event.referrer}
)}
))}
Session started {formatTimeAgo(sessionEvents[sessionEvents.length - 1]?.timestamp || new Date().toISOString())}
)}
) } function getFlagEmoji(countryCode: string) { if (!countryCode || countryCode.length !== 2) return '🌍' const codePoints = countryCode .toUpperCase() .split('') .map(char => 127397 + char.charCodeAt(0)) return String.fromCodePoint(...codePoints) }