diff --git a/app/api/submissions/route.ts b/app/api/submissions/route.ts index 5124528..5abbb27 100644 --- a/app/api/submissions/route.ts +++ b/app/api/submissions/route.ts @@ -37,39 +37,27 @@ type RegistrationBody = { type FeedbackBody = { type: "feedback"; - eventRating: string; - venueSatisfaction: string; - session: string; - sessionLabel?: string; - speakerRating: string; - improve2027: string; - comments: string; - ipv6Readiness: string; - challenge: string; - challengeLabel?: string; - introductions: string; - collaboration: string[]; - collaborationLabels?: string[]; - venueRating: string; + [key: string]: any; }; function buildFeedbackNotes(b: FeedbackBody): string { - const collab = - b.collaborationLabels?.length ? b.collaborationLabels.join(", ") : b.collaboration.join(", ") || "—"; const lines = [ "Source: Event feedback", - b.eventRating && `Overall event rating: ${b.eventRating}/5`, - b.venueSatisfaction && `Venue & logistics satisfaction: ${b.venueSatisfaction}/5`, - (b.sessionLabel || b.session) && `Most valuable session: ${b.sessionLabel || b.session || "—"}`, - b.speakerRating && `Keynote rating: ${b.speakerRating}/5`, - b.ipv6Readiness && `IPv6 readiness: ${b.ipv6Readiness}/5`, - (b.challengeLabel || b.challenge) && - `Infrastructure challenge: ${b.challengeLabel || b.challenge || "—"}`, - b.introductions && `Speaker / company introductions: ${b.introductions}`, - `Collaboration interests: ${collab}`, - b.venueRating && `Venue rating (HCMC): ${b.venueRating}/5`, - b.improve2027 && `Improve for 2027: ${b.improve2027}`, - b.comments && `Comments: ${b.comments}`, + b.overallExperience && `Overall experience: ${b.overallExperience}/5`, + b.venueFacilities && `Venue & Facilities: ${b.venueFacilities}/5`, + b.organisationProcess && `Organisation & Registration: ${b.organisationProcess}/5`, + b.mostValuableSession && `Most valuable session: ${b.mostValuableSession}`, + b.relevance && `Content relevance: ${b.relevance}`, + b.speakerRating && `Speakers/Panels rating: ${b.speakerRating}`, + b.networkingUtility && `Networking utility: ${b.networkingUtility}`, + b.futureTopics && `Future topics: ${Array.isArray(b.futureTopics) ? b.futureTopics.join(", ") : b.futureTopics}${b.futureTopics_other ? ` (Other: ${b.futureTopics_other})` : ""}`, + b.attendAgain && `Attend again in 2027: ${b.attendAgain}`, + b.recommend && `Recommend to others: ${b.recommend}`, + b.enjoyedMost && `Enjoyed most: ${b.enjoyedMost}`, + b.improvementAreas && `Improvement areas: ${b.improvementAreas}`, + b.additionalComments && `Additional comments: ${b.additionalComments}`, + b.industry && `Industry: ${Array.isArray(b.industry) ? b.industry.join(", ") : b.industry}${b.industry_other ? ` (Other: ${b.industry_other})` : ""}`, + b.role && `Role: ${Array.isArray(b.role) ? b.role.join(", ") : b.role}${b.role_other ? ` (Other: ${b.role_other})` : ""}`, ]; return lines.filter(Boolean).join("\n"); } @@ -137,11 +125,9 @@ export async function POST(req: Request) { if (t === "feedback") { const b = body as FeedbackBody; const sessionTitle = - typeof b.sessionLabel === "string" && b.sessionLabel - ? b.sessionLabel - : typeof b.session === "string" && b.session - ? b.session - : "General"; + typeof b.mostValuableSession === "string" && b.mostValuableSession + ? b.mostValuableSession + : "General"; const row: AdminRequestRow = { id: newId(), diff --git a/app/components/forms/FeedbackForm.tsx b/app/components/forms/FeedbackForm.tsx index c29e188..3269001 100644 --- a/app/components/forms/FeedbackForm.tsx +++ b/app/components/forms/FeedbackForm.tsx @@ -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 ( -
- {label} -
- {left} -
- {values.map((n) => ( - - ))} -
- {right} -
+
+ {values.map((n) => ( + + ))}
); } -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>({}); 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 ( -
{ - 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 */} -
-
-

- {data.sections.satisfaction.title} -

-
-
- - setForm((s) => ({ ...s, eventRating: v }))} - /> -
-
- - setForm((s) => ({ ...s, venueSatisfaction: v }))} - /> -
-
- - {/* 2 */} -
-
-

- {data.sections.content.title} -

-
-
- -
- - - expand_more - + + {data.sections.map((section) => ( +
+
+

+ {section.title} +

+

{section.subtitle}

-
-
- - setForm((s) => ({ ...s, speakerRating: v }))} - /> -
-
- {/* 3 */} -
-
-

- {data.sections.qualitative.title} -

-
-
- -