"use client"; /** * /feedback — single-page form for bug reports, feature requests, and * general comments. Submissions persist to the BFF and trigger an * admin notification email. No follow-up UI beyond a success toast * and a redirect back to /. * * Authed visitors don't see the contact-email field — the BFF reads * their account email from the session cookie. Anonymous visitors get * the field and must fill it in so we have a reply path. * * Categories are managed in the admin panel and loaded from the BFF. If * that request fails the form falls back to the original four, so it can * always be submitted. */ import { useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft, MessageSquare, Send } from "lucide-react"; import { toast } from "sonner"; import { feedback as feedbackAPI, SocialError, type FeedbackCategory, } from "@/lib/social"; import { useAuth } from "@/components/auth/auth-provider"; // Used only if the categories can't be loaded — the original four, which // the BFF seeds and keeps accepting unless an admin archives them. const FALLBACK_CATEGORIES: FeedbackCategory[] = [ { key: "general", label: "General feedback", description: "Thoughts, impressions, suggestions — anything else." }, { key: "bug", label: "Bug report", description: "Something is broken or behaving unexpectedly." }, { key: "feature", label: "Feature request", description: "An idea for something new on the site." }, { key: "other", label: "Other", description: "Doesn't fit the above? Use this." }, ]; export default function FeedbackPage() { const router = useRouter(); const { user, loading } = useAuth(); const authed = !loading && !!user; const [categories, setCategories] = useState(FALLBACK_CATEGORIES); // Empty until chosen: the first category on the form. const [picked, setPicked] = useState(""); const category = categories.some((c) => c.key === picked) ? picked : (categories[0]?.key ?? ""); useEffect(() => { let cancelled = false; feedbackAPI .categories() .then((list) => { if (!cancelled && list?.length) setCategories(list); }) .catch(() => { /* keep the fallback list */ }); return () => { cancelled = true; }; }, []); const [message, setMessage] = useState(""); const [contactEmail, setContactEmail] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); async function submit(e: React.FormEvent) { e.preventDefault(); setError(null); const trimmed = message.trim(); if (!trimmed) { setError("Message is required."); return; } if (!authed && !contactEmail.trim()) { setError("Email is required so we can reply."); return; } setSubmitting(true); try { await feedbackAPI.submit({ category, message: trimmed, contact_email: authed ? undefined : contactEmail.trim(), }); toast.success("Thanks for the feedback!", { description: "We've forwarded it to the team.", duration: 5000, }); setMessage(""); setContactEmail(""); router.push("/"); } catch (err) { setError(err instanceof SocialError ? err.message : "Could not send feedback."); } finally { setSubmitting(false); } } const selected = categories.find((c) => c.key === category); return (
Back to home

Send feedback

Tell us what's working, what's broken, or what you'd like to see next. We read every message.

{selected?.description}