Files

276 lines
10 KiB
TypeScript

"use client";
import SubmitToast from "@/app/components/ui/SubmitToast";
import { useMemo, useState } from "react";
type QuestionOption = { value: string; label: string };
type Question = {
id: string;
type: "rating" | "dropdown" | "scale" | "rating_labeled" | "checkbox_group" | "long_text";
label: string;
min?: number;
max?: number;
placeholder?: string;
options?: QuestionOption[];
hasOther?: boolean;
};
type Section = {
id: string;
title: string;
subtitle: string;
questions: Question[];
};
type FeedbackJson = {
title: string;
subtitle: string;
sections: Section[];
submit: { label: string; disclaimer: string };
};
function RadioScale({
name,
label,
min = 1,
max = 5,
value,
onChange,
}: {
name: string;
label: string;
min?: number;
max?: number;
value: string;
onChange: (v: string) => void;
}) {
const values = useMemo(() => {
const out: number[] = [];
for (let i = min; i <= max; i++) out.push(i);
return out;
}, [min, max]);
return (
<div className="flex gap-2 sm:gap-4 flex-wrap">
{values.map((n) => (
<label key={n} className="cursor-pointer group">
<input
className="peer sr-only"
name={name}
type="radio"
value={String(n)}
checked={value === String(n)}
onChange={() => onChange(String(n))}
/>
<div className="w-11 h-11 sm:w-12 sm:h-12 flex items-center justify-center border border-white/10 rounded-lg peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">
{n}
</div>
</label>
))}
</div>
);
}
export default function FeedbackForm({ data }: { data: FeedbackJson }) {
const [form, setForm] = useState<Record<string, any>>({});
const [submitState, setSubmitState] = useState<"idle" | "sending" | "ok" | "error">("idle");
const updateField = (id: string, value: any) => {
setForm((s) => ({ ...s, [id]: value }));
};
const toggleCheckbox = (id: string, value: string) => {
setForm((s) => {
const current = s[id] || [];
return {
...s,
[id]: current.includes(value) ? current.filter((v: string) => v !== value) : [...current, value],
};
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitState("sending");
try {
const res = await fetch("/ipv6/api/submissions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "feedback",
...form,
}),
});
if (!res.ok) throw new Error("Submission failed");
setSubmitState("ok");
} catch {
setSubmitState("error");
}
};
return (
<form className="space-y-12 md:space-y-16" onSubmit={handleSubmit}>
{data.sections.map((section) => (
<section key={section.id} className="space-y-8">
<div className="border-l-4 border-primary pl-5">
<h2 className="font-[var(--font-display-lg)] text-2xl md:text-3xl text-on-surface tracking-tight leading-none mb-2">
{section.title}
</h2>
<p className="text-primary/80 font-medium">{section.subtitle}</p>
</div>
<div className="space-y-10">
{section.questions.map((q) => (
<div key={q.id} className="space-y-4">
<div className="space-y-1">
<label className="block text-lg font-semibold text-on-surface">{q.label}</label>
</div>
{q.type === "rating" && (
<RadioScale
name={q.id}
label={q.label}
min={q.min}
max={q.max}
value={form[q.id] || ""}
onChange={(v) => updateField(q.id, v)}
/>
)}
{q.type === "dropdown" && (
<div className="relative max-w-md">
<select
className="w-full bg-surface-container-low border border-white/10 rounded-xl px-5 py-4 text-on-surface appearance-none focus:outline-none focus:border-primary/50 transition-colors"
value={form[q.id] || ""}
onChange={(e) => updateField(q.id, e.target.value)}
>
<option value="">{q.placeholder}</option>
{q.options?.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<span className="material-symbols-outlined absolute right-4 top-1/2 -translate-y-1/2 text-outline pointer-events-none">
expand_more
</span>
</div>
)}
{q.type === "scale" && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
{q.options?.map((opt) => (
<label
key={opt.value}
className="flex items-center gap-4 cursor-pointer group rounded-xl border border-white/10 bg-surface-container-low/30 px-5 py-4 hover:border-primary/40 hover:bg-surface-container-low/50 transition-all"
>
<input
className="size-5 accent-primary cursor-pointer"
name={q.id}
type="radio"
value={opt.value}
checked={form[q.id] === opt.value}
onChange={() => updateField(q.id, opt.value)}
/>
<span className="text-on-surface font-medium">{opt.label}</span>
</label>
))}
</div>
)}
{q.type === "rating_labeled" && (
<div className="space-y-3 max-w-md">
{q.options?.map((opt) => (
<label
key={opt.value}
className="flex items-center gap-4 cursor-pointer group rounded-xl border border-white/10 bg-surface-container-low/30 px-5 py-4 hover:border-primary/40 hover:bg-surface-container-low/50 transition-all"
>
<input
className="size-5 accent-primary cursor-pointer"
name={q.id}
type="radio"
value={opt.value}
checked={form[q.id] === opt.value}
onChange={() => updateField(q.id, opt.value)}
/>
<span className="text-on-surface font-medium">{opt.label}</span>
</label>
))}
</div>
)}
{q.type === "checkbox_group" && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{q.options?.map((opt) => (
<label
key={opt.value}
className="flex items-start gap-4 cursor-pointer group rounded-xl border border-white/10 bg-surface-container-low/30 px-5 py-4 hover:border-primary/40 hover:bg-surface-container-low/50 transition-all"
>
<input
type="checkbox"
className="mt-1 size-5 accent-primary cursor-pointer"
checked={(form[q.id] || []).includes(opt.value)}
onChange={() => toggleCheckbox(q.id, opt.value)}
/>
<span className="text-on-surface font-medium">{opt.label}</span>
</label>
))}
</div>
{q.hasOther && (
<div className="flex items-center gap-4 px-5 py-2">
<span className="text-on-surface-variant font-medium whitespace-nowrap">Others:</span>
<input
className="flex-1 bg-transparent border-b border-white/10 py-2 text-on-surface focus:outline-none focus:border-primary transition-colors"
placeholder="Please specify..."
type="text"
value={form[`${q.id}_other`] || ""}
onChange={(e) => updateField(`${q.id}_other`, e.target.value)}
/>
</div>
)}
</div>
)}
{q.type === "long_text" && (
<textarea
className="w-full bg-surface-container-low border border-white/10 rounded-xl px-5 py-4 text-on-surface focus:outline-none focus:border-primary/50 transition-colors min-h-[120px] resize-none"
placeholder="Your message / suggestions..."
rows={4}
value={form[q.id] || ""}
onChange={(e) => updateField(q.id, e.target.value)}
/>
)}
</div>
))}
</div>
</section>
))}
<div className="pt-10 border-t border-white/5 flex flex-col items-center gap-6">
<button
className="gold-gradient-bg text-on-primary w-full md:w-auto px-16 py-5 rounded-xl font-[var(--font-display-lg)] text-xl uppercase tracking-wider hover:shadow-[0_0_30px_rgba(197,160,89,0.4)] transition-all duration-300 active:scale-[0.98] disabled:opacity-60 disabled:pointer-events-none"
type="submit"
disabled={submitState === "sending"}
>
{submitState === "sending" ? "Submitting…" : data.submit.label}
</button>
<SubmitToast
state={submitState}
successTitle="Feedback Submitted"
successMessage={data.submit.disclaimer}
onDismiss={() => {
setSubmitState("idle");
setForm({});
}}
/>
</div>
</form>
);
}