Feedback form: load categories from the BFF

The form offers the admin-managed categories, falling back to the original
four if they can't be loaded. "My feedback" shows the server's labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gosangam
2026-10-18 13:47:09 +05:30
co-authored by Claude Opus 5.5
parent 0a23ab59f7
commit 52cd48b12e
3 changed files with 61 additions and 16 deletions
+43 -14
View File
@@ -9,23 +9,31 @@
* Authed visitors don't see the contact-email field — the BFF reads * Authed visitors don't see the contact-email field — the BFF reads
* their account email from the session cookie. Anonymous visitors get * their account email from the session cookie. Anonymous visitors get
* the field and must fill it in so we have a reply path. * 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 { useState } from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ArrowLeft, MessageSquare, Send } from "lucide-react"; import { ArrowLeft, MessageSquare, Send } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { feedback as feedbackAPI, SocialError } from "@/lib/social"; import {
feedback as feedbackAPI,
SocialError,
type FeedbackCategory,
} from "@/lib/social";
import { useAuth } from "@/components/auth/auth-provider"; import { useAuth } from "@/components/auth/auth-provider";
type Category = "bug" | "feature" | "general" | "other"; // 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 CATEGORIES: { value: Category; label: string; description: string }[] = [ const FALLBACK_CATEGORIES: FeedbackCategory[] = [
{ value: "general", label: "General feedback", description: "Thoughts, impressions, suggestions — anything else." }, { key: "general", label: "General feedback", description: "Thoughts, impressions, suggestions — anything else." },
{ value: "bug", label: "Bug report", description: "Something is broken or behaving unexpectedly." }, { key: "bug", label: "Bug report", description: "Something is broken or behaving unexpectedly." },
{ value: "feature", label: "Feature request", description: "An idea for something new on the site." }, { key: "feature", label: "Feature request", description: "An idea for something new on the site." },
{ value: "other", label: "Other", description: "Doesn't fit the above? Use this." }, { key: "other", label: "Other", description: "Doesn't fit the above? Use this." },
]; ];
export default function FeedbackPage() { export default function FeedbackPage() {
@@ -33,7 +41,28 @@ export default function FeedbackPage() {
const { user, loading } = useAuth(); const { user, loading } = useAuth();
const authed = !loading && !!user; const authed = !loading && !!user;
const [category, setCategory] = useState<Category>("general"); const [categories, setCategories] =
useState<FeedbackCategory[]>(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 [message, setMessage] = useState("");
const [contactEmail, setContactEmail] = useState(""); const [contactEmail, setContactEmail] = useState("");
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -72,7 +101,7 @@ export default function FeedbackPage() {
} }
} }
const selected = CATEGORIES.find((c) => c.value === category); const selected = categories.find((c) => c.key === category);
return ( return (
<main className="flex-1 max-w-xl w-full mx-auto px-4 sm:px-6 py-10 sm:py-14"> <main className="flex-1 max-w-xl w-full mx-auto px-4 sm:px-6 py-10 sm:py-14">
@@ -111,12 +140,12 @@ export default function FeedbackPage() {
<select <select
id="category" id="category"
value={category} value={category}
onChange={(e) => setCategory(e.target.value as Category)} onChange={(e) => setPicked(e.target.value)}
className="mt-1.5 h-10 w-full rounded-lg border border-border bg-card px-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring" className="mt-1.5 h-10 w-full rounded-lg border border-border bg-card px-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
disabled={submitting} disabled={submitting}
> >
{CATEGORIES.map((c) => ( {categories.map((c) => (
<option key={c.value} value={c.value}>{c.label}</option> <option key={c.key} value={c.key}>{c.label}</option>
))} ))}
</select> </select>
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-xs text-muted-foreground">
+3 -1
View File
@@ -152,7 +152,9 @@ export default function FeedbacksPage() {
function FeedbackCard({ feedback: f }: { feedback: Feedback }) { function FeedbackCard({ feedback: f }: { feedback: Feedback }) {
const status = statusOf(f); const status = statusOf(f);
const meta = STATUS_META[status]; const meta = STATUS_META[status];
const category = CATEGORY_LABEL[f.category] ?? f.category; // The server's label tracks renames in the admin; the map covers an older
// BFF that doesn't send one yet.
const category = f.category_label || CATEGORY_LABEL[f.category] || f.category;
return ( return (
<li className="rounded-xl border border-border bg-card p-4 sm:p-5"> <li className="rounded-xl border border-border bg-card p-4 sm:p-5">
+15 -1
View File
@@ -212,6 +212,8 @@ export type Feedback = {
id: string; id: string;
user_id?: string; user_id?: string;
category: string; category: string;
/** The category's current display name (renames apply to past feedback). */
category_label?: string;
message: string; message: string;
contact_email?: string; contact_email?: string;
created_at: string; created_at: string;
@@ -560,13 +562,25 @@ export const collections = {
// ─── Feedback ──────────────────────────────────────────────────────────── // ─── Feedback ────────────────────────────────────────────────────────────
/** One option on the /feedback form, managed from the admin panel. */
export type FeedbackCategory = {
key: string;
label: string;
description: string;
};
export const feedback = { export const feedback = {
/** The form's categories, in order. Public. */
categories() {
return call<FeedbackCategory[]>("/feedback/categories");
},
/** Submit a /feedback page entry. When the viewer is signed in the /** Submit a /feedback page entry. When the viewer is signed in the
* BFF captures their user_id automatically via the auth cookie — * BFF captures their user_id automatically via the auth cookie —
* the form doesn't ask for it. `contact_email` is required only * the form doesn't ask for it. `contact_email` is required only
* for anonymous submissions; the BFF re-enforces that on its end. */ * for anonymous submissions; the BFF re-enforces that on its end. */
submit(input: { submit(input: {
category: "bug" | "feature" | "general" | "other"; /** A FeedbackCategory key. */
category: string;
message: string; message: string;
contact_email?: string; contact_email?: string;
}) { }) {