"use client"; import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { ChevronDown, Key, Loader2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { formatDate, formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { type ApiTokenInfo } from "./_shared"; export function ApiTokenOverviewSection() { const { t } = useTranslations(); // v1.5 phase-4b moved this to a dedicated route (`/admin/api-tokens`), // so the user has already opted into seeing tokens by visiting the // page. Default to expanded; the toggle stays as an escape hatch. const [expanded, setExpanded] = useState(true); const { data: tokens, isLoading } = useQuery({ queryKey: ["admin", "tokens"], queryFn: async () => { const res = await fetch("/api/admin/tokens"); if (!res.ok) throw new Error("Failed"); return (await res.json()).data as ApiTokenInfo[]; }, enabled: expanded, }); return (
{t("admin.apiTokens")}
{expanded && (
{isLoading ? (
) : !tokens?.length ? ( // v1.4.15 phase-C5: empty state explains where tokens come // from (native client sign-in / Settings → Account) so a // fresh admin doesn't think they need to create them here. } title={t("admin.tokensEmptyTitle")} description={t("admin.tokensEmptyDescription")} /> ) : ( <> {/* Desktop table — verbatim layout for md+. The wrapping `hidden md:block` swaps it out for the card-list below on mobile, mirroring the pattern used in ``. The inner `overflow-x-auto` is a belt-and-suspenders guard for medium viewports with extra-long permission lists. */}
{tokens.map((token, i) => { const isExpired = token.expiresAt && new Date(token.expiresAt) < new Date(); return ( ); })}
{t("admin.tokenUser")} {t("admin.tokenName")} {t("admin.tokenPermissions")} {t("admin.tokenStatus")} {t("admin.tokenLastUsed")} {t("admin.tokenCreated")}
{token.user.username} {token.name}
{token.permissions.map((p) => ( {p} ))}
{token.revoked ? ( {t("settings.tokenRevoked")} ) : isExpired ? ( {t("settings.tokenExpired")} ) : ( {t("common.active")} )} {token.lastUsedAt ? formatDateTime(token.lastUsedAt) : t("admin.tokenNeverUsed")} {formatDate(token.createdAt)}
{/* Mobile card list — each token renders as a self- contained card with a stacked meta layout. No horizontal scroll: every cell wraps within the card width. Mirrors ``. */}
    {tokens.map((token) => { const isExpired = token.expiresAt && new Date(token.expiresAt) < new Date(); return (
  • {token.name} {token.revoked ? ( {t("settings.tokenRevoked")} ) : isExpired ? ( {t("settings.tokenExpired")} ) : ( {t("common.active")} )}

    {token.user.username}

    {token.permissions.map((p) => ( {p} ))}

    {t("admin.tokenLastUsed")}:{" "} {token.lastUsedAt ? formatDateTime(token.lastUsedAt) : t("admin.tokenNeverUsed")}

    {t("admin.tokenCreated")}: {formatDate(token.createdAt)}

  • ); })}
)}
)}
); }