"use client"; import { useCallback, useState } from "react"; import { Can } from "@/components/staff-context"; import { toast } from "sonner"; import { api, AdminError, type AdminFeedback } from "@/lib/api"; import { usePagedList } from "@/lib/use-paged-list"; import { ListShell, EmptyRow } from "@/components/list-shell"; import { Table, THead, TR, TH, TD } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Modal } from "@/components/ui/modal"; import { useConfirm } from "@/components/confirm"; import { RichTextarea } from "@/components/ui/rich-textarea"; import { RichText } from "@/lib/rich-text"; import { cn, fmtDate } from "@/lib/utils"; import { Clamp } from "@/components/ui/clamp"; const STATUSES = ["all", "open", "resolved"] as const; type Status = (typeof STATUSES)[number]; export default function FeedbackPage() { const [status, setStatus] = useState("all"); const fetcher = useCallback( (limit: number, offset: number) => api.feedback(status, limit, offset), [status], ); const list = usePagedList(fetcher, [status]); const confirm = useConfirm(); const [reply, setReply] = useState(null); async function run(action: Promise, successMsg: string) { try { await action; toast.success(successMsg); list.refresh(); } catch (err) { toast.error(err instanceof AdminError ? err.message : "Action failed."); } } async function acknowledge(f: AdminFeedback) { if ( await confirm({ title: "Acknowledge this feedback?", message: "The author will be emailed that we've seen it and started work.", confirmLabel: "Acknowledge", }) ) { run(api.acknowledgeFeedback(f.id), "Feedback acknowledged."); } } async function resolve(f: AdminFeedback) { if ( await confirm({ title: "Mark resolved?", message: "The author will be emailed a thank-you. This can't be undone.", confirmLabel: "Mark resolved", }) ) { run(api.resolveFeedback(f.id), "Feedback resolved."); } } return ( {STATUSES.map((s) => ( ))} } > {/* Phone: each field stacked, label above value, so a long message gets the card's full width. Desktop: ordinary table rows. */} {list.items.length === 0 && !list.loading ? ( ) : ( list.items.map((f) => ( )) )}
From Message Status Received Actions
{f.author_handle ? ( <> {f.author_name} {`@${f.author_handle}`} ) : ( {f.contact_email ?? "Anonymous"} )}
{f.category}
{f.message}
{f.reply_message ? (
Your reply:
) : null}
{fmtDate(f.created_at)}
{!f.resolved_at && !f.acknowledged_at ? ( ) : null} {!f.resolved_at ? ( ) : null}
setReply(null)} onDone={() => { setReply(null); list.refresh(); }} />
); } function StatusBadge({ f }: { f: AdminFeedback }) { const label = f.resolved_at ? "Resolved" : f.replied_at ? "Replied" : f.acknowledged_at ? "Acknowledged" : "Open"; const tone = f.replied_at || f.resolved_at ? "text-primary" : f.acknowledged_at ? "text-amber-600" : "text-muted-foreground"; return {label}; } function ReplyModal({ feedback, onClose, onDone, }: { feedback: AdminFeedback | null; onClose: () => void; onDone: () => void; }) { const [message, setMessage] = useState(""); const [busy, setBusy] = useState(false); async function send() { if (!feedback || message.trim() === "") return; setBusy(true); try { const { data } = (await api.replyFeedback( feedback.id, message.trim(), )) as { data: { replied: boolean; emailed: boolean }; }; toast.success( data?.emailed ? "Reply saved and emailed to the submitter." : "Reply saved (no contact email on file to send to).", ); setMessage(""); onDone(); } catch (err) { toast.error(err instanceof AdminError ? err.message : "Could not reply."); } finally { setBusy(false); } } return (
{feedback ? (
{feedback.category}
{feedback.message}
) : null}
); }