'use client' import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { getSite, type Site } from '@/lib/api/sites' import { getSessionDetails, type Visitor, type SessionEvent } from '@/lib/api/realtime' import { useRealtimeSSE } from '@/lib/hooks/useRealtimeSSE' import { toast } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@ciphera-net/ui' import { UserIcon } from '@ciphera-net/ui' import { RealtimeSkeleton, SessionEventsSkeleton, useMinimumLoading } from '@/components/skeletons' import { motion, AnimatePresence } from 'framer-motion' 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 } = useRealtimeSSE(siteId) const [selectedVisitor, setSelectedVisitor] = useState(null) const [sessionEvents, setSessionEvents] = useState([]) const [loading, setLoading] = useState(true) const [loadingEvents, setLoadingEvents] = useState(false) // Load site info useEffect(() => { const init = async () => { try { const siteData = await getSite(siteId) setSite(siteData) } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to load site') } finally { setLoading(false) } } init() }, [siteId]) // Auto-select the first visitor when the list populates and nothing is selected useEffect(() => { if (visitors.length > 0 && !selectedVisitor) { handleSelectVisitor(visitors[0]) } }, [visitors]) // eslint-disable-line react-hooks/exhaustive-deps const handleSelectVisitor = async (visitor: Visitor) => { setSelectedVisitor(visitor) setLoadingEvents(true) try { const events = await getSessionDetails(siteId, visitor.session_id) setSessionEvents(events || []) } catch (error: unknown) { toast.error(getAuthErrorMessage(error) || 'Failed to load session events') } finally { setLoadingEvents(false) } } useEffect(() => { if (site?.domain) document.title = `Realtime · ${site.domain} | Pulse` }, [site?.domain]) const showSkeleton = useMinimumLoading(loading) if (showSkeleton) 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

New visitors will appear here in real-time

) : (
{visitors.map((visitor) => ( handleSelectVisitor(visitor)} className={`w-full text-left p-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-inset ${ selectedVisitor?.session_id === visitor.session_id ? 'bg-neutral-50 dark:bg-neutral-800/50 ring-1 ring-inset ring-neutral-200 dark:ring-neutral-700' : '' }`} >
{visitor.country ? `${getFlagEmoji(visitor.country)} ${visitor.city || 'Unknown City'}` : 'Unknown Location'}
{formatTimeAgo(visitor.last_seen)}
{visitor.current_path}
{visitor.device_type} {visitor.browser} {visitor.os} {visitor.pageviews} views
))}
)}
{/* 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) }