diff --git a/.kiro/specs/ultimate-lists-app/tasks.md b/.kiro/specs/ultimate-lists-app/tasks.md index 145629b..3cc1251 100644 --- a/.kiro/specs/ultimate-lists-app/tasks.md +++ b/.kiro/specs/ultimate-lists-app/tasks.md @@ -85,7 +85,7 @@ - **Property 1: Task creation preserves description** - **Validates: Requirements 1.1** -- [ ] 6. Enhance list management UI +- [x] 6. Enhance list management UI - Add ListForm component for creating and editing lists - Implement list deletion with confirmation dialog - Add list renaming functionality diff --git a/app/page.tsx b/app/page.tsx index 6c58c85..eadc93f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ -import { PlusIcon } from 'lucide-react' import { getLists } from '@/lib/actions' import ListCard from '@/components/ListCard' +import NewListButton from '@/components/NewListButton' export default async function Home() { const lists = await getLists() @@ -9,10 +9,7 @@ export default async function Home() {

Ultimate Lists

- +
diff --git a/components/DeleteListButton.tsx b/components/DeleteListButton.tsx new file mode 100644 index 0000000..c5ac822 --- /dev/null +++ b/components/DeleteListButton.tsx @@ -0,0 +1,88 @@ +'use client' + +import { useState } from 'react' +import { Trash2 } from 'lucide-react' +import { deleteList } from '@/lib/actions' +import { useRouter } from 'next/navigation' +import Modal from './Modal' + +interface DeleteListButtonProps { + listId: string + listTitle: string + itemCount: number + redirectAfterDelete?: boolean +} + +export default function DeleteListButton({ + listId, + listTitle, + itemCount, + redirectAfterDelete = false +}: DeleteListButtonProps) { + const router = useRouter() + const [isConfirmOpen, setIsConfirmOpen] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + + const handleDelete = async () => { + setIsDeleting(true) + try { + await deleteList(listId) + setIsConfirmOpen(false) + if (redirectAfterDelete) { + router.push('/') + } else { + router.refresh() + } + } catch (err) { + console.error('Failed to delete list:', err) + alert('Failed to delete list. Please try again.') + } finally { + setIsDeleting(false) + } + } + + return ( + <> + + + setIsConfirmOpen(false)} + title="Delete List" + > +
+

+ Are you sure you want to delete {listTitle}? +

+ {itemCount > 0 && ( +

+ This will permanently delete {itemCount} {itemCount === 1 ? 'item' : 'items'}. +

+ )} +
+ + +
+
+
+ + ) +} diff --git a/components/ListCard.tsx b/components/ListCard.tsx index ac99b89..c8fdfd9 100644 --- a/components/ListCard.tsx +++ b/components/ListCard.tsx @@ -1,5 +1,11 @@ +'use client' + import Link from 'next/link' -import { CheckCircle2, Circle } from 'lucide-react' +import { useState } from 'react' +import { Edit2 } from 'lucide-react' +import Modal from './Modal' +import ListForm from './ListForm' +import DeleteListButton from './DeleteListButton' interface List { id: string @@ -12,38 +18,80 @@ interface List { } export default function ListCard({ list }: { list: List }) { + const [isEditModalOpen, setIsEditModalOpen] = useState(false) const completedCount = list.items.filter(item => item.completed).length const totalCount = list.items.length const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0 + const handleEditClick = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setIsEditModalOpen(true) + } + + const handleDeleteClick = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + } + return ( - -
-
-
-

{list.title}

-
- -
-
- {completedCount} of {totalCount} completed - {Math.round(progress)}% + <> + +
+
+
+
+

{list.title}

+
+
+ + +
-
-
+
+
+ {completedCount} of {totalCount} completed + {Math.round(progress)}% +
+ +
+
+
-
- + + + setIsEditModalOpen(false)} + title="Edit List" + > + setIsEditModalOpen(false)} + onSuccess={() => setIsEditModalOpen(false)} + /> + + ) } \ No newline at end of file diff --git a/components/ListForm.tsx b/components/ListForm.tsx new file mode 100644 index 0000000..cf0a44a --- /dev/null +++ b/components/ListForm.tsx @@ -0,0 +1,134 @@ +'use client' + +import { useState } from 'react' +import { createList, updateList } from '@/lib/actions' +import { useRouter } from 'next/navigation' + +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 [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 }) + } else { + // Create new list + await createList(title.trim(), color) + } + + if (onSuccess) { + onSuccess() + } else { + router.refresh() + } + } catch (err) { + setError('Failed to save list. Please try again.') + console.error(err) + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ + setTitle(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" + placeholder="Enter list title" + disabled={isSubmitting} + autoFocus + /> +
+ +
+ +
+ {PRESET_COLORS.map((presetColor) => ( +
+
+ + {error && ( +
{error}
+ )} + +
+ {onCancel && ( + + )} + +
+
+ ) +} diff --git a/components/Modal.tsx b/components/Modal.tsx new file mode 100644 index 0000000..b6ab376 --- /dev/null +++ b/components/Modal.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useEffect } from 'react' +import { X } from 'lucide-react' + +interface ModalProps { + isOpen: boolean + onClose: () => void + title: string + children: React.ReactNode +} + +export default function Modal({ isOpen, onClose, title, children }: ModalProps) { + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden' + } else { + document.body.style.overflow = 'unset' + } + + return () => { + document.body.style.overflow = 'unset' + } + }, [isOpen]) + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose() + } + } + + if (isOpen) { + document.addEventListener('keydown', handleEscape) + } + + return () => { + document.removeEventListener('keydown', handleEscape) + } + }, [isOpen, onClose]) + + if (!isOpen) return null + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+
+

{title}

+ +
+ +
+ {children} +
+
+
+ ) +} diff --git a/components/NewListButton.tsx b/components/NewListButton.tsx new file mode 100644 index 0000000..209a1bd --- /dev/null +++ b/components/NewListButton.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useState } from 'react' +import { PlusIcon } from 'lucide-react' +import Modal from './Modal' +import ListForm from './ListForm' + +export default function NewListButton() { + const [isModalOpen, setIsModalOpen] = useState(false) + + return ( + <> + + + setIsModalOpen(false)} + title="Create New List" + > + setIsModalOpen(false)} + onSuccess={() => setIsModalOpen(false)} + /> + + + ) +}