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:
2025-12-01 19:16:37 +00:00
parent 3aedfb0954
commit 282a0295ec
11 changed files with 1309 additions and 4 deletions

View File

@@ -0,0 +1,536 @@
# Design Document: Ultimate Lists App
## Overview
The Ultimate Lists App is a Next.js 14-based Progressive Web App that provides an enhanced task management experience. The application uses a server-side architecture with React Server Components for optimal performance, Prisma ORM with MySQL for data persistence, and PWA capabilities for offline functionality and home screen installation on iOS devices.
The system is built on a three-tier architecture:
1. **Presentation Layer**: React components with Server Components and Client Components
2. **Business Logic Layer**: Server Actions for data mutations and queries
3. **Data Layer**: Prisma ORM with MySQL database
The application leverages Next.js App Router for routing, Server Actions for data mutations, and implements PWA features through a service worker and web app manifest.
## Architecture
### High-Level Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Client Browser │
│ ┌──────────────────────────────────────────────────┐ │
│ │ React Components (RSC + Client) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌───────────┐ │ │
│ │ │ Pages │ │ Components │ │ Hooks │ │ │
│ │ └────────────┘ └────────────┘ └───────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Service Worker (PWA) │ │
│ │ Cache API + Offline Support │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ HTTP/Server Actions
▼
┌─────────────────────────────────────────────────────────┐
│ Next.js Server │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Server Actions │ │
│ │ (lib/actions.ts - Business Logic) │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Prisma Client │ │
│ │ (lib/db.ts - Data Access) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
│ SQL
▼
┌─────────────────────────────────────────────────────────┐
│ MySQL Database │
│ (Lists, Items, Metadata) │
└─────────────────────────────────────────────────────────┘
```
### Technology Stack
- **Framework**: Next.js 14 with App Router
- **Language**: TypeScript
- **UI Library**: React 18 with Server Components
- **Styling**: Tailwind CSS
- **Database**: MySQL
- **ORM**: Prisma
- **Icons**: Lucide React
- **PWA**: Service Worker + Web App Manifest
## Components and Interfaces
### Data Access Layer
**Prisma Client (`lib/db.ts`)**
- Singleton Prisma client instance
- Connection pooling management
- Type-safe database queries
**Server Actions (`lib/actions.ts`)**
- `getLists()`: Retrieve all lists with item counts
- `createList(title, color)`: Create a new list
- `updateList(id, data)`: Update list properties
- `deleteList(id)`: Delete a list and cascade to items
- `getListWithItems(listId)`: Retrieve a specific list with all items
- `createItem(listId, title, description, metadata)`: Create a new item
- `updateItem(id, data)`: Update item properties
- `deleteItem(id)`: Delete an item
- `toggleItem(id)`: Toggle item completion status
- `searchItems(query)`: Search across all items
- `getItemsByFilter(listId, filters)`: Filter items by criteria
### Presentation Layer
**Pages**
- `/` - Home page displaying all lists
- `/list/[id]` - Individual list view with items
- `/list/[id]/item/[itemId]` - Item detail view
**Components**
- `ListCard`: Display list summary with item count
- `ListForm`: Create/edit list form
- `ItemCard`: Display individual item
- `ItemForm`: Create/edit item form with metadata
- `ItemDetail`: Full item view with all metadata
- `SearchBar`: Search input with live results
- `FilterPanel`: Filter controls for sorting and filtering
- `PriorityBadge`: Visual priority indicator
- `DueDateDisplay`: Due date with overdue indication
- `TagList`: Display and manage item tags
**Client Hooks**
- `useOptimisticUpdate`: Optimistic UI updates
- `useSearch`: Search functionality with debouncing
- `useFilters`: Filter state management
- `useOfflineSync`: Handle offline data synchronization
### PWA Layer
**Service Worker (`public/sw.js`)**
- Cache static assets (HTML, CSS, JS, images)
- Cache API responses with stale-while-revalidate strategy
- Offline fallback pages
- Background sync for pending mutations
**Web App Manifest (`public/manifest.json`)**
- App metadata (name, icons, colors)
- Display mode: standalone
- Start URL and scope
- Icon definitions for various sizes
## Data Models
### Database Schema
**List Model**
```typescript
{
id: string (cuid)
title: string
color: string (hex color)
createdAt: DateTime
updatedAt: DateTime
items: Item[]
}
```
**Item Model**
```typescript
{
id: string (cuid)
title: string
description: string | null
completed: boolean
priority: 'low' | 'medium' | 'high'
dueDate: DateTime | null
tags: string[] (JSON)
notes: string | null
createdAt: DateTime
updatedAt: DateTime
listId: string
list: List
}
```
### Client State Models
**FilterState**
```typescript
{
priority: 'low' | 'medium' | 'high' | null
completed: boolean | null
tags: string[]
dueDateRange: { start: Date | null, end: Date | null }
sortBy: 'createdAt' | 'dueDate' | 'priority' | 'title'
sortOrder: 'asc' | 'desc'
}
```
**SearchState**
```typescript
{
query: string
results: Item[]
isSearching: boolean
}
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Task Management Properties
**Property 1: Task creation preserves description**
*For any* valid task description and list, creating a task with that description should result in a task that, when retrieved, has the exact same description.
**Validates: Requirements 1.1**
**Property 2: Task updates preserve identity**
*For any* task and any valid update data, updating the task should preserve the task's unique identifier while changing the specified fields.
**Validates: Requirements 1.2**
**Property 3: Task deletion removes from queries**
*For any* task, after deleting that task, querying for tasks should not return the deleted task.
**Validates: Requirements 1.3**
**Property 4: Task metadata round-trip**
*For any* task and any valid metadata (priority, due date, tags, notes), setting the metadata then retrieving the task should return equivalent metadata values.
**Validates: Requirements 1.4, 5.1, 5.2, 5.3, 5.4**
**Property 5: Task completion toggle**
*For any* task, toggling its completion status should result in the opposite completion state when retrieved.
**Validates: Requirements 1.5**
### List Management Properties
**Property 6: List creation with unique identifiers**
*For any* valid list name, creating a list should result in a list with a unique identifier that differs from all existing list identifiers.
**Validates: Requirements 2.1**
**Property 7: List retrieval completeness**
*For any* set of created lists, querying all lists should return exactly those lists with accurate item counts.
**Validates: Requirements 2.2**
**Property 8: List filtering correctness**
*For any* list, querying items for that list should return only items where the listId matches, and should return all such items.
**Validates: Requirements 2.3**
**Property 9: Cascade deletion**
*For any* list with associated items, deleting the list should result in both the list and all its items being removed from the database.
**Validates: Requirements 2.4**
**Property 10: List rename preserves items**
*For any* list with items, renaming the list should preserve the count and identifiers of all associated items.
**Validates: Requirements 2.5**
### Data Persistence Properties
**Property 11: Application state round-trip**
*For any* application state (lists and items), persisting the state, simulating an app restart, and loading the state should result in equivalent data.
**Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5**
### Offline and PWA Properties
**Property 12: Offline resource availability**
*For any* cached resource, when the application is offline, requesting that resource should succeed using the cached version.
**Validates: Requirements 4.4**
**Property 13: Offline mutation synchronization**
*For any* mutation performed while offline, when connectivity is restored, the mutation should be applied to the server and reflected in subsequent queries.
**Validates: Requirements 4.5**
### Enhanced Features Properties
**Property 14: Overdue task identification**
*For any* task with a due date in the past relative to the current time, the task should be marked as overdue.
**Validates: Requirements 5.5**
### Sorting and Filtering Properties
**Property 15: Sort order correctness**
*For any* list of tasks and sort criteria (createdAt, dueDate, priority, title), the sorted result should be ordered according to the specified criteria and direction.
**Validates: Requirements 6.1**
**Property 16: Filter matching correctness**
*For any* filter criteria, all returned tasks should match the criteria, and all tasks in the database matching the criteria should be returned.
**Validates: Requirements 6.2**
**Property 17: Compound filter conjunction**
*For any* set of multiple filter criteria, all returned tasks should match every criterion (AND logic).
**Validates: Requirements 6.3**
**Property 18: Filter clear restoration**
*For any* filtered view, clearing all filters should return the complete unfiltered task list.
**Validates: Requirements 6.4**
**Property 19: Null due date sorting**
*For any* list of tasks sorted by due date, tasks without due dates should appear after all tasks with due dates.
**Validates: Requirements 6.5**
### Navigation Properties
**Property 20: Scroll position preservation**
*For any* scroll position in a view, navigating away and returning to that view should restore the same scroll position.
**Validates: Requirements 7.4**
### Search Properties
**Property 21: Search result completeness**
*For any* search query, all returned tasks should contain the query text in their description, notes, or tags, and all tasks containing the query should be in the results.
**Validates: Requirements 8.1**
**Property 22: Search highlight correctness**
*For any* search query and matching text, the highlight function should mark all occurrences of the query text in the result.
**Validates: Requirements 8.2**
**Property 23: Search clear restoration**
*For any* search state, clearing the search query should restore the previous unfiltered view.
**Validates: Requirements 8.3**
**Property 24: Search scope correctness**
*For any* search query, when no list is selected, results should include matches from all lists; when a list is selected, results should only include matches from that list.
**Validates: Requirements 8.5**
## Error Handling
### Database Errors
**Connection Failures**
- Retry logic with exponential backoff
- Graceful degradation to cached data
- User notification of connectivity issues
**Constraint Violations**
- Validate data before database operations
- Return descriptive error messages
- Prevent orphaned records through transactions
**Query Failures**
- Catch and log database errors
- Return empty results rather than crashing
- Provide user-friendly error messages
### Client-Side Errors
**Invalid Input**
- Validate form inputs before submission
- Display inline validation errors
- Prevent submission of invalid data
**Network Errors**
- Queue mutations for retry when offline
- Show offline indicator
- Sync when connection restored
**State Inconsistencies**
- Implement optimistic updates with rollback
- Validate state before rendering
- Recover from corrupted local storage
### PWA-Specific Errors
**Service Worker Registration**
- Gracefully handle registration failures
- Fall back to online-only mode
- Log errors for debugging
**Cache Failures**
- Handle quota exceeded errors
- Implement cache eviction strategy
- Provide fallback for uncached resources
## Testing Strategy
### Unit Testing
The application will use **Vitest** as the testing framework for unit tests. Unit tests will focus on:
**Data Access Layer**
- Server action functions (create, read, update, delete operations)
- Database query correctness
- Error handling in database operations
**Business Logic**
- Filter logic implementation
- Sort algorithm correctness
- Search algorithm accuracy
- Date calculations (overdue detection)
**Utility Functions**
- Data serialization/deserialization
- Input validation
- Text highlighting for search results
**Component Logic**
- Form validation
- State management
- Event handlers
Unit tests will use realistic test data and verify specific examples, edge cases, and error conditions. They complement property-based tests by catching concrete bugs in specific scenarios.
### Property-Based Testing
The application will use **fast-check** as the property-based testing library. Property-based tests will verify the universal correctness properties defined in this document.
**Configuration Requirements:**
- Each property-based test MUST run a minimum of 100 iterations
- Each test MUST be tagged with a comment referencing the correctness property
- Tag format: `// Feature: ultimate-lists-app, Property {number}: {property_text}`
- Each correctness property MUST be implemented by a SINGLE property-based test
**Property Test Coverage:**
- Task CRUD operations (Properties 1-5)
- List CRUD operations (Properties 6-10)
- Data persistence and round-trips (Property 11)
- Offline functionality (Properties 12-13)
- Enhanced features (Property 14)
- Sorting and filtering (Properties 15-19)
- Navigation behavior (Property 20)
- Search functionality (Properties 21-24)
**Test Data Generation:**
Property-based tests will use custom generators for:
- Valid task descriptions (non-empty strings)
- List names (non-empty strings)
- Priority levels (low, medium, high)
- Due dates (past, present, future)
- Tags (arrays of strings)
- Filter criteria (combinations of filters)
- Sort options (field and direction)
- Search queries (substrings of existing data)
### Integration Testing
Integration tests will verify:
- End-to-end user flows (create list → add items → complete items)
- Server Action integration with database
- PWA installation and offline functionality
- Service worker caching behavior
### Manual Testing
Manual testing will cover:
- PWA installation on iOS Safari
- Offline functionality in real-world scenarios
- UI/UX on various screen sizes
- Touch interaction quality
- Visual design and accessibility
## Performance Considerations
### Database Optimization
- Index frequently queried fields (listId, completed, dueDate, priority)
- Use database-level sorting and filtering where possible
- Implement pagination for large lists
- Use connection pooling for efficient database access
### Client-Side Optimization
- Use React Server Components for initial page loads
- Implement optimistic updates for perceived performance
- Lazy load item details
- Debounce search input
- Virtualize long lists
### Caching Strategy
- Cache static assets aggressively (CSS, JS, images)
- Use stale-while-revalidate for API responses
- Implement cache versioning for updates
- Set appropriate cache expiration times
### Bundle Optimization
- Code splitting by route
- Tree shaking unused code
- Minimize and compress assets
- Use Next.js automatic optimizations
## Security Considerations
### Input Validation
- Sanitize all user inputs
- Validate data types and formats
- Prevent SQL injection through Prisma's parameterized queries
- Limit input lengths to prevent abuse
### Data Protection
- Use environment variables for sensitive configuration
- Implement HTTPS in production
- Secure database credentials
- Validate all server-side inputs
### PWA Security
- Serve service worker over HTTPS
- Validate cached resources
- Implement Content Security Policy
- Use Subresource Integrity for external resources
## Deployment Considerations
### Environment Setup
- Development: Local MySQL database
- Production: Managed MySQL instance (e.g., PlanetScale, AWS RDS)
- Environment variables for database connection
- Separate environments for testing and production
### Build Process
1. Run database migrations (`prisma db push`)
2. Generate Prisma client
3. Build Next.js application
4. Generate service worker
5. Optimize and compress assets
### Hosting
- Deploy to Vercel (recommended for Next.js)
- Configure custom domain
- Set up SSL certificate
- Configure environment variables
### Monitoring
- Error tracking (e.g., Sentry)
- Performance monitoring
- Database query performance
- User analytics (optional)
## Future Enhancements
### Potential Features
- Collaborative lists (multi-user)
- Recurring tasks
- Task dependencies
- Attachments and file uploads
- Calendar integration
- Notifications and reminders
- Dark mode
- Customizable themes
- Export/import functionality
- Task templates
- Subtasks and checklists
- Time tracking
- Analytics and insights
### Technical Improvements
- Real-time synchronization with WebSockets
- Backend API for mobile apps
- Advanced caching strategies
- Performance optimizations
- Accessibility improvements
- Internationalization (i18n)

View File

@@ -0,0 +1,114 @@
# Requirements Document
## Introduction
The Ultimate Lists App is an enhanced todo list application designed to provide a superior task management experience beyond traditional todo apps. The system SHALL function as a Progressive Web App (PWA) that users can install on their iOS devices via Safari's "Add to Home Screen" feature, providing a native-like experience without requiring App Store distribution. The application SHALL offer advanced features for organizing, prioritizing, and managing tasks across multiple lists with rich metadata and flexible viewing options.
## Glossary
- **Ultimate Lists App**: The web-based task management system being specified
- **Task**: An individual item representing work to be completed, containing a description and optional metadata
- **List**: A collection of related tasks organized under a common theme or project
- **PWA (Progressive Web App)**: A web application that uses modern web capabilities to deliver an app-like experience
- **Service Worker**: A script that enables offline functionality and caching for PWAs
- **Web App Manifest**: A JSON file that provides metadata for PWA installation
- **Local Storage**: Browser-based persistent storage mechanism for data
- **Task Metadata**: Additional information about a task such as priority, due date, tags, or notes
## Requirements
### Requirement 1: Task Management
**User Story:** As a user, I want to create, edit, and delete tasks with rich metadata, so that I can capture detailed information about my work items.
#### Acceptance Criteria
1. WHEN a user enters a task description and submits it, THE Ultimate Lists App SHALL create a new task with the provided description
2. WHEN a user edits a task, THE Ultimate Lists App SHALL update the task with the modified information and preserve the task's unique identifier
3. WHEN a user deletes a task, THE Ultimate Lists App SHALL remove the task from the list and update the display immediately
4. WHEN a user adds metadata to a task, THE Ultimate Lists App SHALL store the metadata and associate it with the task
5. WHEN a user marks a task as complete, THE Ultimate Lists App SHALL update the task status and visually distinguish it from incomplete tasks
### Requirement 2: Multiple Lists
**User Story:** As a user, I want to organize tasks into multiple lists, so that I can separate different projects or areas of my life.
#### Acceptance Criteria
1. WHEN a user creates a new list, THE Ultimate Lists App SHALL create the list with a unique identifier and user-provided name
2. WHEN a user views their lists, THE Ultimate Lists App SHALL display all created lists with their names and task counts
3. WHEN a user selects a list, THE Ultimate Lists App SHALL display only the tasks belonging to that list
4. WHEN a user deletes a list, THE Ultimate Lists App SHALL remove the list and all associated tasks
5. WHEN a user renames a list, THE Ultimate Lists App SHALL update the list name while preserving all associated tasks
### Requirement 3: Data Persistence
**User Story:** As a user, I want my tasks and lists to be saved automatically, so that I don't lose my data when I close the app.
#### Acceptance Criteria
1. WHEN a user creates or modifies a task, THE Ultimate Lists App SHALL persist the change to local storage immediately
2. WHEN a user creates or modifies a list, THE Ultimate Lists App SHALL persist the change to local storage immediately
3. WHEN a user reopens the application, THE Ultimate Lists App SHALL load all previously saved tasks and lists from local storage
4. WHEN data is persisted to local storage, THE Ultimate Lists App SHALL serialize the data in a consistent format
5. WHEN data is loaded from local storage, THE Ultimate Lists App SHALL deserialize the data and restore the application state
### Requirement 4: Progressive Web App Capabilities
**User Story:** As a user, I want to install the app on my iPhone home screen, so that I can access it quickly like a native app.
#### Acceptance Criteria
1. WHEN a user visits the application in Safari, THE Ultimate Lists App SHALL provide a web app manifest with appropriate metadata
2. WHEN a user adds the app to their home screen, THE Ultimate Lists App SHALL display with a custom icon and name
3. WHEN a user opens the installed app, THE Ultimate Lists App SHALL launch in standalone mode without browser UI
4. WHEN the application is offline, THE Ultimate Lists App SHALL continue to function using cached resources via a service worker
5. WHEN the application regains connectivity, THE Ultimate Lists App SHALL synchronize any cached changes if applicable
### Requirement 5: Enhanced Task Features
**User Story:** As a user, I want advanced task features like priorities, due dates, and tags, so that I can better organize and manage my work.
#### Acceptance Criteria
1. WHEN a user assigns a priority to a task, THE Ultimate Lists App SHALL store the priority level and allow filtering by priority
2. WHEN a user sets a due date on a task, THE Ultimate Lists App SHALL store the date and display it with the task
3. WHEN a user adds tags to a task, THE Ultimate Lists App SHALL store the tags and enable filtering tasks by tag
4. WHEN a user adds notes to a task, THE Ultimate Lists App SHALL store the notes and display them when viewing task details
5. WHEN a user views tasks with due dates, THE Ultimate Lists App SHALL visually indicate overdue tasks
### Requirement 6: Task Sorting and Filtering
**User Story:** As a user, I want to sort and filter my tasks in different ways, so that I can focus on what's most important.
#### Acceptance Criteria
1. WHEN a user selects a sort option, THE Ultimate Lists App SHALL reorder tasks according to the selected criteria
2. WHEN a user applies a filter, THE Ultimate Lists App SHALL display only tasks matching the filter criteria
3. WHEN a user combines multiple filters, THE Ultimate Lists App SHALL display tasks matching all active filter criteria
4. WHEN a user clears filters, THE Ultimate Lists App SHALL restore the full task list view
5. WHEN tasks are sorted by due date, THE Ultimate Lists App SHALL place tasks without due dates at the end of the list
### Requirement 7: User Interface and Experience
**User Story:** As a user, I want a clean, intuitive interface that works well on mobile devices, so that I can efficiently manage my tasks on the go.
#### Acceptance Criteria
1. WHEN a user interacts with the application on a mobile device, THE Ultimate Lists App SHALL provide touch-optimized controls with appropriate sizing
2. WHEN a user views the application, THE Ultimate Lists App SHALL display a responsive layout that adapts to different screen sizes
3. WHEN a user performs an action, THE Ultimate Lists App SHALL provide immediate visual feedback
4. WHEN a user navigates between views, THE Ultimate Lists App SHALL maintain smooth transitions and preserve scroll position where appropriate
5. WHEN the application displays lists and tasks, THE Ultimate Lists App SHALL use clear visual hierarchy and readable typography
### Requirement 8: Task Search
**User Story:** As a user, I want to search across all my tasks and lists, so that I can quickly find specific items.
#### Acceptance Criteria
1. WHEN a user enters a search query, THE Ultimate Lists App SHALL display tasks containing the query text in their description, notes, or tags
2. WHEN a user searches, THE Ultimate Lists App SHALL highlight matching text in search results
3. WHEN a user clears a search query, THE Ultimate Lists App SHALL restore the previous view
4. WHEN a user searches with no results, THE Ultimate Lists App SHALL display a message indicating no matches were found
5. WHEN a user performs a search, THE Ultimate Lists App SHALL search across all lists unless a specific list is selected

View File

@@ -33,7 +33,7 @@
- **Property 10: List rename preserves items** - **Property 10: List rename preserves items**
- **Validates: Requirements 2.5** - **Validates: Requirements 2.5**
- [ ] 3. Implement search functionality - [x] 3. Implement search functionality
- Create searchItems server action that searches across title, description, notes, and tags - Create searchItems server action that searches across title, description, notes, and tags
- Add support for list-scoped search vs global search - Add support for list-scoped search vs global search
- Implement text highlighting utility function - Implement text highlighting utility function
@@ -51,7 +51,7 @@
- **Property 24: Search scope correctness** - **Property 24: Search scope correctness**
- **Validates: Requirements 8.5** - **Validates: Requirements 8.5**
- [ ] 4. Implement filtering and sorting - [x] 4. Implement filtering and sorting
- Create getItemsByFilter server action with support for priority, completed, tags, and due date filters - Create getItemsByFilter server action with support for priority, completed, tags, and due date filters
- Implement compound filter logic (AND conditions) - Implement compound filter logic (AND conditions)
- Add sorting by createdAt, dueDate, priority, and title - Add sorting by createdAt, dueDate, priority, and title
@@ -74,7 +74,7 @@
- **Property 19: Null due date sorting** - **Property 19: Null due date sorting**
- **Validates: Requirements 6.5** - **Validates: Requirements 6.5**
- [ ] 5. Create item detail page and form components - [x] 5. Create item detail page and form components
- Build /list/[id]/item/[itemId] page for viewing full item details - Build /list/[id]/item/[itemId] page for viewing full item details
- Create ItemForm component with fields for all metadata (priority, due date, tags, notes) - Create ItemForm component with fields for all metadata (priority, due date, tags, notes)
- Create ItemDetail component to display all item information - Create ItemDetail component to display all item information

2
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,41 @@
import { notFound } from 'next/navigation'
import { getItem } from '@/lib/actions'
import ItemForm from '@/components/ItemForm'
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
interface PageProps {
params: {
id: string
itemId: string
}
}
export default async function EditItemPage({ params }: PageProps) {
const item = await getItem(params.itemId)
if (!item || item.listId !== params.id) {
notFound()
}
return (
<main className="container mx-auto px-4 py-8 max-w-2xl">
{/* Header */}
<div className="flex items-center gap-4 mb-6">
<Link
href={`/list/${params.id}/item/${params.itemId}`}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
>
<ArrowLeft size={20} />
<span>Back</span>
</Link>
<h1 className="text-2xl font-bold text-gray-900">Edit Task</h1>
</div>
{/* Form */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<ItemForm listId={params.id} item={item} mode="edit" />
</div>
</main>
)
}

View File

@@ -0,0 +1,54 @@
import { notFound } from 'next/navigation'
import { getItem } from '@/lib/actions'
import ItemDetail from '@/components/ItemDetail'
import Link from 'next/link'
import { ArrowLeft, Edit, Trash2 } from 'lucide-react'
import DeleteItemButton from '@/components/DeleteItemButton'
import ToggleItemButton from '@/components/ToggleItemButton'
interface PageProps {
params: {
id: string
itemId: string
}
}
export default async function ItemDetailPage({ params }: PageProps) {
const item = await getItem(params.itemId)
if (!item || item.listId !== params.id) {
notFound()
}
return (
<main className="container mx-auto px-4 py-8 max-w-3xl">
{/* Header Actions */}
<div className="flex items-center justify-between mb-6">
<Link
href={`/list/${params.id}`}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors"
>
<ArrowLeft size={20} />
<span>Back to List</span>
</Link>
<div className="flex items-center gap-2">
<ToggleItemButton itemId={item.id} completed={item.completed} />
<Link
href={`/list/${params.id}/item/${params.itemId}/edit`}
className="flex items-center gap-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors"
>
<Edit size={18} />
<span>Edit</span>
</Link>
<DeleteItemButton itemId={item.id} listId={params.id} />
</div>
</div>
{/* Item Detail */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<ItemDetail item={item} listTitle={item.list.title} />
</div>
</main>
)
}

View File

@@ -0,0 +1,60 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { deleteItem } from '@/lib/actions'
import { Trash2 } from 'lucide-react'
interface DeleteItemButtonProps {
itemId: string
listId: string
}
export default function DeleteItemButton({ itemId, listId }: DeleteItemButtonProps) {
const router = useRouter()
const [isDeleting, setIsDeleting] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const handleDelete = async () => {
setIsDeleting(true)
try {
await deleteItem(itemId)
router.push(`/list/${listId}`)
} catch (error) {
console.error('Error deleting item:', error)
setIsDeleting(false)
setShowConfirm(false)
}
}
if (showConfirm) {
return (
<div className="flex items-center gap-2">
<button
onClick={handleDelete}
disabled={isDeleting}
className="px-3 py-2 bg-red-500 hover:bg-red-600 disabled:bg-red-300 text-white rounded-lg text-sm transition-colors"
>
{isDeleting ? 'Deleting...' : 'Confirm'}
</button>
<button
onClick={() => setShowConfirm(false)}
disabled={isDeleting}
className="px-3 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg text-sm transition-colors"
>
Cancel
</button>
</div>
)
}
return (
<button
onClick={() => setShowConfirm(true)}
className="flex items-center gap-2 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg transition-colors"
>
<Trash2 size={18} />
<span>Delete</span>
</button>
)
}

163
components/ItemDetail.tsx Normal file
View 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>
)
}

267
components/ItemForm.tsx Normal file
View File

@@ -0,0 +1,267 @@
'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>
)
}

View File

@@ -0,0 +1,45 @@
'use client'
import { useState } from 'react'
import { toggleItem } from '@/lib/actions'
import { CheckCircle2, Circle } from 'lucide-react'
interface ToggleItemButtonProps {
itemId: string
completed: boolean
}
export default function ToggleItemButton({ itemId, completed }: ToggleItemButtonProps) {
const [isToggling, setIsToggling] = useState(false)
const handleToggle = async () => {
setIsToggling(true)
try {
await toggleItem(itemId)
} catch (error) {
console.error('Error toggling item:', error)
} finally {
setIsToggling(false)
}
}
return (
<button
onClick={handleToggle}
disabled={isToggling}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 disabled:bg-gray-50 text-gray-700 rounded-lg transition-colors"
>
{completed ? (
<>
<CheckCircle2 size={18} className="text-green-500" />
<span>Mark Incomplete</span>
</>
) : (
<>
<Circle size={18} />
<span>Mark Complete</span>
</>
)}
</button>
)
}

View File

@@ -38,12 +38,35 @@ export async function getListWithItems(listId: string) {
}) })
} }
export async function createItem(listId: string, title: string, description?: string) { export async function getItem(itemId: string) {
return await prisma.item.findUnique({
where: { id: itemId },
include: {
list: true,
},
})
}
export async function createItem(
listId: string,
title: string,
description?: string,
metadata?: {
priority?: 'low' | 'medium' | 'high'
dueDate?: Date | null
tags?: string[]
notes?: string | null
}
) {
const item = await prisma.item.create({ const item = await prisma.item.create({
data: { data: {
title, title,
description, description,
listId, listId,
...(metadata?.priority && { priority: metadata.priority }),
...(metadata?.dueDate !== undefined && { dueDate: metadata.dueDate }),
...(metadata?.tags && { tags: metadata.tags }),
...(metadata?.notes !== undefined && { notes: metadata.notes }),
}, },
}) })
revalidatePath(`/list/${listId}`) revalidatePath(`/list/${listId}`)