'use client'; import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { ClayCard } from '@/components/ClayCard'; import { api } from '@/lib/api/client'; import { Search, Plus, MoreVertical, Mail, Phone, Tag, Trash2, Edit, User, X, Eye, Calendar, Loader2, AlertCircle, Users, ChevronUp, ChevronDown, Filter, MessageSquare, Building, Clock, Activity } from 'lucide-react'; // ============================================================================= // Types // ============================================================================= interface Contact { id: string; firstName?: string; lastName?: string; name?: string; email?: string; phone?: string; tags?: string[]; createdAt: string; companyName?: string; } interface ContactFormData { firstName: string; lastName: string; email: string; phone: string; tags: string[]; companyName: string; } type SortField = 'name' | 'email' | 'phone' | 'createdAt'; type SortDirection = 'asc' | 'desc'; interface SortConfig { field: SortField; direction: SortDirection; } // ============================================================================= // Constants // ============================================================================= const TAG_COLORS: Record = { 'Investor': { bg: 'bg-emerald-50', text: 'text-emerald-700', border: 'border-emerald-200' }, 'Hot Lead': { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-200' }, 'Buyer': { bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-200' }, 'Developer': { bg: 'bg-purple-50', text: 'text-purple-700', border: 'border-purple-200' }, 'VIP': { bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-200' }, 'Tenant Rep': { bg: 'bg-cyan-50', text: 'text-cyan-700', border: 'border-cyan-200' }, 'Institutional': { bg: 'bg-indigo-50', text: 'text-indigo-700', border: 'border-indigo-200' }, 'Seller': { bg: 'bg-orange-50', text: 'text-orange-700', border: 'border-orange-200' }, 'Landlord': { bg: 'bg-teal-50', text: 'text-teal-700', border: 'border-teal-200' }, 'default': { bg: 'bg-slate-50', text: 'text-slate-700', border: 'border-slate-200' }, }; const FILTER_OPTIONS = [ { value: 'all', label: 'All Contacts' }, { value: 'recent', label: 'Added This Week' }, { value: 'investor', label: 'Investors' }, { value: 'buyer', label: 'Buyers' }, { value: 'hot-lead', label: 'Hot Leads' }, { value: 'vip', label: 'VIP Contacts' }, ]; const FIELD_LIMITS = { firstName: 50, lastName: 50, email: 100, phone: 20, companyName: 100, }; // ============================================================================= // Mock Data (replace with real API calls) // ============================================================================= const mockContacts: Contact[] = [ { id: '1', firstName: 'John', lastName: 'Smith', email: 'john.smith@example.com', phone: '(555) 123-4567', tags: ['Investor', 'Hot Lead'], createdAt: '2024-01-15T10:30:00Z', companyName: 'Smith Holdings LLC', }, { id: '2', firstName: 'Sarah', lastName: 'Johnson', email: 'sarah.j@realestate.com', phone: '(555) 987-6543', tags: ['Buyer'], createdAt: '2024-01-10T14:20:00Z', companyName: 'Johnson Properties', }, { id: '3', firstName: 'Michael', lastName: 'Chen', email: 'mchen@capitalgroup.com', phone: '(555) 456-7890', tags: ['Developer', 'VIP'], createdAt: '2024-01-08T09:15:00Z', companyName: 'Capital Development Group', }, { id: '4', firstName: 'Emily', lastName: 'Rodriguez', email: 'emily.r@commercialre.com', phone: '(555) 321-0987', tags: ['Tenant Rep'], createdAt: '2024-01-05T16:45:00Z', companyName: 'Commercial RE Solutions', }, { id: '5', firstName: 'David', lastName: 'Thompson', email: 'dthompson@ventures.io', phone: '(555) 654-3210', tags: ['Investor', 'Institutional'], createdAt: '2024-01-03T11:00:00Z', companyName: 'Thompson Ventures', }, ]; // ============================================================================= // Utility Functions // ============================================================================= const formatDate = (dateString: string): string => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); }; const getContactDisplayName = (contact: Contact): string => { if (contact.name) return contact.name; if (contact.firstName || contact.lastName) { return `${contact.firstName || ''} ${contact.lastName || ''}`.trim(); } return 'Unknown'; }; const getTagColor = (tag: string) => { return TAG_COLORS[tag] || TAG_COLORS['default']; }; const isWithinLastWeek = (dateString: string): boolean => { const date = new Date(dateString); const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); return date >= weekAgo; }; // ============================================================================= // Tag Chip Component // ============================================================================= interface TagChipProps { tag: string; onRemove?: () => void; size?: 'sm' | 'md'; } const TagChip: React.FC = ({ tag, onRemove, size = 'sm' }) => { const colors = getTagColor(tag); const sizeClasses = size === 'sm' ? 'text-xs px-3.5 py-1.5' : 'text-sm px-3.5 py-1.5'; return ( {tag} {onRemove && ( )} ); }; // ============================================================================= // Loading Skeleton Component // ============================================================================= const TableSkeleton: React.FC = () => (
{[...Array(5)].map((_, i) => (
))}
); // ============================================================================= // Empty State Component // ============================================================================= interface EmptyStateProps { onAddContact: () => void; hasSearchQuery?: boolean; searchQuery?: string; onClearSearch?: () => void; } const EmptyState: React.FC = ({ onAddContact, hasSearchQuery = false, searchQuery = '', onClearSearch }) => (
{hasSearchQuery ? ( ) : ( )}
{hasSearchQuery ? ( <>

No contacts found

No contacts match your search for "{searchQuery}". Try adjusting your search terms or filters.

) : ( <>

No contacts yet

Get started by adding your first contact or import contacts from a CSV file.

)}
); // ============================================================================= // Error State Component // ============================================================================= const ErrorState: React.FC<{ error: string; onRetry: () => void }> = ({ error, onRetry }) => (

Failed to load contacts

{error}

); // ============================================================================= // Tag Input Component // ============================================================================= interface TagInputProps { tags: string[]; onChange: (tags: string[]) => void; placeholder?: string; } const TagInput: React.FC = ({ tags, onChange, placeholder = 'Add tags...' }) => { const [inputValue, setInputValue] = useState(''); const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.key === 'Enter' || e.key === ',') && inputValue.trim()) { e.preventDefault(); const newTag = inputValue.trim().replace(/,/g, ''); if (newTag && !tags.includes(newTag)) { onChange([...tags, newTag]); } setInputValue(''); } else if (e.key === 'Backspace' && !inputValue && tags.length > 0) { onChange(tags.slice(0, -1)); } }; const removeTag = (tagToRemove: string) => { onChange(tags.filter(tag => tag !== tagToRemove)); }; return (
{tags.map((tag, index) => ( removeTag(tag)} size="md" /> ))} setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder={tags.length === 0 ? placeholder : ''} className="flex-1 min-w-[120px] bg-transparent outline-none text-slate-900 placeholder:text-slate-400" />
); }; // ============================================================================= // Contact Modal Component (Add/Edit) // ============================================================================= interface ContactModalProps { isOpen: boolean; onClose: () => void; contact?: Contact | null; onSave: (data: ContactFormData) => void; isLoading?: boolean; } const ContactModal: React.FC = ({ isOpen, onClose, contact, onSave, isLoading = false, }) => { const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', phone: '', tags: [], companyName: '', }); useEffect(() => { if (contact) { setFormData({ firstName: contact.firstName || '', lastName: contact.lastName || '', email: contact.email || '', phone: contact.phone || '', tags: contact.tags || [], companyName: contact.companyName || '', }); } else { setFormData({ firstName: '', lastName: '', email: '', phone: '', tags: [], companyName: '', }); } }, [contact, isOpen]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSave(formData); }; const handleChange = (field: keyof Omit) => ( e: React.ChangeEvent ) => { const limit = FIELD_LIMITS[field as keyof typeof FIELD_LIMITS]; const value = limit ? e.target.value.slice(0, limit) : e.target.value; setFormData((prev) => ({ ...prev, [field]: value })); }; if (!isOpen) return null; return (
{/* Backdrop */} ); }; // ============================================================================= // View Contact Modal Component // ============================================================================= interface ViewContactModalProps { isOpen: boolean; onClose: () => void; contact: Contact | null; onEdit: () => void; } const ViewContactModal: React.FC = ({ isOpen, onClose, contact, onEdit, }) => { if (!isOpen || !contact) return null; const handleCall = () => { if (contact.phone) { window.location.href = `tel:${contact.phone}`; } }; const handleEmail = () => { if (contact.email) { window.location.href = `mailto:${contact.email}`; } }; const handleMessage = () => { if (contact.phone) { window.location.href = `sms:${contact.phone}`; } }; return (
{/* Backdrop */} ); }; // ============================================================================= // Delete Confirmation Modal // ============================================================================= interface DeleteModalProps { isOpen: boolean; onClose: () => void; onConfirm: () => void; contactName: string; isLoading?: boolean; } const DeleteModal: React.FC = ({ isOpen, onClose, onConfirm, contactName, isLoading = false, }) => { if (!isOpen) return null; return (
{/* Backdrop */} ); }; // ============================================================================= // Action Menu Component // ============================================================================= interface ActionMenuProps { onView: () => void; onEdit: () => void; onDelete: () => void; contactName: string; } const ActionMenu: React.FC = ({ onView, onEdit, onDelete, contactName }) => { const [isOpen, setIsOpen] = useState(false); return (
{isOpen && ( <>
setIsOpen(false)} aria-hidden="true" />
)}
); }; // ============================================================================= // Sortable Column Header Component // ============================================================================= interface SortableHeaderProps { field: SortField; label: string; currentSort: SortConfig; onSort: (field: SortField) => void; className?: string; } const SortableHeader: React.FC = ({ field, label, currentSort, onSort, className = '', }) => { const isActive = currentSort.field === field; const isAsc = currentSort.direction === 'asc'; return ( ); }; // ============================================================================= // Contact Card Component (Mobile) // ============================================================================= interface ContactCardProps { contact: Contact; onView: () => void; onEdit: () => void; onDelete: () => void; } const ContactCard: React.FC = ({ contact, onView, onEdit, onDelete }) => (

{getContactDisplayName(contact)}

{contact.companyName && (

{contact.companyName}

)}
{contact.email && (
{contact.email}
)} {contact.phone && (
{contact.phone}
)}
{contact.tags && contact.tags.length > 0 && (
{contact.tags.map((tag, index) => ( ))}
)}

Added {formatDate(contact.createdAt)}

); // ============================================================================= // Contacts Table Component (Desktop) // ============================================================================= interface ContactsTableProps { contacts: Contact[]; onView: (contact: Contact) => void; onEdit: (contact: Contact) => void; onDelete: (contact: Contact) => void; sortConfig: SortConfig; onSort: (field: SortField) => void; } const ContactsTable: React.FC = ({ contacts, onView, onEdit, onDelete, sortConfig, onSort, }) => (
{contacts.map((contact) => ( onView(contact)} className="border-b border-slate-100 hover:bg-indigo-50/50 transition-colors cursor-pointer group" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onView(contact); } }} role="button" aria-label={`View details for ${getContactDisplayName(contact)}`} > ))}
Tags Actions

{getContactDisplayName(contact)}

{contact.companyName && (

{contact.companyName}

)}
{contact.email || -} {contact.phone || -}
{contact.tags?.slice(0, 2).map((tag, index) => ( ))} {contact.tags && contact.tags.length > 2 && ( +{contact.tags.length - 2} )} {(!contact.tags || contact.tags.length === 0) && ( - )}
{formatDate(contact.createdAt)} onView(contact)} onEdit={() => onEdit(contact)} onDelete={() => onDelete(contact)} contactName={getContactDisplayName(contact)} />
); // ============================================================================= // Filter Dropdown Component // ============================================================================= interface FilterDropdownProps { value: string; onChange: (value: string) => void; } const FilterDropdown: React.FC = ({ value, onChange }) => { const [isOpen, setIsOpen] = useState(false); const selectedOption = FILTER_OPTIONS.find(opt => opt.value === value) || FILTER_OPTIONS[0]; return (
{isOpen && ( <>
setIsOpen(false)} aria-hidden="true" />
{FILTER_OPTIONS.map((option) => ( ))}
)}
); }; // ============================================================================= // Main Page Component // ============================================================================= export default function ContactsPage() { // State const [contacts, setContacts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [filterValue, setFilterValue] = useState('all'); const [sortConfig, setSortConfig] = useState({ field: 'name', direction: 'asc' }); // Modal states const [isAddEditModalOpen, setIsAddEditModalOpen] = useState(false); const [isViewModalOpen, setIsViewModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedContact, setSelectedContact] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); // Fetch contacts from GHL const fetchContacts = async () => { setIsLoading(true); setError(null); try { const response = await api.contacts.getAll({ limit: 100 }); // Map GHL response to our Contact interface const ghlContacts: Contact[] = (response.data || []).map((c: any) => ({ id: c.id, firstName: c.firstName, lastName: c.lastName, name: c.name, email: c.email, phone: c.phone, tags: c.tags || [], createdAt: c.dateAdded || c.createdAt || new Date().toISOString(), companyName: c.companyName, })); setContacts(ghlContacts); } catch (err: any) { console.error('Failed to fetch contacts:', err); if (err.message?.includes('GHL not configured')) { setError('GHL is not configured. Please set up your GHL credentials in Admin → Settings.'); } else { setError('Unable to load contacts. Please try again.'); } } finally { setIsLoading(false); } }; useEffect(() => { fetchContacts(); }, []); // Sort handler const handleSort = useCallback((field: SortField) => { setSortConfig((prev) => ({ field, direction: prev.field === field && prev.direction === 'asc' ? 'desc' : 'asc', })); }, []); // Filtered and sorted contacts const filteredAndSortedContacts = useMemo(() => { let result = [...contacts]; // Apply filter if (filterValue !== 'all') { result = result.filter((contact) => { switch (filterValue) { case 'recent': return isWithinLastWeek(contact.createdAt); case 'investor': return contact.tags?.some(tag => tag.toLowerCase() === 'investor'); case 'buyer': return contact.tags?.some(tag => tag.toLowerCase() === 'buyer'); case 'hot-lead': return contact.tags?.some(tag => tag.toLowerCase() === 'hot lead'); case 'vip': return contact.tags?.some(tag => tag.toLowerCase() === 'vip'); default: return true; } }); } // Apply search if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); result = result.filter((contact) => { const name = getContactDisplayName(contact).toLowerCase(); const email = contact.email?.toLowerCase() || ''; const phone = contact.phone?.toLowerCase() || ''; const company = contact.companyName?.toLowerCase() || ''; const tags = contact.tags?.join(' ').toLowerCase() || ''; return ( name.includes(query) || email.includes(query) || phone.includes(query) || company.includes(query) || tags.includes(query) ); }); } // Apply sort result.sort((a, b) => { let aValue: string; let bValue: string; switch (sortConfig.field) { case 'name': aValue = getContactDisplayName(a).toLowerCase(); bValue = getContactDisplayName(b).toLowerCase(); break; case 'email': aValue = a.email?.toLowerCase() || ''; bValue = b.email?.toLowerCase() || ''; break; case 'phone': aValue = a.phone || ''; bValue = b.phone || ''; break; case 'createdAt': aValue = a.createdAt; bValue = b.createdAt; break; default: return 0; } if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1; if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1; return 0; }); return result; }, [contacts, searchQuery, filterValue, sortConfig]); // Handlers const handleAddContact = () => { setSelectedContact(null); setIsAddEditModalOpen(true); }; const handleViewContact = (contact: Contact) => { setSelectedContact(contact); setIsViewModalOpen(true); }; const handleEditContact = (contact: Contact) => { setSelectedContact(contact); setIsAddEditModalOpen(true); setIsViewModalOpen(false); }; const handleDeleteContact = (contact: Contact) => { setSelectedContact(contact); setIsDeleteModalOpen(true); }; const handleClearSearch = () => { setSearchQuery(''); setFilterValue('all'); }; const handleSaveContact = async (formData: ContactFormData) => { setIsSaving(true); try { if (selectedContact) { // Update existing contact in GHL await api.contacts.update(selectedContact.id, { firstName: formData.firstName, lastName: formData.lastName, email: formData.email || undefined, phone: formData.phone || undefined, tags: formData.tags, }); // Refresh contacts list await fetchContacts(); } else { // Create new contact in GHL await api.contacts.create({ firstName: formData.firstName, lastName: formData.lastName, email: formData.email || undefined, phone: formData.phone || undefined, tags: formData.tags, }); // Refresh contacts list await fetchContacts(); } setIsAddEditModalOpen(false); setSelectedContact(null); } catch (err) { console.error('Failed to save contact:', err); setError('Failed to save contact. Please try again.'); } finally { setIsSaving(false); } }; const handleConfirmDelete = async () => { if (!selectedContact) return; setIsDeleting(true); try { // Delete contact in GHL await api.contacts.delete(selectedContact.id); // Refresh contacts list await fetchContacts(); setIsDeleteModalOpen(false); setSelectedContact(null); } catch (err) { console.error('Failed to delete contact:', err); setError('Failed to delete contact. Please try again.'); } finally { setIsDeleting(false); } }; return (
{/* Header - Level 1 (subtle) since it's a page header */}

Contacts

Manage your leads and clients

{/* Search and Filter Bar */}
setSearchQuery(e.target.value)} className={`clay-input clay-input-icon w-full h-14 ${searchQuery ? 'pr-12' : 'pr-4'} text-base border-2 border-slate-200 focus:border-indigo-400 transition-colors`} aria-label="Search contacts" /> {searchQuery && ( )}
{/* Active filter indicator */} {(filterValue !== 'all' || searchQuery) && (
Active filters: {filterValue !== 'all' && ( {FILTER_OPTIONS.find(opt => opt.value === filterValue)?.label} )} {searchQuery && ( Search: "{searchQuery}" )}
)} {/* Contacts List */}
{isLoading ? (
) : error ? ( ) : filteredAndSortedContacts.length === 0 ? ( opt.value === filterValue)?.label || ''} onClearSearch={handleClearSearch} /> ) : ( <> {/* Desktop Table */}
{/* Mobile Cards */}
{filteredAndSortedContacts.map((contact) => ( handleViewContact(contact)} onEdit={() => handleEditContact(contact)} onDelete={() => handleDeleteContact(contact)} /> ))}
)}
{/* Results count */} {!isLoading && !error && filteredAndSortedContacts.length > 0 && (

Showing {filteredAndSortedContacts.length} of {contacts.length} contacts

)} {/* Modals */} { setIsAddEditModalOpen(false); setSelectedContact(null); }} contact={selectedContact} onSave={handleSaveContact} isLoading={isSaving} /> { setIsViewModalOpen(false); setSelectedContact(null); }} contact={selectedContact} onEdit={() => handleEditContact(selectedContact!)} /> { setIsDeleteModalOpen(false); setSelectedContact(null); }} onConfirm={handleConfirmDelete} contactName={selectedContact ? getContactDisplayName(selectedContact) : ''} isLoading={isDeleting} />
); }