"use client"; import React, { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardDescription, 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 { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { toast } from "sonner"; import { updateForm } from "@/app/actions/form"; import { createQuestion, updateQuestion, deleteQuestion, reorderQuestions, bulkUpdateQuestions, } from "@/app/actions/questions"; import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X } from "lucide-react"; import Image from 'next/image'; 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 FormEditorClientProps { form: { id: string; title: string; description: string | null; isOpen: boolean; }; initialQuestions: Question[]; } export default function FormEditorClient({ form, initialQuestions }: FormEditorClientProps) { 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(); const [isQuestionsSaving, startQuestionsSaveTransition] = useTransition(); const saveFormMetadata = () => { if (!formTitle.trim()) { toast.error("Form title is required."); return; } startFormSaveTransition(async () => { try { await updateForm(form.id, { title: formTitle, description: formDescription, isOpen: formIsOpen, }); toast.success("Form metadata saved!"); router.refresh(); } catch (err: any) { toast.error(err.message || "Failed to update form."); } }); }; const handleAddQuestion = async () => { try { const displayOrder = questions.length; await createQuestion(form.id, { type: "text", label: `Untitled Question ${questions.length + 1}`, required: false, displayOrder, options: [], }); router.refresh(); } catch (err: any) { console.error("Failed to add question:", err); } }; const handleUpdateQuestion = (id: string, updates: Partial) => { setQuestions((prev) => prev.map((q) => (q.id === id ? { ...q, ...updates } as Question : q)) ); }; const saveQuestions = () => { startQuestionsSaveTransition(async () => { try { const formattedUpdates = questions.map(q => ({ ...q, options: q.options === null ? [] : q.options, allowOther: q.allowOther, })); await bulkUpdateQuestions(form.id, formattedUpdates); toast.success("Questions saved!"); router.refresh(); } catch (err: any) { toast.error(err.message || "Failed to save questions."); } }); }; const handleDeleteQuestion = async (id: string) => { try { await deleteQuestion(id, form.id); router.refresh(); } catch (err: any) { console.error("Failed to delete question:", err); } }; const handleMove = (index: number, direction: "up" | "down") => { const newQuestions = [...questions]; const targetIndex = direction === "up" ? index - 1 : index + 1; if (targetIndex < 0 || targetIndex >= newQuestions.length) return; const temp = newQuestions[index]; newQuestions[index] = newQuestions[targetIndex]; newQuestions[targetIndex] = temp; // Update display orders based on new positions newQuestions.forEach((q, idx) => { q.displayOrder = idx; }); setQuestions(newQuestions); }; const handleImageUpload = async (questionId: string, e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const formData = new FormData(); formData.append("file", file); setUploadingId(questionId); toast.promise( new Promise(async (resolve, reject) => { try { const res = await fetch("/api/upload", { method: "POST", body: formData, }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to upload image"); resolve(data.url); } catch (error) { reject(error); } }).then(async (imageUrl) => { await updateQuestion(questionId, form.id, { imageUrl }); setQuestions((prev) => prev.map((q) => (q.id === questionId ? { ...q, imageUrl } : q)) ); router.refresh(); }).finally(() => { setUploadingId(null); }), { loading: "Uploading image...", success: "Image uploaded!", error: (err: any) => err.message || "Failed to upload image.", } ); }; const handleRemoveImage = async (questionId: string) => { toast.promise( updateQuestion(questionId, form.id, { imageUrl: null }).then(() => { setQuestions((prev) => prev.map((q) => (q.id === questionId ? { ...q, imageUrl: null } : q)) ); router.refresh(); }), { loading: "Removing image...", success: "Image removed.", error: (err: any) => err.message || "Failed to remove image.", } ); }; const handleAddOption = (question: Question) => { const options = question.options ? [...question.options] : []; options.push(`Option ${options.length + 1}`); handleUpdateQuestion(question.id, { options }); }; const handleUpdateOption = (question: Question, optIndex: number, val: string) => { if (!question.options) return; const options = [...question.options]; options[optIndex] = val; handleUpdateQuestion(question.id, { options }); }; const handleRemoveOption = (question: Question, optIndex: number) => { if (!question.options) return; const options = question.options.filter((_, idx) => idx !== optIndex); handleUpdateQuestion(question.id, { options }); }; React.useEffect(() => { setQuestions(initialQuestions); }, [initialQuestions]); return (
Form Settings Configure the main title and description of your form.
setFormTitle(e.target.value)} placeholder="e.g. Untitled Form" />