'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { updateItem, createItem } from '@/lib/actions' import { useToast } from './ToastContainer' import { validateItemTitle, validateDescription, validateDateString, validateTag, validateTags, validateNotes, sanitizeString, } from '@/lib/validation' interface ItemFormProps { listId: string item?: { id: string title: string description: string | null priority: string dueDate: Date | null tags: any notes: string | null } mode?: 'create' | 'edit' canEdit?: boolean } export default function ItemForm({ listId, item, mode = 'edit', canEdit = true }: ItemFormProps) { const router = useRouter() const { showToast } = useToast() const [isSubmitting, setIsSubmitting] = useState(false) const [errors, setErrors] = useState>({}) const [touched, setTouched] = useState>({}) // Form state const [title, setTitle] = useState(item?.title || '') const [description, setDescription] = useState(item?.description || '') const [priority, setPriority] = useState<'low' | 'medium' | 'high'>( (item?.priority as 'low' | 'medium' | 'high') || 'medium' ) const [dueDate, setDueDate] = useState( item?.dueDate ? new Date(item.dueDate).toISOString().split('T')[0] : '' ) const [tags, setTags] = useState( Array.isArray(item?.tags) ? item.tags : [] ) const [tagInput, setTagInput] = useState('') const [notes, setNotes] = useState(item?.notes || '') // Inline validation for title useEffect(() => { if (touched.title) { const result = validateItemTitle(title) setErrors((prev) => ({ ...prev, title: result.isValid ? '' : result.error || '', })) } }, [title, touched.title]) // Inline validation for description useEffect(() => { if (touched.description) { const result = validateDescription(description) setErrors((prev) => ({ ...prev, description: result.isValid ? '' : result.error || '', })) } }, [description, touched.description]) // Inline validation for due date useEffect(() => { if (touched.dueDate && dueDate) { const result = validateDateString(dueDate) setErrors((prev) => ({ ...prev, dueDate: result.isValid ? '' : result.error || '', })) } else if (touched.dueDate && !dueDate) { setErrors((prev) => ({ ...prev, dueDate: '' })) } }, [dueDate, touched.dueDate]) // Inline validation for notes useEffect(() => { if (touched.notes) { const result = validateNotes(notes) setErrors((prev) => ({ ...prev, notes: result.isValid ? '' : result.error || '', })) } }, [notes, touched.notes]) // Validate tags whenever they change useEffect(() => { const result = validateTags(tags) setErrors((prev) => ({ ...prev, tags: result.isValid ? '' : result.error || '', })) }, [tags]) const handleBlur = (field: string) => { setTouched((prev) => ({ ...prev, [field]: true })) } const validateForm = () => { const newErrors: Record = {} // Validate title const titleResult = validateItemTitle(title) if (!titleResult.isValid) { newErrors.title = titleResult.error || '' } // Validate description const descResult = validateDescription(description) if (!descResult.isValid) { newErrors.description = descResult.error || '' } // Validate due date if (dueDate) { const dateResult = validateDateString(dueDate) if (!dateResult.isValid) { newErrors.dueDate = dateResult.error || '' } } // Validate tags const tagsResult = validateTags(tags) if (!tagsResult.isValid) { newErrors.tags = tagsResult.error || '' } // Validate notes const notesResult = validateNotes(notes) if (!notesResult.isValid) { newErrors.notes = notesResult.error || '' } setErrors(newErrors) setTouched({ title: true, description: true, dueDate: true, notes: true, }) return Object.keys(newErrors).length === 0 } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!canEdit) { showToast('You do not have permission to edit this item', 'error') return } if (!validateForm()) { showToast('Please fix validation errors', 'error') return } setIsSubmitting(true) try { const data = { title: sanitizeString(title), description: sanitizeString(description) || null, priority, dueDate: dueDate ? new Date(dueDate) : null, tags: tags.map(sanitizeString), notes: sanitizeString(notes) || null, } if (mode === 'create') { await createItem(listId, data.title, data.description || undefined, { priority: data.priority, dueDate: data.dueDate, tags: data.tags, notes: data.notes, }) showToast('Task created successfully', 'success') router.push(`/list/${listId}`) } else if (item) { await updateItem(item.id, data) showToast('Task updated successfully', 'success') router.push(`/list/${listId}/item/${item.id}`) } } catch (error) { console.error('Error saving item:', error) const errorMessage = error instanceof Error ? error.message : 'Failed to save item. Please try again.' setErrors({ submit: errorMessage }) showToast(errorMessage, 'error') } finally { setIsSubmitting(false) } } const handleAddTag = () => { const tag = sanitizeString(tagInput) // Validate tag const tagResult = validateTag(tag) if (!tagResult.isValid) { setErrors((prev) => ({ ...prev, tagInput: tagResult.error || '' })) return } // Check for duplicates if (tags.includes(tag)) { setErrors((prev) => ({ ...prev, tagInput: 'Tag already exists' })) return } // Check max tags if (tags.length >= 20) { setErrors((prev) => ({ ...prev, tagInput: 'Maximum 20 tags allowed' })) return } setTags([...tags, tag]) setTagInput('') setErrors((prev) => ({ ...prev, tagInput: '' })) } const handleRemoveTag = (tagToRemove: string) => { setTags(tags.filter((tag) => tag !== tagToRemove)) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault() handleAddTag() } } return (
{!canEdit && (

You have view-only access to this list. You cannot edit items.

)} {/* Title */}
setTitle(e.target.value)} onBlur={() => handleBlur('title')} disabled={!canEdit} className={`w-full px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 text-sm sm:text-base ${ errors.title && touched.title ? 'border-red-500 focus:ring-red-500' : 'border-gray-300 focus:ring-blue-500' } ${!canEdit ? 'bg-gray-100 cursor-not-allowed' : ''}`} placeholder="Enter task title" maxLength={200} aria-invalid={errors.title && touched.title ? 'true' : 'false'} aria-describedby={errors.title && touched.title ? 'title-error' : undefined} /> {errors.title && touched.title && (

{errors.title}

)}
{/* Description */}