diff --git a/.kiro/specs/ultimate-lists-app/tasks.md b/.kiro/specs/ultimate-lists-app/tasks.md index 3cc1251..9d32e6e 100644 --- a/.kiro/specs/ultimate-lists-app/tasks.md +++ b/.kiro/specs/ultimate-lists-app/tasks.md @@ -100,7 +100,7 @@ - **Property 7: List retrieval completeness** - **Validates: Requirements 2.2** -- [ ] 7. Build list detail page with item management +- [x] 7. Build list detail page with item management - Create /list/[id] page showing all items in a list - Add ItemCard component with completion toggle, priority badge, and due date display - Implement inline item creation diff --git a/app/list/[id]/page.tsx b/app/list/[id]/page.tsx new file mode 100644 index 0000000..92fda00 --- /dev/null +++ b/app/list/[id]/page.tsx @@ -0,0 +1,88 @@ +import { getListWithItems } from '@/lib/actions' +import { notFound } from 'next/navigation' +import Link from 'next/link' +import { ArrowLeft, Plus } from 'lucide-react' +import ItemCard from '@/components/ItemCard' +import InlineItemCreate from '@/components/InlineItemCreate' + +export default async function ListDetailPage({ params }: { params: { id: string } }) { + const list = await getListWithItems(params.id) + + if (!list) { + notFound() + } + + const completedItems = list.items.filter(item => item.completed) + const incompleteItems = list.items.filter(item => !item.completed) + + return ( +
+ {/* Header */} +
+ + + Back to Lists + + +
+
+

{list.title}

+
+ +

+ {completedItems.length} of {list.items.length} tasks completed +

+
+ + {/* Inline Item Creation */} +
+ +
+ + {/* Items List */} +
+ {/* Incomplete Items */} + {incompleteItems.length > 0 && ( +
+

+ To Do ({incompleteItems.length}) +

+
+ {incompleteItems.map((item) => ( + + ))} +
+
+ )} + + {/* Completed Items */} + {completedItems.length > 0 && ( +
+

+ Completed ({completedItems.length}) +

+
+ {completedItems.map((item) => ( + + ))} +
+
+ )} + + {/* Empty State */} + {list.items.length === 0 && ( +
+

No tasks yet

+

Add your first task to get started

+
+ )} +
+
+ ) +} diff --git a/components/DeleteItemButton.tsx b/components/DeleteItemButton.tsx index 416c1c2..a241778 100644 --- a/components/DeleteItemButton.tsx +++ b/components/DeleteItemButton.tsx @@ -7,19 +7,26 @@ import { Trash2 } from 'lucide-react' interface DeleteItemButtonProps { itemId: string - listId: string + listId?: string + variant?: 'full' | 'icon' } -export default function DeleteItemButton({ itemId, listId }: DeleteItemButtonProps) { +export default function DeleteItemButton({ itemId, listId, variant = 'full' }: DeleteItemButtonProps) { const router = useRouter() const [isDeleting, setIsDeleting] = useState(false) const [showConfirm, setShowConfirm] = useState(false) - const handleDelete = async () => { + const handleDelete = async (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() setIsDeleting(true) try { await deleteItem(itemId) - router.push(`/list/${listId}`) + if (listId) { + router.push(`/list/${listId}`) + } else { + router.refresh() + } } catch (error) { console.error('Error deleting item:', error) setIsDeleting(false) @@ -27,6 +34,18 @@ export default function DeleteItemButton({ itemId, listId }: DeleteItemButtonPro } } + const handleShowConfirm = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setShowConfirm(true) + } + + const handleCancel = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setShowConfirm(false) + } + if (showConfirm) { return (
@@ -38,7 +57,7 @@ export default function DeleteItemButton({ itemId, listId }: DeleteItemButtonPro {isDeleting ? 'Deleting...' : 'Confirm'} + ) + } + return ( + ) + } + + return ( +
+ setTitle(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter task title..." + autoFocus + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 mb-3" + /> +
+ + +
+
+ ) +} diff --git a/components/ItemCard.tsx b/components/ItemCard.tsx new file mode 100644 index 0000000..902b765 --- /dev/null +++ b/components/ItemCard.tsx @@ -0,0 +1,105 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' +import { Calendar, Tag, AlertCircle, Trash2 } from 'lucide-react' +import ToggleItemButton from './ToggleItemButton' +import DeleteItemButton from './DeleteItemButton' + +interface ItemCardProps { + item: { + id: string + title: string + description: string | null + completed: boolean + priority: string + dueDate: Date | null + tags: any + notes: string | null + } + listId: string +} + +export default function ItemCard({ item, listId }: ItemCardProps) { + 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-700 border-green-200', + medium: 'bg-yellow-100 text-yellow-700 border-yellow-200', + high: 'bg-red-100 text-red-700 border-red-200', + } + + const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || priorityColors.medium + + return ( +
+
+ {/* Completion Toggle */} +
+ +
+ + {/* Content */} +
+ +

+ {item.title} +

+ + {item.description && ( +

+ {item.description} +

+ )} + + {/* Metadata */} +
+ {/* Priority Badge */} + + + {item.priority.charAt(0).toUpperCase() + item.priority.slice(1)} + + + {/* Due Date */} + {item.dueDate && ( + + + {new Date(item.dueDate).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + })} + {isOverdue && ' (Overdue)'} + + )} + + {/* Tags */} + {tags.length > 0 && ( +
+ + + {tags.slice(0, 2).join(', ')} + {tags.length > 2 && ` +${tags.length - 2}`} + +
+ )} +
+ +
+ + {/* Delete Button */} +
+ +
+
+
+ ) +} diff --git a/components/ToggleItemButton.tsx b/components/ToggleItemButton.tsx index ca38c78..12cd730 100644 --- a/components/ToggleItemButton.tsx +++ b/components/ToggleItemButton.tsx @@ -7,12 +7,15 @@ import { CheckCircle2, Circle } from 'lucide-react' interface ToggleItemButtonProps { itemId: string completed: boolean + variant?: 'full' | 'icon' } -export default function ToggleItemButton({ itemId, completed }: ToggleItemButtonProps) { +export default function ToggleItemButton({ itemId, completed, variant = 'full' }: ToggleItemButtonProps) { const [isToggling, setIsToggling] = useState(false) - const handleToggle = async () => { + const handleToggle = async (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() setIsToggling(true) try { await toggleItem(itemId) @@ -23,6 +26,23 @@ export default function ToggleItemButton({ itemId, completed }: ToggleItemButton } } + if (variant === 'icon') { + return ( + + ) + } + return (