'use client'; import { useState, useEffect, useCallback } from 'react'; import { createClient } from '@/lib/supabase/client'; import { toast } from 'sonner'; import type { Contact, Tag, ContactTag } from '@/types'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog'; import { Search, Plus, Upload, MoreHorizontal, Pencil, Trash2, Loader2, Users, ChevronLeft, ChevronRight, } from 'lucide-react'; import { ContactForm } from '@/components/contacts/contact-form'; import { ContactDetailView } from '@/components/contacts/contact-detail-view'; import { ImportModal } from '@/components/contacts/import-modal'; const PAGE_SIZE = 25; interface ContactWithTags extends Contact { tags?: Tag[]; } export default function ContactsPage() { const supabase = createClient(); const [contacts, setContacts] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [page, setPage] = useState(0); const [totalCount, setTotalCount] = useState(0); // Modals const [formOpen, setFormOpen] = useState(false); const [editContact, setEditContact] = useState(null); const [editContactTags, setEditContactTags] = useState([]); const [detailOpen, setDetailOpen] = useState(false); const [detailContactId, setDetailContactId] = useState(null); const [importOpen, setImportOpen] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); // All tags for display const [tagsMap, setTagsMap] = useState>({}); const fetchTags = useCallback(async () => { const { data } = await supabase.from('tags').select('*'); if (data) { const map: Record = {}; data.forEach((t) => (map[t.id] = t)); setTagsMap(map); } }, [supabase]); const fetchContacts = useCallback(async () => { setLoading(true); const from = page * PAGE_SIZE; const to = from + PAGE_SIZE - 1; let query = supabase .from('contacts') .select('*', { count: 'exact' }) .order('created_at', { ascending: false }) .range(from, to); if (search.trim()) { const term = `%${search.trim()}%`; query = query.or(`name.ilike.${term},phone.ilike.${term},email.ilike.${term}`); } const { data, count, error } = await query; if (error) { toast.error('Failed to load contacts'); setLoading(false); return; } setTotalCount(count ?? 0); if (!data || data.length === 0) { setContacts([]); setLoading(false); return; } // Fetch tags for these contacts const contactIds = data.map((c) => c.id); const { data: contactTags } = await supabase .from('contact_tags') .select('contact_id, tag_id') .in('contact_id', contactIds); const tagsByContact: Record = {}; contactTags?.forEach((ct) => { if (!tagsByContact[ct.contact_id]) tagsByContact[ct.contact_id] = []; tagsByContact[ct.contact_id].push(ct.tag_id); }); const enriched: ContactWithTags[] = data.map((c) => ({ ...c, tags: (tagsByContact[c.id] ?? []) .map((tid) => tagsMap[tid]) .filter(Boolean), })); setContacts(enriched); setLoading(false); }, [supabase, page, search, tagsMap]); // Load-once-on-mount-ish data fetches. Each setter inside runs // inside an async promise completion (Supabase await), not // synchronously in the effect body, so the cascade the lint rule // warns about doesn't apply here. useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect fetchTags(); }, [fetchTags]); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect fetchContacts(); }, [fetchContacts]); function openAddForm() { setEditContact(null); setEditContactTags([]); setFormOpen(true); } async function openEditForm(contact: Contact) { const { data } = await supabase .from('contact_tags') .select('*') .eq('contact_id', contact.id); setEditContact(contact); setEditContactTags(data ?? []); setFormOpen(true); } function openDetail(contactId: string) { setDetailContactId(contactId); setDetailOpen(true); } function confirmDelete(contact: Contact) { setDeleteTarget(contact); setDeleteConfirmOpen(true); } async function handleDelete() { if (!deleteTarget) return; setDeleting(true); const { error } = await supabase .from('contacts') .delete() .eq('id', deleteTarget.id); if (error) { toast.error('Failed to delete contact'); } else { toast.success('Contact deleted'); fetchContacts(); } setDeleting(false); setDeleteConfirmOpen(false); setDeleteTarget(null); } const totalPages = Math.ceil(totalCount / PAGE_SIZE); const hasNext = page < totalPages - 1; const hasPrev = page > 0; return (
{/* Header */}

Contacts

Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`}

{/* Search */}
{ setSearch(e.target.value); // Reset pagination when the query changes — the result // set shrinks/grows, page N may no longer be valid. setPage(0); }} placeholder="Search by name, phone, or email..." className="pl-8 bg-slate-900 border-slate-700 text-white placeholder:text-slate-500" />
{/* Table */}
Name Phone Email Company Tags Created {loading ? (

Loading contacts...

) : contacts.length === 0 ? (

{search ? 'No contacts match your search.' : 'No contacts yet.'}

{!search && ( )}
) : ( contacts.map((contact) => ( openDetail(contact.id)} > {contact.name || Unnamed} {contact.phone} {contact.email || -} {contact.company || -}
{contact.tags && contact.tags.length > 0 ? ( contact.tags.slice(0, 3).map((tag) => ( {tag.name} )) ) : ( - )} {contact.tags && contact.tags.length > 3 && ( +{contact.tags.length - 3} )}
{new Date(contact.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', })} e.stopPropagation()} /> } > { e.stopPropagation(); openEditForm(contact); }} className="text-slate-300 focus:bg-slate-800 focus:text-white" > Edit { e.stopPropagation(); confirmDelete(contact); }} > Delete
)) )}
{/* Pagination */} {totalPages > 1 && (

Showing {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, totalCount)} of{' '} {totalCount}

Page {page + 1} of {totalPages}
)} {/* Contact Form Dialog */} { fetchContacts(); fetchTags(); }} /> {/* Contact Detail Sheet */} {/* Import Modal */} {/* Delete Confirmation */} Delete Contact Are you sure you want to delete{' '} {deleteTarget?.name || deleteTarget?.phone} ? This action cannot be undone.
); }