From ca292c47827d9679e623d72e96825a62dcfbaab1 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 30 Mar 2026 19:32:50 -0400 Subject: [PATCH] feat: add admin feedback review page and nav link --- app/admin/feedback/page.tsx | 194 ++++++++++++++++++++++++++++++++++++ app/admin/layout.tsx | 6 ++ 2 files changed, 200 insertions(+) create mode 100644 app/admin/feedback/page.tsx diff --git a/app/admin/feedback/page.tsx b/app/admin/feedback/page.tsx new file mode 100644 index 0000000..9dfbd31 --- /dev/null +++ b/app/admin/feedback/page.tsx @@ -0,0 +1,194 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useAuth } from '@/context/AuthContext' + +type FeedbackSubmission = { + id: string + category: 'BUG' | 'FEATURE_REQUEST' | 'GENERAL' + message: string + email: string | null + ipAddress: string + reviewed: boolean + createdAt: string +} + +const CATEGORY_LABELS: Record = { + BUG: 'Bug', + FEATURE_REQUEST: 'Feature Request', + GENERAL: 'General', +} + +const CATEGORY_STYLES: Record = { + BUG: 'bg-red-500/10 text-red-400 border border-red-500/20', + FEATURE_REQUEST: 'bg-amber-500/10 text-amber-400 border border-amber-500/20', + GENERAL: 'bg-zinc-800 text-zinc-400 border border-zinc-700', +} + +function formatDate(iso: string) { + return new Date(iso).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) +} + +export default function AdminFeedbackPage() { + const { getAuthHeaders } = useAuth() + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [categoryFilter, setCategoryFilter] = useState('ALL') + const [reviewedFilter, setReviewedFilter] = useState('ALL') + + async function load() { + try { + setLoading(true) + setError(null) + const res = await fetch('/api/admin/feedback', { + headers: getAuthHeaders(), + credentials: 'include', + }) + if (!res.ok) throw new Error(`Failed to load feedback (${res.status})`) + setItems(await res.json()) + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to load') + } finally { + setLoading(false) + } + } + + useEffect(() => { + load() + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + async function toggleReviewed(item: FeedbackSubmission) { + const prev = items + setItems((cur) => + cur.map((i) => (i.id === item.id ? { ...i, reviewed: !i.reviewed } : i)) + ) + try { + const res = await fetch(`/api/admin/feedback/${item.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, + credentials: 'include', + body: JSON.stringify({ reviewed: !item.reviewed }), + }) + if (!res.ok) setItems(prev) + } catch { + setItems(prev) + } + } + + const filtered = items.filter((i) => { + if (categoryFilter !== 'ALL' && i.category !== categoryFilter) return false + if (reviewedFilter === 'REVIEWED' && !i.reviewed) return false + if (reviewedFilter === 'UNREVIEWED' && i.reviewed) return false + return true + }) + + return ( +
+
+
+

Feedback

+

+ User-submitted feedback. Mark items reviewed to track triage. +

+
+ +
+ +
+ + + + {filtered.length} of {items.length} submissions + +
+ + {loading &&

Loading…

} + {error &&

{error}

} + + {!loading && !error && filtered.length === 0 && ( +

No submissions match your filters.

+ )} + + {!loading && !error && filtered.length > 0 && ( +
+ + + + + + + + + + + + {filtered.map((item) => ( + + + + + + + + ))} + +
DateCategoryMessageEmailReviewed
+ {formatDate(item.createdAt)} + + + {CATEGORY_LABELS[item.category]} + + + {item.message.length > 120 + ? `${item.message.slice(0, 120)}…` + : item.message} + + {item.email ?? '—'} + + +
+
+ )} +
+ ) +} diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 751b8f7..19d1cde 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -19,6 +19,7 @@ import { Wand2, FolderKanban, Shield, + MessageSquare, } from "lucide-react"; const navGroups: AdminNavGroup[] = [ @@ -89,6 +90,11 @@ const navGroups: AdminNavGroup[] = [ href: "/admin/email/send", icon: , }, + { + label: "Feedback", + href: "/admin/feedback", + icon: , + }, ], }, {