From 9879fd8ee85dc6818795908cf52c98d0fd9a17c5 Mon Sep 17 00:00:00 2001 From: Gunshiz Date: Thu, 28 May 2026 17:56:03 +0700 Subject: [PATCH] refactor: consolidate form client components and standardize layout modularity --- app/actions/form.ts | 2 +- app/actions/submissions.ts | 6 + .../{form-editor-client.tsx => client.tsx} | 13 + app/form/admin/layout.tsx | 5 +- app/form/admin/page.tsx | 2 +- ...ubmission-detail-client.tsx => client.tsx} | 0 app/form/admin/result/[id]/page.tsx | 4 +- .../{delete-buttons-client.tsx => client.tsx} | 0 app/form/admin/result/page.tsx | 2 +- app/form/client.tsx | 288 ++++++++++ app/form/form-filler-client.tsx | 241 -------- app/form/layout-client.tsx | 36 -- app/form/layout.tsx | 4 +- app/form/page.tsx | 2 +- .../admin => components}/admin-sidebar.tsx | 0 .../delete-form-button.tsx | 0 {app/form => components}/login-dialog.tsx | 0 db/schema.ts | 1 + drizzle/0002_first_dragon_man.sql | 1 + drizzle/meta/0002_snapshot.json | 515 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 3 +- 22 files changed, 844 insertions(+), 288 deletions(-) rename app/form/admin/{form-editor-client.tsx => client.tsx} (96%) rename app/form/admin/result/[id]/{submission-detail-client.tsx => client.tsx} (100%) rename app/form/admin/result/{delete-buttons-client.tsx => client.tsx} (100%) create mode 100644 app/form/client.tsx delete mode 100644 app/form/form-filler-client.tsx delete mode 100644 app/form/layout-client.tsx rename {app/form/admin => components}/admin-sidebar.tsx (100%) rename {app/form/admin => components}/delete-form-button.tsx (100%) rename {app/form => components}/login-dialog.tsx (100%) create mode 100644 drizzle/0002_first_dragon_man.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/app/actions/form.ts b/app/actions/form.ts index c5ae77a..5264f17 100644 --- a/app/actions/form.ts +++ b/app/actions/form.ts @@ -32,7 +32,7 @@ export async function createForm(formData: FormData) { redirect(`/form/admin/${form.id}/edit`); } -export async function updateForm(id: string, data: { title: string; description?: string }) { +export async function updateForm(id: string, data: { title: string; description?: string; isOpen?: boolean }) { const session = await getServerSession(authOptions); const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean); const discordId = (session?.user as { discordId?: string } | undefined)?.discordId; diff --git a/app/actions/submissions.ts b/app/actions/submissions.ts index 7b5687c..f995bb7 100644 --- a/app/actions/submissions.ts +++ b/app/actions/submissions.ts @@ -17,6 +17,12 @@ export async function submitForm( const discordId = (session.user as { discordId?: string }).discordId; if (!discordId) throw new Error("No Discord ID found"); + const form = await db.query.forms.findFirst({ + where: (f, { eq }) => eq(f.id, formId), + }); + if (!form) throw new Error("Form not found"); + if (!form.isOpen) throw new Error("This form is disabled for now"); + const existingSubmission = await db.query.submissions.findFirst({ where: (s, { eq, and }) => and(eq(s.formId, formId), eq(s.userDiscordId, discordId)), diff --git a/app/form/admin/form-editor-client.tsx b/app/form/admin/client.tsx similarity index 96% rename from app/form/admin/form-editor-client.tsx rename to app/form/admin/client.tsx index b8081fe..364e7e9 100644 --- a/app/form/admin/form-editor-client.tsx +++ b/app/form/admin/client.tsx @@ -37,6 +37,7 @@ interface FormEditorClientProps { id: string; title: string; description: string | null; + isOpen: boolean; }; initialQuestions: Question[]; } @@ -45,6 +46,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC const router = useRouter(); const [formTitle, setFormTitle] = useState(form.title); const [formDescription, setFormDescription] = useState(form.description || ""); + const [formIsOpen, setFormIsOpen] = useState(form.isOpen); const [questions, setQuestions] = useState(initialQuestions); const [uploadingId, setUploadingId] = useState(null); const [isFormSaving, startFormSaveTransition] = useTransition(); @@ -60,6 +62,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC await updateForm(form.id, { title: formTitle, description: formDescription, + isOpen: formIsOpen, }); toast.success("Form metadata saved!"); router.refresh(); @@ -210,6 +213,16 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC className="min-h-20" /> +
+ + +
+
+ ); +} + +interface Question { + id: string; + formId: string; + type: "text" | "textarea" | "radio" | "checkbox"; + label: string; + imageUrl: string | null; + displayOrder: number; + required: boolean; + options: string[] | null; +} + +interface FormFillerClientProps { + form: { + id: string; + title: string; + description: string | null; + isOpen: boolean; + }; + questions: Question[]; + initialAnswers?: Record; +} + +export function FormFillerClient({ form, questions, initialAnswers = {} }: FormFillerClientProps) { + const router = useRouter(); + + // Pre-process checkbox answers since they are stored as comma-separated strings + const initialCheckboxes: Record = {}; + for (const q of questions) { + if (q.type === "checkbox" && initialAnswers[q.id]) { + initialCheckboxes[q.id] = initialAnswers[q.id].split(", "); + } + } + + const [answers, setAnswers] = useState>(initialAnswers); + const [checkboxAnswers, setCheckboxAnswers] = useState>(initialCheckboxes); + const [submitting, setSubmitting] = useState(false); + + const isUpdating = Object.keys(initialAnswers).length > 0; + + const handleTextChange = (questionId: string, value: string) => { + setAnswers((prev) => ({ ...prev, [questionId]: value })); + }; + + const handleRadioChange = (questionId: string, value: string) => { + setAnswers((prev) => ({ ...prev, [questionId]: value })); + }; + + const handleCheckboxChange = (questionId: string, option: string, checked: boolean) => { + setCheckboxAnswers((prev) => { + const current = prev[questionId] || []; + const updated = checked + ? [...current, option] + : current.filter((item) => item !== option); + return { ...prev, [questionId]: updated }; + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!form.isOpen) return; + + // Validate required questions + for (const q of questions) { + if (q.required) { + if (q.type === "checkbox") { + const selected = checkboxAnswers[q.id] || []; + if (selected.length === 0) { + toast.error(`"${q.label}" is required.`); + return; + } + } else { + const ans = answers[q.id] || ""; + if (!ans.trim()) { + toast.error(`"${q.label}" is required.`); + return; + } + } + } + } + + setSubmitting(true); + try { + const formattedAnswers = questions.map((q) => { + let value = ""; + if (q.type === "checkbox") { + value = (checkboxAnswers[q.id] || []).join(", "); + } else { + value = answers[q.id] || ""; + } + return { + questionId: q.id, + value, + }; + }); + + const res = await submitForm(form.id, formattedAnswers); + if (res.ok) { + toast.success(isUpdating ? "Form updated successfully!" : "Form submitted successfully!"); + if (!isUpdating) { + setAnswers({}); + setCheckboxAnswers({}); + } + } + } catch (err: any) { + console.error(err); + toast.error(err.message || "Failed to submit form."); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ {!form.isOpen && ( +
+
+

This form disable for now

+
+
+ )} +
+ + + {form.title} + {form.description && ( + + {form.description} + + )} + + + +
+ {questions.map((q) => ( + + +
+ +
+ {q.imageUrl && ( +
+ {q.label} +
+ )} +
+ + {q.type === "text" && ( + handleTextChange(q.id, e.target.value)} + className="w-full" + disabled={!form.isOpen} + /> + )} + + {q.type === "textarea" && ( +