"use client"; /** * FormField — a single form question input, shared by: * - the in-app form builder preview (components/form/form-builder-view), and * - the public submit page (app/f/[slug]/page.tsx). * * Controlled: the parent owns `value` (the raw answer, NOT the RecordValue — * the RecordValue coercion happens once, at submit, via * questionAnswerToValue in hooks/use-forms so both surfaces stay in sync). * * Raw answer shapes per type (the server coerces via coerceAnswer): * SHORT_TEXT/LONG_TEXT/EMAIL → string * NUMBER/RATING → number * DATE → "YYYY-MM-DD" string * CHECKBOX → boolean * SINGLE_SELECT → option NAME string (server stores String(raw)) * MULTI_SELECT → option NAME[] (server wraps single in array) * FILE → fileId[] (uploaded via @/lib/api uploadFile) * * Question types are UPPERCASE (API/Prisma/FormQuestionType). Select options * live in question.validation.options. */ import { useRef, useState } from "react"; import { Upload, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { FormOption, FormQuestion, questionOptions } from "@/hooks/use-forms"; import { uploadFile } from "@/lib/api"; export interface FormFieldProps { question: FormQuestion; value: unknown; onChange: (value: unknown) => void; error?: string; disabled?: boolean; } const inputCls = "w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"; export function FormField({ question: q, value, onChange, error, disabled }: FormFieldProps) { const options: FormOption[] = questionOptions(q); const validation = q.validation ?? {}; const placeholder = q.placeholder ?? ""; switch (q.type) { case "SHORT_TEXT": return ( onChange(e.target.value)} aria-label={q.label} /> ); case "LONG_TEXT": return (