File size: 10,618 Bytes
8134b4e e016c4b 8f4ffac ac07ad9 8f4ffac f7686fe e016c4b 8134b4e 84ca4b1 e016c4b 480db82 8d2acd1 ac07ad9 e016c4b 8f4ffac 480db82 e016c4b e9a187e 8f4ffac 51ee8a8 8f4ffac 51ee8a8 8f4ffac e016c4b 51ee8a8 8f4ffac 51ee8a8 8f4ffac cb49f6a 8f4ffac 4aea54e 698ffee f7686fe e016c4b 8f4ffac e9a187e 8134b4e 8f4ffac 51ee8a8 8f4ffac 51ee8a8 cb49f6a 8f4ffac cb49f6a 8134b4e e016c4b 480db82 51ee8a8 1695b82 e016c4b 60295bc e016c4b 8f4ffac 1695b82 8f4ffac 51ee8a8 60295bc 8f4ffac e016c4b 60295bc 8f4ffac 1695b82 51ee8a8 60295bc 1695b82 8f4ffac 60295bc 1695b82 60295bc 8f4ffac 51ee8a8 8f4ffac 60295bc e016c4b cb49f6a e016c4b 51ee8a8 8f4ffac 97b08c9 e016c4b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | 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 {
/* ignore */
}
}, [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>
);
}
|