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