'use client'; /** * Recent-sessions card for the website-analytics page. Paginated list * of visitor sessions (one row per unique session) with click-through to * a detail sheet showing the full activity stream. * * Umami's session model: one row per anonymous-device-fingerprint+IP+UA * combination, with first/last visit timestamps + visit/view counts + * geo + browser/os/device. The detail page shows the per-event stream * (pageviews + custom events) within that session. */ import { useState } from 'react'; import { Globe, Smartphone, Monitor, Tablet, ChevronRight } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { CountryFlag } from '@/components/shared/country-flag'; import { getCountryName } from '@/lib/i18n/countries'; import { useUmamiSessions } from './use-website-analytics'; import { SessionDetailSheet } from './session-detail-sheet'; import { type DateRange } from '@/lib/analytics/range'; import type { UmamiSession } from '@/lib/services/umami.service'; interface Props { range: DateRange; } export function SessionsList({ range }: Props) { const [page, setPage] = useState(1); const [selected, setSelected] = useState(null); const pageSize = 15; const query = useUmamiSessions(range, { page, pageSize }); const rawSessions = query.data?.data?.data ?? []; // Umami's /sessions page isn't reliably ordered by activity timestamp - // sort by most-recent activity (lastAt) descending so the row at the top // is genuinely the latest session, matching the displayed timestamp. const sessions = [...rawSessions].sort((a, b) => { const la = a.lastAt ? new Date(a.lastAt).getTime() : 0; const lb = b.lastAt ? new Date(b.lastAt).getTime() : 0; return lb - la; }); const total = query.data?.data?.count ?? 0; const hasMore = page * pageSize < total; return ( <> Recent sessions {query.isLoading ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : sessions.length === 0 ? (
No sessions in this range.
) : ( <>
    {sessions.map((s, i) => ( // Umami's sessions endpoint can return rows with the // same session id within a page when activity straddles // a bucket boundary. Compose the key to dedupe.
  • ))}
Showing {(page - 1) * pageSize + 1}–{Math.min(page * pageSize, total)} of{' '} {total.toLocaleString()}
)}
setSelected(null)} /> ); } function DeviceIcon({ device }: { device: string }) { const cls = 'size-5 shrink-0 text-muted-foreground'; switch (device.toLowerCase()) { case 'mobile': return ; case 'tablet': return ; case 'desktop': case 'laptop': return ; default: return ; } } function fmtTime(iso: string): string { const d = new Date(iso); if (isNaN(d.getTime())) return iso; return d.toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' }); }