import { useEffect, useState, useCallback, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { SearchApi } from '../api/client.js'; import EmailRow from '../components/EmailRow.jsx'; import EmailDetail from '../components/EmailDetail.jsx'; import BulkToolbar from '../components/BulkToolbar.jsx'; import { Skeleton, EmptyState } from '../components/ui'; import useSelection from '../hooks/useSelection.js'; import useListKeyboardNav from '../hooks/useListKeyboardNav.js'; import useSavedSearches from '../hooks/useSavedSearches.js'; const PAGE_SIZE = 50; function ListSkeleton({ rows = 6 }) { return ( ); } export default function SearchResults() { const [searchParams] = useSearchParams(); const q = searchParams.get('q') ?? ''; const [emails, setEmails] = useState([]); const [totalCount, setTotalCount] = useState(null); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedEmail, setSelectedEmail] = useState(null); const { selected, toggle, clear, removeIds, selectedIds } = useSelection(); const focusedId = useListKeyboardNav(emails, (id) => setEmails((prev) => prev.filter((x) => x.id !== id))); const { searches: savedSearches, add: addSavedSearch } = useSavedSearches(); const isSaved = savedSearches.some((s) => s.query === q); const handleSave = () => { const label = window.prompt('Name this saved search:', q); if (label && label.trim()) addSavedSearch(label.trim(), q); }; useEffect(() => { setEmails([]); setTotalCount(null); setPage(1); setHasMore(true); setError(null); setSelectedEmail(null); clear(); }, [q, clear]); const fetchPage = useCallback((p) => { if (!q.trim()) return; setLoading(true); SearchApi.query(q, p, PAGE_SIZE) .then((r) => { setEmails((prev) => p === 1 ? r.items : [...prev, ...r.items]); setTotalCount(r.totalCount); setHasMore(p < r.totalPages); setPage(p); }) .catch(() => setError('Search failed. Please try again.')) .finally(() => setLoading(false)); }, [q]); useEffect(() => { fetchPage(1); }, [fetchPage]); const sentinelRef = useRef(null); useEffect(() => { const el = sentinelRef.current; if (!el) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore && !loading) fetchPage(page + 1); }, { rootMargin: '200px' } ); observer.observe(el); return () => observer.disconnect(); }, [hasMore, loading, page, fetchPage]); return (

🔍 "{q}"

{totalCount !== null && ( {totalCount.toLocaleString()} result{totalCount !== 1 ? 's' : ''} )} {q.trim() && ( )}
{!q.trim() && ( )} {error &&
{error}
} { if (action === 'trash' || action === 'archive') { setEmails((prev) => prev.filter((x) => !ids.includes(x.id))); removeIds(ids); } }} /> {q.trim() && (
{loading && emails.length === 0 && !error && } {!loading && !error && emails.length === 0 && !hasMore && ( )} {emails.length > 0 && ( {emails.map((e) => ( setSelectedEmail(email)} onRemove={(id) => { setEmails((prev) => prev.filter((x) => x.id !== id)); setSelectedEmail((cur) => (cur?.id === id ? null : cur)); }} /> ))}
)}
{loading && emails.length > 0 &&
Loading…
} {!hasMore && emails.length > 0 && (
— {emails.length.toLocaleString()} results —
)}
{selectedEmail && ( )}
)}
); }