Files
Todo/components/ListForm.tsx
Paul Bailey e51e88fddd feat: Add visual feedback and polish (Task 15)
- Implement toast notification system for success/error messages
- Add loading states with skeleton loaders for async operations
- Create smooth CSS transitions and animations
- Enhance empty states with EmptyState component
- Add loading.tsx files for Next.js loading states
- Integrate toast notifications across all CRUD operations
- Add fade-in and scale-in animations for better UX
- Improve visual feedback for user actions
2025-12-01 21:14:14 +00:00

140 lines
4.2 KiB
TypeScript

'use client'
import { useState } from 'react'
import { createList, updateList } from '@/lib/actions'
import { useRouter } from 'next/navigation'
import { useToast } from './ToastContainer'
interface ListFormProps {
list?: {
id: string
title: string
color: string
}
onCancel?: () => void
onSuccess?: () => void
}
const PRESET_COLORS = [
'#3B82F6', // blue
'#EF4444', // red
'#10B981', // green
'#F59E0B', // amber
'#8B5CF6', // purple
'#EC4899', // pink
'#06B6D4', // cyan
'#F97316', // orange
]
export default function ListForm({ list, onCancel, onSuccess }: ListFormProps) {
const router = useRouter()
const { showToast } = useToast()
const [title, setTitle] = useState(list?.title || '')
const [color, setColor] = useState(list?.color || PRESET_COLORS[0])
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!title.trim()) {
setError('List title is required')
return
}
setIsSubmitting(true)
setError('')
try {
if (list) {
// Update existing list
await updateList(list.id, { title: title.trim(), color })
showToast('List updated successfully', 'success')
} else {
// Create new list
await createList(title.trim(), color)
showToast('List created successfully', 'success')
}
if (onSuccess) {
onSuccess()
} else {
router.refresh()
}
} catch (err) {
setError('Failed to save list. Please try again.')
showToast('Failed to save list', 'error')
console.error(err)
} finally {
setIsSubmitting(false)
}
}
return (
<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
</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"
placeholder="Enter list title"
disabled={isSubmitting}
autoFocus
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2 sm:mb-3">
Color
</label>
<div className="flex gap-2 sm:gap-3 flex-wrap">
{PRESET_COLORS.map((presetColor) => (
<button
key={presetColor}
type="button"
onClick={() => setColor(presetColor)}
className={`w-11 h-11 sm:w-12 sm:h-12 rounded-lg transition-all tap-target-sm ${
color === presetColor
? 'ring-2 ring-offset-2 ring-gray-400 scale-110'
: 'hover:scale-105 active:scale-95'
}`}
style={{ backgroundColor: presetColor }}
disabled={isSubmitting}
aria-label={`Select color ${presetColor}`}
/>
))}
</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
type="button"
onClick={onCancel}
disabled={isSubmitting}
className="w-full sm:w-auto px-4 sm:px-5 py-3 text-gray-700 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 rounded-lg transition-colors disabled:opacity-50 tap-target font-medium text-sm sm:text-base"
>
Cancel
</button>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full sm:w-auto px-4 sm:px-5 py-3 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50 tap-target font-medium text-sm sm:text-base"
>
{isSubmitting ? 'Saving...' : list ? 'Update List' : 'Create List'}
</button>
</div>
</form>
)
}