Files
Todo/components/ItemForm.tsx
Paul Bailey 282a0295ec feat: implement item detail page and form components
- Created ItemForm component with fields for all metadata (priority, due date, tags, notes)
- Created ItemDetail component to display all item information
- Built /list/[id]/item/[itemId] page for viewing full item details
- Built /list/[id]/item/[itemId]/edit page for editing items
- Added form validation for item inputs (title required, length limits)
- Created DeleteItemButton and ToggleItemButton helper components
- Enhanced createItem action to support full metadata on creation
- Added getItem action to fetch individual items with list data

Requirements: 1.1, 1.2, 5.1, 5.2, 5.3, 5.4
2025-12-01 19:16:37 +00:00

268 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { updateItem, createItem } from '@/lib/actions'
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'
}
export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps) {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
// 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<string[]>(
Array.isArray(item?.tags) ? item.tags : []
)
const [tagInput, setTagInput] = useState('')
const [notes, setNotes] = useState(item?.notes || '')
const validateForm = () => {
const newErrors: Record<string, string> = {}
if (!title.trim()) {
newErrors.title = 'Title is required'
}
if (title.length > 200) {
newErrors.title = 'Title must be less than 200 characters'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) {
return
}
setIsSubmitting(true)
try {
const data = {
title: title.trim(),
description: description.trim() || null,
priority,
dueDate: dueDate ? new Date(dueDate) : null,
tags,
notes: notes.trim() || null,
}
if (mode === 'create') {
await createItem(listId, data.title, data.description || undefined, {
priority: data.priority,
dueDate: data.dueDate,
tags: data.tags,
notes: data.notes,
})
router.push(`/list/${listId}`)
} else if (item) {
await updateItem(item.id, data)
router.push(`/list/${listId}/item/${item.id}`)
}
} catch (error) {
console.error('Error saving item:', error)
setErrors({ submit: 'Failed to save item. Please try again.' })
} finally {
setIsSubmitting(false)
}
}
const handleAddTag = () => {
const tag = tagInput.trim()
if (tag && !tags.includes(tag)) {
setTags([...tags, tag])
setTagInput('')
}
}
const handleRemoveTag = (tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove))
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddTag()
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Title */}
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
Title *
</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.title ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Enter task title"
/>
{errors.title && (
<p className="mt-1 text-sm text-red-600">{errors.title}</p>
)}
</div>
{/* Description */}
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Add a description"
/>
</div>
{/* Priority */}
<div>
<label htmlFor="priority" className="block text-sm font-medium text-gray-700 mb-1">
Priority
</label>
<select
id="priority"
value={priority}
onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high')}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
{/* Due Date */}
<div>
<label htmlFor="dueDate" className="block text-sm font-medium text-gray-700 mb-1">
Due Date
</label>
<input
type="date"
id="dueDate"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Tags */}
<div>
<label htmlFor="tags" className="block text-sm font-medium text-gray-700 mb-1">
Tags
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
id="tags"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleKeyDown}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Add a tag"
/>
<button
type="button"
onClick={handleAddTag}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors"
>
Add
</button>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
>
{tag}
<button
type="button"
onClick={() => handleRemoveTag(tag)}
className="hover:text-blue-600"
>
×
</button>
</span>
))}
</div>
)}
</div>
{/* Notes */}
<div>
<label htmlFor="notes" className="block text-sm font-medium text-gray-700 mb-1">
Notes
</label>
<textarea
id="notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={5}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Add additional notes"
/>
</div>
{/* Error message */}
{errors.submit && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">{errors.submit}</p>
</div>
)}
{/* Actions */}
<div className="flex gap-3">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-4 py-2 rounded-lg transition-colors"
>
{isSubmitting ? 'Saving...' : mode === 'create' ? 'Create Task' : 'Save Changes'}
</button>
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg transition-colors"
>
Cancel
</button>
</div>
</form>
)
}