"use client"; import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, ExternalLink, GitPullRequest, Inbox, Loader2, MessageSquare, Trash2, } from "lucide-react"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { FEEDBACK_STATUS_TABS, type FeedbackCategoryType, type FeedbackItem, type FeedbackListResponse, type FeedbackStatusType, getApiErrorMessage, useSystemStatus, } from "./_shared"; export function FeedbackInboxSection() { const { t } = useTranslations(); const queryClient = useQueryClient(); const { data: status } = useSystemStatus(); const githubConfigured = Boolean(status?.integrations?.bugReport?.configured); const [activeStatus, setActiveStatus] = useState("OPEN"); const [selected, setSelected] = useState(null); const { data, isLoading } = useQuery({ queryKey: queryKeys.adminFeedback(activeStatus), queryFn: async () => { const res = await fetch( `/api/admin/feedback?status=${activeStatus}&limit=100`, ); if (!res.ok) throw new Error("Failed"); return (await res.json()).data as FeedbackListResponse; }, }); const counts = data?.meta?.countsByStatus ?? {}; function refresh() { queryClient.invalidateQueries({ queryKey: queryKeys.adminFeedbackRoot() }); } return (
{/* v1.4.19 A8 / F-08 + F-20: this is the only card on `/admin/feedback`, so the page header already provides the title + description — the card-level repetition was just visual noise. The icon stays as a low-key card affordance. */}
); } function FeedbackCategoryBadge({ category, }: { category: FeedbackCategoryType; }) { const { t } = useTranslations(); const map: Record< FeedbackCategoryType, { label: string; className: string } > = { BUG: { label: t("admin.feedback.categoryBug"), // Bug = something that's broken → semantic `destructive` token instead // of raw `dracula-red`. Visually identical in dark mode (both resolve // to #ff5555); light mode now picks up the AA-contrast `#cb3a2a`. className: "bg-destructive/15 text-destructive border-destructive/30", }, FEATURE_REQUEST: { label: t("admin.feedback.categoryFeature"), className: "bg-dracula-purple/15 text-dracula-purple border-dracula-purple/30", }, QUESTION: { label: t("admin.feedback.categoryQuestion"), className: "bg-dracula-cyan/15 text-dracula-cyan border-dracula-cyan/30", }, OTHER: { label: t("admin.feedback.categoryOther"), className: "bg-muted text-muted-foreground border-border", }, }; const cfg = map[category]; return ( {cfg.label} ); } function FeedbackDetailDialog({ item, open, onOpenChange, githubConfigured, onMutated, }: { item: FeedbackItem; open: boolean; onOpenChange: (open: boolean) => void; githubConfigured: boolean; onMutated: () => void; }) { const { t } = useTranslations(); const [note, setNote] = useState(item.adminNote ?? ""); const [issueUrl, setIssueUrl] = useState(item.gitHubIssueUrl); const update = useMutation({ mutationFn: async (payload: { status?: FeedbackStatusType; adminNote?: string | null; }) => { const res = await fetch(`/api/admin/feedback/${item.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(await getApiErrorMessage(res)); }, onSuccess: (_, vars) => { onMutated(); if (vars.adminNote !== undefined) { toast.success(t("admin.feedback.noteSaved")); } }, onError: (err) => { toast.error( err instanceof Error ? err.message : t("admin.feedback.updateFailed"), ); }, }); const archive = useMutation({ mutationFn: async () => { const res = await fetch(`/api/admin/feedback/${item.id}`, { method: "DELETE", }); if (!res.ok) throw new Error(await getApiErrorMessage(res)); }, onSuccess: () => { onMutated(); onOpenChange(false); }, onError: (err) => { toast.error( err instanceof Error ? err.message : t("admin.feedback.updateFailed"), ); }, }); const publish = useMutation({ mutationFn: async () => { const res = await fetch(`/api/admin/feedback/${item.id}/github`, { method: "POST", }); if (!res.ok) throw new Error(await getApiErrorMessage(res)); return (await res.json()).data as { issueUrl: string }; }, onSuccess: (data) => { setIssueUrl(data.issueUrl); toast.success(t("admin.feedback.publishSuccess")); onMutated(); }, onError: (err) => { toast.error( err instanceof Error ? err.message : t("admin.feedback.publishFailed"), ); }, }); const meta = item.metadata ?? {}; const url = typeof meta.url === "string" ? meta.url : null; const locale = typeof meta.locale === "string" ? meta.locale : null; const userAgent = typeof meta.userAgent === "string" ? meta.userAgent : null; const appVersion = typeof meta.appVersion === "string" ? meta.appVersion : null; return ( {item.subject} } description={ {t("admin.feedback.submittedBy")}:{" "} {item.user?.username ?? t("admin.feedback.anonymous")} · {formatDateTime(item.createdAt)} {issueUrl && ( {t("admin.feedback.viewIssue")} )} } className="sm:max-w-2xl" footer={
{t("admin.feedback.archiveConfirm")} {t("common.cancel")} archive.mutate()} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {t("admin.feedback.actionArchive")} {githubConfigured && !issueUrl && ( )} {issueUrl && ( {t("admin.feedback.publishedToGithub")} )}
} >
{item.description}
{(url || locale || userAgent || appVersion) && (
{/* v1.4.33 IW9 — h4 -> h3 so the sheet outline stays sequential beneath the ResponsiveSheet's h2 title. */}

{t("admin.feedback.metadataHeading")}

{url && ( <>
{t("admin.feedback.metaUrl")}
{url}
)} {locale && ( <>
{t("admin.feedback.metaLocale")}
{locale}
)} {appVersion && ( <>
{t("admin.feedback.metaAppVersion")}
{appVersion}
)} {userAgent && ( <>
{t("admin.feedback.metaUserAgent")}
{userAgent}
)}
)} {item.screenshotBase64 && (

{t("admin.feedback.screenshotHeading")}

{/* eslint-disable-next-line @next/next/no-img-element */} Screenshot
)}