cre-sync/components/control-center/ConversationSidebar.tsx
BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

193 lines
6.3 KiB
TypeScript

'use client';
import React from 'react';
import { Plus, MessageSquare } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
import type { ControlCenterConversation } from '@/types/control-center';
interface ConversationSidebarProps {
/** List of conversations to display */
conversations: ControlCenterConversation[];
/** ID of the currently selected conversation */
currentId?: string;
/** Callback when a conversation is selected */
onSelect: (id: string) => void;
/** Callback when the user wants to start a new conversation */
onNew: () => void;
}
/**
* Format a date string to relative time (e.g., "2 hours ago", "Yesterday")
*/
function formatRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) {
return 'Just now';
}
const diffInMinutes = Math.floor(diffInSeconds / 60);
if (diffInMinutes < 60) {
return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`;
}
const diffInHours = Math.floor(diffInMinutes / 60);
if (diffInHours < 24) {
return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`;
}
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays === 1) {
return 'Yesterday';
}
if (diffInDays < 7) {
return `${diffInDays} days ago`;
}
const diffInWeeks = Math.floor(diffInDays / 7);
if (diffInWeeks < 4) {
return `${diffInWeeks} week${diffInWeeks !== 1 ? 's' : ''} ago`;
}
const diffInMonths = Math.floor(diffInDays / 30);
if (diffInMonths < 12) {
return `${diffInMonths} month${diffInMonths !== 1 ? 's' : ''} ago`;
}
const diffInYears = Math.floor(diffInDays / 365);
return `${diffInYears} year${diffInYears !== 1 ? 's' : ''} ago`;
}
/**
* ConversationSidebar - Displays conversation history for the Control Center
*
* Shows a scrollable list of past conversations with relative timestamps,
* plus a button to start new conversations.
*/
export const ConversationSidebar: React.FC<ConversationSidebarProps> = ({
conversations,
currentId,
onSelect,
onNew,
}) => {
return (
<div
className={cn(
'flex flex-col',
'h-full',
'bg-[#F0F4F8]',
'rounded-2xl',
'shadow-[6px_6px_12px_#bfc3cc,-6px_-6px_12px_#ffffff]',
'overflow-hidden'
)}
>
{/* Header with New Chat button */}
<div className="p-4 border-b border-gray-200/50">
<button
onClick={onNew}
className={cn(
'w-full',
'flex items-center justify-center gap-2',
'px-4 py-3',
'bg-indigo-500',
'text-white',
'rounded-xl',
'font-medium',
'shadow-[4px_4px_8px_#bfc3cc,-4px_-4px_8px_#ffffff]',
'hover:bg-indigo-600',
'hover:shadow-[2px_2px_4px_#bfc3cc,-2px_-2px_4px_#ffffff]',
'active:shadow-[inset_2px_2px_4px_rgba(0,0,0,0.1)]',
'transition-all duration-200'
)}
>
<Plus className="w-5 h-5" />
<span>New Chat</span>
</button>
</div>
{/* Conversation list */}
<ScrollArea className="flex-1">
<div className="p-3">
{conversations.length === 0 ? (
// Empty state
<div className="flex flex-col items-center justify-center py-12 px-4">
<div
className={cn(
'w-16 h-16',
'flex items-center justify-center',
'rounded-2xl',
'bg-[#F0F4F8]',
'shadow-[inset_3px_3px_6px_rgba(0,0,0,0.06),inset_-3px_-3px_6px_rgba(255,255,255,0.9)]',
'mb-4'
)}
>
<MessageSquare className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-sm font-medium text-gray-600 mb-1">
No conversations yet
</h3>
<p className="text-xs text-gray-400 text-center">
Start a new chat to begin exploring your GHL account
</p>
</div>
) : (
// Conversation items
<div className="flex flex-col gap-2">
{conversations.map((conversation) => {
const isSelected = conversation.id === currentId;
const displayTitle = conversation.title || 'New conversation';
return (
<button
key={conversation.id}
onClick={() => onSelect(conversation.id)}
className={cn(
'w-full',
'text-left',
'px-4 py-3',
'rounded-xl',
'transition-all duration-200',
isSelected
? [
'bg-[#F0F4F8]',
'shadow-[inset_4px_4px_8px_rgba(0,0,0,0.05),inset_-4px_-4px_8px_rgba(255,255,255,0.8)]',
'border-2 border-indigo-500',
]
: [
'bg-[#F0F4F8]',
'shadow-[3px_3px_6px_#c5c9d1,-3px_-3px_6px_#ffffff]',
'border-2 border-transparent',
'hover:shadow-[2px_2px_4px_#c5c9d1,-2px_-2px_4px_#ffffff]',
'hover:border-gray-300',
]
)}
>
<div className="flex flex-col gap-1">
<span
className={cn(
'text-sm font-medium truncate',
isSelected ? 'text-indigo-700' : 'text-gray-700'
)}
>
{displayTitle}
</span>
<span className="text-xs text-gray-400">
{formatRelativeTime(conversation.createdAt)}
</span>
</div>
</button>
);
})}
</div>
)}
</div>
</ScrollArea>
</div>
);
};
export default ConversationSidebar;