129 lines
6.8 KiB
TypeScript
129 lines
6.8 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { format } from 'date-fns';
|
|
|
|
interface SearchModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
tasks: any[];
|
|
events: any[];
|
|
onSelectTask: (date: Date) => void;
|
|
}
|
|
|
|
export default function SearchModal({ isOpen, onClose, tasks, events, onSelectTask }: SearchModalProps) {
|
|
const [query, setQuery] = useState('');
|
|
const [results, setResults] = useState<{ type: 'task' | 'event', item: any }[]>([]);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (isOpen && inputRef.current) {
|
|
inputRef.current.focus();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
useEffect(() => {
|
|
if (!query.trim()) {
|
|
setResults([]);
|
|
return;
|
|
}
|
|
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
const filteredTasks = tasks.filter(t =>
|
|
t.title.toLowerCase().includes(lowerQuery) && !t.somedayListId
|
|
).map(t => ({ type: 'task' as const, item: t }));
|
|
|
|
const filteredEvents = events.filter(e =>
|
|
e.title.toLowerCase().includes(lowerQuery)
|
|
).map(e => ({ type: 'event' as const, item: e }));
|
|
|
|
setResults([...filteredTasks, ...filteredEvents].slice(0, 10));
|
|
}, [query, tasks, events]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-start justify-center pt-20" onClick={onClose}>
|
|
<div className="bg-white rounded-lg shadow-2xl w-[600px] max-w-[90%] overflow-hidden" onClick={e => e.stopPropagation()}>
|
|
<div className="p-4 border-b border-gray-100 flex items-center gap-3">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-gray-400">
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
</svg>
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
placeholder="Search tasks and events..."
|
|
className="flex-1 text-lg outline-none text-gray-700 placeholder-gray-400"
|
|
value={query}
|
|
onChange={e => setQuery(e.target.value)}
|
|
onKeyDown={e => e.key === 'Escape' && onClose()}
|
|
/>
|
|
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="max-h-[60vh] overflow-y-auto">
|
|
{results.length > 0 ? (
|
|
<div className="py-2">
|
|
{results.map((result, idx) => (
|
|
<div
|
|
key={`${result.type}-${result.item.id}-${idx}`}
|
|
className="px-4 py-3 hover:bg-gray-50 cursor-pointer flex items-center gap-3 border-b border-gray-50 last:border-0"
|
|
onClick={() => {
|
|
const date = result.type === 'task'
|
|
? (result.item.scheduledDate ? new Date(result.item.scheduledDate) : new Date())
|
|
: (new Date(result.item.startTime));
|
|
|
|
onSelectTask(date);
|
|
onClose();
|
|
}}
|
|
>
|
|
<div className={`p-2 rounded-full ${result.type === 'task' ? 'bg-blue-100 text-blue-600' : 'bg-teal-100 text-teal-600'}`}>
|
|
{result.type === 'task' ? (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path><path d="m9 12 2 2 4-4"></path></svg>
|
|
) : (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
|
)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-gray-800">{result.item.title}</div>
|
|
<div className="text-xs text-gray-500">
|
|
{format(
|
|
result.type === 'task'
|
|
? (result.item.scheduledDate ? new Date(result.item.scheduledDate) : new Date())
|
|
: new Date(result.item.startTime),
|
|
'PPP'
|
|
)}
|
|
</div>
|
|
</div>
|
|
{result.type === 'task' && result.item.completed && (
|
|
<span className="text-xs bg-gray-100 text-gray-500 px-2 py-1 rounded">Completed</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : query.trim() ? (
|
|
<div className="p-8 text-center text-gray-400">
|
|
No Item found.
|
|
</div>
|
|
) : (
|
|
<div className="p-8 text-center text-gray-400">
|
|
Start typing to search...
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<style jsx>{`
|
|
.slide-in-from-top-2 { animation: slideIn 0.2s ease-out; }
|
|
@keyframes slideIn {
|
|
from { opacity: 0; transform: translateY(-10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|