import { createFileRoute } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer, PageHeader } from "@/components/app-shell"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Loader2, Inbox as InboxIcon, RefreshCcw, Search, Mail, MailOpen, Archive, ArchiveRestore, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useAuth } from "@/lib/auth"; export const Route = createFileRoute("/inbox/")({ component: InboxPage, }); interface IncomingEmail { id: string; received_at: string; from_address: string | null; from_name: string | null; to_addresses: string[]; subject: string | null; snippet: string | null; body_text: string | null; body_html: string | null; is_read: boolean; is_archived: boolean; has_attachments: boolean; attachment_count: number; } function InboxPage() { const { isAdmin } = useAuth(); const [emails, setEmails] = useState([]); const [loading, setLoading] = useState(true); const [polling, setPolling] = useState(false); const [showArchived, setShowArchived] = useState(false); const [search, setSearch] = useState(""); const [selectedId, setSelectedId] = useState(null); const load = async () => { setLoading(true); let query = supabase .from("incoming_emails") .select("id, received_at, from_address, from_name, to_addresses, subject, snippet, body_text, body_html, is_read, is_archived, has_attachments, attachment_count") .order("received_at", { ascending: false }) .limit(200); query = showArchived ? query.eq("is_archived", true) : query.eq("is_archived", false); const { data, error } = await query; if (error) { toast.error("Failed to load inbox", { description: error.message }); } else { setEmails((data as IncomingEmail[]) ?? []); } setLoading(false); }; useEffect(() => { load(); }, [showArchived]); const pollNow = async () => { setPolling(true); try { const res = await fetch("/hooks/poll-imap", { method: "POST" }); const body = await res.json(); if (!res.ok || body.error) { toast.error("Poll failed", { description: body.error }); } else { toast.success(`Imported ${body.imported ?? 0} new email(s)`); load(); } } catch (e) { toast.error("Poll failed", { description: e instanceof Error ? e.message : String(e), }); } finally { setPolling(false); } }; const markRead = async (id: string, isRead: boolean) => { await supabase.from("incoming_emails").update({ is_read: isRead }).eq("id", id); setEmails((prev) => prev.map((e) => (e.id === id ? { ...e, is_read: isRead } : e)), ); }; const toggleArchive = async (id: string, archive: boolean) => { await supabase.from("incoming_emails").update({ is_archived: archive }).eq("id", id); setEmails((prev) => prev.filter((e) => e.id !== id)); if (selectedId === id) setSelectedId(null); toast.success(archive ? "Archived" : "Restored"); }; const deleteEmail = async (id: string) => { if (!confirm("Delete this email permanently?")) return; const { error } = await supabase.from("incoming_emails").delete().eq("id", id); if (error) { toast.error("Delete failed", { description: error.message }); return; } setEmails((prev) => prev.filter((e) => e.id !== id)); if (selectedId === id) setSelectedId(null); }; const filtered = emails.filter((e) => { if (!search.trim()) return true; const q = search.toLowerCase(); return ( (e.subject ?? "").toLowerCase().includes(q) || (e.from_address ?? "").toLowerCase().includes(q) || (e.from_name ?? "").toLowerCase().includes(q) || (e.snippet ?? "").toLowerCase().includes(q) ); }); const selected = emails.find((e) => e.id === selectedId) ?? null; const unreadCount = emails.filter((e) => !e.is_read).length; return ( {polling ? ( ) : ( )} Check for new mail } />
setSearch(e.target.value)} className="pl-8" />
{loading ? (
) : filtered.length === 0 ? ( {showArchived ? "No archived emails." : "No emails yet. "} {!showArchived && ( <>Configure IMAP in Settings → Email (IMAP) and click "Check for new mail". )} ) : (
{filtered.map((email) => ( ))}
{selected ? (

{selected.subject || "(no subject)"}

From: {selected.from_name ? `${selected.from_name} <${selected.from_address}>` : selected.from_address}
To: {selected.to_addresses.join(", ")}
{new Date(selected.received_at).toLocaleString()}
{isAdmin && ( )}
{selected.body_html ? (