'use client'; /** * NOVA CMS - DYNAMIC CONTENT FORM * ================================ * * Formulario dinámico que se genera automáticamente basado en * la configuración de un ContentType. Este es el corazón del * sistema headless CMS. * * CARACTERÍSTICAS: * - Genera campos dinámicamente según el ContentType * - Validación automática basada en field configuration * - Auto-save con useFormPersistence * - iPhone-style design consistente * - Guarda en tabla ContentEntry genérica */ import { useForm, FormProvider } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'; import { useToast } from '@/hooks/use-toast'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { ChevronsUpDown, Save, FileText, Calendar, Hash, ToggleRight, Image } from 'lucide-react'; import { cn } from '@/lib/utils'; // Types para el contenido dinámico interface Field { id: string; label: string; apiIdentifier: string; type: 'TEXT' | 'RICH_TEXT' | 'NUMBER' | 'BOOLEAN' | 'DATE' | 'MEDIA'; isRequired: boolean; } interface ContentType { id: string; name: string; apiIdentifier: string; description?: string; fields: Field[]; } interface DynamicContentFormProps { contentType: ContentType; initialData?: Record; entryId?: string; mode?: 'create' | 'edit'; onSave?: (data: Record) => Promise; } // Generador de schema dinámico basado en fields function generateDynamicSchema(fields: Field[]) { const schemaShape: Record = {}; fields.forEach(field => { let fieldValidator: any; switch (field.type) { case 'TEXT': case 'RICH_TEXT': fieldValidator = z.string(); if (field.isRequired) { fieldValidator = fieldValidator.min(1, `${field.label} es requerido`); } break; case 'NUMBER': fieldValidator = z.number(); if (field.isRequired) { fieldValidator = fieldValidator.min(0, `${field.label} es requerido`); } break; case 'BOOLEAN': fieldValidator = z.boolean(); break; case 'DATE': fieldValidator = z.string().refine(date => !isNaN(Date.parse(date)), { message: 'Fecha inválida' }); if (field.isRequired) { fieldValidator = fieldValidator.min(1, `${field.label} es requerido`); } break; case 'MEDIA': fieldValidator = z.string().url(); if (field.isRequired) { fieldValidator = fieldValidator.min(1, `${field.label} es requerido`); } break; default: fieldValidator = z.string(); } if (!field.isRequired) { fieldValidator = fieldValidator.optional(); } schemaShape[field.apiIdentifier] = fieldValidator; }); return z.object(schemaShape); } // Componente para renderizar un campo individual con diseño iOS profesional function DynamicField({ field, control }: { field: Field; control: any }) { const getFieldIcon = (type: Field['type']) => { switch (type) { case 'TEXT': return ; case 'RICH_TEXT': return ; case 'NUMBER': return ; case 'BOOLEAN': return ; case 'DATE': return ; case 'MEDIA': return ; default: return ; } }; const getFieldDescription = (type: Field['type']) => { switch (type) { case 'TEXT': return 'Texto simple de una línea'; case 'RICH_TEXT': return 'Texto con formato y múltiples líneas'; case 'NUMBER': return 'Valores numéricos y decimales'; case 'BOOLEAN': return 'Verdadero o falso (sí/no)'; case 'DATE': return 'Fechas y horarios'; case 'MEDIA': return 'URLs de imágenes y archivos'; default: return 'Campo de contenido'; } }; const renderFieldInput = (formField: any) => { const baseClasses = "w-full px-4 py-3 rounded-xl border border-gray-200 bg-white/70 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all duration-200 placeholder:text-gray-400"; switch (field.type) { case 'TEXT': return ( ); case 'RICH_TEXT': return (