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:
co-authored by
Claude Opus 5.5
parent
0a23ab59f7
commit
52cd48b12e
+43
-14
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user