import { useEffect, useState, useCallback, useRef } from 'react'; import { useParams } from 'react-router-dom'; import { SearchApi } from '../api/client.js'; const FOLDER_META = { inbox: { icon: '๐Ÿ“ฅ', label: 'Inbox' }, allmail: { icon: '๐Ÿ“ฌ', label: 'All Mail' }, unread: { icon: '๐Ÿ”ต', label: 'Unread' }, starred: { icon: 'โญ', label: 'Starred' }, sent: { icon: '๐Ÿ“ค', label: 'Sent' }, drafts: { icon: 'โœ๏ธ', label: 'Drafts' }, archive: { icon: '๐Ÿ“ฆ', label: 'Archive' }, spam: { icon: '๐Ÿšซ', label: 'Spam' }, trash: { icon: '๐Ÿ—‘๏ธ', label: 'Trash' }, unlabeled: { icon: '๐Ÿท๏ธ', label: 'Unlabeled' }, pinned: { icon: '๐Ÿ“Œ', label: 'Pinned' }, readlater: { icon: '๐Ÿ”–', label: 'Read Later' }, large: { icon: '๐Ÿ“Ž', label: 'Large Mail' }, old: { icon: '๐Ÿ•ฐ๏ธ', label: 'Old Mail' }, automated: { icon: '๐Ÿค–', label: 'Automated' }, noreply: { icon: '๐Ÿ”‡', label: 'No-Reply' }, shopping: { icon: '๐Ÿ›๏ธ', label: 'Online Shopping' }, gaming: { icon: '๐ŸŽฎ', label: 'Gaming' }, finance: { icon: '๐Ÿ’ณ', label: 'Finance & Insurance' }, sales: { icon: '๐Ÿท๏ธ', label: 'Seasonal Sales' }, ridesharing:{ icon: '๐Ÿš—', label: 'Ride Sharing' }, food: { icon: '๐Ÿ•', label: 'Food Delivery' }, social: { icon: '๐Ÿ“ฑ', label: 'Social Notifications' }, wellness: { icon: '๐Ÿƒ', label: 'Wellness & Sport' }, }; const PAGE_SIZE = 50; const fmtDate = (iso) => { const d = new Date(iso); const now = new Date(); const diffDays = (now - d) / 86400000; if (diffDays < 1) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (diffDays < 7) return d.toLocaleDateString([], { weekday: 'short' }); return d.toLocaleDateString([], { month: 'short', day: 'numeric' }); }; const fmtSize = (b) => { if (b < 1024) return `${b} B`; if (b < 1048576) return `${(b / 1024).toFixed(0)} KB`; return `${(b / 1048576).toFixed(1)} MB`; }; export default function FolderView() { const { slug } = useParams(); const meta = FOLDER_META[slug] ?? { icon: '๐Ÿ“', label: slug }; 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); // Reset when the folder changes useEffect(() => { setEmails([]); setTotalCount(null); setPage(1); setHasMore(true); setError(null); }, [slug]); // Fetch a page and append results const fetchPage = useCallback((p) => { setLoading(true); SearchApi.folder(slug, 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('Failed to load emails.')) .finally(() => setLoading(false)); }, [slug]); // Initial load useEffect(() => { fetchPage(1); }, [fetchPage]); // Sentinel div observed to trigger next page 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 (

{meta.icon} {meta.label}

{totalCount !== null && ( {totalCount.toLocaleString()} email{totalCount !== 1 ? 's' : ''} )}
{error &&
{error}
} {!loading && !error && emails.length === 0 && (
No emails in this folder.
)} {emails.length > 0 && ( {emails.map((e) => ( ))}
{e.isUnread && } {e.senderDisplayName || e.senderAddress} {e.subject || '(no subject)'} {e.snippet && โ€” {e.snippet}} {e.hasAttachments && ๐Ÿ“Ž} {e.sizeEstimateBytes > 1048576 && {fmtSize(e.sizeEstimateBytes)}} {fmtDate(e.sentAtUtc)}
)} {/* Sentinel โ€” triggers next page load when scrolled into view */}
{loading &&
Loadingโ€ฆ
} {!hasMore && emails.length > 0 && (
โ€” {emails.length.toLocaleString()} emails โ€”
)}
); }