diff --git a/app/(app)/settings/feedbacks/page.tsx b/app/(app)/settings/feedbacks/page.tsx new file mode 100644 index 00000000..ecdd4a58 --- /dev/null +++ b/app/(app)/settings/feedbacks/page.tsx @@ -0,0 +1,216 @@ +"use client"; + +/** + * /settings/feedbacks — the signed-in user's own feedback history. + * + * Lists every submission they've made (newest first) with its current + * status — Submitted → Acknowledged → Resolved — and, when the team has + * written back, the response inline. Read-only: submitting new feedback + * still happens on /feedback. + * + * Anonymous visitors are prompted to sign in and bounced home if they + * dismiss, mirroring /settings/profile — the underlying endpoint is + * auth-only. + */ + +import { useCallback, useEffect, useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { CheckCircle2, Clock, Eye, MessageSquarePlus } from "lucide-react"; +import { feedback as feedbackAPI, SocialError, type Feedback } from "@/lib/social"; +import { useAuth } from "@/components/auth/auth-provider"; +import { useAuthGate } from "@/components/auth/auth-gate"; +import { SettingsShell } from "@/components/settings/settings-ui"; +import { cn } from "@/lib/utils"; + +const CATEGORY_LABEL: Record = { + bug: "Bug report", + feature: "Feature request", + general: "General feedback", + other: "Other", +}; + +type Status = "resolved" | "acknowledged" | "open"; + +function statusOf(f: Feedback): Status { + if (f.resolved_at) return "resolved"; + if (f.acknowledged_at) return "acknowledged"; + return "open"; +} + +const STATUS_META: Record< + Status, + { label: string; icon: React.ReactNode; className: string } +> = { + resolved: { + label: "Resolved", + icon: , + className: + "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", + }, + acknowledged: { + label: "Acknowledged", + icon: , + className: "bg-amber-500/10 text-amber-700 dark:text-amber-300", + }, + open: { + label: "Submitted", + icon: , + className: "bg-muted text-muted-foreground", + }, +}; + +function formatDate(iso: string): string { + try { + return new Date(iso).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); + } catch { + return ""; + } +} + +export default function FeedbacksPage() { + const { user, loading } = useAuth(); + const { requireAuth } = useAuthGate(); + const router = useRouter(); + + const [items, setItems] = useState(null); + const [error, setError] = useState(null); + + // Same auth gate as /settings/profile: prompt anonymous visitors and + // bounce them home if they dismiss. + useEffect(() => { + if (loading || user) return; + let cancelled = false; + (async () => { + const ok = await requireAuth(); + if (!ok && !cancelled) router.replace("/"); + })(); + return () => { + cancelled = true; + }; + }, [loading, user, requireAuth, router]); + + const load = useCallback(async () => { + setError(null); + try { + setItems(await feedbackAPI.mine()); + } catch (err) { + setError( + err instanceof SocialError + ? err.message + : "Could not load your feedback.", + ); + } + }, []); + + useEffect(() => { + if (loading || !user) return; + void load(); + }, [loading, user, load]); + + return ( + + {loading || !user ? ( +

+ {loading ? "Loading…" : "Sign in to see your feedback."} +

+ ) : error ? ( +
+

{error}

+ +
+ ) : items === null ? ( +

Loading…

+ ) : items.length === 0 ? ( + + ) : ( +
    + {items.map((f) => ( + + ))} +
+ )} +
+ ); +} + +function FeedbackCard({ feedback: f }: { feedback: Feedback }) { + const status = statusOf(f); + const meta = STATUS_META[status]; + const category = CATEGORY_LABEL[f.category] ?? f.category; + + return ( +
  • +
    + + {meta.icon} + {meta.label} + + + {category} + + + {formatDate(f.created_at)} + +
    + +

    + {f.message} +

    + + {f.reply_message ? ( +
    +

    + Response from the team + {f.replied_at ? ( + + {" · "} + {formatDate(f.replied_at)} + + ) : null} +

    +

    + {f.reply_message} +

    +
    + ) : null} +
  • + ); +} + +function EmptyState() { + return ( +
    + +

    + You haven’t sent any feedback yet. +

    + + Send feedback + +
    + ); +} diff --git a/app/(app)/settings/page.tsx b/app/(app)/settings/page.tsx index 511b9fe6..b7e7282d 100644 --- a/app/(app)/settings/page.tsx +++ b/app/(app)/settings/page.tsx @@ -21,6 +21,7 @@ import { BookOpenText, DownloadCloud, LogOut, + MessageSquareText, Scale, ShieldCheck, UserRound, @@ -66,6 +67,12 @@ export default function SettingsPage() { label="Account & security" description="Connected accounts, email, and password." /> + } + label="My feedback" + description="Your submissions, their status, and our responses." + /> } diff --git a/lib/social.ts b/lib/social.ts index 567d33c6..1a43a358 100644 --- a/lib/social.ts +++ b/lib/social.ts @@ -202,6 +202,13 @@ export type Feedback = { message: string; contact_email?: string; created_at: string; + /** Set when the team has seen the submission (one-way). */ + acknowledged_at?: string; + /** Set when the submission has been addressed (one-way). */ + resolved_at?: string; + /** The team's written reply, if any, and when it was sent. */ + reply_message?: string; + replied_at?: string; }; // ─── Posts ─────────────────────────────────────────────────────────────── @@ -559,6 +566,11 @@ export const feedback = { }), }); }, + /** The signed-in user's own submissions, newest first — each with its + * status (acknowledged / resolved) and any admin reply. Auth-only. */ + mine() { + return call("/feedback/mine"); + }, }; // ─── Public profile ──────────────────────────────────────────────────────