'use client';
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { api } from '@/lib/api/client';
import {
Search,
Send,
Mail,
MessageSquare,
Star,
Plus,
ArrowLeft,
Phone,
MoreVertical,
Paperclip,
Smile,
Check,
CheckCheck,
Clock,
X,
Inbox,
Users
} from 'lucide-react';
// Types
interface Contact {
id: string;
name: string;
email: string;
phone: string;
avatar?: string;
}
interface Message {
id: string;
conversationId: string;
content: string;
timestamp: Date;
direction: 'inbound' | 'outbound';
status: 'sent' | 'delivered' | 'read' | 'pending';
channel: 'sms' | 'email';
}
interface Conversation {
id: string;
contact: Contact;
lastMessage: string;
lastMessageTime: Date;
unreadCount: number;
isStarred: boolean;
channel: 'sms' | 'email';
messages: Message[];
}
type FilterTab = 'all' | 'unread' | 'starred';
// Mock data
const mockConversations: Conversation[] = [
{
id: '1',
contact: { id: 'c1', name: 'John Smith', email: 'john.smith@example.com', phone: '+1 (555) 123-4567' },
lastMessage: 'I am very interested in the property on Main Street. When can we schedule a viewing?',
lastMessageTime: new Date(Date.now() - 1000 * 60 * 5), // 5 minutes ago
unreadCount: 2,
isStarred: true,
channel: 'sms',
messages: [
{ id: 'm1', conversationId: '1', content: 'Hi, I saw your listing for the commercial property on Main Street.', timestamp: new Date(Date.now() - 1000 * 60 * 60), direction: 'inbound', status: 'read', channel: 'sms' },
{ id: 'm2', conversationId: '1', content: 'Hello John! Yes, it is still available. Would you like to schedule a viewing?', timestamp: new Date(Date.now() - 1000 * 60 * 45), direction: 'outbound', status: 'read', channel: 'sms' },
{ id: 'm3', conversationId: '1', content: 'That would be great. What times work for you?', timestamp: new Date(Date.now() - 1000 * 60 * 30), direction: 'inbound', status: 'read', channel: 'sms' },
{ id: 'm4', conversationId: '1', content: 'I have availability tomorrow at 2 PM or Thursday at 10 AM.', timestamp: new Date(Date.now() - 1000 * 60 * 20), direction: 'outbound', status: 'delivered', channel: 'sms' },
{ id: 'm5', conversationId: '1', content: 'I am very interested in the property on Main Street. When can we schedule a viewing?', timestamp: new Date(Date.now() - 1000 * 60 * 5), direction: 'inbound', status: 'delivered', channel: 'sms' },
]
},
{
id: '2',
contact: { id: 'c2', name: 'Sarah Johnson', email: 'sarah.j@realty.com', phone: '+1 (555) 234-5678' },
lastMessage: 'Thank you for the documents. I will review and get back to you.',
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
unreadCount: 0,
isStarred: false,
channel: 'email',
messages: [
{ id: 'm6', conversationId: '2', content: 'Hi Sarah, please find attached the property documents.', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 3), direction: 'outbound', status: 'read', channel: 'email' },
{ id: 'm7', conversationId: '2', content: 'Thank you for the documents. I will review and get back to you.', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), direction: 'inbound', status: 'read', channel: 'email' },
]
},
{
id: '3',
contact: { id: 'c3', name: 'Michael Chen', email: 'mchen@business.com', phone: '+1 (555) 345-6789' },
lastMessage: 'Perfect, I will bring the signed lease agreement.',
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago
unreadCount: 1,
isStarred: true,
channel: 'sms',
messages: [
{ id: 'm8', conversationId: '3', content: 'Michael, just confirming our meeting tomorrow at 3 PM.', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 25), direction: 'outbound', status: 'read', channel: 'sms' },
{ id: 'm9', conversationId: '3', content: 'Perfect, I will bring the signed lease agreement.', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), direction: 'inbound', status: 'delivered', channel: 'sms' },
]
},
{
id: '4',
contact: { id: 'c4', name: 'Emily Davis', email: 'emily.davis@corp.com', phone: '+1 (555) 456-7890' },
lastMessage: 'Looking forward to discussing the investment opportunity.',
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 48), // 2 days ago
unreadCount: 0,
isStarred: false,
channel: 'email',
messages: [
{ id: 'm10', conversationId: '4', content: 'Looking forward to discussing the investment opportunity.', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 48), direction: 'inbound', status: 'read', channel: 'email' },
]
},
{
id: '5',
contact: { id: 'c5', name: 'Robert Wilson', email: 'rwilson@investors.com', phone: '+1 (555) 567-8901' },
lastMessage: 'Can you send me the updated financials?',
lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 72), // 3 days ago
unreadCount: 0,
isStarred: false,
channel: 'sms',
messages: [
{ id: 'm11', conversationId: '5', content: 'Can you send me the updated financials?', timestamp: new Date(Date.now() - 1000 * 60 * 60 * 72), direction: 'inbound', status: 'read', channel: 'sms' },
]
},
];
// Avatar color palette based on name
const avatarColors = [
'bg-blue-500',
'bg-emerald-500',
'bg-violet-500',
'bg-amber-500',
'bg-rose-500',
'bg-cyan-500',
'bg-indigo-500',
'bg-pink-500',
];
function getAvatarColor(name: string): string {
const charSum = name.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0);
return avatarColors[charSum % avatarColors.length];
}
function getInitials(name: string): string {
const parts = name.trim().split(' ').filter(Boolean);
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
return name.substring(0, 2).toUpperCase();
}
// Helper functions
function formatMessageTime(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (minutes < 1) return 'Now';
if (minutes < 60) return `${minutes}m`;
if (hours < 24) return `${hours}h`;
if (days === 1) return 'Yesterday';
if (days < 7) return date.toLocaleDateString([], { weekday: 'short' });
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
function formatFullTime(date: Date): string {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// Components
// Avatar Component
function Avatar({
name,
size = 'md',
showOnlineIndicator = false
}: {
name: string;
size?: 'sm' | 'md' | 'lg';
showOnlineIndicator?: boolean;
}) {
const sizeClasses = {
sm: 'w-8 h-8 text-xs',
md: 'w-11 h-11 text-sm',
lg: 'w-14 h-14 text-lg'
};
return (
{getInitials(name)}
{showOnlineIndicator && (
)}
);
}
// Conversation List Item
const ConversationItem = React.memo(function ConversationItem({
conversation,
isSelected,
onClick
}: {
conversation: Conversation;
isSelected: boolean;
onClick: () => void;
}) {
const isUnread = conversation.unreadCount > 0;
return (
{/* Avatar with channel indicator */}
{conversation.channel === 'sms' ? (
) : (
)}
{/* Content */}
{conversation.contact.name}
{conversation.isStarred && (
)}
{formatMessageTime(conversation.lastMessageTime)}
{conversation.lastMessage}
{isUnread && (
{conversation.unreadCount}
)}
);
});
// Conversation List (Left Panel)
function ConversationList({
conversations,
selectedId,
onSelect,
searchQuery,
onSearchChange,
activeFilter,
onFilterChange,
isLoading,
onNewMessage
}: {
conversations: Conversation[];
selectedId: string | null;
onSelect: (id: string) => void;
searchQuery: string;
onSearchChange: (query: string) => void;
activeFilter: FilterTab;
onFilterChange: (filter: FilterTab) => void;
isLoading: boolean;
onNewMessage: () => void;
}) {
const filters: { id: FilterTab; label: string; count?: number }[] = [
{ id: 'all', label: 'All' },
{ id: 'unread', label: 'Unread', count: conversations.filter(c => c.unreadCount > 0).length },
{ id: 'starred', label: 'Starred', count: conversations.filter(c => c.isStarred).length },
];
return (
{/* Header */}
{/* Search */}
onSearchChange(e.target.value)}
className={`w-full pl-12 ${searchQuery ? 'pr-12' : 'pr-4'} py-3 bg-muted/50 border border-border/50 rounded-xl text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/50 transition-all`}
/>
{searchQuery && (
onSearchChange('')}
className="absolute right-3 top-1/2 transform -translate-y-1/2 p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
>
)}
{/* Filter Tabs */}
{filters.map((filter) => (
onFilterChange(filter.id)}
className={`
flex-1 py-2.5 px-4 text-sm font-medium rounded-lg transition-all duration-200
flex items-center justify-center gap-2
${activeFilter === filter.id
? 'bg-background text-primary shadow-sm'
: 'text-slate-500 hover:text-foreground hover:bg-background/50'
}
`}
>
{filter.label}
{filter.count !== undefined && filter.count > 0 && (
{filter.count}
)}
))}
{/* Conversation List */}
{isLoading ? (
) : conversations.length === 0 ? (
{searchQuery ? (
) : activeFilter === 'unread' ? (
) : activeFilter === 'starred' ? (
) : (
)}
{searchQuery
? 'No results found'
: activeFilter === 'unread'
? 'All caught up!'
: activeFilter === 'starred'
? 'No starred conversations'
: 'No conversations yet'
}
{searchQuery
? 'Try a different search term'
: activeFilter === 'unread'
? 'You have no unread messages'
: activeFilter === 'starred'
? 'Star important conversations to find them here'
: 'Start a conversation to get going'
}
{!searchQuery && activeFilter === 'all' && (
New Message
)}
) : (
conversations.map((conversation) => (
onSelect(conversation.id)}
/>
))
)}
);
}
// Message Bubble
function MessageBubble({ message }: { message: Message }) {
const isOutbound = message.direction === 'outbound';
const StatusIcon = () => {
switch (message.status) {
case 'pending':
return ;
case 'sent':
return ;
case 'delivered':
return ;
case 'read':
return ;
default:
return null;
}
};
return (
{message.content}
{formatFullTime(message.timestamp)}
{isOutbound && }
);
}
// Message Composer
function MessageComposer({
channel,
onChannelChange,
message,
onMessageChange,
onSend,
isSending
}: {
channel: 'sms' | 'email';
onChannelChange: (channel: 'sms' | 'email') => void;
message: string;
onMessageChange: (message: string) => void;
onSend: () => void;
isSending: boolean;
}) {
const textareaRef = useRef(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (message.trim()) {
onSend();
}
}
};
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 150) + 'px';
}
}, [message]);
return (
{/* Channel Toggle */}
Send via:
onChannelChange('sms')}
className={`
flex items-center gap-2 py-2 px-4 text-sm font-medium rounded-lg transition-all
${channel === 'sms'
? 'bg-background text-emerald-600 shadow-sm'
: 'text-slate-500 hover:text-foreground hover:bg-background/50'
}
`}
>
SMS
onChannelChange('email')}
className={`
flex items-center gap-2 py-2 px-4 text-sm font-medium rounded-lg transition-all
${channel === 'email'
? 'bg-background text-blue-600 shadow-sm'
: 'text-slate-500 hover:text-foreground hover:bg-background/50'
}
`}
>
Email
{/* Composer */}
Press Enter to send, Shift + Enter for new line
);
}
// Empty State Illustration
function EmptyStateIllustration() {
return (
{/* Background circles */}
{/* Message bubbles illustration */}
{/* Main message icon */}
{/* Floating elements */}
);
}
// Message Thread (Right Panel)
function MessageThread({
conversation,
onBack,
showBackButton,
onNewMessage
}: {
conversation: Conversation | null;
onBack: () => void;
showBackButton: boolean;
onNewMessage: () => void;
}) {
const [message, setMessage] = useState('');
const [channel, setChannel] = useState<'sms' | 'email'>('sms');
const [isSending, setIsSending] = useState(false);
const messagesEndRef = useRef(null);
useEffect(() => {
if (conversation) {
setChannel(conversation.channel);
}
}, [conversation]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [conversation?.messages]);
const handleSend = async () => {
if (!message.trim() || !conversation) return;
setIsSending(true);
try {
// Send message via GHL API
if (conversation.channel === 'sms') {
await api.conversations.sendSMS(conversation.contact.id, message);
} else {
await api.conversations.sendEmail(conversation.contact.id, 'Reply', message);
}
setMessage('');
// Refresh messages would happen here via realtime or refetch
} catch (err) {
console.error('Failed to send message:', err);
} finally {
setIsSending(false);
}
};
if (!conversation) {
return (
Your Messages
Select a conversation from the list to view your messages, or start a new conversation to connect with clients and leads.
New Message
View Contacts
{/* Quick tips */}
Quick tips
Press Enter to send
Star important chats
Search to filter
);
}
return (
{/* Header */}
{showBackButton && (
)}
{/* Contact Avatar */}
{/* Contact Info */}
{conversation.contact.name}
{conversation.isStarred && (
)}
{conversation.channel === 'sms' ? conversation.contact.phone : conversation.contact.email}
{/* Actions */}
{/* Messages */}
{/* Date separator example */}
{conversation.messages.map((msg) => (
))}
{/* Composer */}
);
}
// Main Page Component
export default function ConversationsPage() {
const [conversations, setConversations] = useState([]);
const [selectedConversationId, setSelectedConversationId] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [activeFilter, setActiveFilter] = useState('all');
const [isLoading, setIsLoading] = useState(true);
const [isMobileThreadOpen, setIsMobileThreadOpen] = useState(false);
// Load conversations from GHL
useEffect(() => {
let isMounted = true;
const loadConversations = async () => {
setIsLoading(true);
try {
const response = await api.conversations.getAll({ limit: 50 });
if (isMounted && response.data) {
// Map GHL conversations to our format
const ghlConversations: Conversation[] = response.data.map((conv: any) => ({
id: conv.id,
contact: {
id: conv.contactId || conv.contact?.id || '',
name: conv.contactName || conv.contact?.name || conv.contact?.firstName + ' ' + (conv.contact?.lastName || '') || 'Unknown',
email: conv.contact?.email || '',
phone: conv.contact?.phone || '',
},
lastMessage: conv.lastMessageBody || conv.snippet || '',
lastMessageTime: new Date(conv.lastMessageDate || conv.dateUpdated || Date.now()),
unreadCount: conv.unreadCount || 0,
isStarred: conv.starred || false,
channel: conv.type === 'SMS' ? 'sms' : 'email',
messages: [], // Messages loaded separately when conversation is selected
}));
setConversations(ghlConversations);
}
} catch (err: any) {
console.error('Failed to fetch conversations:', err);
// Fall back to empty state - don't use mock data in production
if (isMounted) {
setConversations([]);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
loadConversations();
return () => {
isMounted = false;
};
}, []);
// Filter conversations with memoization to prevent unnecessary recalculations
const filteredConversations = useMemo(() => {
return conversations.filter((conv) => {
// Search filter
const searchLower = searchQuery.toLowerCase();
const matchesSearch = searchQuery === '' ||
conv.contact.name.toLowerCase().includes(searchLower) ||
conv.contact.email.toLowerCase().includes(searchLower) ||
conv.lastMessage.toLowerCase().includes(searchLower);
// Tab filter
let matchesFilter = true;
if (activeFilter === 'unread') {
matchesFilter = conv.unreadCount > 0;
} else if (activeFilter === 'starred') {
matchesFilter = conv.isStarred;
}
return matchesSearch && matchesFilter;
});
}, [conversations, searchQuery, activeFilter]);
const selectedConversation = useMemo(() => {
return conversations.find(c => c.id === selectedConversationId) || null;
}, [conversations, selectedConversationId]);
const handleSelectConversation = useCallback((id: string) => {
setSelectedConversationId(id);
setIsMobileThreadOpen(true);
}, []);
const handleBackToList = useCallback(() => {
setIsMobileThreadOpen(false);
}, []);
const handleNewMessage = useCallback(() => {
// Placeholder for new message functionality
console.log('New message clicked');
}, []);
return (
{/* Desktop Layout */}
{/* Left Panel - Conversation List */}
{/* Right Panel - Message Thread */}
{/* Mobile Layout */}
{/* List View */}
{/* Thread View (Full Screen) */}
);
}