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
This commit is contained in:
163
components/ItemDetail.tsx
Normal file
163
components/ItemDetail.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Calendar, Tag, FileText, AlertCircle, CheckCircle2, Circle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface ItemDetailProps {
|
||||
item: {
|
||||
id: string
|
||||
title: string
|
||||
description: string | null
|
||||
completed: boolean
|
||||
priority: string
|
||||
dueDate: Date | null
|
||||
tags: any
|
||||
notes: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
listId: string
|
||||
}
|
||||
listTitle: string
|
||||
}
|
||||
|
||||
export default function ItemDetail({ item, listTitle }: ItemDetailProps) {
|
||||
const tags = Array.isArray(item.tags) ? item.tags : []
|
||||
const isOverdue = item.dueDate && new Date(item.dueDate) < new Date() && !item.completed
|
||||
|
||||
const priorityColors = {
|
||||
low: 'bg-green-100 text-green-800',
|
||||
medium: 'bg-yellow-100 text-yellow-800',
|
||||
high: 'bg-red-100 text-red-800',
|
||||
}
|
||||
|
||||
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || priorityColors.medium
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 mb-2">
|
||||
<Link href={`/list/${item.listId}`} className="hover:text-blue-600">
|
||||
{listTitle}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span>Task Details</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="pt-1">
|
||||
{item.completed ? (
|
||||
<CheckCircle2 className="text-green-500" size={24} />
|
||||
) : (
|
||||
<Circle className="text-gray-400" size={24} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className={`text-2xl font-bold ${item.completed ? 'text-gray-500 line-through' : 'text-gray-900'}`}>
|
||||
{item.title}
|
||||
</h1>
|
||||
{item.completed && (
|
||||
<p className="text-sm text-green-600 mt-1">Completed</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Priority */}
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mb-1">
|
||||
<AlertCircle size={16} />
|
||||
<span className="font-medium">Priority</span>
|
||||
</div>
|
||||
<span className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${priorityColor}`}>
|
||||
{item.priority.charAt(0).toUpperCase() + item.priority.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Due Date */}
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 mb-1">
|
||||
<Calendar size={16} />
|
||||
<span className="font-medium">Due Date</span>
|
||||
</div>
|
||||
{item.dueDate ? (
|
||||
<div>
|
||||
<p className={`text-gray-900 ${isOverdue ? 'text-red-600 font-semibold' : ''}`}>
|
||||
{new Date(item.dueDate).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
{isOverdue && (
|
||||
<p className="text-sm text-red-600 mt-1">Overdue</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">No due date</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{item.description && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h2 className="text-sm font-medium text-gray-600 mb-2">Description</h2>
|
||||
<p className="text-gray-900 whitespace-pre-wrap">{item.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
{tags.length > 0 && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-gray-600 mb-3">
|
||||
<Tag size={16} />
|
||||
<span>Tags</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
{item.notes && (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-gray-600 mb-3">
|
||||
<FileText size={16} />
|
||||
<span>Notes</span>
|
||||
</div>
|
||||
<p className="text-gray-900 whitespace-pre-wrap">{item.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="border-t pt-4 text-sm text-gray-500 space-y-1">
|
||||
<p>
|
||||
Created: {new Date(item.createdAt).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
Updated: {new Date(item.updatedAt).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user