'use client'; /** * /website-analytics page shell. Mirrors the dashboard shell's layout: * - PageHeader with date-range picker (presets + custom) * - KPI tiles (pageviews / visitors / visits / bounces) * - Two-column grid: pageviews trend + active visitors badge * - Top-N tables: pages, referrers, countries * * Gracefully renders an empty state when Umami isn't configured for the * port - points the operator at /admin/website-analytics to set creds. */ import { useState } from 'react'; import Link from 'next/link'; import { Globe, Settings, ExternalLink } from 'lucide-react'; import { PageHeader } from '@/components/shared/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { KPITile } from '@/components/ui/kpi-tile'; import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button'; import { DateRangePicker } from '@/components/dashboard/date-range-picker'; import { type DateRange } from '@/lib/analytics/range'; import { useUIStore } from '@/stores/ui-store'; import { useUmamiActive, useUmamiPageviews, useUmamiStats, useUmamiTopCountries, useUmamiTopPages, useUmamiTopReferrers, } from './use-website-analytics'; import { PageviewsChart } from './pageviews-chart'; import { TopList } from './top-list'; export function WebsiteAnalyticsShell() { const [range, setRange] = useState('30d'); const portSlug = useUIStore((s) => s.currentPortSlug); const stats = useUmamiStats(range); const pageviews = useUmamiPageviews(range); const active = useUmamiActive(range); const topPages = useUmamiTopPages(range); const topReferrers = useUmamiTopReferrers(range); const topCountries = useUmamiTopCountries(range); // API surfaces `notConfigured: true` on a 200 response (not 4xx) so // React Query doesn't infinite-retry — that retry loop saturated the // postgres pool and stalled the dev server. Single empty state covers // all widgets so the page doesn't show six loading spinners forever. const notConfigured = stats.data?.notConfigured === true; return (
} /> {notConfigured ? ( ) : ( <> {/* Live indicator + KPI tiles */}
{/* Pageviews trend */} Pageviews trend {pageviews.isLoading ? ( ) : ( )} {/* Top-N tables */}
)}
); } function ActiveVisitorsBadge({ value, loading }: { value?: number; loading: boolean }) { return (
Active right now
{loading ? ( ) : ( <> {value ?? 0} )}
); } function KpiPair({ label, value, prev, accent, loading, invertDelta = false, }: { label: string; value: number | undefined; prev: number | undefined; accent: 'brand' | 'success' | 'warning' | 'mint' | 'teal' | 'purple'; loading: boolean; /** For metrics where lower is better (bounces). Flip the sign so green * still means "good" in the UI. */ invertDelta?: boolean; }) { if (loading) { return (
); } const v = value ?? 0; const p = prev ?? 0; let delta: number | undefined; if (p > 0) { delta = Math.round(((v - p) / p) * 100); if (invertDelta) delta = -delta; } return ; } function NotConfiguredEmptyState({ portSlug }: { portSlug: string | null }) { return (

Connect Umami to see your website analytics

Add your Umami URL, API token (or username/password), and Website ID for this port to unlock pageview trends, top pages, referrers, and audience geography.

); }