2026-05-04 22:53:06 +02:00
|
|
|
'use client';
|
|
|
|
|
|
2026-05-20 15:53:41 +02:00
|
|
|
import Link from 'next/link';
|
|
|
|
|
import { ArrowRight } from 'lucide-react';
|
|
|
|
|
|
2026-05-25 03:50:46 +02:00
|
|
|
import { Button } from '@/components/ui/button';
|
2026-05-04 22:53:06 +02:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
|
|
|
import type { UmamiMetricRow } from '@/lib/services/umami.service';
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
title: string;
|
|
|
|
|
rows: UmamiMetricRow[] | null;
|
|
|
|
|
loading: boolean;
|
|
|
|
|
/** Label substituted when `x` is empty (e.g. direct traffic referrers). */
|
|
|
|
|
defaultLabel?: string;
|
2026-05-20 15:53:41 +02:00
|
|
|
/** Optional "View all" link target. When set, renders a link in the
|
|
|
|
|
* card header that opens a full ranked-list page for this metric. */
|
|
|
|
|
viewAllHref?: string;
|
|
|
|
|
/** Cap for the inline list (default 10). The full page uses no cap. */
|
|
|
|
|
limit?: number;
|
2026-05-25 03:50:46 +02:00
|
|
|
/** Optional callback invoked when the empty-state "expand range" CTA is
|
|
|
|
|
* clicked. When supplied, the empty state shows a nudge button suggesting
|
|
|
|
|
* the rep try a wider range. */
|
|
|
|
|
onExpandRange?: () => void;
|
|
|
|
|
/** Label for the expand-range button when `onExpandRange` is supplied. */
|
|
|
|
|
expandRangeLabel?: string;
|
2026-05-04 22:53:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Compact "top N" list used for top pages / referrers / countries.
|
|
|
|
|
* Renders each row as label + numeric count, with a thin progress bar
|
|
|
|
|
* scaled to the largest count in the set so the visual density tells
|
|
|
|
|
* the same story at a glance as the numbers.
|
|
|
|
|
*/
|
2026-05-20 15:53:41 +02:00
|
|
|
export function TopList({
|
|
|
|
|
title,
|
|
|
|
|
rows,
|
|
|
|
|
loading,
|
|
|
|
|
defaultLabel = '-',
|
|
|
|
|
viewAllHref,
|
|
|
|
|
limit = 10,
|
2026-05-25 03:50:46 +02:00
|
|
|
onExpandRange,
|
|
|
|
|
expandRangeLabel = 'Try last 30 days',
|
2026-05-20 15:53:41 +02:00
|
|
|
}: Props) {
|
2026-05-04 22:53:06 +02:00
|
|
|
return (
|
|
|
|
|
<Card>
|
2026-05-20 15:53:41 +02:00
|
|
|
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0">
|
2026-05-04 22:53:06 +02:00
|
|
|
<CardTitle className="text-base">{title}</CardTitle>
|
2026-05-20 15:53:41 +02:00
|
|
|
{viewAllHref ? (
|
|
|
|
|
<Link
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
// typedRoutes is enabled - viewAllHref is constructed at the
|
2026-05-20 15:53:41 +02:00
|
|
|
// call site from string interpolation, so opt out of the
|
|
|
|
|
// literal-string check here.
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
href={viewAllHref as any}
|
|
|
|
|
className="inline-flex items-center gap-0.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
View all
|
|
|
|
|
<ArrowRight className="size-3" aria-hidden />
|
|
|
|
|
</Link>
|
|
|
|
|
) : null}
|
2026-05-04 22:53:06 +02:00
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-full" />
|
|
|
|
|
<Skeleton className="h-4 w-5/6" />
|
|
|
|
|
<Skeleton className="h-4 w-4/6" />
|
|
|
|
|
<Skeleton className="h-4 w-3/6" />
|
|
|
|
|
</div>
|
|
|
|
|
) : !rows || rows.length === 0 ? (
|
2026-05-25 03:50:46 +02:00
|
|
|
<div className="space-y-2 py-6 text-center text-sm text-muted-foreground">
|
|
|
|
|
<p>No data in this range.</p>
|
|
|
|
|
{onExpandRange ? (
|
|
|
|
|
<Button size="sm" variant="outline" onClick={onExpandRange}>
|
|
|
|
|
{expandRangeLabel}
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-05-04 22:53:06 +02:00
|
|
|
) : (
|
|
|
|
|
<ul className="space-y-1.5">
|
2026-05-20 15:53:41 +02:00
|
|
|
{rows.slice(0, limit).map((row, i) => {
|
2026-05-04 22:53:06 +02:00
|
|
|
const max = rows[0]?.y ?? 1;
|
|
|
|
|
const pct = (row.y / max) * 100;
|
|
|
|
|
const label = row.x?.trim() || defaultLabel;
|
|
|
|
|
return (
|
|
|
|
|
<li key={`${row.x}-${i}`} className="text-sm">
|
|
|
|
|
<div className="flex items-baseline justify-between gap-2">
|
|
|
|
|
<span className="truncate font-medium">{label}</span>
|
|
|
|
|
<span className="shrink-0 tabular-nums text-muted-foreground">
|
|
|
|
|
{row.y.toLocaleString()}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-0.5 h-1 w-full rounded-full bg-muted">
|
|
|
|
|
<div
|
|
|
|
|
className="h-1 rounded-full bg-brand"
|
|
|
|
|
style={{ width: `${Math.max(2, pct)}%` }}
|
|
|
|
|
aria-hidden
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|