import { Link, NavLink, Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { useEffect, useState, useCallback, useRef } from 'react'; import { LayoutDashboard, Users, Sparkles, MailX, Search, RefreshCw, LogOut, ChevronLeft, ChevronRight, ChevronDown, X, Plus, Wand2, ShieldCheck, ScrollText, ListChecks, } from 'lucide-react'; import { AuthApi, SyncApi, AnalyticsApi } from '../api/client.js'; import Logo from './Logo.jsx'; import DevBanner from './DevBanner.jsx'; import SyncStatus from './SyncStatus.jsx'; import DigestToggle from './DigestToggle.jsx'; import useSavedSearches from '../hooks/useSavedSearches.js'; import { cn } from '../lib/utils.js'; import { Button, Input, Separator, ThemeToggle, Tooltip, TooltipContent, TooltipTrigger, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from './ui'; // ── Folder definitions ──────────────────────────────────────────────────────── const MAILBOX = [ { slug: 'inbox', icon: '📥', label: 'Inbox', countKey: 'inbox' }, { slug: 'allmail', icon: '📬', label: 'All Mail', countKey: 'allMail' }, { slug: 'starred', icon: '⭐', label: 'Starred', countKey: 'starred' }, { slug: 'sent', icon: '📤', label: 'Sent', countKey: 'sent' }, { slug: 'drafts', icon: '✏️', label: 'Drafts', countKey: 'drafts' }, { slug: 'archive', icon: '📦', label: 'Archive', countKey: null }, { slug: 'spam', icon: '🚫', label: 'Spam', countKey: 'spam' }, { slug: 'trash', icon: '🗑️', label: 'Trash', countKey: 'trash' }, { slug: 'unlabeled', icon: '🏷️', label: 'Unlabeled', countKey: null }, { slug: 'pinned', icon: '📌', label: 'Pinned', countKey: null }, { slug: 'readlater', icon: '🔖', label: 'Read Later', countKey: null }, ]; const FAV_SPECIALS = { inbox: { slug: 'inbox', icon: '📥', label: 'Inbox', countKey: 'inbox', type: 'mailbox' }, unread: { slug: 'unread', icon: '🔵', label: 'Unread Mail', countKey: 'unread', type: 'filter' }, large: { slug: 'large', icon: '📎', label: 'Large Mail', countKey: 'large', type: 'filter' }, old: { slug: 'old', icon: '🕰️', label: 'Old Mail', countKey: 'old', type: 'filter' }, readlater: { slug: 'readlater', icon: '🔖', label: 'Read Later', countKey: null, type: 'filter' }, }; const DEFAULT_FAV_SLUGS = ['inbox', 'unread', 'large', 'old', 'readlater']; const SMART_FOLDERS = [ { slug: 'automated', icon: '🤖', label: 'Automated', countKey: 'automated' }, { slug: 'noreply', icon: '🔇', label: 'No-Reply', countKey: 'noreply' }, { slug: 'shopping', icon: '🛍️', label: 'Online Shopping', countKey: 'shopping' }, { slug: 'gaming', icon: '🎮', label: 'Gaming', countKey: 'gaming' }, { slug: 'finance', icon: '💳', label: 'Finance & Insurance', countKey: 'finance' }, { slug: 'sales', icon: '🏷️', label: 'Seasonal Sales', countKey: 'sales' }, { slug: 'ridesharing', icon: '🚗', label: 'Ride Sharing', countKey: 'ridesharing' }, { slug: 'food', icon: '🍕', label: 'Food Delivery', countKey: 'food' }, { slug: 'social', icon: '📱', label: 'Social Notifications', countKey: 'social' }, { slug: 'wellness', icon: '🏃', label: 'Wellness & Sport', countKey: 'wellness' }, { slug: 'travel', icon: '✈️', label: 'Travel', countKey: 'travel' }, { slug: 'subscriptions',icon: '🔄', label: 'Subscriptions & SaaS', countKey: 'subscriptions' }, { slug: 'parcels', icon: '📦', label: 'Parcels & Delivery', countKey: 'parcels' }, { slug: 'recruitment', icon: '💼', label: 'Recruitment & Jobs', countKey: 'recruitment' }, { slug: 'events', icon: '🎟️', label: 'Events & Tickets', countKey: 'events' }, { slug: 'security', icon: '🔐', label: 'Security & Alerts', countKey: 'security' }, { slug: 'healthcare', icon: '🏥', label: 'Healthcare', countKey: 'healthcare' }, { slug: 'education', icon: '🎓', label: 'Education', countKey: 'education' }, { slug: 'news', icon: '📰', label: 'News & Media', countKey: 'news' }, { slug: 'property', icon: '🏠', label: 'Property & Utilities', countKey: 'property' }, { slug: 'charity', icon: '❤️', label: 'Charities', countKey: 'charity' }, { slug: 'government', icon: '🏛️', label: 'Government', countKey: 'government' }, { slug: 'crypto', icon: '📈', label: 'Crypto & Investing', countKey: 'crypto' }, { slug: 'family', icon: '👨‍👩‍👧', label: 'Family & School', countKey: 'family' }, ]; // Primary app navigation (top of the sidebar). Automation pages are placeholders // until their feature work lands (see docs/specs); links are kept here so the IA // is visible, and they no-op gracefully to routes added later. const PRIMARY_NAV = [ { to: '/app', label: 'Dashboard', icon: LayoutDashboard, end: true }, { to: '/app/senders', label: 'Senders', icon: Users }, { to: '/app/cleanup', label: 'Cleanup', icon: Sparkles }, { to: '/app/unsubscribe', label: 'Unsubscribe', icon: MailX }, ]; // ── Helpers ─────────────────────────────────────────────────────────────────── const fmtCount = (n) => { if (!n) return null; return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }; const LS_FAV = 'ii:fav-slugs'; const LS_OPEN = 'ii:sidebar-open'; const LS_SECTS = 'ii:sidebar-sections'; const loadFavs = () => { try { return JSON.parse(localStorage.getItem(LS_FAV)) ?? DEFAULT_FAV_SLUGS; } catch { return DEFAULT_FAV_SLUGS; } }; const saveFavs = (v) => localStorage.setItem(LS_FAV, JSON.stringify(v)); const loadSections = () => { try { return JSON.parse(localStorage.getItem(LS_SECTS)) ?? { fav: true, mailbox: true, smart: true, saved: true }; } catch { return { fav: true, mailbox: true, smart: true, saved: true }; } }; const saveSections = (v) => localStorage.setItem(LS_SECTS, JSON.stringify(v)); // ── Sidebar sub-components ────────────────────────────────────────────────────── function SectionHead({ label, open, onToggle, collapsed }) { if (collapsed) return ; return ( ); } function FolderRow({ item, active, count, collapsed, extra }) { const content = ( {item.icon} {!collapsed && ( <> {item.label} {count && {count}} {extra} )} ); if (collapsed) { return ( {content} {item.label} ); } return content; } // ── Main Layout ─────────────────────────────────────────────────────────────── export default function Layout() { const [user, setUser] = useState(null); const [searchParams] = useSearchParams(); const [searchQuery, setSearchQuery] = useState(() => searchParams.get('q') ?? ''); const [sidebarOpen, setSidebarOpen] = useState(() => { try { return localStorage.getItem(LS_OPEN) !== 'false'; } catch { return true; } }); const [sections, setSections] = useState(loadSections); const [favSlugs, setFavSlugs] = useState(loadFavs); const [counts, setCounts] = useState(null); const { searches: savedSearches, remove: removeSavedSearch } = useSavedSearches(); const loc = useLocation(); const navigate = useNavigate(); const searchInputRef = useRef(null); const collapsed = !sidebarOpen; useEffect(() => { AuthApi.me().then(setUser).catch(() => {}); }, []); useEffect(() => { AnalyticsApi.sidebarCounts().then(setCounts).catch(() => {}); }, []); // "/" focuses the search bar from anywhere, unless already typing in a field. useEffect(() => { const handler = (e) => { if (e.key !== '/') return; const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; e.preventDefault(); searchInputRef.current?.focus(); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, []); const toggleSidebar = () => { setSidebarOpen((o) => { localStorage.setItem(LS_OPEN, String(!o)); return !o; }); }; const toggleSection = (key) => { setSections((s) => { const next = { ...s, [key]: !s[key] }; saveSections(next); return next; }); }; const toggleFav = useCallback((slug) => { setFavSlugs((prev) => { const next = prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug]; saveFavs(next); return next; }); }, []); const activeSlug = loc.pathname.startsWith('/app/folder/') ? loc.pathname.split('/app/folder/')[1] : null; const logout = async () => { try { await AuthApi.logout(); } catch { /* ignore */ } navigate('/'); }; const submitSearch = (e) => { e.preventDefault(); const q = searchQuery.trim(); if (q) navigate(`/app/search?q=${encodeURIComponent(q)}`); }; const startSync = async () => { try { await SyncApi.incremental(); } catch { /* ignore */ } if (loc.pathname !== '/app') navigate('/app'); window.dispatchEvent(new Event('inboxintel:sync-started')); }; const mailboxCount = (item) => fmtCount(item.countKey ? counts?.[item.countKey] : null); const smartCount = (item) => fmtCount(counts?.smartFolders?.[item.slug]); const favItems = favSlugs.map((slug) => { if (FAV_SPECIALS[slug]) return { ...FAV_SPECIALS[slug], source: 'special' }; const sf = SMART_FOLDERS.find((f) => f.slug === slug); return sf ? { ...sf, source: 'smart' } : null; }).filter(Boolean); const favCount = (item) => item.source === 'smart' ? smartCount(item) : fmtCount(item.countKey ? counts?.[item.countKey] : null); const sortedSmart = [...SMART_FOLDERS] .filter((f) => !counts || (counts.smartFolders?.[f.slug] ?? 0) > 0) .sort((a, b) => (counts?.smartFolders?.[b.slug] ?? 0) - (counts?.smartFolders?.[a.slug] ?? 0)); const isPinnedSmart = (slug) => favSlugs.includes(slug); const userInitial = (user?.email?.[0] || '?').toUpperCase(); return (
{/* ── Topbar ── */}
setSearchQuery(e.target.value)} aria-label="Search emails" className="max-w-xl pl-9" />
{user?.email || 'Signed in'} Log out
{/* ── Sidebar ── */} {/* ── Main content ── */}
); }