forked from UKSOURCE/ipv6
Change content of Feedback form, delete title icon.
This commit is contained in:
@@ -3,62 +3,46 @@
|
||||
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;
|
||||
labelVi: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
placeholder?: string;
|
||||
options?: QuestionOption[];
|
||||
hasOther?: boolean;
|
||||
};
|
||||
|
||||
type Section = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
questions: Question[];
|
||||
};
|
||||
|
||||
type FeedbackJson = {
|
||||
sections: {
|
||||
satisfaction: {
|
||||
title: string;
|
||||
eventRating: { label: string; left: string; right: string; min: number; max: number };
|
||||
venueSatisfaction: { label: string; left: string; right: string; min: number; max: number };
|
||||
};
|
||||
content: {
|
||||
title: string;
|
||||
session: { label: string; placeholder: string; options: { value: string; label: string }[] };
|
||||
speakerRating: { label: string; left: string; right: string; min: number; max: number };
|
||||
};
|
||||
qualitative: {
|
||||
title: string;
|
||||
improve2027: { label: string; placeholder: string };
|
||||
comments: { label: string; placeholder: string };
|
||||
};
|
||||
industry: {
|
||||
title: string;
|
||||
ipv6Readiness: { label: string; hint: string; left: string; right: string; min: number; max: number };
|
||||
challenge: {
|
||||
label: string;
|
||||
hint: string;
|
||||
placeholder: string;
|
||||
options: { value: string; label: string }[];
|
||||
};
|
||||
};
|
||||
networking: {
|
||||
title: string;
|
||||
introductions: { label: string; hint: string; placeholder: string };
|
||||
collaboration: { label: string; hint: string; options: { value: string; label: string }[] };
|
||||
};
|
||||
logistics: {
|
||||
title: string;
|
||||
venueRating: { label: string; hint: string; left: string; right: string; min: number; max: number };
|
||||
};
|
||||
};
|
||||
title: string;
|
||||
subtitle: string;
|
||||
sections: Section[];
|
||||
submit: { label: string; disclaimer: string };
|
||||
};
|
||||
|
||||
function RadioScale({
|
||||
name,
|
||||
label,
|
||||
left,
|
||||
right,
|
||||
min,
|
||||
max,
|
||||
min = 1,
|
||||
max = 5,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
left: string;
|
||||
right: string;
|
||||
min: number;
|
||||
max: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
@@ -69,339 +53,207 @@ function RadioScale({
|
||||
}, [min, max]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<span className="sr-only">{label}</span>
|
||||
<div className="flex justify-between items-center gap-2 max-w-md flex-wrap">
|
||||
<span className="text-sm opacity-50">{left}</span>
|
||||
<div className="flex gap-2 sm:gap-4 flex-wrap justify-center">
|
||||
{values.map((n) => (
|
||||
<label key={n} className="cursor-pointer group">
|
||||
<span className="sr-only">
|
||||
{label} {n}
|
||||
</span>
|
||||
<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 peer-checked:bg-primary peer-checked:text-on-primary hover:border-primary transition-all">
|
||||
{n}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm opacity-50">{right}</span>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyFeedback = {
|
||||
eventRating: "",
|
||||
venueSatisfaction: "",
|
||||
session: "",
|
||||
speakerRating: "",
|
||||
improve2027: "",
|
||||
comments: "",
|
||||
ipv6Readiness: "",
|
||||
challenge: "",
|
||||
introductions: "",
|
||||
collaboration: [] as string[],
|
||||
venueRating: "",
|
||||
};
|
||||
|
||||
export default function FeedbackForm({ data }: { data: FeedbackJson }) {
|
||||
const [form, setForm] = useState({ ...emptyFeedback, collaboration: [] as string[] });
|
||||
const [form, setForm] = useState<Record<string, any>>({});
|
||||
const [submitState, setSubmitState] = useState<"idle" | "sending" | "ok" | "error">("idle");
|
||||
|
||||
const toggleCollaboration = (value: string) => {
|
||||
setForm((s) => ({
|
||||
...s,
|
||||
collaboration: s.collaboration.includes(value)
|
||||
? s.collaboration.filter((x) => x !== value)
|
||||
: [...s.collaboration, value],
|
||||
}));
|
||||
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("/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-10 md:space-y-14"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setSubmitState("sending");
|
||||
const sessionLabel =
|
||||
data.sections.content.session.options.find((o) => o.value === form.session)?.label ?? "";
|
||||
const challengeLabel =
|
||||
data.sections.industry.challenge.options.find((o) => o.value === form.challenge)?.label ?? "";
|
||||
const collaborationLabels = form.collaboration
|
||||
.map(
|
||||
(v) =>
|
||||
data.sections.networking.collaboration.options.find((o) => o.value === v)?.label ?? v,
|
||||
)
|
||||
.filter(Boolean);
|
||||
try {
|
||||
const res = await fetch("/api/submissions", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
type: "feedback",
|
||||
...form,
|
||||
sessionLabel,
|
||||
challengeLabel,
|
||||
collaborationLabels,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(typeof err?.error === "string" ? err.error : res.statusText);
|
||||
}
|
||||
setSubmitState("ok");
|
||||
} catch {
|
||||
setSubmitState("error");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 1 */}
|
||||
<section className="space-y-6">
|
||||
<div className="border-l-2 border-primary pl-4">
|
||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
||||
{data.sections.satisfaction.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.satisfaction.eventRating.label}
|
||||
</label>
|
||||
<RadioScale
|
||||
name="event-rating"
|
||||
label={data.sections.satisfaction.eventRating.label}
|
||||
left={data.sections.satisfaction.eventRating.left}
|
||||
right={data.sections.satisfaction.eventRating.right}
|
||||
min={data.sections.satisfaction.eventRating.min}
|
||||
max={data.sections.satisfaction.eventRating.max}
|
||||
value={form.eventRating}
|
||||
onChange={(v) => setForm((s) => ({ ...s, eventRating: v }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.satisfaction.venueSatisfaction.label}
|
||||
</label>
|
||||
<RadioScale
|
||||
name="venue-satisfaction"
|
||||
label={data.sections.satisfaction.venueSatisfaction.label}
|
||||
left={data.sections.satisfaction.venueSatisfaction.left}
|
||||
right={data.sections.satisfaction.venueSatisfaction.right}
|
||||
min={data.sections.satisfaction.venueSatisfaction.min}
|
||||
max={data.sections.satisfaction.venueSatisfaction.max}
|
||||
value={form.venueSatisfaction}
|
||||
onChange={(v) => setForm((s) => ({ ...s, venueSatisfaction: v }))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2 */}
|
||||
<section className="space-y-6">
|
||||
<div className="border-l-2 border-primary pl-4">
|
||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
||||
{data.sections.content.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.content.session.label}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface appearance-none"
|
||||
value={form.session}
|
||||
onChange={(e) => setForm((s) => ({ ...s, session: e.target.value }))}
|
||||
>
|
||||
<option value="">{data.sections.content.session.placeholder}</option>
|
||||
{data.sections.content.session.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>
|
||||
<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>
|
||||
<div className="space-y-4">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.content.speakerRating.label}
|
||||
</label>
|
||||
<RadioScale
|
||||
name="speaker-rating"
|
||||
label={data.sections.content.speakerRating.label}
|
||||
left={data.sections.content.speakerRating.left}
|
||||
right={data.sections.content.speakerRating.right}
|
||||
min={data.sections.content.speakerRating.min}
|
||||
max={data.sections.content.speakerRating.max}
|
||||
value={form.speakerRating}
|
||||
onChange={(v) => setForm((s) => ({ ...s, speakerRating: v }))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3 */}
|
||||
<section className="space-y-6">
|
||||
<div className="border-l-2 border-primary pl-4">
|
||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
||||
{data.sections.qualitative.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.qualitative.improve2027.label}
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface resize-none min-h-[100px]"
|
||||
placeholder={data.sections.qualitative.improve2027.placeholder}
|
||||
rows={4}
|
||||
value={form.improve2027}
|
||||
onChange={(e) => setForm((s) => ({ ...s, improve2027: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.qualitative.comments.label}
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface resize-none min-h-[88px]"
|
||||
placeholder={data.sections.qualitative.comments.placeholder}
|
||||
rows={3}
|
||||
value={form.comments}
|
||||
onChange={(e) => setForm((s) => ({ ...s, comments: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<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>
|
||||
<p className="text-on-surface-variant/70 italic text-sm">{q.labelVi}</p>
|
||||
</div>
|
||||
|
||||
{/* 4 */}
|
||||
<section className="space-y-6">
|
||||
<div className="border-l-2 border-primary pl-4">
|
||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
||||
{data.sections.industry.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.industry.ipv6Readiness.label}
|
||||
</label>
|
||||
<p className="text-on-surface-variant text-sm">{data.sections.industry.ipv6Readiness.hint}</p>
|
||||
<RadioScale
|
||||
name="ipv6-readiness"
|
||||
label={data.sections.industry.ipv6Readiness.label}
|
||||
left={data.sections.industry.ipv6Readiness.left}
|
||||
right={data.sections.industry.ipv6Readiness.right}
|
||||
min={data.sections.industry.ipv6Readiness.min}
|
||||
max={data.sections.industry.ipv6Readiness.max}
|
||||
value={form.ipv6Readiness}
|
||||
onChange={(v) => setForm((s) => ({ ...s, ipv6Readiness: v }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.industry.challenge.label}
|
||||
</label>
|
||||
<p className="text-on-surface-variant text-sm">{data.sections.industry.challenge.hint}</p>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface appearance-none"
|
||||
value={form.challenge}
|
||||
onChange={(e) => setForm((s) => ({ ...s, challenge: e.target.value }))}
|
||||
>
|
||||
<option value="">{data.sections.industry.challenge.placeholder}</option>
|
||||
{data.sections.industry.challenge.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>
|
||||
</div>
|
||||
</section>
|
||||
{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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 5 */}
|
||||
<section className="space-y-6">
|
||||
<div className="border-l-2 border-primary pl-4">
|
||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
||||
{data.sections.networking.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.networking.introductions.label}
|
||||
</label>
|
||||
<p className="text-on-surface-variant text-sm">{data.sections.networking.introductions.hint}</p>
|
||||
<input
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface"
|
||||
placeholder={data.sections.networking.introductions.placeholder}
|
||||
type="text"
|
||||
value={form.introductions}
|
||||
onChange={(e) => setForm((s) => ({ ...s, introductions: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.networking.collaboration.label}
|
||||
</label>
|
||||
<p className="text-on-surface-variant text-sm">{data.sections.networking.collaboration.hint}</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{data.sections.networking.collaboration.options.map((opt) => (
|
||||
<label
|
||||
key={opt.value}
|
||||
className="flex items-start gap-3 cursor-pointer group rounded-lg border border-white/10 bg-surface-container-low/50 px-4 py-3 hover:border-primary/40 transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 size-4 accent-primary"
|
||||
checked={form.collaboration.includes(opt.value)}
|
||||
onChange={() => toggleCollaboration(opt.value)}
|
||||
/>
|
||||
<span className="text-on-surface text-sm">{opt.label}</span>
|
||||
</label>
|
||||
{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 / Ý kiến của bạn..."
|
||||
rows={4}
|
||||
value={form[q.id] || ""}
|
||||
onChange={(e) => updateField(q.id, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
))}
|
||||
|
||||
{/* 6 */}
|
||||
<section className="space-y-6">
|
||||
<div className="border-l-2 border-primary pl-4">
|
||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
||||
{data.sections.logistics.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
||||
{data.sections.logistics.venueRating.label}
|
||||
</label>
|
||||
<p className="text-on-surface-variant text-sm">{data.sections.logistics.venueRating.hint}</p>
|
||||
<RadioScale
|
||||
name="venue-rating"
|
||||
label={data.sections.logistics.venueRating.label}
|
||||
left={data.sections.logistics.venueRating.left}
|
||||
right={data.sections.logistics.venueRating.right}
|
||||
min={data.sections.logistics.venueRating.min}
|
||||
max={data.sections.logistics.venueRating.max}
|
||||
value={form.venueRating}
|
||||
onChange={(v) => setForm((s) => ({ ...s, venueRating: v }))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="pt-6 border-t border-white/5 flex flex-col items-center gap-4">
|
||||
<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-12 py-4 rounded-lg font-[var(--font-display-lg)] text-lg uppercase tracking-wider hover:shadow-[0_0_20px_rgba(197,160,89,0.35)] transition-all duration-300 active:scale-[0.98] disabled:opacity-60 disabled:pointer-events-none"
|
||||
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"}
|
||||
>
|
||||
@@ -411,14 +263,14 @@ export default function FeedbackForm({ data }: { data: FeedbackJson }) {
|
||||
<SubmitToast
|
||||
state={submitState}
|
||||
successTitle="Feedback submitted"
|
||||
successMessage="Thank you. Your responses were saved and will appear in the admin console."
|
||||
successMessage="Thank you for your valuable feedback! It has been successfully recorded."
|
||||
onDismiss={() => {
|
||||
setSubmitState("idle");
|
||||
setForm({ ...emptyFeedback, collaboration: [] });
|
||||
setForm({});
|
||||
}}
|
||||
/>
|
||||
|
||||
<p className="text-on-surface-variant font-[var(--font-label-caps)] text-[10px] opacity-60 uppercase text-center tracking-widest">
|
||||
<p className="text-on-surface-variant font-medium text-sm text-center max-w-xl leading-relaxed opacity-80 uppercase tracking-wide">
|
||||
{data.submit.disclaimer}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user