Seth commited on
Commit
8f4ffac
·
1 Parent(s): cf0c608
frontend/src/components/layout/AppShell.jsx CHANGED
@@ -1,7 +1,16 @@
1
- import React from 'react';
2
  import { Link, useLocation } from 'react-router-dom';
3
- import { Zap, LayoutDashboard, Users, Inbox, Handshake } from 'lucide-react';
4
- import { Button } from "@/components/ui/button";
 
 
 
 
 
 
 
 
 
5
 
6
  const NAV_ITEMS = [
7
  { label: 'Generator', href: '/', icon: LayoutDashboard },
@@ -10,6 +19,8 @@ const NAV_ITEMS = [
10
  { label: 'Deals', href: '/deals', icon: Handshake },
11
  ];
12
 
 
 
13
  function pathMatches(locationPath, href) {
14
  if (href === '/') return locationPath === '/';
15
  return locationPath === href || locationPath.startsWith(`${href}/`);
@@ -17,21 +28,110 @@ function pathMatches(locationPath, href) {
17
 
18
  export default function AppShell({ title, subtitle, rightContent, children }) {
19
  const location = useLocation();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  return (
22
- <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-violet-50">
23
- <div className="flex min-h-screen">
24
- <aside className="hidden md:flex w-72 border-r border-slate-200 bg-white p-5 flex-col gap-8 sticky top-0 h-screen">
25
- <div className="flex items-center gap-3 px-2 py-1 border-b border-slate-100 pb-5">
26
- <div className="h-11 w-11 rounded-2xl bg-gradient-to-br from-violet-600 to-purple-600 flex items-center justify-center shadow-lg shadow-violet-200">
27
- <Zap className="h-5 w-5 text-white" />
28
- </div>
29
- <div>
30
- <h1 className="font-bold text-slate-800 text-lg">SequenceAI</h1>
31
- <p className="text-xs text-slate-500">CRM Workspace</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  </div>
 
33
  </div>
34
- <nav className="space-y-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  {NAV_ITEMS.map((item) => {
36
  const Icon = item.icon;
37
  const active = pathMatches(location.pathname, item.href);
@@ -39,59 +139,45 @@ export default function AppShell({ title, subtitle, rightContent, children }) {
39
  <Link
40
  to={item.href}
41
  key={item.href}
42
- className={`w-full flex items-center gap-3 rounded-2xl px-3 py-3 transition-all ${
 
 
 
 
 
43
  active
44
  ? 'bg-violet-100 text-violet-700'
45
  : 'text-slate-700 hover:bg-slate-50'
46
- }`}
47
  >
48
  <div
49
- className={`h-10 w-10 rounded-xl border flex items-center justify-center ${
 
50
  active
51
  ? 'border-violet-200 bg-white text-violet-600'
52
  : 'border-slate-200 bg-white text-slate-500'
53
- }`}
54
  >
55
  <Icon className="h-5 w-5" />
56
  </div>
57
- <span className={`text-base font-medium ${active ? 'text-violet-700' : 'text-slate-700'}`}>
58
- {item.label}
59
- </span>
 
 
 
 
 
 
 
60
  </Link>
61
  );
62
  })}
63
  </nav>
64
  </aside>
65
 
66
- <div className="flex-1 min-w-0">
67
- <header className="border-b border-slate-100 bg-white/80 backdrop-blur-sm sticky top-0 z-40">
68
- <div className="mx-auto w-full px-4 sm:px-5 md:px-6 lg:px-8 xl:px-10 2xl:px-12 py-4">
69
- <div className="flex items-center justify-between gap-4">
70
- <div>
71
- <h2 className="text-xl font-bold text-slate-800">{title}</h2>
72
- {subtitle && <p className="text-sm text-slate-500">{subtitle}</p>}
73
- </div>
74
- <div className="flex items-center gap-2">{rightContent}</div>
75
- </div>
76
- <nav className="md:hidden flex items-center gap-2 mt-3">
77
- {NAV_ITEMS.map((item) => {
78
- const active = pathMatches(location.pathname, item.href);
79
- return (
80
- <Button
81
- asChild
82
- key={item.href}
83
- size="sm"
84
- variant={active ? "default" : "outline"}
85
- className={active ? "bg-violet-600 hover:bg-violet-700" : ""}
86
- >
87
- <Link to={item.href}>{item.label}</Link>
88
- </Button>
89
- );
90
- })}
91
- </nav>
92
- </div>
93
- </header>
94
- <main className="w-full min-w-0 max-w-none mx-auto px-4 sm:px-5 md:px-6 lg:px-8 xl:px-10 2xl:px-12 py-6 md:py-8">
95
  {children}
96
  </main>
97
  </div>
 
1
+ import React, { useEffect, useState } from 'react';
2
  import { Link, useLocation } from 'react-router-dom';
3
+ import {
4
+ Zap,
5
+ LayoutDashboard,
6
+ Users,
7
+ Inbox,
8
+ Handshake,
9
+ ChevronLeft,
10
+ ChevronRight,
11
+ } from 'lucide-react';
12
+ import { Button } from '@/components/ui/button';
13
+ import { cn } from '@/lib/utils';
14
 
15
  const NAV_ITEMS = [
16
  { label: 'Generator', href: '/', icon: LayoutDashboard },
 
19
  { label: 'Deals', href: '/deals', icon: Handshake },
20
  ];
21
 
22
+ const SIDEBAR_COLLAPSED_KEY = 'sequenceai-sidebar-collapsed';
23
+
24
  function pathMatches(locationPath, href) {
25
  if (href === '/') return locationPath === '/';
26
  return locationPath === href || locationPath.startsWith(`${href}/`);
 
28
 
29
  export default function AppShell({ title, subtitle, rightContent, children }) {
30
  const location = useLocation();
31
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
32
+ try {
33
+ return typeof window !== 'undefined' && localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1';
34
+ } catch {
35
+ return false;
36
+ }
37
+ });
38
+
39
+ useEffect(() => {
40
+ try {
41
+ localStorage.setItem(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed ? '1' : '0');
42
+ } catch {
43
+ /* ignore */
44
+ }
45
+ }, [sidebarCollapsed]);
46
 
47
  return (
48
+ <div className="flex min-h-screen flex-col bg-gradient-to-br from-slate-50 via-white to-violet-50">
49
+ {/* Single full-width rule under branding + page title */}
50
+ <header className="sticky top-0 z-40 flex flex-col border-b border-slate-200 bg-white/80 backdrop-blur-sm">
51
+ <div className="flex min-h-[4.25rem] items-stretch">
52
+ <div
53
+ className={cn(
54
+ 'hidden md:flex shrink-0 border-r border-slate-200 bg-white/90 transition-[width] duration-200 ease-out',
55
+ sidebarCollapsed
56
+ ? 'w-16 flex-col items-center justify-center gap-1 px-1 py-3'
57
+ : 'w-72 flex flex-row items-center gap-3 px-4'
58
+ )}
59
+ >
60
+ {sidebarCollapsed ? (
61
+ <>
62
+ <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">
63
+ <Zap className="h-5 w-5 text-white" />
64
+ </div>
65
+ <Button
66
+ type="button"
67
+ variant="ghost"
68
+ size="icon"
69
+ className="mt-1 h-8 w-8 shrink-0 text-slate-500 hover:text-slate-800 hover:bg-slate-100"
70
+ aria-label="Expand sidebar"
71
+ onClick={() => setSidebarCollapsed(false)}
72
+ >
73
+ <ChevronRight className="h-4 w-4" />
74
+ </Button>
75
+ </>
76
+ ) : (
77
+ <>
78
+ <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">
79
+ <Zap className="h-5 w-5 text-white" />
80
+ </div>
81
+ <div className="min-w-0 flex-1">
82
+ <h1 className="font-bold text-slate-800 text-lg leading-tight truncate">
83
+ SequenceAI
84
+ </h1>
85
+ <p className="text-xs text-slate-500 truncate">CRM Workspace</p>
86
+ </div>
87
+ <Button
88
+ type="button"
89
+ variant="ghost"
90
+ size="icon"
91
+ className="shrink-0 h-9 w-9 text-slate-500 hover:text-slate-800 hover:bg-slate-100"
92
+ aria-label="Collapse sidebar"
93
+ onClick={() => setSidebarCollapsed(true)}
94
+ >
95
+ <ChevronLeft className="h-5 w-5" />
96
+ </Button>
97
+ </>
98
+ )}
99
+ </div>
100
+
101
+ <div className="flex min-h-[4.25rem] flex-1 items-center justify-between gap-4 px-4 sm:px-5 md:px-6 lg:px-8 xl:px-10 2xl:px-12">
102
+ <div className="min-w-0">
103
+ <h2 className="text-xl font-bold text-slate-800">{title}</h2>
104
+ {subtitle && <p className="text-sm text-slate-500">{subtitle}</p>}
105
  </div>
106
+ <div className="flex shrink-0 items-center gap-2">{rightContent}</div>
107
  </div>
108
+ </div>
109
+ <nav className="flex items-center gap-2 border-t border-slate-100 bg-white/90 px-4 py-2 md:hidden">
110
+ {NAV_ITEMS.map((item) => {
111
+ const active = pathMatches(location.pathname, item.href);
112
+ return (
113
+ <Button
114
+ asChild
115
+ key={item.href}
116
+ size="sm"
117
+ variant={active ? 'default' : 'outline'}
118
+ className={active ? 'bg-violet-600 hover:bg-violet-700' : ''}
119
+ >
120
+ <Link to={item.href}>{item.label}</Link>
121
+ </Button>
122
+ );
123
+ })}
124
+ </nav>
125
+ </header>
126
+
127
+ <div className="flex min-h-0 flex-1">
128
+ <aside
129
+ className={cn(
130
+ 'hidden md:flex shrink-0 flex-col gap-2 border-r border-slate-200 bg-white py-4 transition-[width] duration-200 ease-out',
131
+ sidebarCollapsed ? 'w-16 items-center px-2' : 'w-72 px-4'
132
+ )}
133
+ >
134
+ <nav className={cn('flex w-full flex-col gap-2', sidebarCollapsed && 'items-center')}>
135
  {NAV_ITEMS.map((item) => {
136
  const Icon = item.icon;
137
  const active = pathMatches(location.pathname, item.href);
 
139
  <Link
140
  to={item.href}
141
  key={item.href}
142
+ title={sidebarCollapsed ? item.label : undefined}
143
+ className={cn(
144
+ 'flex rounded-2xl py-3 transition-all',
145
+ sidebarCollapsed
146
+ ? 'w-12 justify-center px-0'
147
+ : 'items-center gap-3 px-3',
148
  active
149
  ? 'bg-violet-100 text-violet-700'
150
  : 'text-slate-700 hover:bg-slate-50'
151
+ )}
152
  >
153
  <div
154
+ className={cn(
155
+ 'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border',
156
  active
157
  ? 'border-violet-200 bg-white text-violet-600'
158
  : 'border-slate-200 bg-white text-slate-500'
159
+ )}
160
  >
161
  <Icon className="h-5 w-5" />
162
  </div>
163
+ {!sidebarCollapsed && (
164
+ <span
165
+ className={cn(
166
+ 'text-base font-medium',
167
+ active ? 'text-violet-700' : 'text-slate-700'
168
+ )}
169
+ >
170
+ {item.label}
171
+ </span>
172
+ )}
173
  </Link>
174
  );
175
  })}
176
  </nav>
177
  </aside>
178
 
179
+ <div className="min-w-0 flex-1 overflow-auto">
180
+ <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">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  {children}
182
  </main>
183
  </div>