Skip to content

Commit 0566f2e

Browse files
committed
collapsible, mobile
1 parent 1ad33da commit 0566f2e

File tree

2 files changed

+266
-39
lines changed

2 files changed

+266
-39
lines changed

apps/web/src/components/sidebar.tsx

Lines changed: 126 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ import { checkToolState, InvalidTokenError } from '@/lib/tinybird';
88
import { TOOLS, type AppGridItem, type ToolState } from '@/lib/constants';
99
import { SectionHeader } from '@/components/section-header';
1010
import { ScrollArea } from '@/components/ui/scroll-area';
11-
import { MessageSquare, HardDriveDownload, Settings } from 'lucide-react';
11+
import { MessageSquare, HardDriveDownload, Settings, ChevronRight, ChevronLeft, Menu, LayoutDashboard, Download, Plus } from 'lucide-react';
12+
import { Button } from '@/components/ui/button';
13+
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
1214
import Image from 'next/image';
1315

1416
function AppCard({
1517
app,
1618
state,
1719
token,
1820
isActive,
21+
isCollapsed
1922
}: {
2023
app: AppGridItem;
2124
state: ToolState;
2225
token?: string | null;
2326
isActive: boolean;
27+
isCollapsed?: boolean;
2428
}) {
2529
const stateColors = {
2630
configured: '',
@@ -30,19 +34,21 @@ function AppCard({
3034

3135
return (
3236
<Link
33-
key={app.id}
37+
className="block"
3438
href={`/${app.id}${token ? `?token=${token}` : ''}`}
3539
>
36-
<Card className={`p-3 hover:bg-accent mb-2 ${stateColors[state]} ${isActive ? 'bg-accent' : ''}`}>
37-
<div className="flex items-center justify-between w-full">
38-
<div className="flex items-center gap-3">
39-
{app.icon_url && <Image src={app.icon_url} width={16} height={16} alt={app.name} />}
40-
<span className="font-medium">{app.name}</span>
41-
</div>
42-
<div>
43-
{state === 'available' && <HardDriveDownload className="w-4 h-4 text-muted-foreground" />}
44-
{state === 'installed' && <Settings className="w-4 h-4 text-muted-foreground" />}
40+
<Card className={`h-[42px] hover:bg-accent mb-2 ${stateColors[state]} ${isActive ? 'bg-accent' : ''} ${isCollapsed ? 'w-10' : ''}`}>
41+
<div className={`flex items-center justify-between h-full ${isCollapsed ? 'px-2' : 'px-3'} w-full`}>
42+
<div className={`flex items-center gap-3 min-w-0 ${isCollapsed && 'mx-auto'}`}>
43+
{app.icon_url && <Image src={app.icon_url} width={16} height={16} alt={app.name} className="flex-shrink-0" />}
44+
{!isCollapsed && <span className="font-medium truncate">{app.name}</span>}
4545
</div>
46+
{!isCollapsed && (
47+
<div>
48+
{state === 'available' && <HardDriveDownload className="w-4 h-4 text-muted-foreground" />}
49+
{state === 'installed' && <Settings className="w-4 h-4 text-muted-foreground" />}
50+
</div>
51+
)}
4652
</div>
4753
</Card>
4854
</Link>
@@ -72,7 +78,7 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
7278
const [token, setToken] = useQueryState('token');
7379
const [toolStates, setToolStates] = useState<Record<string, ToolState>>({});
7480
const [isLoading, setIsLoading] = useState(false);
75-
// const [error,setError] = useState<string>();
81+
const [isCollapsed, setIsCollapsed] = useState(false);
7682

7783
useEffect(() => {
7884
async function fetchToolStates() {
@@ -83,7 +89,6 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
8389
setToolStates(Object.fromEntries(states));
8490
} else {
8591
setIsLoading(true);
86-
// setError(undefined);
8792
try {
8893
const allStates = await checkToolState(token);
8994
const states = Object.values(TOOLS).map((app) => {
@@ -92,11 +97,9 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
9297
setToolStates(Object.fromEntries(states));
9398
} catch (error) {
9499
if (error instanceof InvalidTokenError) {
95-
// setError('Invalid token');
96100
setToken(null);
97101
} else {
98102
console.error('Failed to fetch tool states:', error);
99-
// setError('Failed to fetch tool states');
100103
}
101104
} finally {
102105
setIsLoading(false);
@@ -107,17 +110,78 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
107110
}, [token, setToken]);
108111

109112
return (
110-
<div className="w-64 border-r h-screen">
113+
<>
114+
{/* Mobile Menu Button */}
115+
<Sheet>
116+
<SheetTrigger asChild>
117+
<Button variant="ghost" size="icon" className="fixed top-4 left-4 md:hidden">
118+
<Menu className="h-6 w-6" />
119+
</Button>
120+
</SheetTrigger>
121+
<SheetContent side="left" className="p-0 w-80">
122+
<SidebarInner
123+
token={token}
124+
activeAppId={activeAppId}
125+
toolStates={toolStates}
126+
isLoading={isLoading}
127+
/>
128+
</SheetContent>
129+
</Sheet>
130+
131+
{/* Desktop Sidebar */}
132+
<div className={`border-r h-screen hidden md:block relative ${isCollapsed ? 'w-auto' : 'w-64'} transition-all duration-300`}>
133+
<SidebarInner
134+
token={token}
135+
activeAppId={activeAppId}
136+
toolStates={toolStates}
137+
isLoading={isLoading}
138+
isCollapsed={isCollapsed}
139+
/>
140+
141+
{/* Collapse Button */}
142+
<Button
143+
variant="ghost"
144+
size="icon"
145+
className={`absolute ${isCollapsed ? '-right-4' : '-right-3'} bottom-4 rounded-full border shadow-md bg-background`}
146+
onClick={() => setIsCollapsed(!isCollapsed)}
147+
>
148+
{isCollapsed ? (
149+
<ChevronRight className="h-4 w-4" />
150+
) : (
151+
<ChevronLeft className="h-4 w-4" />
152+
)}
153+
</Button>
154+
</div>
155+
</>
156+
);
157+
}
158+
159+
// New component for the inner content of the sidebar
160+
function SidebarInner({
161+
token,
162+
activeAppId,
163+
toolStates,
164+
isLoading,
165+
isCollapsed
166+
}: {
167+
token: string | null | undefined;
168+
activeAppId?: string;
169+
toolStates: Record<string, ToolState>;
170+
isLoading: boolean;
171+
isCollapsed?: boolean;
172+
}) {
173+
return (
174+
<>
111175
<div className="p-4 border-b">
112176
<Link
113177
href={token ? `/?token=${token}` : '/'}
114-
className="text-xl font-bold hover:text-primary transition-colors"
178+
className={`font-bold hover:text-primary transition-colors ${isCollapsed ? 'text-lg' : 'text-xl'}`}
115179
>
116-
tinynest
180+
{isCollapsed ? 'tn' : 'tinynest'}
117181
</Link>
118182
</div>
119183

120-
<ScrollArea className="h-[calc(100vh-65px)] px-4 py-6">
184+
<ScrollArea className={`h-[calc(100vh-65px)] py-6 ${isCollapsed ? 'px-1' : 'px-4'}`}>
121185
{isLoading ? (
122186
<div className="flex items-center justify-center">
123187
<p className="text-sm font-semibold">Loading...</p>
@@ -128,11 +192,15 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
128192
href={token ? `/chat?token=${token}` : '/chat'}
129193
className="block"
130194
>
131-
<Card className={`p-3 hover:bg-accent mb-2 ${activeAppId === 'chat' ? 'bg-accent' : ''}`}>
132-
<div className="flex items-center gap-3">
133-
<MessageSquare className="w-5 h-5" />
134-
<div>
135-
<h3 className="font-semibold text-sm">Chat</h3>
195+
<Card className={`h-[42px] hover:bg-accent mb-2 ${activeAppId === 'chat' ? 'bg-accent' : ''} ${isCollapsed ? 'w-10' : ''}`}>
196+
<div className={`flex items-center h-full ${isCollapsed ? 'px-2' : 'px-3'} w-full`}>
197+
<div className={`flex items-center gap-3 min-w-0 ${isCollapsed && 'mx-auto'}`}>
198+
<MessageSquare className="w-4 h-4 flex-shrink-0" />
199+
{!isCollapsed && (
200+
<div>
201+
<h3 className="font-medium text-sm">Chat</h3>
202+
</div>
203+
)}
136204
</div>
137205
</div>
138206
</Card>
@@ -141,10 +209,16 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
141209
{/* Configured Apps */}
142210
{Object.values(TOOLS).some(app => toolStates[app.id] === 'configured') && (
143211
<div className="space-y-2">
144-
<SectionHeader
145-
title="Dashboards"
146-
tooltip="These apps are fully set up and have data. They're ready to use!"
147-
/>
212+
{isCollapsed ? (
213+
<div className="h-[24px] flex items-center justify-center mb-2 w-10">
214+
<LayoutDashboard className="w-4 h-4 text-muted-foreground" />
215+
</div>
216+
) : (
217+
<SectionHeader
218+
title="Dashboards"
219+
tooltip="These apps are fully set up and have data. They're ready to use!"
220+
/>
221+
)}
148222
<div className="space-y-2">
149223
{Object.values(TOOLS)
150224
.filter(app => toolStates[app.id] === 'configured')
@@ -155,6 +229,7 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
155229
state={toolStates[app.id]}
156230
token={token}
157231
isActive={app.id === activeAppId}
232+
isCollapsed={isCollapsed}
158233
/>
159234
))}
160235
</div>
@@ -164,10 +239,16 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
164239
{/* Installed Apps */}
165240
{Object.values(TOOLS).some(app => toolStates[app.id] === 'installed') && (
166241
<div className="space-y-2">
167-
<SectionHeader
168-
title="Installed"
169-
tooltip="Your Tinybird Workspace has the Data Sources installed, but you're not receiving data. Click an app to learn how to add data."
170-
/>
242+
{isCollapsed ? (
243+
<div className="h-[24px] flex items-center justify-center mb-2 w-10">
244+
<Settings className="w-4 h-4 text-muted-foreground" />
245+
</div>
246+
) : (
247+
<SectionHeader
248+
title="Installed"
249+
tooltip="Your Tinybird Workspace has the Data Sources installed, but you're not receiving data. Click an app to learn how to add data."
250+
/>
251+
)}
171252
<div className="space-y-2">
172253
{Object.values(TOOLS)
173254
.filter(app => toolStates[app.id] === 'installed')
@@ -178,6 +259,7 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
178259
state={toolStates[app.id]}
179260
token={token}
180261
isActive={app.id === activeAppId}
262+
isCollapsed={isCollapsed}
181263
/>
182264
))}
183265
</div>
@@ -187,10 +269,16 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
187269
{/* Available Apps */}
188270
{Object.values(TOOLS).some(app => toolStates[app.id] === 'available') && (
189271
<div className="space-y-2">
190-
<SectionHeader
191-
title="Available"
192-
tooltip="Your Tinybird Workspace doesn't have the Data Sources installed yet. Click an app to learn how to install it."
193-
/>
272+
{isCollapsed ? (
273+
<div className="h-[24px] flex items-center justify-center mb-2 w-10">
274+
<Plus className="w-4 h-4 text-muted-foreground" />
275+
</div>
276+
) : (
277+
<SectionHeader
278+
title="Available"
279+
tooltip="Your Tinybird Workspace doesn't have the Data Sources installed yet. Click an app to learn how to install it."
280+
/>
281+
)}
194282
<div className="space-y-2">
195283
{Object.values(TOOLS)
196284
.filter(app => !toolStates[app.id] || toolStates[app.id] === 'available')
@@ -201,16 +289,15 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
201289
state={toolStates[app.id] || 'available'}
202290
token={token}
203291
isActive={app.id === activeAppId}
292+
isCollapsed={isCollapsed}
204293
/>
205294
))}
206295
</div>
207296
</div>
208297
)}
209-
210298
</div>
211-
212299
)}
213300
</ScrollArea>
214-
</div>
301+
</>
215302
);
216303
}

0 commit comments

Comments
 (0)