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