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:
536
.kiro/specs/ultimate-lists-app/design.md
Normal file
536
.kiro/specs/ultimate-lists-app/design.md
Normal 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)
|
||||
114
.kiro/specs/ultimate-lists-app/requirements.md
Normal file
114
.kiro/specs/ultimate-lists-app/requirements.md
Normal 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
|
||||
@@ -33,7 +33,7 @@
|
||||
- **Property 10: List rename preserves items**
|
||||
- **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
|
||||
- Add support for list-scoped search vs global search
|
||||
- Implement text highlighting utility function
|
||||
@@ -51,7 +51,7 @@
|
||||
- **Property 24: Search scope correctness**
|
||||
- **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
|
||||
- Implement compound filter logic (AND conditions)
|
||||
- Add sorting by createdAt, dueDate, priority, and title
|
||||
@@ -74,7 +74,7 @@
|
||||
- **Property 19: Null due date sorting**
|
||||
- **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
|
||||
- Create ItemForm component with fields for all metadata (priority, due date, tags, notes)
|
||||
- Create ItemDetail component to display all item information
|
||||
|
||||
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
41
app/list/[id]/item/[itemId]/edit/page.tsx
Normal file
41
app/list/[id]/item/[itemId]/edit/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
54
app/list/[id]/item/[itemId]/page.tsx
Normal file
54
app/list/[id]/item/[itemId]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
60
components/DeleteItemButton.tsx
Normal file
60
components/DeleteItemButton.tsx
Normal 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
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>
|
||||
)
|
||||
}
|
||||
267
components/ItemForm.tsx
Normal file
267
components/ItemForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
45
components/ToggleItemButton.tsx
Normal file
45
components/ToggleItemButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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({
|
||||
data: {
|
||||
title,
|
||||
description,
|
||||
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}`)
|
||||
|
||||
Reference in New Issue
Block a user