forked from UKSOURCE/ipv6
Change content of Feedback form, delete title icon.
This commit is contained in:
@@ -37,39 +37,27 @@ type RegistrationBody = {
|
|||||||
|
|
||||||
type FeedbackBody = {
|
type FeedbackBody = {
|
||||||
type: "feedback";
|
type: "feedback";
|
||||||
eventRating: string;
|
[key: string]: any;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildFeedbackNotes(b: FeedbackBody): string {
|
function buildFeedbackNotes(b: FeedbackBody): string {
|
||||||
const collab =
|
|
||||||
b.collaborationLabels?.length ? b.collaborationLabels.join(", ") : b.collaboration.join(", ") || "—";
|
|
||||||
const lines = [
|
const lines = [
|
||||||
"Source: Event feedback",
|
"Source: Event feedback",
|
||||||
b.eventRating && `Overall event rating: ${b.eventRating}/5`,
|
b.overallExperience && `Overall experience: ${b.overallExperience}/5`,
|
||||||
b.venueSatisfaction && `Venue & logistics satisfaction: ${b.venueSatisfaction}/5`,
|
b.venueFacilities && `Venue & Facilities: ${b.venueFacilities}/5`,
|
||||||
(b.sessionLabel || b.session) && `Most valuable session: ${b.sessionLabel || b.session || "—"}`,
|
b.organisationProcess && `Organisation & Registration: ${b.organisationProcess}/5`,
|
||||||
b.speakerRating && `Keynote rating: ${b.speakerRating}/5`,
|
b.mostValuableSession && `Most valuable session: ${b.mostValuableSession}`,
|
||||||
b.ipv6Readiness && `IPv6 readiness: ${b.ipv6Readiness}/5`,
|
b.relevance && `Content relevance: ${b.relevance}`,
|
||||||
(b.challengeLabel || b.challenge) &&
|
b.speakerRating && `Speakers/Panels rating: ${b.speakerRating}`,
|
||||||
`Infrastructure challenge: ${b.challengeLabel || b.challenge || "—"}`,
|
b.networkingUtility && `Networking utility: ${b.networkingUtility}`,
|
||||||
b.introductions && `Speaker / company introductions: ${b.introductions}`,
|
b.futureTopics && `Future topics: ${Array.isArray(b.futureTopics) ? b.futureTopics.join(", ") : b.futureTopics}${b.futureTopics_other ? ` (Other: ${b.futureTopics_other})` : ""}`,
|
||||||
`Collaboration interests: ${collab}`,
|
b.attendAgain && `Attend again in 2027: ${b.attendAgain}`,
|
||||||
b.venueRating && `Venue rating (HCMC): ${b.venueRating}/5`,
|
b.recommend && `Recommend to others: ${b.recommend}`,
|
||||||
b.improve2027 && `Improve for 2027: ${b.improve2027}`,
|
b.enjoyedMost && `Enjoyed most: ${b.enjoyedMost}`,
|
||||||
b.comments && `Comments: ${b.comments}`,
|
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");
|
return lines.filter(Boolean).join("\n");
|
||||||
}
|
}
|
||||||
@@ -137,11 +125,9 @@ export async function POST(req: Request) {
|
|||||||
if (t === "feedback") {
|
if (t === "feedback") {
|
||||||
const b = body as FeedbackBody;
|
const b = body as FeedbackBody;
|
||||||
const sessionTitle =
|
const sessionTitle =
|
||||||
typeof b.sessionLabel === "string" && b.sessionLabel
|
typeof b.mostValuableSession === "string" && b.mostValuableSession
|
||||||
? b.sessionLabel
|
? b.mostValuableSession
|
||||||
: typeof b.session === "string" && b.session
|
: "General";
|
||||||
? b.session
|
|
||||||
: "General";
|
|
||||||
|
|
||||||
const row: AdminRequestRow = {
|
const row: AdminRequestRow = {
|
||||||
id: newId(),
|
id: newId(),
|
||||||
|
|||||||
@@ -3,62 +3,46 @@
|
|||||||
import SubmitToast from "@/app/components/ui/SubmitToast";
|
import SubmitToast from "@/app/components/ui/SubmitToast";
|
||||||
import { useMemo, useState } from "react";
|
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 = {
|
type FeedbackJson = {
|
||||||
sections: {
|
title: string;
|
||||||
satisfaction: {
|
subtitle: string;
|
||||||
title: string;
|
sections: Section[];
|
||||||
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 };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
submit: { label: string; disclaimer: string };
|
submit: { label: string; disclaimer: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
function RadioScale({
|
function RadioScale({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
left,
|
min = 1,
|
||||||
right,
|
max = 5,
|
||||||
min,
|
|
||||||
max,
|
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
left: string;
|
min?: number;
|
||||||
right: string;
|
max?: number;
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -69,339 +53,207 @@ function RadioScale({
|
|||||||
}, [min, max]);
|
}, [min, max]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="flex gap-2 sm:gap-4 flex-wrap">
|
||||||
<span className="sr-only">{label}</span>
|
{values.map((n) => (
|
||||||
<div className="flex justify-between items-center gap-2 max-w-md flex-wrap">
|
<label key={n} className="cursor-pointer group">
|
||||||
<span className="text-sm opacity-50">{left}</span>
|
<input
|
||||||
<div className="flex gap-2 sm:gap-4 flex-wrap justify-center">
|
className="peer sr-only"
|
||||||
{values.map((n) => (
|
name={name}
|
||||||
<label key={n} className="cursor-pointer group">
|
type="radio"
|
||||||
<span className="sr-only">
|
value={String(n)}
|
||||||
{label} {n}
|
checked={value === String(n)}
|
||||||
</span>
|
onChange={() => onChange(String(n))}
|
||||||
<input
|
/>
|
||||||
className="peer sr-only"
|
<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">
|
||||||
name={name}
|
{n}
|
||||||
type="radio"
|
</div>
|
||||||
value={String(n)}
|
</label>
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyFeedback = {
|
|
||||||
eventRating: "",
|
|
||||||
venueSatisfaction: "",
|
|
||||||
session: "",
|
|
||||||
speakerRating: "",
|
|
||||||
improve2027: "",
|
|
||||||
comments: "",
|
|
||||||
ipv6Readiness: "",
|
|
||||||
challenge: "",
|
|
||||||
introductions: "",
|
|
||||||
collaboration: [] as string[],
|
|
||||||
venueRating: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function FeedbackForm({ data }: { data: FeedbackJson }) {
|
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 [submitState, setSubmitState] = useState<"idle" | "sending" | "ok" | "error">("idle");
|
||||||
|
|
||||||
const toggleCollaboration = (value: string) => {
|
const updateField = (id: string, value: any) => {
|
||||||
setForm((s) => ({
|
setForm((s) => ({ ...s, [id]: value }));
|
||||||
...s,
|
};
|
||||||
collaboration: s.collaboration.includes(value)
|
|
||||||
? s.collaboration.filter((x) => x !== value)
|
const toggleCheckbox = (id: string, value: string) => {
|
||||||
: [...s.collaboration, value],
|
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 (
|
return (
|
||||||
<form
|
<form className="space-y-12 md:space-y-16" onSubmit={handleSubmit}>
|
||||||
className="space-y-10 md:space-y-14"
|
{data.sections.map((section) => (
|
||||||
onSubmit={async (e) => {
|
<section key={section.id} className="space-y-8">
|
||||||
e.preventDefault();
|
<div className="border-l-4 border-primary pl-5">
|
||||||
setSubmitState("sending");
|
<h2 className="font-[var(--font-display-lg)] text-2xl md:text-3xl text-on-surface tracking-tight leading-none mb-2">
|
||||||
const sessionLabel =
|
{section.title}
|
||||||
data.sections.content.session.options.find((o) => o.value === form.session)?.label ?? "";
|
</h2>
|
||||||
const challengeLabel =
|
<p className="text-primary/80 font-medium">{section.subtitle}</p>
|
||||||
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>
|
|
||||||
</div>
|
</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 */}
|
<div className="space-y-10">
|
||||||
<section className="space-y-6">
|
{section.questions.map((q) => (
|
||||||
<div className="border-l-2 border-primary pl-4">
|
<div key={q.id} className="space-y-4">
|
||||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
<div className="space-y-1">
|
||||||
{data.sections.qualitative.title}
|
<label className="block text-lg font-semibold text-on-surface">{q.label}</label>
|
||||||
</h2>
|
<p className="text-on-surface-variant/70 italic text-sm">{q.labelVi}</p>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
{/* 4 */}
|
{q.type === "rating" && (
|
||||||
<section className="space-y-6">
|
<RadioScale
|
||||||
<div className="border-l-2 border-primary pl-4">
|
name={q.id}
|
||||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
label={q.label}
|
||||||
{data.sections.industry.title}
|
min={q.min}
|
||||||
</h2>
|
max={q.max}
|
||||||
</div>
|
value={form[q.id] || ""}
|
||||||
<div className="space-y-4">
|
onChange={(v) => updateField(q.id, v)}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 5 */}
|
{q.type === "dropdown" && (
|
||||||
<section className="space-y-6">
|
<div className="relative max-w-md">
|
||||||
<div className="border-l-2 border-primary pl-4">
|
<select
|
||||||
<h2 className="font-[var(--font-display-lg)] text-xl md:text-2xl text-on-surface uppercase tracking-tight">
|
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"
|
||||||
{data.sections.networking.title}
|
value={form[q.id] || ""}
|
||||||
</h2>
|
onChange={(e) => updateField(q.id, e.target.value)}
|
||||||
</div>
|
>
|
||||||
<div className="space-y-2">
|
<option value="">{q.placeholder}</option>
|
||||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
{q.options?.map((opt) => (
|
||||||
{data.sections.networking.introductions.label}
|
<option key={opt.value} value={opt.value}>
|
||||||
</label>
|
{opt.label}
|
||||||
<p className="text-on-surface-variant text-sm">{data.sections.networking.introductions.hint}</p>
|
</option>
|
||||||
<input
|
))}
|
||||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface"
|
</select>
|
||||||
placeholder={data.sections.networking.introductions.placeholder}
|
<span className="material-symbols-outlined absolute right-4 top-1/2 -translate-y-1/2 text-outline pointer-events-none">
|
||||||
type="text"
|
expand_more
|
||||||
value={form.introductions}
|
</span>
|
||||||
onChange={(e) => setForm((s) => ({ ...s, introductions: e.target.value }))}
|
</div>
|
||||||
/>
|
)}
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
{q.type === "scale" && (
|
||||||
<label className="block font-[var(--font-label-caps)] text-xs text-primary uppercase tracking-wider">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
{data.sections.networking.collaboration.label}
|
{q.options?.map((opt) => (
|
||||||
</label>
|
<label
|
||||||
<p className="text-on-surface-variant text-sm">{data.sections.networking.collaboration.hint}</p>
|
key={opt.value}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
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"
|
||||||
{data.sections.networking.collaboration.options.map((opt) => (
|
>
|
||||||
<label
|
<input
|
||||||
key={opt.value}
|
className="size-5 accent-primary cursor-pointer"
|
||||||
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"
|
name={q.id}
|
||||||
>
|
type="radio"
|
||||||
<input
|
value={opt.value}
|
||||||
type="checkbox"
|
checked={form[q.id] === opt.value}
|
||||||
className="mt-1 size-4 accent-primary"
|
onChange={() => updateField(q.id, opt.value)}
|
||||||
checked={form.collaboration.includes(opt.value)}
|
/>
|
||||||
onChange={() => toggleCollaboration(opt.value)}
|
<span className="text-on-surface font-medium">{opt.label}</span>
|
||||||
/>
|
</label>
|
||||||
<span className="text-on-surface text-sm">{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>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
))}
|
||||||
|
|
||||||
{/* 6 */}
|
<div className="pt-10 border-t border-white/5 flex flex-col items-center gap-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">
|
|
||||||
<button
|
<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"
|
type="submit"
|
||||||
disabled={submitState === "sending"}
|
disabled={submitState === "sending"}
|
||||||
>
|
>
|
||||||
@@ -411,14 +263,14 @@ export default function FeedbackForm({ data }: { data: FeedbackJson }) {
|
|||||||
<SubmitToast
|
<SubmitToast
|
||||||
state={submitState}
|
state={submitState}
|
||||||
successTitle="Feedback submitted"
|
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={() => {
|
onDismiss={() => {
|
||||||
setSubmitState("idle");
|
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}
|
{data.submit.disclaimer}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+197
-99
@@ -1,111 +1,209 @@
|
|||||||
{
|
{
|
||||||
"title": "Event Feedback",
|
"title": "Event Feedback",
|
||||||
"subtitle": "Your insights drive the future of IPv6 infrastructure. Help us shape the 2027 experience.",
|
"subtitle": "Your insights drive the future of IPv6 infrastructure. Help us shape the 2027 experience.",
|
||||||
"sections": {
|
"sections": [
|
||||||
"satisfaction": {
|
{
|
||||||
"title": "1. General satisfaction",
|
"id": "overall",
|
||||||
"eventRating": {
|
"title": "1. OVERALL EVENT EXPERIENCE",
|
||||||
"label": "Overall event rating",
|
"subtitle": "Tổng Quan Trải Nghiệm Sự Kiện",
|
||||||
"left": "Poor",
|
"questions": [
|
||||||
"right": "Excellent",
|
{
|
||||||
"min": 2,
|
"id": "overallExperience",
|
||||||
"max": 5
|
"type": "rating",
|
||||||
},
|
"label": "How would you rate your overall experience at IPv6 Summit 2026?",
|
||||||
"venueSatisfaction": {
|
"labelVi": "Đánh giá tổng thể của bạn về IPv6 Summit 2026?",
|
||||||
"label": "Venue & logistics satisfaction",
|
"min": 1,
|
||||||
"left": "Dissatisfied",
|
"max": 5
|
||||||
"right": "Very satisfied",
|
},
|
||||||
"min": 2,
|
{
|
||||||
"max": 5
|
"id": "venueFacilities",
|
||||||
}
|
"type": "rating",
|
||||||
|
"label": "How satisfied were you with the venue, facilities, and catering services?",
|
||||||
|
"labelVi": "Mức độ hài lòng của bạn về địa điểm, cơ sở vật chất và dịch vụ tiệc?",
|
||||||
|
"min": 1,
|
||||||
|
"max": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "organisationProcess",
|
||||||
|
"type": "rating",
|
||||||
|
"label": "How would you rate the event organisation and registration process?",
|
||||||
|
"labelVi": "Đánh giá của bạn về công tác tổ chức và quy trình đăng ký?",
|
||||||
|
"min": 1,
|
||||||
|
"max": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"content": {
|
{
|
||||||
"title": "2. Content evaluation",
|
"id": "content",
|
||||||
"session": {
|
"title": "2. CONTENT & PROGRAMME EVALUATION",
|
||||||
"label": "Most valuable session",
|
"subtitle": "Đánh Giá Nội Dung & Chương Trình",
|
||||||
"placeholder": "Select a session",
|
"questions": [
|
||||||
"options": [
|
{
|
||||||
{ "value": "ai", "label": "AI in Data Centres" },
|
"id": "mostValuableSession",
|
||||||
{ "value": "green", "label": "Green Tech" },
|
"type": "dropdown",
|
||||||
{ "value": "cyber", "label": "Cybersecurity" },
|
"label": "Which session did you find most valuable?",
|
||||||
{ "value": "policy", "label": "Policy & Scale" },
|
"labelVi": "Phiên thảo luận nào mang lại giá trị nhất cho bạn?",
|
||||||
{ "value": "local", "label": "Local Partner Session" }
|
"placeholder": "Select a session / Danh sách lựa chọn",
|
||||||
]
|
"options": [
|
||||||
},
|
{ "value": "opening", "label": "Opening Session / Phiên Khai mạc" },
|
||||||
"speakerRating": {
|
{ "value": "keynote1", "label": "Keynote 1 / Diễn văn chính 1" },
|
||||||
"label": "Keynote speaker rating",
|
{ "value": "panel1", "label": "Panel Discussion 1 / Tọa đàm 1" },
|
||||||
"left": "Needs improvement",
|
{ "value": "technical", "label": "Technical Track / Phiên Kỹ thuật" },
|
||||||
"right": "Exceptional",
|
{ "value": "policy", "label": "Policy Track / Phiên Chính sách" }
|
||||||
"min": 1,
|
]
|
||||||
"max": 5
|
},
|
||||||
}
|
{
|
||||||
|
"id": "relevance",
|
||||||
|
"type": "scale",
|
||||||
|
"label": "How relevant was the conference content to your professional interests?",
|
||||||
|
"labelVi": "Nội dung hội thảo có phù hợp với lĩnh vực/chuyên môn của bạn không?",
|
||||||
|
"options": [
|
||||||
|
{ "value": "1", "label": "Not Relevant" },
|
||||||
|
{ "value": "2", "label": "Slightly Relevant" },
|
||||||
|
{ "value": "3", "label": "Moderately Relevant" },
|
||||||
|
{ "value": "4", "label": "Relevant" },
|
||||||
|
{ "value": "5", "label": "Highly Relevant" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "speakerRating",
|
||||||
|
"type": "rating_labeled",
|
||||||
|
"label": "How would you rate the keynote speakers and panel discussions?",
|
||||||
|
"labelVi": "Đánh giá của bạn về diễn giả chính và các phiên tọa đàm?",
|
||||||
|
"options": [
|
||||||
|
{ "value": "1", "label": "1 – Needs Improvement" },
|
||||||
|
{ "value": "2", "label": "2 – Fair" },
|
||||||
|
{ "value": "3", "label": "3 – Good" },
|
||||||
|
{ "value": "4", "label": "4 – Strong" },
|
||||||
|
{ "value": "5", "label": "5 – Outstanding" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "networkingUtility",
|
||||||
|
"type": "scale",
|
||||||
|
"label": "How useful were the networking opportunities provided during the Summit?",
|
||||||
|
"labelVi": "Các cơ hội networking tại sự kiện có hữu ích không?",
|
||||||
|
"options": [
|
||||||
|
{ "value": "1", "label": "Not Useful" },
|
||||||
|
{ "value": "2", "label": "Slightly Useful" },
|
||||||
|
{ "value": "3", "label": "Useful" },
|
||||||
|
{ "value": "4", "label": "Very Useful" },
|
||||||
|
{ "value": "5", "label": "Extremely Valuable" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "futureTopics",
|
||||||
|
"type": "checkbox_group",
|
||||||
|
"label": "Which topics would you like to see more of in future editions?",
|
||||||
|
"labelVi": "Bạn muốn thấy thêm chủ đề nào trong các kỳ Summit tiếp theo?",
|
||||||
|
"hasOther": true,
|
||||||
|
"options": [
|
||||||
|
{ "value": "deployment", "label": "IPv6 Deployment Strategies" },
|
||||||
|
{ "value": "cybersecurity", "label": "Cybersecurity & Infrastructure" },
|
||||||
|
{ "value": "smart-cities", "label": "Smart Cities & IoT" },
|
||||||
|
{ "value": "ai-networks", "label": "AI & Future Networks" },
|
||||||
|
{ "value": "government", "label": "Government Policies" },
|
||||||
|
{ "value": "cloud-dc", "label": "Cloud & Data Centers" },
|
||||||
|
{ "value": "case-studies", "label": "Case Studies from Enterprises" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"qualitative": {
|
{
|
||||||
"title": "3. Qualitative feedback",
|
"id": "future",
|
||||||
"improve2027": {
|
"title": "4. FUTURE PARTICIPATION",
|
||||||
"label": "What can we improve for 2027?",
|
"subtitle": "Khả Năng Tham Dự Trong Tương Lai",
|
||||||
"placeholder": "Your suggestions for next year..."
|
"questions": [
|
||||||
},
|
{
|
||||||
"comments": {
|
"id": "attendAgain",
|
||||||
"label": "General comments",
|
"type": "scale",
|
||||||
"placeholder": "Any additional thoughts..."
|
"label": "Would you attend IPv6 Summit again in 2027?",
|
||||||
}
|
"labelVi": "Bạn có mong muốn tham dự IPv6 Summit 2027 không?",
|
||||||
|
"options": [
|
||||||
|
{ "value": "definitely-yes", "label": "Definitely Yes" },
|
||||||
|
{ "value": "probably-yes", "label": "Probably Yes" },
|
||||||
|
{ "value": "not-sure", "label": "Not Sure" },
|
||||||
|
{ "value": "probably-no", "label": "Probably No" },
|
||||||
|
{ "value": "definitely-no", "label": "Definitely No" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "recommend",
|
||||||
|
"type": "scale",
|
||||||
|
"label": "Would you recommend IPv6 Summit to colleagues or partners?",
|
||||||
|
"labelVi": "Bạn có sẵn sàng giới thiệu IPv6 Summit cho đồng nghiệp hoặc đối tác không?",
|
||||||
|
"options": [
|
||||||
|
{ "value": "yes", "label": "Yes" },
|
||||||
|
{ "value": "maybe", "label": "Maybe" },
|
||||||
|
{ "value": "no", "label": "No" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"industry": {
|
{
|
||||||
"title": "4. Industry & technical insights",
|
"id": "qualitative",
|
||||||
"ipv6Readiness": {
|
"title": "5. QUALITATIVE FEEDBACK",
|
||||||
"label": "IPv6 readiness",
|
"subtitle": "Ý Kiến & Đề Xuất",
|
||||||
"hint": "Your organization's current IPv6 deployment level",
|
"questions": [
|
||||||
"left": "1",
|
{
|
||||||
"right": "5",
|
"id": "enjoyedMost",
|
||||||
"min": 1,
|
"type": "long_text",
|
||||||
"max": 5
|
"label": "What did you enjoy most about the Summit?",
|
||||||
},
|
"labelVi": "Điều bạn thích nhất tại IPv6 Summit là gì?"
|
||||||
"challenge": {
|
},
|
||||||
"label": "Infrastructure challenges",
|
{
|
||||||
"hint": "What is the biggest challenge for your AI infrastructure?",
|
"id": "improvementAreas",
|
||||||
"placeholder": "Select a challenge",
|
"type": "long_text",
|
||||||
"options": [
|
"label": "What areas should we improve for future events?",
|
||||||
{ "value": "budget", "label": "Budget" },
|
"labelVi": "Bạn đề xuất cải thiện điều gì cho các sự kiện tiếp theo?"
|
||||||
{ "value": "technical", "label": "Technical expertise" },
|
},
|
||||||
{ "value": "policy", "label": "Policy / regulatory" },
|
{
|
||||||
{ "value": "workforce", "label": "Workforce" },
|
"id": "additionalComments",
|
||||||
{ "value": "hardware", "label": "Hardware availability" }
|
"type": "long_text",
|
||||||
]
|
"label": "Any additional comments, suggestions, or partnership interests?",
|
||||||
}
|
"labelVi": "Các góp ý hoặc đề xuất hợp tác khác?"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"networking": {
|
{
|
||||||
"title": "5. Networking & future cooperation",
|
"id": "profile",
|
||||||
"introductions": {
|
"title": "ATTENDEE PROFILE (OPTIONAL)",
|
||||||
"label": "Speaker connection",
|
"subtitle": "Thông Tin Người Tham Dự (Không bắt buộc)",
|
||||||
"hint": "Are there any specific speakers or companies you wish to be introduced to?",
|
"questions": [
|
||||||
"placeholder": "Name of speaker or company..."
|
{
|
||||||
},
|
"id": "industry",
|
||||||
"collaboration": {
|
"type": "checkbox_group",
|
||||||
"label": "Collaboration interests",
|
"label": "Industry / Sector",
|
||||||
"hint": "I am interested in:",
|
"labelVi": "Lĩnh vực hoạt động",
|
||||||
"options": [
|
"hasOther": true,
|
||||||
{ "value": "ipv6-partnerships", "label": "IPv6 deployment partnerships" },
|
"options": [
|
||||||
{ "value": "ai-dc", "label": "AI / Data Centre networking" },
|
{ "value": "government", "label": "Government" },
|
||||||
{ "value": "policy", "label": "Policy & regulatory roundtables" },
|
{ "value": "telecom", "label": "Telecom" },
|
||||||
{ "value": "workshops", "label": "Technical workshops & labs" }
|
{ "value": "education", "label": "Education" },
|
||||||
]
|
{ "value": "enterprise", "label": "Enterprise" },
|
||||||
}
|
{ "value": "technology", "label": "Technology" },
|
||||||
},
|
{ "value": "startup", "label": "Startup" }
|
||||||
"logistics": {
|
]
|
||||||
"title": "6. Logistics feedback",
|
},
|
||||||
"venueRating": {
|
{
|
||||||
"label": "Venue rating",
|
"id": "role",
|
||||||
"hint": "Venue facilities & location (Ho Chi Minh City)",
|
"type": "checkbox_group",
|
||||||
"left": "Poor",
|
"label": "Your role",
|
||||||
"right": "Excellent",
|
"labelVi": "Vai trò của bạn",
|
||||||
"min": 1,
|
"hasOther": true,
|
||||||
"max": 5
|
"options": [
|
||||||
}
|
{ "value": "executive", "label": "Executive / Leadership" },
|
||||||
|
{ "value": "engineer", "label": "Engineer / Technical Specialist" },
|
||||||
|
{ "value": "researcher", "label": "Researcher / Academic" },
|
||||||
|
{ "value": "student", "label": "Student" },
|
||||||
|
{ "value": "bizdev", "label": "Business Development" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
"submit": {
|
"submit": {
|
||||||
"label": "Submit feedback",
|
"label": "Submit feedback",
|
||||||
"disclaimer": "Thank you for your valuable contribution to the IPv6 ecosystem."
|
"disclaimer": "THANK YOU FOR YOUR VALUABLE CONTRIBUTION. Your feedback helps us build a stronger and more impactful IPv6 ecosystem for the future."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -29,12 +29,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
{/* Favicon */}
|
|
||||||
<link
|
|
||||||
rel="shortcut icon"
|
|
||||||
href="/assets/img/favicon.png"
|
|
||||||
type="image/x-icon"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body suppressHydrationWarning>
|
<body suppressHydrationWarning>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 170 KiB |
Reference in New Issue
Block a user