"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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; 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 { FEEDBACK_STATUS_TABS, type FeedbackCategoryType, type FeedbackItem, type FeedbackListResponse, type FeedbackStatusType, getApiErrorMessage, useSystemStatus, } from "./_shared"; export function FeedbackInboxSection({ id }: { id: string }) { 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: ["admin", "feedback", 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: ["admin", "feedback"] }); } return (

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

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

setActiveStatus(v as FeedbackStatusType)} className="mt-4" > {FEEDBACK_STATUS_TABS.map((s) => ( {t( `admin.feedback.tab${s.charAt(0) + s.slice(1).toLowerCase()}`, )} {counts[s] ?? 0} ))} {FEEDBACK_STATUS_TABS.map((s) => ( {isLoading ? (
{t("admin.feedback.loading")}
) : !data?.items?.length ? (

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

) : (
{data.items.map((item, i) => ( setSelected(item)} > ))}
{t("admin.feedback.createdAt")} {t("admin.feedback.category")} {t("admin.feedback.subject")} {t("admin.feedback.user")} {t("admin.feedback.actions")}
{formatDateTime(item.createdAt)} {item.subject} {item.user?.username ?? t("admin.feedback.anonymous")}
)}
))}
{selected && ( !open && setSelected(null)} githubConfigured={githubConfigured} onMutated={refresh} /> )}
); } function FeedbackCategoryBadge({ category, }: { category: FeedbackCategoryType; }) { const { t } = useTranslations(); const map: Record< FeedbackCategoryType, { label: string; className: string } > = { BUG: { label: t("admin.feedback.categoryBug"), className: "bg-red-500/15 text-red-400 border-red-500/30", }, FEATURE_REQUEST: { label: t("admin.feedback.categoryFeature"), className: "bg-purple-500/15 text-purple-400 border-purple-500/30", }, QUESTION: { label: t("admin.feedback.categoryQuestion"), className: "bg-blue-500/15 text-blue-400 border-blue-500/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} {t("admin.feedback.submittedBy")}:{" "} {item.user?.username ?? t("admin.feedback.anonymous")} ยท {formatDateTime(item.createdAt)} {issueUrl && ( {t("admin.feedback.viewIssue")} )}
{item.description}
{(url || locale || userAgent || appVersion) && (

{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
)}