"use client"; import React, { useState, useRef, useCallback, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; 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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Checkbox } from "@/components/ui/checkbox"; import { toast } from "sonner"; import { updateForm } from "@/app/admin/form/actions"; import { createQuestion, updateQuestion, deleteQuestion, bulkUpdateQuestions, } from "@/app/actions/questions"; import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarHeader, SidebarFooter, } from "@/components/ui/sidebar"; import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Image as ImageIcon, X, Check, LayoutDashboard, ClipboardList, ArrowLeft, Webhook } from "lucide-react"; import Image from 'next/image'; import { RichTextEditor } from "@/components/rich-text-editor"; import { FormattedText } from "@/components/formatted-text"; import { ThemeToggle } from "@/components/theme-toggle"; 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[]; } type AutoSaveStatus = "idle" | "saving" | "saved"; 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 [formSaveStatus, setFormSaveStatus] = useState("idle"); const [questionsSaveStatus, setQuestionsSaveStatus] = useState("idle"); const formTimerRef = useRef | null>(null); const questionsTimerRef = useRef | null>(null); // Refs to always read latest state inside debounced callbacks const formTitleRef = useRef(formTitle); const formDescriptionRef = useRef(formDescription); const formIsOpenRef = useRef(formIsOpen); const questionsRef = useRef(questions); formTitleRef.current = formTitle; formDescriptionRef.current = formDescription; formIsOpenRef.current = formIsOpen; questionsRef.current = questions; const autoSaveForm = useCallback(() => { if (formTimerRef.current) clearTimeout(formTimerRef.current); setFormSaveStatus("idle"); formTimerRef.current = setTimeout(async () => { const title = formTitleRef.current; if (!title.trim()) return; setFormSaveStatus("saving"); try { await updateForm(form.id, { title, description: formDescriptionRef.current, isOpen: formIsOpenRef.current, }); setFormSaveStatus("saved"); router.refresh(); setTimeout(() => setFormSaveStatus("idle"), 2000); } catch { toast.error("Failed to auto-save form settings."); setFormSaveStatus("idle"); } }, 1000); }, [form.id, router]); const autoSaveQuestions = useCallback(() => { if (questionsTimerRef.current) clearTimeout(questionsTimerRef.current); setQuestionsSaveStatus("idle"); questionsTimerRef.current = setTimeout(async () => { const current = questionsRef.current; if (current.length === 0) return; setQuestionsSaveStatus("saving"); try { const formattedUpdates = current.map(q => ({ ...q, options: q.options === null ? [] : q.options, allowOther: q.allowOther, })); await bulkUpdateQuestions(form.id, formattedUpdates); setQuestionsSaveStatus("saved"); router.refresh(); setTimeout(() => setQuestionsSaveStatus("idle"), 2000); } catch { toast.error("Failed to auto-save questions."); setQuestionsSaveStatus("idle"); } }, 1500); }, [form.id, router]); // Cleanup timers on unmount useEffect(() => { return () => { if (formTimerRef.current) clearTimeout(formTimerRef.current); if (questionsTimerRef.current) clearTimeout(questionsTimerRef.current); }; }, []); const handleFormTitleChange = (val: string) => { setFormTitle(val); autoSaveForm(); }; const handleFormDescriptionChange = (val: string) => { setFormDescription(val); autoSaveForm(); }; const handleFormIsOpenChange = (val: boolean) => { setFormIsOpen(val); autoSaveForm(); }; 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)) ); autoSaveQuestions(); }; 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); autoSaveQuestions(); }; 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.
handleFormTitleChange(e.target.value)} placeholder="e.g. Untitled Form" />
handleFormDescriptionChange(html)} placeholder="Provide a description..." className="min-h-20 w-full" />

Questions

{questions.map((q, index) => (
handleUpdateQuestion(q.id, { label: html })} placeholder="Question Text" className="w-full" />
{q.imageUrl ? (
{q.label}
) : (
handleImageUpload(q.id, e)} className="hidden" disabled={uploadingId !== null} />
)}
{(q.type === "radio" || q.type === "checkbox") && (
{(q.options || []).map((opt, optIdx) => (