"use client"; import { useState, useCallback } from "react"; import type { Contact, ContactSource } from "@/actions/communications/contacts"; import { getContacts, deleteContact, exportContacts } from "@/actions/communications/contacts"; import { formatDate } from "@/lib/format-date"; import { AdminButton } from "./design-system"; const SOURCE_COLORS: Record = { order: "bg-blue-100 text-blue-700", import: "bg-purple-100 text-purple-700", manual: "bg-emerald-100 text-emerald-700", admin: "bg-stone-100 text-stone-600", }; // Icon components const Icons = { users: (className: string) => ( ), download: (className: string) => ( ), search: (className: string) => ( ), trash: (className: string) => ( ), chevronLeft: (className: string) => ( ), chevronRight: (className: string) => ( ), plus: (className: string) => ( ), upload: (className: string) => ( ), mail: (className: string) => ( ), }; // Empty state component function EmptyState({ hasFilters }: { hasFilters: boolean }) { return (
{Icons.users("w-10 h-10 text-emerald-600")}

{hasFilters ? "No contacts match your filters" : "No contacts yet"}

{hasFilters ? "Try adjusting your search or filter criteria to find contacts." : "Contacts are added when customers place orders or are imported from other sources."}

); } // Loading skeleton function ContactSkeleton() { return ( <> {[1, 2, 3, 4, 5].map((i) => (
))} ); } export default function ContactListPanel({ initialContacts, initialTotal, brandId, }: { initialContacts: Contact[]; initialTotal: number; brandId: string; }) { const [contacts, setContacts] = useState(initialContacts); const [total, setTotal] = useState(initialTotal); const [search, setSearch] = useState(""); const [sourceFilter, setSourceFilter] = useState(""); const [page, setPage] = useState(0); const [loading, setLoading] = useState(false); const [deleting, setDeleting] = useState(null); const [exporting, setExporting] = useState(false); const limit = 50; const hasFilters = search.length > 0 || sourceFilter !== ""; const loadPage = useCallback(async (searchVal: string, sourceVal: string, pageNum: number) => { setLoading(true); const result = await getContacts({ brandId, search: searchVal || undefined, source: sourceVal || undefined, limit, offset: pageNum * limit, }); setLoading(false); if (result.success) { setContacts(result.contacts); setTotal(result.total); } }, [brandId]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setPage(0); loadPage(search, sourceFilter, 0); }; const handleSourceFilter = (val: ContactSource | "") => { setSourceFilter(val); setPage(0); loadPage(search, val, 0); }; const handlePage = (dir: number) => { const next = page + dir; setPage(next); loadPage(search, sourceFilter, next); }; const handleDelete = async (id: string) => { if (!confirm("Delete this contact? This action cannot be undone.")) return; setDeleting(id); const result = await deleteContact(id); setDeleting(null); if (result.success) { setContacts((prev) => prev.filter((c) => c.id !== id)); setTotal((t) => t - 1); } }; const handleExport = async () => { setExporting(true); const result = await exportContacts({ brandId, brandSlug: "contacts", search: search || undefined, source: sourceFilter || undefined, }); setExporting(false); if (!result.success) { alert("Export failed: " + result.error); return; } const blob = new Blob([result.csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = result.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; return (
{/* Header */}
{Icons.users("w-6 h-6 text-white")}

Contacts

{total.toLocaleString()} contact{total !== 1 ? "s" : ""}

{exporting ? "Exporting..." : "Export"}
{/* Search + filters */}
{Icons.search("h-5 w-5 text-stone-400")}
setSearch(e.target.value)} placeholder="Search email, name, phone..." className="w-full pl-11 pr-4 py-2.5 text-sm border border-stone-200 rounded-xl bg-white text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all" />
{/* Table */} {loading ? (
Name Email Phone Source Email Opt Unsub
) : contacts.length === 0 ? (
) : ( <> {/* Desktop Table */}
{contacts.map((c) => ( ))}
Name Email Phone Source Email Opt Unsubscribed
{(c.full_name || `${c.first_name || ""} ${c.last_name || ""}`.trim() || "??").slice(0, 2).toUpperCase()}
{c.full_name || `${c.first_name || ""} ${c.last_name || ""}`.trim() || "—"}
{Icons.mail("w-4 h-4 text-stone-400")} {c.email || "—"}
{c.phone || "—"} {c.source} {c.email_opt_in ? ( Opted in ) : ( Opted out )} {c.unsubscribed_at ? formatDate(new Date(c.unsubscribed_at)) : "—"}
{/* Mobile cards */}
{contacts.map((c) => (
{(c.full_name || `${c.first_name || ""} ${c.last_name || ""}`.trim() || "??").slice(0, 2).toUpperCase()}
{c.full_name || `${c.first_name || ""} ${c.last_name || ""}`.trim() || "—"}
{c.email || "—"}
{c.source} {c.phone && ( {c.phone} )} {c.email_opt_in ? ( Opted in ) : ( Opted out )}
))}
{/* Pagination */}
Showing {page * limit + 1}–{Math.min((page + 1) * limit, total)} of {total.toLocaleString()}
)}
); }