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:
2025-12-01 21:26:26 +00:00
parent 12ce70e495
commit 796bde22e7
6 changed files with 667 additions and 83 deletions

View File

@@ -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