feat: Add comprehensive input validation
- Created lib/validation.ts with validation utilities for all form inputs - Added validation for list titles, item titles, colors, dates, tags, descriptions, and notes - Implemented inline validation with real-time error feedback in ListForm - Enhanced ItemForm with comprehensive validation for all fields - Updated InlineItemCreate with validation - Added input sanitization to prevent XSS attacks - Updated server actions to use validation utilities - Added maxLength attributes to prevent excessive input - Improved accessibility with aria-invalid and aria-describedby attributes - Added visual feedback for validation errors with red borders - Validates Requirements 1.1 and 2.1
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createList, updateList } from '@/lib/actions'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useToast } from './ToastContainer'
|
||||
import { validateListTitle } from '@/lib/validation'
|
||||
|
||||
interface ListFormProps {
|
||||
list?: {
|
||||
@@ -33,12 +34,30 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
|
||||
const [color, setColor] = useState(list?.color || PRESET_COLORS[0])
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [touched, setTouched] = useState(false)
|
||||
|
||||
// Inline validation when title changes
|
||||
useEffect(() => {
|
||||
if (touched) {
|
||||
const result = validateListTitle(title)
|
||||
setError(result.isValid ? '' : result.error || '')
|
||||
}
|
||||
}, [title, touched])
|
||||
|
||||
const handleTitleBlur = () => {
|
||||
setTouched(true)
|
||||
const result = validateListTitle(title)
|
||||
setError(result.isValid ? '' : result.error || '')
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!title.trim()) {
|
||||
setError('List title is required')
|
||||
// Validate before submission
|
||||
const result = validateListTitle(title)
|
||||
if (!result.isValid) {
|
||||
setError(result.error || 'Invalid input')
|
||||
setTouched(true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,8 +81,9 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
|
||||
router.refresh()
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to save list. Please try again.')
|
||||
showToast('Failed to save list', 'error')
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to save list. Please try again.'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
@@ -74,18 +94,31 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
|
||||
<form onSubmit={handleSubmit} className="space-y-4 sm:space-y-5">
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
List Title
|
||||
List Title *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full px-3 sm:px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
||||
onBlur={handleTitleBlur}
|
||||
className={`w-full px-3 sm:px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 text-sm sm:text-base ${
|
||||
error && touched
|
||||
? 'border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Enter list title"
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
maxLength={200}
|
||||
aria-invalid={error && touched ? 'true' : 'false'}
|
||||
aria-describedby={error && touched ? 'title-error' : undefined}
|
||||
/>
|
||||
{error && touched && (
|
||||
<p id="title-error" className="mt-1.5 text-xs sm:text-sm text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -111,10 +144,6 @@ export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 text-xs sm:text-sm font-medium">{error}</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col-reverse sm:flex-row gap-2 sm:gap-3 justify-end pt-2">
|
||||
{onCancel && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user