SettingsLayout.tsx raw

   1  import React, { useState } from "react";
   2  import { NavLink, Outlet, useNavigate } from "react-router";
   3  import AppHeader from "src/components/AppHeader";
   4  
   5  import { useInfo } from "src/hooks/useInfo";
   6  
   7  import {
   8    ArrowRightLeftIcon,
   9    BoxIcon,
  10    BugIcon,
  11    CloudBackupIcon,
  12    CodeIcon,
  13    FingerprintIcon,
  14    InfoIcon,
  15    KeyRoundIcon,
  16    type LucideIcon,
  17    PowerIcon,
  18    SlidersHorizontalIcon,
  19    UserIcon,
  20  } from "lucide-react";
  21  import { toast } from "sonner";
  22  import {
  23    AlertDialog,
  24    AlertDialogAction,
  25    AlertDialogCancel,
  26    AlertDialogContent,
  27    AlertDialogDescription,
  28    AlertDialogFooter,
  29    AlertDialogHeader,
  30    AlertDialogTitle,
  31    AlertDialogTrigger,
  32  } from "src/components/ui/alert-dialog";
  33  import { LoadingButton } from "src/components/ui/custom/loading-button";
  34  import { Separator } from "src/components/ui/separator";
  35  import { cn } from "src/lib/utils";
  36  import { request } from "src/utils/request";
  37  
  38  export default function SettingsLayout() {
  39    const {
  40      data: info,
  41      mutate: refetchInfo,
  42      hasMnemonic,
  43      hasNodeBackup,
  44    } = useInfo();
  45    const navigate = useNavigate();
  46    const [shuttingDown, setShuttingDown] = useState(false);
  47  
  48    const shutdown = React.useCallback(async () => {
  49      setShuttingDown(true);
  50      try {
  51        await request("/api/stop", {
  52          method: "POST",
  53          headers: {
  54            "Content-Type": "application/json",
  55          },
  56        });
  57  
  58        await refetchInfo();
  59        setShuttingDown(false);
  60        navigate("/", { replace: true });
  61        toast("Your node has been turned off.");
  62      } catch (error) {
  63        console.error(error);
  64        toast.error("Failed to shutdown node", {
  65          description: "" + error,
  66        });
  67      }
  68    }, [navigate, refetchInfo]);
  69  
  70    return (
  71      <>
  72        <AppHeader
  73          title="Settings"
  74          breadcrumb={false}
  75          contentRight={
  76            <div className="flex items-center gap-4">
  77              <div className="font-medium slashed-zero text-muted-foreground text-sm">
  78                {info?.version}
  79              </div>
  80              <AlertDialog>
  81                <AlertDialogTrigger asChild>
  82                  <LoadingButton
  83                    variant="destructive"
  84                    size="icon"
  85                    loading={shuttingDown}
  86                  >
  87                    {!shuttingDown && <PowerIcon className="size-4" />}
  88                  </LoadingButton>
  89                </AlertDialogTrigger>
  90                <AlertDialogContent>
  91                  <AlertDialogHeader>
  92                    <AlertDialogTitle>
  93                      Do you want to turn off your Alby Hub?
  94                    </AlertDialogTitle>
  95                    <AlertDialogDescription>
  96                      This will turn off your Alby Hub and make your node offline.
  97                      You won't be able to send or receive bitcoin until you
  98                      unlock it.
  99                    </AlertDialogDescription>
 100                  </AlertDialogHeader>
 101                  <AlertDialogFooter>
 102                    <AlertDialogCancel>Cancel</AlertDialogCancel>
 103                    <AlertDialogAction onClick={shutdown}>
 104                      Continue
 105                    </AlertDialogAction>
 106                  </AlertDialogFooter>
 107                </AlertDialogContent>
 108              </AlertDialog>
 109            </div>
 110          }
 111        />
 112  
 113        <div className="flex flex-col space-y-8 lg:flex-row lg:space-x-4 lg:space-y-0 h-full">
 114          <aside className="flex flex-col justify-between lg:w-1/5">
 115            <nav className="flex overflow-x-auto pb-2 gap-1 lg:flex-col lg:overflow-x-visible lg:pb-0 lg:gap-0 lg:space-y-0.5">
 116              <MenuItem to="/settings" icon={SlidersHorizontalIcon}>
 117                General
 118              </MenuItem>
 119  
 120              <NavGroup label="Security">
 121                <MenuItem
 122                  to="/settings/change-unlock-password"
 123                  icon={KeyRoundIcon}
 124                >
 125                  Unlock Password
 126                </MenuItem>
 127                {info?.autoUnlockPasswordSupported && (
 128                  <MenuItem to="/settings/auto-unlock" icon={FingerprintIcon}>
 129                    Auto Unlock
 130                  </MenuItem>
 131                )}
 132              </NavGroup>
 133  
 134              {(hasMnemonic || hasNodeBackup) && (
 135                <NavGroup label="Data">
 136                  {hasMnemonic && (
 137                    <MenuItem to="/settings/backup" icon={CloudBackupIcon}>
 138                      Backup
 139                    </MenuItem>
 140                  )}
 141                  {hasNodeBackup && (
 142                    <MenuItem
 143                      to="/settings/node-migrate"
 144                      icon={ArrowRightLeftIcon}
 145                    >
 146                      Migrate Alby Hub
 147                    </MenuItem>
 148                  )}
 149                </NavGroup>
 150              )}
 151  
 152              <NavGroup label="Account">
 153                {info?.albyAccountConnected && (
 154                  <MenuItem to="/settings/alby-account" icon={UserIcon}>
 155                    Your Alby Account
 156                  </MenuItem>
 157                )}
 158                {info && !info.albyAccountConnected && (
 159                  <MenuItem to="/alby/account" icon={UserIcon}>
 160                    Alby Account
 161                  </MenuItem>
 162                )}
 163              </NavGroup>
 164  
 165              <NavGroup label="Advanced">
 166                {info?.backendType === "LDK" && (
 167                  <MenuItem to="/settings/node" icon={BoxIcon}>
 168                    Node
 169                  </MenuItem>
 170                )}
 171                <MenuItem to="/settings/developer" icon={CodeIcon}>
 172                  Developer
 173                </MenuItem>
 174                <MenuItem to="/settings/debug-tools" icon={BugIcon}>
 175                  Debug Tools
 176                </MenuItem>
 177                <MenuItem to="/settings/about" icon={InfoIcon}>
 178                  About
 179                </MenuItem>
 180              </NavGroup>
 181            </nav>
 182          </aside>
 183          <Separator orientation="vertical" className="hidden lg:block" />
 184          <div className="flex-1 lg:max-w-2xl">
 185            <div className="grid gap-6">
 186              <Outlet />
 187            </div>
 188          </div>
 189        </div>
 190      </>
 191    );
 192  }
 193  
 194  const NavGroup = ({
 195    label,
 196    children,
 197  }: {
 198    label: string;
 199    children: React.ReactNode;
 200  }) => (
 201    <div className="contents lg:block lg:pt-4 lg:space-y-0.5">
 202      <span className="hidden lg:block px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
 203        {label}
 204      </span>
 205      {children}
 206    </div>
 207  );
 208  
 209  export const MenuItem = ({
 210    to,
 211    icon: Icon,
 212    children,
 213  }: {
 214    to: string;
 215    icon?: LucideIcon;
 216    children: React.ReactNode | string;
 217  }) => (
 218    <NavLink
 219      end
 220      to={to}
 221      className={({ isActive }) =>
 222        cn(
 223          "relative flex items-center gap-2 whitespace-nowrap rounded-md px-3 py-2 text-sm transition-colors text-foreground",
 224          isActive ? "bg-muted font-medium" : "hover:bg-muted/50"
 225        )
 226      }
 227    >
 228      {() => (
 229        <>
 230          {Icon && <Icon className="size-4 shrink-0 text-foreground" />}
 231          {children}
 232        </>
 233      )}
 234    </NavLink>
 235  );
 236