"use client"; import React, { useState, useEffect, useMemo } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Checkbox } from "@/components/ui/checkbox"; import { stripHtml } from "@/lib/utils"; import { HtmlDisplay } from "@/components/html-display"; import { FormattedText } from "@/components/formatted-text"; import { Dialog, DialogContent, DialogTrigger, DialogTitle } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { toast } from "sonner"; import { submitForm, deleteOwnSubmission } from "@/app/actions/submissions"; import { signIn } from "next-auth/react"; import Image from "next/image"; import { z } from "zod"; import { useForm, useWatch } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormField, FormItem, FormMessage, } from "@/components/ui/form"; import { DropdownMenuAvatar, DiscordMemberProfile } from "@/components/dropdown-menu-avatar"; import { getErrorMessage } from "@/lib/errors"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Badge } from "@/components/ui/badge"; import { Field, FieldGroup, FieldLabel, FieldLegend, FieldSet, } from "@/components/ui/field"; import { Spinner } from "@/components/ui/spinner"; import { decodeCheckboxAnswer, type FormAnswerValue, } from "@/lib/form-answer-values"; export function FormLayoutClient({ userName, userImage, profile, }: { userName: string; userImage: string; profile?: DiscordMemberProfile | null; }) { return ( ); } export function LoginButton() { return ( ); } interface Question { id: string; formId: string; type: "text" | "textarea" | "radio" | "checkbox"; label: string; imageUrl: string | null; displayOrder: number; required: boolean; options: string[] | null; allowOther: boolean; } interface FormFillerClientProps { form: { id: string; title: string; description: string | null; isOpen: boolean; }; questions: Question[]; initialAnswers?: Record; hasSubmission?: boolean; } const LS_KEY = (formId: string) => `form-draft-${formId}`; type FormValues = Record; type StoredDraft = { version: 1; answers: FormValues; otherValues: Record; }; const buildSchema = (questions: Question[]) => { const shape: Record = {}; questions.forEach(q => { if (q.type === "checkbox") { shape[q.id] = q.required ? z.array(z.string()).min(1, `จำเป็นต้องตอบข้อนี้`) : z.array(z.string()).optional(); } else { shape[q.id] = q.required ? z.string().trim().min(1, `จำเป็นต้องตอบข้อนี้`) : z.string().optional(); } }); return z.object(shape); }; function prepareInitialState( questions: Question[], answers: Record, savedOtherValues: Record = {} ) { const values: FormValues = {}; const otherValues = { ...savedOtherValues }; questions.forEach((question) => { const answer = answers[question.id]; if (question.type === "checkbox") { let selected = decodeCheckboxAnswer(answer); if (question.allowOther && question.options) { const customAnswers = selected.filter( (value) => !question.options?.includes(value) && value !== "__OTHER__" ); if (customAnswers.length > 0) { otherValues[question.id] = customAnswers.join(", "); } if (selected.includes("__OTHER__") || customAnswers.length > 0) { selected = selected.filter((value) => question.options?.includes(value)); selected.push("__OTHER__"); } } values[question.id] = selected; return; } if (question.type === "radio") { let selected = typeof answer === "string" ? answer : ""; if ( question.allowOther && question.options && selected && !question.options.includes(selected) ) { if (selected !== "__OTHER__") { otherValues[question.id] = selected; } selected = "__OTHER__"; } values[question.id] = selected; return; } values[question.id] = typeof answer === "string" ? answer : ""; }); return { values, otherValues }; } function parseStoredDraft(raw: string): { answers: FormValues; otherValues: Record; } { const parsed: unknown = JSON.parse(raw); if ( parsed && typeof parsed === "object" && "version" in parsed && parsed.version === 1 && "answers" in parsed ) { const draft = parsed as StoredDraft; return { answers: draft.answers ?? {}, otherValues: draft.otherValues ?? {}, }; } return { answers: (parsed as FormValues) ?? {}, otherValues: {}, }; } export function FormFillerClient({ form, questions, initialAnswers = {}, hasSubmission = false, }: FormFillerClientProps) { const router = useRouter(); const isUpdating = hasSubmission; const formSchema = useMemo(() => buildSchema(questions), [questions]); const serverInitialState = useMemo( () => prepareInitialState(questions, initialAnswers), [initialAnswers, questions] ); const [otherValues, setOtherValues] = useState>( serverInitialState.otherValues ); const [draftReady, setDraftReady] = useState(false); const [draftSaved, setDraftSaved] = useState(false); const [showSuccessDialog, setShowSuccessDialog] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const methods = useForm>({ resolver: zodResolver(formSchema), defaultValues: serverInitialState.values, mode: "onTouched", }); const { clearErrors, getValues, handleSubmit, control, formState: { isSubmitting }, reset, setError, } = methods; const watchedValues = useWatch({ control }); useEffect(() => { const frame = requestAnimationFrame(() => { if (isUpdating) { setDraftReady(true); return; } try { const raw = localStorage.getItem(LS_KEY(form.id)); if (raw) { const draft = parseStoredDraft(raw); const restored = prepareInitialState( questions, draft.answers, draft.otherValues ); reset(restored.values); setOtherValues(restored.otherValues); setDraftSaved(true); } } catch { localStorage.removeItem(LS_KEY(form.id)); } finally { setDraftReady(true); } }); return () => cancelAnimationFrame(frame); }, [form.id, isUpdating, questions, reset]); useEffect(() => { if (!draftReady || !form.isOpen) { return; } try { const draft: StoredDraft = { version: 1, answers: getValues() as FormValues, otherValues, }; localStorage.setItem(LS_KEY(form.id), JSON.stringify(draft)); queueMicrotask(() => setDraftSaved(true)); } catch { queueMicrotask(() => setDraftSaved(false)); } }, [draftReady, form.id, form.isOpen, getValues, otherValues, watchedValues]); const onSubmit = async (values: z.infer) => { if (!form.isOpen) return; for (const question of questions) { const value = values[question.id] as FormAnswerValue | undefined; const usesOther = Array.isArray(value) ? value.includes("__OTHER__") : value === "__OTHER__"; if (usesOther && !otherValues[question.id]?.trim()) { setError( question.id, { type: "required", message: "โปรดระบุคำตอบสำหรับตัวเลือก “อื่นๆ”", }, { shouldFocus: true } ); return; } } try { const formattedAnswers = questions.map((q) => { let val = values[q.id] as FormAnswerValue | undefined; if (q.allowOther) { const customText = otherValues[q.id]?.trim() || ""; if (Array.isArray(val)) { val = val.map(v => v === "__OTHER__" ? customText : v); } else if (val === "__OTHER__") { val = customText; } } return { questionId: q.id, value: val ?? (q.type === "checkbox" ? [] : ""), }; }); const res = await submitForm(form.id, formattedAnswers); if (res.ok) { if (isUpdating) { toast.success("บันทึกการแก้ไขแล้ว"); } else { setShowSuccessDialog(true); router.refresh(); } } } catch (err: unknown) { console.error(err); toast.error(getErrorMessage(err, "ไม่สามารถส่งฟอร์มได้")); } }; const handleDelete = async () => { setIsDeleting(true); try { const res = await deleteOwnSubmission(form.id); if (res.ok) { toast.success("ลบคำตอบแล้ว"); localStorage.removeItem(LS_KEY(form.id)); reset({}); setOtherValues({}); setDraftSaved(false); setDeleteDialogOpen(false); router.refresh(); } } catch (err: unknown) { console.error(err); toast.error(getErrorMessage(err, "ไม่สามารถลบข้อมูลได้")); } finally { setIsDeleting(false); } }; if (!form.isOpen && !isUpdating) { return ( ปิดรับคำตอบแล้ว แบบฟอร์ม “{form.title}” ไม่รับคำตอบใหม่ในขณะนี้ ); } return (
{form.title} {form.isOpen ? isUpdating ? "กำลังแก้ไขคำตอบ" : "คำตอบใหม่" : "ปิดรับคำตอบแล้ว"} {questions.length} คำถาม {form.description && ( {stripHtml(form.description).length > 280 ? ( ดูรายละเอียดและเงื่อนไขทั้งหมด ) : ( )} )} {questions.some((question) => question.required) && ( ช่องที่มีป้าย “จำเป็น” ต้องตอบให้ครบ )} {!form.isOpen && ( คุณยังดูหรือลบคำตอบเดิมได้ แต่บันทึกการแก้ไขไม่ได้ )}
{questions.length === 0 ? ( ยังไม่มีคำถาม แบบฟอร์มนี้ยังไม่พร้อมรับคำตอบ ) : ( {questions.map((q, questionIndex) => { const questionLabelId = `question-${q.id}`; const questionErrorId = `question-error-${q.id}`; const plainQuestionLabel = stripHtml(q.label); return ( ( ข้อ {questionIndex + 1} จาก {questions.length}
{q.required && ( จำเป็น )} {q.imageUrl && ( รูปประกอบคำถาม {plainQuestionLabel}
{`รูปประกอบคำถาม
)}
{q.type === "text" && ( )} {q.type === "textarea" && (