Skip to content

Commit 41d0cfb

Browse files
authored
Merge pull request #52 from tinybirdco/sidebarapps
side bar improvements
2 parents c1a88cd + 0ae5978 commit 41d0cfb

15 files changed

+301
-53
lines changed

apps/web/src/components/sidebar.tsx

Lines changed: 148 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,47 @@ 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 } from 'lucide-react';
11+
import { MessageSquare, HardDriveDownload, Settings, ChevronRight, ChevronLeft, Menu, LayoutDashboard, Plus } from 'lucide-react';
12+
import { Button } from '@/components/ui/button';
13+
import { Sheet, SheetContent, SheetTrigger, SheetTitle } 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 = {
26-
configured: 'border-green-500',
27-
installed: 'border-blue-500',
30+
configured: '',
31+
installed: '',
2832
available: ''
2933
};
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 gap-3">
38-
{app.icon_url && <Image src={app.icon_url} width={16} height={16} alt={app.name} />}
39-
<div>
40-
<div className="flex items-center gap-2">
41-
<h3 className="font-semibold text-sm">{app.name}</h3>
42-
<span className="text-xs text-muted-foreground">({state})</span>
43-
</div>
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>}
4445
</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+
)}
4552
</div>
4653
</Card>
4754
</Link>
@@ -71,46 +78,111 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
7178
const [token, setToken] = useQueryState('token');
7279
const [toolStates, setToolStates] = useState<Record<string, ToolState>>({});
7380
const [isLoading, setIsLoading] = useState(false);
74-
// const [error,setError] = useState<string>();
81+
const [isCollapsed, setIsCollapsed] = useState(false);
7582

7683
useEffect(() => {
7784
async function fetchToolStates() {
78-
if (!token) return;
79-
setIsLoading(true);
80-
// setError(undefined);
81-
try {
82-
const allStates = await checkToolState(token);
85+
if (!token) {
8386
const states = Object.values(TOOLS).map((app) => {
84-
return [app.id, allStates[app.ds]] as const;
87+
return [app.id, 'available'] as const;
8588
});
8689
setToolStates(Object.fromEntries(states));
87-
} catch (error) {
88-
if (error instanceof InvalidTokenError) {
89-
// setError('Invalid token');
90-
setToken(null);
91-
} else {
92-
console.error('Failed to fetch tool states:', error);
93-
// setError('Failed to fetch tool states');
90+
} else {
91+
setIsLoading(true);
92+
try {
93+
const allStates = await checkToolState(token);
94+
const states = Object.values(TOOLS).map((app) => {
95+
return [app.id, allStates[app.ds] ?? 'available'] as const;
96+
});
97+
setToolStates(Object.fromEntries(states));
98+
} catch (error) {
99+
if (error instanceof InvalidTokenError) {
100+
setToken(null);
101+
} else {
102+
console.error('Failed to fetch tool states:', error);
103+
}
104+
} finally {
105+
setIsLoading(false);
94106
}
95-
} finally {
96-
setIsLoading(false);
97107
}
98108
}
99-
if (token) fetchToolStates();
109+
fetchToolStates();
100110
}, [token, setToken]);
101111

102112
return (
103-
<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+
<SheetTitle className="sr-only">Navigation Menu</SheetTitle>
123+
<SidebarInner
124+
token={token}
125+
activeAppId={activeAppId}
126+
toolStates={toolStates}
127+
isLoading={isLoading}
128+
/>
129+
</SheetContent>
130+
</Sheet>
131+
132+
{/* Desktop Sidebar */}
133+
<div className={`border-r h-screen hidden md:block relative ${isCollapsed ? 'w-auto' : 'w-64'} transition-all duration-300`}>
134+
<SidebarInner
135+
token={token}
136+
activeAppId={activeAppId}
137+
toolStates={toolStates}
138+
isLoading={isLoading}
139+
isCollapsed={isCollapsed}
140+
/>
141+
142+
{/* Collapse Button */}
143+
<Button
144+
variant="ghost"
145+
size="icon"
146+
className={`absolute ${isCollapsed ? '-right-4' : '-right-3'} bottom-4 rounded-full border shadow-md bg-background`}
147+
onClick={() => setIsCollapsed(!isCollapsed)}
148+
>
149+
{isCollapsed ? (
150+
<ChevronRight className="h-4 w-4" />
151+
) : (
152+
<ChevronLeft className="h-4 w-4" />
153+
)}
154+
</Button>
155+
</div>
156+
</>
157+
);
158+
}
159+
160+
// New component for the inner content of the sidebar
161+
function SidebarInner({
162+
token,
163+
activeAppId,
164+
toolStates,
165+
isLoading,
166+
isCollapsed
167+
}: {
168+
token: string | null | undefined;
169+
activeAppId?: string;
170+
toolStates: Record<string, ToolState>;
171+
isLoading: boolean;
172+
isCollapsed?: boolean;
173+
}) {
174+
return (
175+
<>
104176
<div className="p-4 border-b">
105177
<Link
106178
href={token ? `/?token=${token}` : '/'}
107-
className="text-xl font-bold hover:text-primary transition-colors"
179+
className={`font-bold hover:text-primary transition-colors ${isCollapsed ? 'text-lg' : 'text-xl'}`}
108180
>
109-
tinynest
181+
{isCollapsed ? 'tn' : 'tinynest'}
110182
</Link>
111183
</div>
112184

113-
<ScrollArea className="h-[calc(100vh-65px)] px-4 py-6">
185+
<ScrollArea className={`h-[calc(100vh-65px)] py-6 ${isCollapsed ? 'px-1' : 'px-4'}`}>
114186
{isLoading ? (
115187
<div className="flex items-center justify-center">
116188
<p className="text-sm font-semibold">Loading...</p>
@@ -121,11 +193,15 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
121193
href={token ? `/chat?token=${token}` : '/chat'}
122194
className="block"
123195
>
124-
<Card className={`p-3 hover:bg-accent mb-2 ${activeAppId === 'chat' ? 'bg-accent' : ''}`}>
125-
<div className="flex items-center gap-3">
126-
<MessageSquare className="w-5 h-5" />
127-
<div>
128-
<h3 className="font-semibold text-sm">Chat</h3>
196+
<Card className={`h-[42px] hover:bg-accent mb-2 ${activeAppId === 'chat' ? 'bg-accent' : ''} ${isCollapsed ? 'w-10' : ''}`}>
197+
<div className={`flex items-center h-full ${isCollapsed ? 'px-2' : 'px-3'} w-full`}>
198+
<div className={`flex items-center gap-3 min-w-0 ${isCollapsed && 'mx-auto'}`}>
199+
<MessageSquare className="w-4 h-4 flex-shrink-0" />
200+
{!isCollapsed && (
201+
<div>
202+
<h3 className="font-medium text-sm">Chat</h3>
203+
</div>
204+
)}
129205
</div>
130206
</div>
131207
</Card>
@@ -134,10 +210,16 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
134210
{/* Configured Apps */}
135211
{Object.values(TOOLS).some(app => toolStates[app.id] === 'configured') && (
136212
<div className="space-y-2">
137-
<SectionHeader
138-
title="Configured Apps"
139-
tooltip="These apps are fully set up and have data. They're ready to use!"
140-
/>
213+
{isCollapsed ? (
214+
<div className="h-[24px] flex items-center justify-center mb-2 w-10">
215+
<LayoutDashboard className="w-4 h-4 text-muted-foreground" />
216+
</div>
217+
) : (
218+
<SectionHeader
219+
title="Dashboards"
220+
tooltip="These apps are fully set up and have data. They're ready to use!"
221+
/>
222+
)}
141223
<div className="space-y-2">
142224
{Object.values(TOOLS)
143225
.filter(app => toolStates[app.id] === 'configured')
@@ -148,6 +230,7 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
148230
state={toolStates[app.id]}
149231
token={token}
150232
isActive={app.id === activeAppId}
233+
isCollapsed={isCollapsed}
151234
/>
152235
))}
153236
</div>
@@ -157,10 +240,16 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
157240
{/* Installed Apps */}
158241
{Object.values(TOOLS).some(app => toolStates[app.id] === 'installed') && (
159242
<div className="space-y-2">
160-
<SectionHeader
161-
title="Installed Apps"
162-
tooltip="Your Tinybird Workspace has the Data Sources installed, but you're not receiving data. Click an app to learn how to add data."
163-
/>
243+
{isCollapsed ? (
244+
<div className="h-[24px] flex items-center justify-center mb-2 w-10">
245+
<Settings className="w-4 h-4 text-muted-foreground" />
246+
</div>
247+
) : (
248+
<SectionHeader
249+
title="Installed"
250+
tooltip="Your Tinybird Workspace has the Data Sources installed, but you're not receiving data. Click an app to learn how to add data."
251+
/>
252+
)}
164253
<div className="space-y-2">
165254
{Object.values(TOOLS)
166255
.filter(app => toolStates[app.id] === 'installed')
@@ -171,6 +260,7 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
171260
state={toolStates[app.id]}
172261
token={token}
173262
isActive={app.id === activeAppId}
263+
isCollapsed={isCollapsed}
174264
/>
175265
))}
176266
</div>
@@ -180,10 +270,16 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
180270
{/* Available Apps */}
181271
{Object.values(TOOLS).some(app => toolStates[app.id] === 'available') && (
182272
<div className="space-y-2">
183-
<SectionHeader
184-
title="Available Apps"
185-
tooltip="Your Tinybird Workspace doesn't have the Data Sources installed yet. Click an app to learn how to install it."
186-
/>
273+
{isCollapsed ? (
274+
<div className="h-[24px] flex items-center justify-center mb-2 w-10">
275+
<Plus className="w-4 h-4 text-muted-foreground" />
276+
</div>
277+
) : (
278+
<SectionHeader
279+
title="Available"
280+
tooltip="Your Tinybird Workspace doesn't have the Data Sources installed yet. Click an app to learn how to install it."
281+
/>
282+
)}
187283
<div className="space-y-2">
188284
{Object.values(TOOLS)
189285
.filter(app => !toolStates[app.id] || toolStates[app.id] === 'available')
@@ -194,16 +290,15 @@ function SidebarContent({ activeAppId }: { activeAppId?: string }) {
194290
state={toolStates[app.id] || 'available'}
195291
token={token}
196292
isActive={app.id === activeAppId}
293+
isCollapsed={isCollapsed}
197294
/>
198295
))}
199296
</div>
200297
</div>
201298
)}
202-
203299
</div>
204-
205300
)}
206301
</ScrollArea>
207-
</div>
302+
</>
208303
);
209304
}

0 commit comments

Comments
 (0)