AppSidebar.tsx raw
1 import {
2 BotIcon,
3 BoxIcon,
4 ChevronsUpDownIcon,
5 CreditCardIcon,
6 CircleHelpIcon,
7 HandCoinsIcon,
8 HomeIcon,
9 LogOutIcon,
10 LucideIcon,
11 Plug2Icon,
12 PlugZapIcon,
13 SettingsIcon,
14 SparklesIcon,
15 SquareStackIcon,
16 WalletIcon,
17 } from "lucide-react";
18 import React from "react";
19
20 import { Link, NavLink, useLocation, useNavigate } from "react-router";
21
22 import ExternalLink from "src/components/ExternalLink";
23 import { AlbyIcon } from "src/components/icons/Alby";
24 import { AlbyHubIcon } from "src/components/icons/AlbyHubIcon";
25 import { AlbyHubLogo } from "src/components/icons/AlbyHubLogo";
26 import { ProBadge } from "src/components/ProBadge";
27 import SidebarHint from "src/components/SidebarHint";
28 import { Badge } from "src/components/ui/badge";
29 import {
30 DropdownMenu,
31 DropdownMenuContent,
32 DropdownMenuItem,
33 DropdownMenuLabel,
34 DropdownMenuSeparator,
35 DropdownMenuTrigger,
36 } from "src/components/ui/dropdown-menu";
37 import {
38 Sidebar,
39 SidebarContent,
40 SidebarFooter,
41 SidebarGroup,
42 SidebarGroupContent,
43 SidebarHeader,
44 SidebarMenu,
45 SidebarMenuButton,
46 SidebarMenuItem,
47 useSidebar,
48 } from "src/components/ui/sidebar";
49 import { UpgradeDialog } from "src/components/UpgradeDialog";
50 import UserAvatar from "src/components/UserAvatar";
51 import { useAlbyMe } from "src/hooks/useAlbyMe";
52 import { useHealthCheck } from "src/hooks/useHealthCheck";
53 import { useInfo } from "src/hooks/useInfo";
54 import { deleteAuthToken } from "src/lib/auth";
55 import { isHttpMode } from "src/utils/isHttpMode";
56
57 function isPathActive(pathname: string, url: string) {
58 return pathname === url || pathname.startsWith(`${url}/`);
59 }
60
61 export function AppSidebar() {
62 const { data: albyMe } = useAlbyMe();
63
64 const { data: info, mutate: refetchInfo } = useInfo();
65 const { isMobile, setOpenMobile } = useSidebar();
66 const { hasChannelManagement } = useInfo();
67 const navigate = useNavigate();
68 const location = useLocation();
69
70 const _isHttpMode = isHttpMode();
71
72 const logout = React.useCallback(async () => {
73 deleteAuthToken();
74 await refetchInfo();
75
76 if (_isHttpMode) {
77 window.location.href = "/logout";
78 } else {
79 navigate("/", { replace: true });
80 }
81 }, [_isHttpMode, navigate, refetchInfo]);
82
83 const data = {
84 navMain: [
85 {
86 title: "Home",
87 url: "/home",
88 icon: HomeIcon,
89 },
90 {
91 title: "Wallet",
92 url: "/wallet",
93 icon: WalletIcon,
94 },
95 {
96 title: "Sub-wallets",
97 url: "/sub-wallets",
98 icon: SquareStackIcon,
99 },
100 {
101 title: "Connections",
102 url: "/apps",
103 icon: Plug2Icon,
104 },
105 {
106 title: "AI & Agents",
107 url: "/ai",
108 icon: BotIcon,
109 },
110 {
111 title: "Cards",
112 url: "/cards",
113 icon: CreditCardIcon,
114 badge: "NEW",
115 },
116 ],
117 navSecondary: [
118 ...(hasChannelManagement
119 ? [
120 {
121 title: "Node",
122 url: "/channels",
123 icon: BoxIcon,
124 },
125 ]
126 : []),
127 {
128 title: "Settings",
129 url: "/settings",
130 icon: SettingsIcon,
131 },
132 {
133 title: "Earn",
134 url: "/earn",
135 icon: HandCoinsIcon,
136 },
137 ],
138 };
139
140 return (
141 <Sidebar
142 className="top-(--header-height) h-[calc(100svh-var(--header-height))]!"
143 collapsible="offcanvas"
144 >
145 <SidebarHeader>
146 <div className="p-2 flex flex-row items-center justify-between">
147 <Link to="/home" onClick={() => setOpenMobile(false)}>
148 <AlbyHubLogo className="h-7" />
149 </Link>
150 <div className="flex gap-3 items-center">
151 <HealthIndicator />
152 </div>
153 </div>
154 </SidebarHeader>
155 <SidebarContent>
156 <SidebarGroup>
157 <SidebarGroupContent>
158 <SidebarMenu>
159 {data.navMain.map((item) => (
160 <SidebarMenuItem key={item.title}>
161 <SidebarMenuButton
162 asChild
163 isActive={isPathActive(location.pathname, item.url)}
164 >
165 <Link
166 to={item.url}
167 onClick={() => {
168 setOpenMobile(false);
169 }}
170 >
171 <item.icon />
172 <span>{item.title}</span>
173 {item.badge && (
174 <Badge className="ml-auto text-[10px] px-1.5 py-0">
175 {item.badge}
176 </Badge>
177 )}
178 </Link>
179 </SidebarMenuButton>
180 </SidebarMenuItem>
181 ))}
182 </SidebarMenu>
183 </SidebarGroupContent>
184 </SidebarGroup>
185 <div className="mt-auto">
186 <SidebarGroup>
187 <SidebarGroupContent>
188 <SidebarHint />
189 </SidebarGroupContent>
190 </SidebarGroup>
191 <NavSecondary items={data.navSecondary} />
192 </div>
193 </SidebarContent>
194 <SidebarFooter>
195 <SidebarMenu>
196 <SidebarMenuItem>
197 <DropdownMenu>
198 <SidebarMenuButton size="lg" asChild>
199 <DropdownMenuTrigger className="w-full">
200 {info?.albyAccountConnected ? (
201 <>
202 <UserAvatar />
203 <div className="grid flex-1 text-left text-sm leading-tight">
204 <div className="flex items-center gap-1 min-w-0">
205 <span className="truncate font-semibold">
206 {albyMe?.name || albyMe?.email}
207 </span>
208 {albyMe?.subscription?.plan_code && <ProBadge />}
209 </div>
210
211 <div className="truncate text-xs">
212 {albyMe?.lightning_address}
213 </div>
214 </div>
215 </>
216 ) : (
217 <>
218 <div className="size-8 flex items-center justify-center bg-sidebar-primary/80 text-sidebar-primary-foreground rounded-md">
219 <AlbyHubIcon className="size-4" />
220 </div>
221 <div className="font-semibold text-left text-sm leading-tight">
222 My Alby Hub
223 </div>
224 </>
225 )}
226 <ChevronsUpDownIcon className="ml-auto size-4" />
227 </DropdownMenuTrigger>
228 </SidebarMenuButton>
229 <DropdownMenuContent
230 className="w-64"
231 side={isMobile ? "bottom" : "right"}
232 align="end"
233 sideOffset={4}
234 >
235 {info?.albyAccountConnected && (
236 <>
237 <DropdownMenuLabel className="p-0 font-normal">
238 <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
239 <UserAvatar />
240 <div className="grid flex-1 text-left text-sm leading-tight">
241 <div className="flex items-center gap-1 min-w-0">
242 <span className="truncate font-semibold">
243 {albyMe?.name || albyMe?.email}
244 </span>
245 {albyMe?.subscription?.plan_code && <ProBadge />}
246 </div>
247 <span className="truncate text-xs">
248 {albyMe?.lightning_address}
249 </span>
250 </div>
251 </div>
252 </DropdownMenuLabel>
253 <DropdownMenuSeparator />
254 </>
255 )}
256 {!info?.albyAccountConnected ? (
257 <DropdownMenuItem asChild>
258 <Link
259 to="/alby/account"
260 className="w-full flex flex-row items-center gap-2"
261 >
262 <PlugZapIcon />
263 Connect Alby Account
264 </Link>
265 </DropdownMenuItem>
266 ) : (
267 <DropdownMenuItem asChild>
268 <Link
269 to="/settings/alby-account"
270 className="w-full flex flex-row items-center gap-2"
271 >
272 <AlbyIcon className="size-4" />
273 Alby Account Settings
274 </Link>
275 </DropdownMenuItem>
276 )}
277 {!albyMe?.subscription.plan_code && (
278 <>
279 <UpgradeDialog>
280 <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
281 <SparklesIcon />
282 Upgrade to Pro
283 </DropdownMenuItem>
284 </UpgradeDialog>
285 </>
286 )}
287 {_isHttpMode && (
288 <>
289 <DropdownMenuSeparator />
290 <DropdownMenuItem onClick={logout}>
291 <LogOutIcon className="size-4" />
292 Log out
293 </DropdownMenuItem>
294 </>
295 )}
296 </DropdownMenuContent>
297 </DropdownMenu>
298 </SidebarMenuItem>
299 </SidebarMenu>
300 </SidebarFooter>
301 </Sidebar>
302 );
303 }
304
305 export function NavSecondary({
306 items,
307 ...props
308 }: {
309 items: {
310 title: string;
311 url: string;
312 icon: LucideIcon;
313 }[];
314 } & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
315 const { setOpenMobile } = useSidebar();
316 const location = useLocation();
317
318 return (
319 <SidebarGroup {...props}>
320 <SidebarGroupContent>
321 <SidebarMenu>
322 {items.map((item) => (
323 <SidebarMenuItem key={item.title}>
324 <SidebarMenuButton
325 asChild
326 isActive={isPathActive(location.pathname, item.url)}
327 >
328 <NavLink
329 to={item.url}
330 end
331 onClick={() => {
332 setOpenMobile(false);
333 }}
334 >
335 <item.icon />
336 <span>{item.title}</span>
337 </NavLink>
338 </SidebarMenuButton>
339 </SidebarMenuItem>
340 ))}
341 <SidebarMenuItem>
342 <SidebarMenuButton asChild>
343 <ExternalLink to="https://support.getalby.com">
344 <CircleHelpIcon className="h-4 w-4" />
345 Help
346 </ExternalLink>
347 </SidebarMenuButton>
348 </SidebarMenuItem>
349 </SidebarMenu>
350 </SidebarGroupContent>
351 </SidebarGroup>
352 );
353 }
354
355 function HealthIndicator() {
356 const { data: health } = useHealthCheck();
357 const { setOpenMobile } = useSidebar();
358
359 if (!health) {
360 return null;
361 }
362
363 const ok = !health.alarms?.length;
364 if (ok) {
365 return null;
366 }
367
368 return (
369 <Link to="/channels" onClick={() => setOpenMobile(false)}>
370 <div className="w-2 h-2 rounded-full bg-destructive" />
371 </Link>
372 );
373 }
374