Files
Todo/components/SearchResults.tsx
Paul Bailey 0143097e69 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
2025-12-01 19:25:09 +00:00

156 lines
5.1 KiB
TypeScript

'use client'
import { highlightText } from '@/lib/utils'
import Link from 'next/link'
import { Calendar, Tag, AlertCircle } from 'lucide-react'
interface SearchResultsProps {
results: Array<{
id: string
title: string
description: string | null
completed: boolean
priority: string
dueDate: Date | null
tags: any
notes: string | null
list: {
id: string
title: string
color: string
}
}>
query: string
onClose?: () => void
}
export default function SearchResults({ results, query, onClose }: SearchResultsProps) {
if (!query.trim()) {
return null
}
if (results.length === 0) {
return (
<div className="bg-white rounded-lg border border-gray-200 p-8 text-center">
<p className="text-gray-500">No tasks found matching "{query}"</p>
<p className="text-sm text-gray-400 mt-2">Try a different search term</p>
</div>
)
}
const priorityColors = {
low: 'bg-green-100 text-green-700 border-green-200',
medium: 'bg-yellow-100 text-yellow-700 border-yellow-200',
high: 'bg-red-100 text-red-700 border-red-200',
}
return (
<div className="space-y-2">
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-gray-600">
Found {results.length} {results.length === 1 ? 'task' : 'tasks'}
</p>
</div>
{results.map((item) => {
const tags = Array.isArray(item.tags) ? item.tags : []
const isOverdue = item.dueDate && new Date(item.dueDate) < new Date() && !item.completed
const priorityColor = priorityColors[item.priority as keyof typeof priorityColors] || priorityColors.medium
return (
<Link
key={item.id}
href={`/list/${item.list.id}/item/${item.id}`}
onClick={onClose}
className="block bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow"
>
{/* List Badge */}
<div className="flex items-center gap-2 mb-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: item.list.color }}
/>
<span className="text-xs text-gray-500">{item.list.title}</span>
</div>
{/* Title with highlighting */}
<h3
className={`font-medium mb-1 ${
item.completed ? 'text-gray-500 line-through' : 'text-gray-900'
}`}
dangerouslySetInnerHTML={{
__html: highlightText(item.title, query)
}}
/>
{/* Description with highlighting */}
{item.description && (
<p
className={`text-sm mb-2 line-clamp-2 ${
item.completed ? 'text-gray-400' : 'text-gray-600'
}`}
dangerouslySetInnerHTML={{
__html: highlightText(item.description, query)
}}
/>
)}
{/* Notes with highlighting (if matches) */}
{item.notes && item.notes.toLowerCase().includes(query.toLowerCase()) && (
<p
className="text-sm text-gray-500 mb-2 line-clamp-1"
dangerouslySetInnerHTML={{
__html: 'Notes: ' + highlightText(item.notes, query)
}}
/>
)}
{/* Metadata */}
<div className="flex flex-wrap items-center gap-2 mt-2">
{/* Priority Badge */}
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium border ${priorityColor}`}>
<AlertCircle size={12} />
{item.priority.charAt(0).toUpperCase() + item.priority.slice(1)}
</span>
{/* Due Date */}
{item.dueDate && (
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs ${
isOverdue
? 'bg-red-100 text-red-700 border border-red-200 font-semibold'
: 'bg-gray-100 text-gray-700'
}`}>
<Calendar size={12} />
{new Date(item.dueDate).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})}
{isOverdue && ' (Overdue)'}
</span>
)}
{/* Tags with highlighting */}
{tags.length > 0 && (
<div className="inline-flex items-center gap-1">
<Tag size={12} className="text-gray-400" />
<span
className="text-xs text-gray-600"
dangerouslySetInnerHTML={{
__html: highlightText(tags.slice(0, 2).join(', '), query)
}}
/>
{tags.length > 2 && (
<span className="text-xs text-gray-600">
{` +${tags.length - 2}`}
</span>
)}
</div>
)}
</div>
</Link>
)
})}
</div>
)
}