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:
178
components/ListDetailView.tsx
Normal file
178
components/ListDetailView.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user