| import React, { useEffect, useState } from 'react'; |
| import { Link, useLocation } from 'react-router-dom'; |
| import { |
| Zap, |
| LayoutDashboard, |
| Users, |
| Inbox, |
| Handshake, |
| PieChart, |
| ChevronLeft, |
| ChevronRight, |
| } from 'lucide-react'; |
| import { Button } from '@/components/ui/button'; |
| import { cn } from '@/lib/utils'; |
| import GoogleAuthBar from '@/components/layout/GoogleAuthBar'; |
|
|
| const NAV_ITEMS = [ |
| { label: 'Campaigns', href: '/', icon: LayoutDashboard }, |
| { label: 'Contacts', href: '/contacts', icon: Users }, |
| { label: 'Leads', href: '/leads', icon: Inbox }, |
| { label: 'Deals', href: '/deals', icon: Handshake }, |
| { label: 'Dashboard', href: '/dashboard', icon: PieChart }, |
| ]; |
|
|
| const SIDEBAR_COLLAPSED_KEY = 'sequenceai-sidebar-collapsed'; |
|
|
| function pathMatches(locationPath, href) { |
| if (href === '/') return locationPath === '/'; |
| return locationPath === href || locationPath.startsWith(`${href}/`); |
| } |
|
|
| export default function AppShell({ title, subtitle, rightContent, children }) { |
| const location = useLocation(); |
|
|
| const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { |
| try { |
| if (typeof window === 'undefined') return true; |
| const v = localStorage.getItem(SIDEBAR_COLLAPSED_KEY); |
| if (v === null || v === '') return true; |
| return v === '1'; |
| } catch { |
| return true; |
| } |
| }); |
|
|
| useEffect(() => { |
| try { |
| localStorage.setItem(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed ? '1' : '0'); |
| } catch { |
| |
| } |
| }, [sidebarCollapsed]); |
|
|
| return ( |
| <div className="flex h-[100dvh] max-h-[100dvh] flex-col overflow-hidden bg-gradient-to-br from-slate-50 via-white to-violet-50"> |
| {/* Single full-width rule under branding + page title */} |
| <header className="z-40 flex shrink-0 flex-col border-b border-slate-200 bg-white/80 backdrop-blur-sm"> |
| <div className="flex min-h-[4.25rem] items-stretch"> |
| <div |
| className={cn( |
| 'hidden md:flex shrink-0 border-r border-slate-200 bg-white/90 transition-[width] duration-200 ease-out', |
| sidebarCollapsed |
| ? 'w-16 flex-col items-center justify-center gap-1 px-1 py-3' |
| : 'w-72 flex flex-row items-center gap-3 px-4' |
| )} |
| > |
| {sidebarCollapsed ? ( |
| <div className="h-11 w-11 shrink-0 rounded-2xl bg-gradient-to-br from-violet-600 to-purple-600 flex items-center justify-center shadow-lg shadow-violet-200"> |
| <Zap className="h-5 w-5 text-white" /> |
| </div> |
| ) : ( |
| <> |
| <div className="h-11 w-11 shrink-0 rounded-2xl bg-gradient-to-br from-violet-600 to-purple-600 flex items-center justify-center shadow-lg shadow-violet-200"> |
| <Zap className="h-5 w-5 text-white" /> |
| </div> |
| <div className="min-w-0 flex-1"> |
| <h1 className="font-bold text-slate-800 text-lg leading-tight truncate"> |
| SequenceAI |
| </h1> |
| <p className="text-xs text-slate-500 truncate">CRM Workspace</p> |
| </div> |
| </> |
| )} |
| </div> |
| |
| <div |
| className={cn( |
| 'flex min-h-[4.25rem] flex-1 items-center gap-4 px-4 sm:px-5 md:px-6 lg:px-8 xl:px-10 2xl:px-12', |
| title || subtitle ? 'justify-between' : 'justify-end' |
| )} |
| > |
| {title || subtitle ? ( |
| <div className="min-w-0"> |
| {title ? <h2 className="text-xl font-bold text-slate-800">{title}</h2> : null} |
| {subtitle ? <p className="text-sm text-slate-500">{subtitle}</p> : null} |
| </div> |
| ) : ( |
| <span className="min-w-0 flex-1" aria-hidden /> |
| )} |
| <div className="relative flex shrink-0 items-center gap-2"> |
| <GoogleAuthBar /> |
| {rightContent} |
| </div> |
| </div> |
| </div> |
| <nav className="flex items-center gap-2 border-t border-slate-100 bg-white/90 px-4 py-2 md:hidden flex-wrap"> |
| {NAV_ITEMS.map((item) => { |
| const active = pathMatches(location.pathname, item.href); |
| return ( |
| <Button |
| asChild |
| key={item.href} |
| size="sm" |
| variant={active ? 'default' : 'outline'} |
| className={active ? 'bg-violet-600 hover:bg-violet-700' : ''} |
| > |
| <Link to={item.href}>{item.label}</Link> |
| </Button> |
| ); |
| })} |
| </nav> |
| </header> |
|
|
| <div className="flex min-h-0 flex-1 overflow-hidden"> |
| <aside |
| className={cn( |
| 'hidden md:flex h-full min-h-0 shrink-0 flex-col border-r border-slate-200 bg-white py-4 transition-[width] duration-200 ease-out', |
| sidebarCollapsed ? 'w-16 items-stretch px-2' : 'w-72 px-4' |
| )} |
| > |
| <nav |
| className={cn( |
| 'flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto', |
| sidebarCollapsed && 'items-center' |
| )} |
| > |
| {NAV_ITEMS.map((item) => { |
| const Icon = item.icon; |
| const active = pathMatches(location.pathname, item.href); |
| const activeHighlight = active && !sidebarCollapsed; |
| const collapsedActive = active && sidebarCollapsed; |
| return ( |
| <Link |
| to={item.href} |
| key={item.href} |
| title={sidebarCollapsed ? item.label : undefined} |
| aria-current={active ? 'page' : undefined} |
| className={cn( |
| 'flex rounded-2xl py-3 transition-all', |
| sidebarCollapsed |
| ? 'w-12 justify-center px-0' |
| : 'items-center gap-3 px-3', |
| activeHighlight |
| ? 'bg-violet-100 text-violet-700' |
| : 'text-slate-700 hover:bg-slate-50' |
| )} |
| > |
| <div |
| className={cn( |
| 'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border transition-colors', |
| activeHighlight |
| ? 'border-violet-200 bg-white text-violet-600' |
| : collapsedActive |
| ? 'border-violet-300 bg-violet-50/90 text-violet-800' |
| : 'border-slate-200 bg-white text-slate-500' |
| )} |
| > |
| <Icon className="h-5 w-5" strokeWidth={collapsedActive ? 2.25 : 2} /> |
| </div> |
| {!sidebarCollapsed && ( |
| <span |
| className={cn( |
| 'text-base font-medium', |
| activeHighlight ? 'text-violet-700' : 'text-slate-700' |
| )} |
| > |
| {item.label} |
| </span> |
| )} |
| </Link> |
| ); |
| })} |
| </nav> |
| <div |
| className={cn( |
| 'mt-auto flex shrink-0 border-t border-slate-100 pt-3', |
| sidebarCollapsed ? 'justify-center' : 'justify-start' |
| )} |
| > |
| <Button |
| type="button" |
| variant="ghost" |
| size="icon" |
| className="h-9 w-9 text-slate-500 hover:text-slate-800 hover:bg-slate-100" |
| aria-label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'} |
| onClick={() => setSidebarCollapsed((c) => !c)} |
| > |
| {sidebarCollapsed ? ( |
| <ChevronRight className="h-5 w-5" /> |
| ) : ( |
| <ChevronLeft className="h-5 w-5" /> |
| )} |
| </Button> |
| </div> |
| </aside> |
| |
| <div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"> |
| <main className="mx-auto w-full min-w-0 max-w-none px-4 sm:px-5 md:px-6 lg:px-8 xl:px-10 2xl:px-12 py-6 md:py-8"> |
| {children} |
| </main> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|