- 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>
191 lines
6.1 KiB
TypeScript
191 lines
6.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
LayoutDashboard,
|
|
MessageSquare,
|
|
Users,
|
|
TrendingUp,
|
|
Target,
|
|
Zap,
|
|
CheckSquare,
|
|
BarChart2,
|
|
ShoppingBag,
|
|
Wrench,
|
|
Award,
|
|
ClipboardCheck,
|
|
Shield,
|
|
Menu,
|
|
X,
|
|
ChevronRight,
|
|
User,
|
|
Sparkles,
|
|
} from 'lucide-react';
|
|
import { ViewState } from '../types';
|
|
|
|
interface SidebarProps {
|
|
currentView: ViewState;
|
|
onNavigate: (view: ViewState) => void;
|
|
isAdmin: boolean;
|
|
userName?: string;
|
|
userEmail?: string;
|
|
}
|
|
|
|
interface NavItem {
|
|
id: ViewState;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
disabled?: boolean;
|
|
comingSoon?: boolean;
|
|
adminOnly?: boolean;
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{ id: ViewState.DASHBOARD, label: 'Dashboard', icon: <LayoutDashboard size={20} /> },
|
|
{ id: ViewState.CONTROL_CENTER, label: 'Control Center', icon: <Sparkles size={20} /> },
|
|
{ id: ViewState.CONVERSATIONS, label: 'Conversations', icon: <MessageSquare size={20} /> },
|
|
{ id: ViewState.CONTACTS, label: 'Contacts', icon: <Users size={20} /> },
|
|
{ id: ViewState.OPPORTUNITIES, label: 'Opportunities', icon: <TrendingUp size={20} /> },
|
|
{ id: ViewState.GET_LEADS, label: 'Get Leads', icon: <Target size={20} /> },
|
|
{ id: ViewState.AUTOMATIONS, label: 'Automations', icon: <Zap size={20} /> },
|
|
{ id: ViewState.TODO_LIST, label: 'To-Do List', icon: <CheckSquare size={20} /> },
|
|
{ id: ViewState.REPORTING, label: 'Reporting', icon: <BarChart2 size={20} /> },
|
|
{ id: ViewState.MARKETPLACE, label: 'Town Hall', icon: <ShoppingBag size={20} /> },
|
|
{ id: ViewState.EXTERNAL_TOOLS, label: 'External Tools', icon: <Wrench size={20} /> },
|
|
{ id: ViewState.LEADERBOARD, label: 'Leaderboard', icon: <Award size={20} />, disabled: true, comingSoon: true },
|
|
{ id: ViewState.QUIZ, label: 'Performance Quiz', icon: <ClipboardCheck size={20} /> },
|
|
{ id: ViewState.ADMIN, label: 'Admin', icon: <Shield size={20} />, adminOnly: true },
|
|
];
|
|
|
|
export const Sidebar: React.FC<SidebarProps> = ({
|
|
currentView,
|
|
onNavigate,
|
|
isAdmin,
|
|
userName,
|
|
userEmail,
|
|
}) => {
|
|
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
|
|
|
const toggleMobile = () => setIsMobileOpen(!isMobileOpen);
|
|
|
|
const handleNavClick = (itemId: ViewState, disabled?: boolean) => {
|
|
if (disabled) return;
|
|
onNavigate(itemId);
|
|
setIsMobileOpen(false);
|
|
};
|
|
|
|
// Filter out admin-only items for non-admin users
|
|
const visibleNavItems = navItems.filter(item => !item.adminOnly || isAdmin);
|
|
|
|
const renderNavItem = (item: NavItem) => {
|
|
const isActive = currentView === item.id;
|
|
const isDisabled = item.disabled;
|
|
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => handleNavClick(item.id, item.disabled)}
|
|
disabled={isDisabled}
|
|
className={`
|
|
w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left
|
|
transition-all duration-200 group relative
|
|
${isActive
|
|
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-600/30'
|
|
: isDisabled
|
|
? 'text-slate-500 cursor-not-allowed'
|
|
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
|
}
|
|
`}
|
|
>
|
|
<span className={`flex-shrink-0 ${isActive ? 'text-white' : isDisabled ? 'text-slate-500' : 'text-slate-400 group-hover:text-indigo-400'}`}>
|
|
{item.icon}
|
|
</span>
|
|
<span className="flex-1 font-medium text-sm">{item.label}</span>
|
|
{item.comingSoon && (
|
|
<span className="text-xs bg-slate-700 text-slate-400 px-2 py-0.5 rounded-full">
|
|
Soon
|
|
</span>
|
|
)}
|
|
{isActive && (
|
|
<ChevronRight size={16} className="text-white/70" />
|
|
)}
|
|
</button>
|
|
);
|
|
};
|
|
|
|
const sidebarContent = (
|
|
<>
|
|
{/* Logo */}
|
|
<div className="p-6 border-b border-slate-800">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-indigo-600 rounded-xl shadow-lg shadow-indigo-600/30 flex items-center justify-center text-white font-bold text-lg">
|
|
C
|
|
</div>
|
|
<span className="text-xl font-bold text-white tracking-tight">CRESync</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
|
{visibleNavItems.map(renderNavItem)}
|
|
</nav>
|
|
|
|
{/* User Profile Section */}
|
|
<div className="p-4 border-t border-slate-800">
|
|
<div className="flex items-center gap-3 px-4 py-3 rounded-xl bg-slate-800/50">
|
|
<div className="w-10 h-10 bg-indigo-600/20 rounded-full flex items-center justify-center text-indigo-400">
|
|
<User size={20} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
{userName ? (
|
|
<>
|
|
<p className="text-sm font-medium text-white truncate">{userName}</p>
|
|
{userEmail && (
|
|
<p className="text-xs text-slate-400 truncate">{userEmail}</p>
|
|
)}
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-slate-400">Not signed in</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile Hamburger Button */}
|
|
<button
|
|
onClick={toggleMobile}
|
|
className="lg:hidden fixed top-4 left-4 z-50 p-3 bg-slate-900 text-white rounded-xl shadow-lg hover:bg-slate-800 transition-colors"
|
|
aria-label="Toggle menu"
|
|
>
|
|
{isMobileOpen ? <X size={24} /> : <Menu size={24} />}
|
|
</button>
|
|
|
|
{/* Mobile Overlay */}
|
|
{isMobileOpen && (
|
|
<div
|
|
className="lg:hidden fixed inset-0 bg-black/50 z-40 backdrop-blur-sm"
|
|
onClick={() => setIsMobileOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar - Desktop */}
|
|
<aside className="hidden lg:flex lg:flex-col fixed left-0 top-0 h-full w-64 bg-slate-900 z-40">
|
|
{sidebarContent}
|
|
</aside>
|
|
|
|
{/* Sidebar - Mobile */}
|
|
<aside
|
|
className={`
|
|
lg:hidden fixed left-0 top-0 h-full w-72 bg-slate-900 z-50
|
|
transform transition-transform duration-300 ease-in-out flex flex-col
|
|
${isMobileOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
`}
|
|
>
|
|
{sidebarContent}
|
|
</aside>
|
|
</>
|
|
);
|
|
};
|