"use client"; import React, { useState, useEffect, useMemo } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } 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 { Label } from "@/components/ui/label"; import { cn, 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 { Loader2, LogOut } from "lucide-react"; import { signOut } from "next-auth/react"; import Image from "next/image"; import { z } from "zod"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; export function FormLayoutClient({ userName, userImage, }: { userName: string; userImage: string; }) { const [imgError, setImgError] = useState(false); return (
{userImage && !imgError && ( {userName} setImgError(true)} unoptimized /> )} {userName}
); } 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; } const LS_KEY = (formId: string) => `form-draft-${formId}`; const buildSchema = (questions: Question[]) => { const shape: Record = {}; questions.forEach(q => { const plainLabel = stripHtml(q.label); if (q.type === "checkbox") { shape[q.id] = q.required ? z.array(z.string()).min(1, `จำเป็นต้องตอบ "${plainLabel}"`) : z.array(z.string()).optional(); } else { shape[q.id] = q.required ? z.string().min(1, `จำเป็นต้องตอบ "${plainLabel}"`) : z.string().optional(); } }); return z.object(shape); }; export function FormFillerClient({ form, questions, initialAnswers = {} }: FormFillerClientProps) { const router = useRouter(); const isUpdating = Object.keys(initialAnswers).length > 0; const formSchema = useMemo(() => buildSchema(questions), [questions]); const { defaultValues, initialOtherValues } = useMemo(() => { const serverAnswers = { ...initialAnswers }; let draft: Record = {}; try { const raw = localStorage.getItem(LS_KEY(form.id)); if (raw) draft = JSON.parse(raw); } catch {} const merged = { ...draft, ...serverAnswers }; const vals: Record = {}; const others: Record = {}; questions.forEach(q => { const ans = merged[q.id]; if (q.type === "checkbox") { let arr = ans ? (Array.isArray(ans) ? ans : ans.split(", ").filter(Boolean)) : []; if (q.allowOther && q.options) { const standardOptions = q.options; const customAnswers = arr.filter((x: string) => !standardOptions.includes(x) && x !== "__OTHER__"); if (customAnswers.length > 0) { others[q.id] = customAnswers.join(", "); } // If the draft had __OTHER__ or if there's a custom answer from server, ensure __OTHER__ is selected if (arr.includes("__OTHER__") || customAnswers.length > 0) { arr = arr.filter((x: string) => standardOptions.includes(x)); arr.push("__OTHER__"); } } vals[q.id] = arr; } else if (q.type === "radio") { let val = ans || ""; if (q.allowOther && q.options && val !== "" && !q.options.includes(val)) { if (val !== "__OTHER__") { others[q.id] = val; } val = "__OTHER__"; } vals[q.id] = val; } else { vals[q.id] = ans || ""; } }); return { defaultValues: vals, initialOtherValues: others }; }, [form.id, initialAnswers, questions]); const [otherValues, setOtherValues] = useState>(initialOtherValues); const [showSuccessDialog, setShowSuccessDialog] = useState(false); const methods = useForm>({ resolver: zodResolver(formSchema), defaultValues, mode: "onTouched", }); const { handleSubmit, control, watch, formState: { isSubmitting }, trigger, reset } = methods; useEffect(() => { const subscription = watch((value) => { try { localStorage.setItem(LS_KEY(form.id), JSON.stringify(value)); } catch {} }); return () => subscription.unsubscribe(); }, [watch, form.id]); const onSubmit = async (values: z.infer) => { if (!form.isOpen) return; try { const formattedAnswers = questions.map((q) => { let val = values[q.id] as string | string[] | undefined; if (q.allowOther) { const customText = otherValues[q.id] || ""; if (Array.isArray(val)) { val = val.map(v => v === "__OTHER__" ? customText : v); } else if (val === "__OTHER__") { val = customText; } } const finalString = Array.isArray(val) ? val.join(", ") : (val || ""); if (q.required && !finalString.trim()) { throw new Error(`จำเป็นต้องตอบ "${stripHtml(q.label)}" (สำหรับตัวเลือก 'อื่นๆ')`); } return { questionId: q.id, value: finalString, }; }); const res = await submitForm(form.id, formattedAnswers); if (res.ok) { if (isUpdating) { toast.success("อัปเดตฟอร์มสำเร็จ!"); } else { setShowSuccessDialog(true); router.refresh(); } } } catch (err: any) { console.error(err); toast.error(err.message || "ไม่สามารถส่งฟอร์มได้"); } }; const handleDelete = async () => { try { const res = await deleteOwnSubmission(form.id); if (res.ok) { toast.success("ลบข้อมูลสำเร็จ!"); localStorage.removeItem(LS_KEY(form.id)); reset({}); router.refresh(); } } catch (err: any) { console.error(err); toast.error(err.message || "ไม่สามารถลบข้อมูลได้"); } }; return (
{!form.isOpen && (
ฟอร์มนี้ถูกปิดใช้งานชั่วคราว
)} {form.title} {form.description && (
)}
{questions.map((q) => (
{q.imageUrl && (
{q.label}
Enlarged image for {q.label}
{q.label}
)} {q.required && *}
( {q.type === "text" && ( )} {q.type === "textarea" && (