'use client'; /** * Realtime panel - Umami's "what's happening RIGHT NOW" view, surfaced * as a collapsible card at the top of the website-analytics page. * * Folds in five things from Umami's /api/realtime/ endpoint: * - Totals strip (visitors / views / events / countries in the last 30m) * - Top URLs being viewed * - Top countries * - Top referrers * - Recent event stream (pageviews + named events as they arrive) * * Polling pauses when the card is collapsed so we're not hammering * Umami at 5 s intervals while no one is looking. */ import { useState } from 'react'; import { ChevronDown, ChevronUp, Globe, Activity, MapPin, ExternalLink } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { CountryFlag } from '@/components/shared/country-flag'; import { getCountryName } from '@/lib/i18n/countries'; import { useUmamiRealtime } from './use-website-analytics'; export function RealtimePanel() { const [open, setOpen] = useState(false); const query = useUmamiRealtime(open); const data = query.data?.data ?? null; // Hide the entire bar when Umami reports a quiet 30-minute window - // a "Live activity (0 visitors)" header is just noise. We still poll // every 60 s while hidden so the bar reappears the moment traffic // arrives. const isQuiet = !!data && data.totals.visitors === 0 && data.events.length === 0; if (isQuiet && !open) return null; return ( {open ? ( {query.isLoading ? ( ) : !data ? (
No realtime data available.
) : (
{/* Totals strip */}
{/* Three-column ranked-list strip */}
} title="Top pages right now" rows={recordToRows(data.urls).map((r) => ({ ...r, label: r.label === '/' ? 'Homepage' : r.label, }))} emptyLabel="No pageviews yet" /> } title="Top countries" rows={recordToRows(data.countries).map((r) => ({ ...r, label: getCountryName(r.label, 'en') || r.label || 'Unknown', }))} emptyLabel="No country data yet" /> } title="Top referrers" rows={recordToRows(data.referrers).map((r) => ({ ...r, label: r.label || '(direct)', }))} emptyLabel="No referrers yet" />
{/* Recent event stream */}

Recent activity

{data.events.length === 0 ? (
No events in the last 30 minutes.
) : (
    {data.events.slice(0, 20).map((ev, i) => (
  1. {ev.eventName ? `Event: ${ev.eventName}` : !ev.urlPath || ev.urlPath === '/' ? 'Homepage' : ev.urlPath} {ev.country ? ( ) : null} {[ ev.country && getCountryName(ev.country, 'en'), ev.browser, ev.device, ] .filter(Boolean) .join(' · ')}
    {fmtAgo(ev.createdAt)}
  2. ))}
)}
)}
) : null}
); } function Stat({ label, value }: { label: string; value: number }) { return (
{label}
{value.toLocaleString()}
); } interface RankRow { label: string; value: number; } function RankList({ icon, title, rows, emptyLabel, }: { icon: React.ReactNode; title: string; rows: RankRow[]; emptyLabel: string; }) { const max = rows[0]?.value ?? 1; return (

{icon} {title}

{rows.length === 0 ? (
{emptyLabel}
) : ( )}
); } function recordToRows(rec: Record): RankRow[] { return Object.entries(rec) .map(([label, value]) => ({ label, value })) .sort((a, b) => b.value - a.value); } function fmtAgo(iso: string): string { const t = new Date(iso).getTime(); if (isNaN(t)) return iso; const diff = Date.now() - t; const seconds = Math.max(1, Math.round(diff / 1000)); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.round(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.round(minutes / 60); return `${hours}h ago`; }