feat: Add search and filter UI components

- Created SearchBar component with debounced input
- Built FilterPanel component with controls for priority, completion, tags, and due date
- Added sort controls (field and direction)
- Implemented filter clear functionality
- Display search results with highlighted text
- Added GlobalSearch component for home page
- Integrated search and filter into list detail view
- Added CSS styles for search highlighting

Requirements: 6.1, 6.2, 6.3, 6.4, 8.1, 8.2, 8.3
This commit is contained in:
2025-12-01 19:25:09 +00:00
parent 477e378d08
commit 0143097e69
9 changed files with 781 additions and 43 deletions

View File

@@ -0,0 +1,178 @@
'use client'
import { useState, useCallback, useMemo } from 'react'
import ItemCard from './ItemCard'
import SearchBar from './SearchBar'
import FilterPanel, { FilterState } from './FilterPanel'
import SearchResults from './SearchResults'
import { searchItems, getItemsByFilter } from '@/lib/actions'
interface 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
}
interface ItemWithList extends Item {
list: {
id: string
title: string
color: string
}
}
interface ListDetailViewProps {
listId: string
initialItems: Item[]
}
export default function ListDetailView({ listId, initialItems }: ListDetailViewProps) {
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<ItemWithList[]>([])
const [isSearching, setIsSearching] = useState(false)
const [filters, setFilters] = useState<FilterState>({
priority: null,
completed: null,
tags: [],
dueDateRange: { start: null, end: null },
sortBy: 'createdAt',
sortOrder: 'desc'
})
const [filteredItems, setFilteredItems] = useState<Item[]>(initialItems)
// Extract all unique tags from items
const availableTags = useMemo(() => {
const tagSet = new Set<string>()
initialItems.forEach(item => {
const tags = Array.isArray(item.tags) ? item.tags : []
tags.forEach(tag => {
if (typeof tag === 'string') {
tagSet.add(tag)
}
})
})
return Array.from(tagSet).sort()
}, [initialItems])
// Handle search
const handleSearch = useCallback(async (query: string) => {
setSearchQuery(query)
if (!query.trim()) {
setSearchResults([])
setIsSearching(false)
return
}
setIsSearching(true)
try {
const results = await searchItems(query, listId)
setSearchResults(results as ItemWithList[])
} catch (error) {
console.error('Search error:', error)
setSearchResults([])
}
}, [listId])
// Handle filter changes
const handleFiltersChange = useCallback(async (newFilters: FilterState) => {
setFilters(newFilters)
try {
const results = await getItemsByFilter(listId, {
priority: newFilters.priority,
completed: newFilters.completed,
tags: newFilters.tags,
dueDateRange: newFilters.dueDateRange,
sortBy: newFilters.sortBy,
sortOrder: newFilters.sortOrder
})
setFilteredItems(results as Item[])
} catch (error) {
console.error('Filter error:', error)
setFilteredItems(initialItems)
}
}, [listId, initialItems])
// Determine which items to display
const displayItems = searchQuery.trim() ? searchResults : filteredItems
const completedItems = displayItems.filter(item => item.completed)
const incompleteItems = displayItems.filter(item => !item.completed)
return (
<div className="space-y-6">
{/* Search Bar */}
<SearchBar
onSearch={handleSearch}
placeholder="Search tasks in this list..."
/>
{/* Filter Panel */}
<FilterPanel
filters={filters}
onFiltersChange={handleFiltersChange}
availableTags={availableTags}
/>
{/* Results */}
{searchQuery.trim() ? (
<SearchResults
results={searchResults}
query={searchQuery}
/>
) : (
<div className="space-y-6">
{/* Incomplete Items */}
{incompleteItems.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">
To Do ({incompleteItems.length})
</h2>
<div className="space-y-2">
{incompleteItems.map((item) => (
<ItemCard key={item.id} item={item} listId={listId} />
))}
</div>
</div>
)}
{/* Completed Items */}
{completedItems.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-500 mb-3">
Completed ({completedItems.length})
</h2>
<div className="space-y-2">
{completedItems.map((item) => (
<ItemCard key={item.id} item={item} listId={listId} />
))}
</div>
</div>
)}
{/* Empty State */}
{displayItems.length === 0 && (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<p className="text-gray-500 text-lg mb-2">No tasks found</p>
<p className="text-gray-400">
{filters.priority || filters.completed !== null || filters.tags.length > 0
? 'Try adjusting your filters'
: 'Add your first task to get started'}
</p>
</div>
)}
</div>
)}
</div>
)
}