diff --git a/.kiro/specs/ultimate-lists-app/tasks.md b/.kiro/specs/ultimate-lists-app/tasks.md index 153e227..ed3e225 100644 --- a/.kiro/specs/ultimate-lists-app/tasks.md +++ b/.kiro/specs/ultimate-lists-app/tasks.md @@ -215,7 +215,7 @@ - Sanitize user inputs - _Requirements: 1.1, 2.1_ -- [ ] 18. Optimize performance +- [-] 18. Optimize performance - Add database indexes for frequently queried fields - Implement pagination for large lists - Optimize bundle size with code splitting diff --git a/components/EmptyState.tsx b/components/EmptyState.tsx index f46c68d..b92b696 100644 --- a/components/EmptyState.tsx +++ b/components/EmptyState.tsx @@ -1,6 +1,6 @@ 'use client' -import { ReactNode } from 'react' +import { memo, ReactNode } from 'react' import { Inbox, ListTodo, Search, Filter } from 'lucide-react' interface EmptyStateProps { @@ -10,7 +10,7 @@ interface EmptyStateProps { action?: ReactNode } -export default function EmptyState({ icon = 'inbox', title, description, action }: EmptyStateProps) { +function EmptyState({ icon = 'inbox', title, description, action }: EmptyStateProps) { const icons = { inbox: , list: , @@ -39,3 +39,5 @@ export default function EmptyState({ icon = 'inbox', title, description, action ) } + +export default memo(EmptyState) diff --git a/components/FilterPanel.tsx b/components/FilterPanel.tsx index 81554f8..73b3488 100644 --- a/components/FilterPanel.tsx +++ b/components/FilterPanel.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { memo, useState } from 'react' import { Filter, X, ChevronDown, ChevronUp } from 'lucide-react' export interface FilterState { @@ -19,7 +19,7 @@ interface FilterPanelProps { showListScope?: boolean } -export default function FilterPanel({ +function FilterPanel({ filters, onFiltersChange, availableTags = [], @@ -294,3 +294,5 @@ export default function FilterPanel({ ) } + +export default memo(FilterPanel) diff --git a/components/ItemCard.tsx b/components/ItemCard.tsx index d4de6be..9b1a7fd 100644 --- a/components/ItemCard.tsx +++ b/components/ItemCard.tsx @@ -1,5 +1,6 @@ 'use client' +import { memo } from 'react' import Link from 'next/link' import { Calendar, Tag, AlertCircle } from 'lucide-react' import ToggleItemButton from './ToggleItemButton' @@ -19,7 +20,7 @@ interface ItemCardProps { listId: string } -export default function ItemCard({ item, listId }: ItemCardProps) { +function ItemCard({ item, listId }: ItemCardProps) { const tags = Array.isArray(item.tags) ? item.tags : [] const isOverdue = item.dueDate && new Date(item.dueDate) < new Date() && !item.completed @@ -104,3 +105,5 @@ export default function ItemCard({ item, listId }: ItemCardProps) { ) } + +export default memo(ItemCard) diff --git a/components/ListCard.tsx b/components/ListCard.tsx index 435925d..b3cb0dc 100644 --- a/components/ListCard.tsx +++ b/components/ListCard.tsx @@ -1,7 +1,7 @@ 'use client' +import { memo, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { Edit2 } from 'lucide-react' import Modal from './Modal' import ListForm from './ListForm' @@ -17,7 +17,7 @@ interface List { }> } -export default function ListCard({ list }: { list: List }) { +function ListCard({ list }: { list: List }) { const [isEditModalOpen, setIsEditModalOpen] = useState(false) const completedCount = list.items.filter(item => item.completed).length const totalCount = list.items.length @@ -94,4 +94,6 @@ export default function ListCard({ list }: { list: List }) { ) -} \ No newline at end of file +} + +export default memo(ListCard) \ No newline at end of file diff --git a/components/ListDetailView.tsx b/components/ListDetailView.tsx index d2877a8..1ab2eb3 100644 --- a/components/ListDetailView.tsx +++ b/components/ListDetailView.tsx @@ -93,7 +93,9 @@ export default function ListDetailView({ listId, initialItems }: ListDetailViewP sortBy: newFilters.sortBy, sortOrder: newFilters.sortOrder }) - setFilteredItems(results as Item[]) + // Extract items from pagination result + const items = 'items' in results ? results.items : results + setFilteredItems(items as Item[]) } catch (error) { console.error('Filter error:', error) setFilteredItems(initialItems) diff --git a/components/SearchBar.tsx b/components/SearchBar.tsx index a949bf3..57a03e1 100644 --- a/components/SearchBar.tsx +++ b/components/SearchBar.tsx @@ -1,5 +1,6 @@ 'use client' +import { memo } from 'react' import { Search, X } from 'lucide-react' import { useSearch } from '@/hooks' @@ -10,7 +11,7 @@ interface SearchBarProps { initialValue?: string } -export default function SearchBar({ +function SearchBar({ onSearch, placeholder = 'Search tasks...', debounceMs = 300, @@ -53,3 +54,5 @@ export default function SearchBar({ ) } + +export default memo(SearchBar) diff --git a/components/SearchResults.tsx b/components/SearchResults.tsx index 13f6b01..9a52938 100644 --- a/components/SearchResults.tsx +++ b/components/SearchResults.tsx @@ -1,5 +1,6 @@ 'use client' +import { memo } from 'react' import { highlightText } from '@/lib/utils' import Link from 'next/link' import { Calendar, Tag, AlertCircle } from 'lucide-react' @@ -25,7 +26,7 @@ interface SearchResultsProps { onClose?: () => void } -export default function SearchResults({ results, query, onClose }: SearchResultsProps) { +function SearchResults({ results, query, onClose }: SearchResultsProps) { if (!query.trim()) { return null } @@ -155,3 +156,5 @@ export default function SearchResults({ results, query, onClose }: SearchResults ) } + +export default memo(SearchResults) diff --git a/lib/actions.ts b/lib/actions.ts index 4435752..f15b8f3 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -525,6 +525,8 @@ export async function getItemsByFilter( dueDateRange?: { start: Date | null; end: Date | null } sortBy?: 'createdAt' | 'dueDate' | 'priority' | 'title' sortOrder?: 'asc' | 'desc' + page?: number + pageSize?: number } = {} ) { try { @@ -560,14 +562,26 @@ export async function getItemsByFilter( } } - // Fetch items from database - const items = await retryOperation(() => - prisma.item.findMany({ - where: whereClause, - include: { - list: true, - }, - }) + // Pagination parameters + const page = filters.page || 1 + const pageSize = filters.pageSize || 50 + const skip = (page - 1) * pageSize + + // Fetch items from database with pagination + const [items, totalCount] = await retryOperation(() => + Promise.all([ + prisma.item.findMany({ + where: whereClause, + include: { + list: true, + }, + skip, + take: pageSize, + }), + prisma.item.count({ + where: whereClause, + }), + ]) ) // Filter by tags (client-side since tags are stored as JSON) @@ -618,7 +632,16 @@ export async function getItemsByFilter( return sortOrder === 'asc' ? comparison : -comparison }) - return sortedItems + return { + items: sortedItems, + pagination: { + page, + pageSize, + totalCount, + totalPages: Math.ceil(totalCount / pageSize), + hasMore: page * pageSize < totalCount, + }, + } } catch (error) { console.error('Error filtering items:', error) throw new Error('Failed to filter items. Please try again.') diff --git a/next.config.js b/next.config.js index a0501d7..9761814 100644 --- a/next.config.js +++ b/next.config.js @@ -1,8 +1,23 @@ /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { - serverActions: true, + optimizePackageImports: ['lucide-react'], }, + compiler: { + removeConsole: process.env.NODE_ENV === 'production' ? { + exclude: ['error', 'warn'], + } : false, + }, + // Enable SWC minification for better performance + swcMinify: true, + // Optimize images + images: { + formats: ['image/avif', 'image/webp'], + }, + // Enable compression + compress: true, + // Production source maps (smaller) + productionBrowserSourceMaps: false, } module.exports = nextConfig \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8a9e93f..cd1fb37 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,4 +29,12 @@ model Item { updatedAt DateTime @updatedAt listId String list List @relation(fields: [listId], references: [id], onDelete: Cascade) + + @@index([listId]) + @@index([completed]) + @@index([priority]) + @@index([dueDate]) + @@index([createdAt]) + @@index([listId, completed]) + @@index([listId, priority]) }