cre-sync/components/Sidebar.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

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>
</>
);
};