112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Check, Clipboard, FileCode2, Terminal } from "lucide-react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export type RankedAnalyticsItem = {
|
|
label: string;
|
|
count: number;
|
|
};
|
|
|
|
type RankedAnalyticsListProps = {
|
|
title: string;
|
|
items: RankedAnalyticsItem[];
|
|
kind: "file" | "command";
|
|
};
|
|
|
|
async function copyValue(value: string, onCopied: () => void) {
|
|
if (!value || typeof navigator === "undefined" || !navigator.clipboard)
|
|
return;
|
|
await navigator.clipboard.writeText(value);
|
|
onCopied();
|
|
}
|
|
|
|
export function RankedAnalyticsList({
|
|
title,
|
|
items,
|
|
kind,
|
|
}: RankedAnalyticsListProps) {
|
|
const [copied, setCopied] = useState<string | null>(null);
|
|
const Icon = kind === "command" ? Terminal : FileCode2;
|
|
|
|
const markCopied = (value: string) => {
|
|
setCopied(value);
|
|
window.setTimeout(() => setCopied(null), 1400);
|
|
};
|
|
|
|
return (
|
|
<section
|
|
className="min-w-0 rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-sm"
|
|
aria-labelledby={`${title.toLowerCase().replace(/\s+/g, "-")}-heading`}
|
|
>
|
|
<div className="mb-4 flex items-center justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<h3
|
|
id={`${title.toLowerCase().replace(/\s+/g, "-")}-heading`}
|
|
className="truncate text-sm font-semibold text-[color:var(--text)]"
|
|
>
|
|
{title}
|
|
</h3>
|
|
<p className="mt-1 text-xs text-[color:var(--text-muted)]">
|
|
Top {items.length.toLocaleString()} by count
|
|
</p>
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
"flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ring-1",
|
|
kind === "command"
|
|
? "bg-amber-500/15 text-amber-300 ring-amber-400/20"
|
|
: "bg-violet-500/15 text-violet-300 ring-violet-400/20",
|
|
)}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
</span>
|
|
</div>
|
|
|
|
{items.length === 0 ? (
|
|
<div className="rounded-xl border border-dashed border-[color:var(--border-strong)] p-5 text-sm text-[color:var(--text-muted)]">
|
|
No entries in this range.
|
|
</div>
|
|
) : (
|
|
<ol className="space-y-2">
|
|
{items.map((item, index) => (
|
|
<li
|
|
key={`${item.label}-${index}`}
|
|
className="group flex min-w-0 items-center gap-3 rounded-xl border border-[color:var(--border)] bg-[color:var(--bg)] px-3 py-2.5 transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-muted)]"
|
|
>
|
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[color:var(--surface-muted)] text-xs font-bold text-[color:var(--text-muted)]">
|
|
{index + 1}
|
|
</span>
|
|
<span
|
|
className="min-w-0 flex-1 truncate font-mono text-xs text-[color:var(--text)]"
|
|
title={item.label}
|
|
>
|
|
{item.label}
|
|
</span>
|
|
<span className="shrink-0 rounded-full bg-[color:var(--accent-soft)] px-2 py-1 text-xs font-semibold text-[color:var(--accent-strong)]">
|
|
{item.count.toLocaleString()}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-[color:var(--border)] text-[color:var(--text-muted)] opacity-100 transition hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)] md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
aria-label={`Copy ${item.label}`}
|
|
onClick={() =>
|
|
copyValue(item.label, () => markCopied(item.label))
|
|
}
|
|
>
|
|
{copied === item.label ? (
|
|
<Check className="h-3.5 w-3.5" />
|
|
) : (
|
|
<Clipboard className="h-3.5 w-3.5" />
|
|
)}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|