'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import { ArrowUp, ArrowDown, Trash2, Plus } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { SectionTitle } from '@/components/ui/page-title' import { useConfirm } from '@/components/ui/confirm' import { toast } from 'sonner' import { createForm, updateForm, setFormStatus, deleteForm, } from '@/lib/actions' import { formFieldTypes } from '@/lib/validations' import type { FormField } from '@/lib/forms-schema' import { slugify, deriveFieldKeys } from '@/lib/forms-logic' import { FormRenderer } from './form-renderer' type Kind = 'form' | 'waiver' type Visibility = 'public_anon' | 'public_auth' | 'members' type Status = 'draft' | 'published' | 'closed' export type BuilderForm = { id?: string spaceSlug: string slug: string title: string description: string kind: Kind visibility: Visibility legal_text: string status?: Status schema: FormField[] } const TYPE_LABELS: Record<(typeof formFieldTypes)[number], string> = { short_text: 'Text', long_text: 'Paragraph', email: 'Email', number: 'Number', date: 'Date', checkbox: 'Checkbox', select: 'Dropdown', radio: 'Choice', } const deriveKeys = (fields: FormField[]): FormField[] => deriveFieldKeys(fields) export function FormBuilder({ initial }: { initial: BuilderForm }) { const router = useRouter() const confirm = useConfirm() const isEdit = Boolean(initial.id) const [slug, setSlug] = useState(initial.slug) const [slugTouched, setSlugTouched] = useState(isEdit) const [title, setTitle] = useState(initial.title) const [description, setDescription] = useState(initial.description) const [kind, setKind] = useState(initial.kind) const [visibility, setVisibility] = useState(initial.visibility) const [legalText, setLegalText] = useState(initial.legal_text) const [fields, setFields] = useState(initial.schema) const [preview, setPreview] = useState>({}) const [saving, setSaving] = useState(false) const [busy, setBusy] = useState(false) function onTitle(v: string) { setTitle(v) if (!isEdit && !slugTouched) setSlug(slugify(v, '-')) } function addField(type: (typeof formFieldTypes)[number]) { setFields(f => [ ...f, { key: '', type, label: `Untitled ${TYPE_LABELS[type]}`, required: false, options: type === 'select' || type === 'radio' ? ['Option 1'] : undefined, }, ]) } function patch(i: number, p: Partial) { setFields(f => f.map((x, idx) => (idx === i ? { ...x, ...p } : x))) } function move(i: number, dir: -1 | 1) { setFields(f => { const j = i + dir if (j < 0 || j >= f.length) return f const next = [...f] ;[next[i], next[j]] = [next[j], next[i]] return next }) } async function removeField(i: number) { const ok = await confirm({ title: 'Remove this field?', description: 'It will be gone from the form. Save to make it permanent.', destructive: true, confirmText: 'Remove', }) if (ok) setFields(f => f.filter((_, idx) => idx !== i)) } async function save() { if (!title.trim()) { toast.error('Title is required') return } setSaving(true) const schema = deriveKeys(fields) const common = { title: title.trim(), description: description.trim() || null, visibility, schema, legal_text: kind === 'waiver' ? legalText : null, } const res = isEdit ? await updateForm({ formId: initial.id, ...common }) : await createForm({ slug, kind, ...common }) setSaving(false) if ('error' in res && res.error) { toast.error(res.error || 'Could not save') return } toast.success(isEdit ? 'Form saved' : 'Form created — now publish it to make it live') if (isEdit) { router.push('/forms') } else { // Land on the new form's edit page so the Publish step is right there // (creating a form leaves it as a draft). const newId = 'data' in res && res.data ? res.data.id : undefined router.push(newId ? `/forms/${newId}/edit` : '/forms') } router.refresh() } async function changeStatus(status: Status) { setBusy(true) const res = await setFormStatus({ formId: initial.id, status }) setBusy(false) if ('error' in res && res.error) { toast.error(res.error || 'Could not update') return } toast.success(`Form ${status}`) router.refresh() } async function remove() { const ok = await confirm({ title: 'Delete this form?', description: 'This cannot be undone. Forms with submissions cannot be deleted.', destructive: true, confirmText: 'Delete', }) if (!ok) return setBusy(true) const res = await deleteForm({ formId: initial.id }) setBusy(false) if ('error' in res && res.error) { toast.error(res.error || 'Could not delete') return } toast.success('Form deleted') router.push('/forms') router.refresh() } const publicPath = `/f/${initial.spaceSlug}/${initial.slug}` const publicUrl = typeof window !== 'undefined' ? `${window.location.origin}${publicPath}` : publicPath return (
{/* Editor */}
{isEdit && initial.status === 'draft' && (
Draft. This form is not visible to anyone yet. Use{' '} Publish below to make it live.
)} {isEdit && initial.status === 'published' && (
Published. This form is live and accepting responses.
)} {isEdit && initial.status === 'closed' && (
Closed. No longer accepting responses. Re-publish to reopen.
)} {!isEdit && (
Build your form, then Create. It starts as a draft; you publish it on the next screen.
)} {isEdit && (
{initial.status} {initial.status !== 'published' && ( )} {initial.status === 'published' && ( )} {initial.status !== 'draft' && ( )}
)} {isEdit && initial.status === 'published' && visibility !== 'members' && (
{publicUrl}
)} {isEdit && initial.status === 'published' && visibility === 'members' && (
Members-only: there is no public link. Members fill this from their{' '} Forms page in the app.
)}
Form details
onTitle(e.target.value)} />
{ setSlugTouched(true) setSlug(e.target.value) }} /> {isEdit ? (

The slug cannot change after creation.

) : (

Public address: /f/{initial.spaceSlug}/{slug || '…'}

)}