main
tsx 238 lines 9.31 KB
Raw
1 import { useEffect, useState } from "react";
2 import { KeyRound, Loader2, LogOut } from "lucide-react";
3 import { Button } from "@/components/ui/button";
4 import { ThemeToggleButton } from "@/components/ThemeToggleButton";
5 import { useAuth } from "@/hooks/useAuth";
6 import { apiClient } from "@/lib/apiClient";
7 import { BROWSER_API_PATHS } from "@/lib/apiPaths";
8 import type { DomainResponse } from "@/types/api";
9 import {
10 Tooltip,
11 TooltipContent,
12 TooltipProvider,
13 TooltipTrigger,
14 } from "@/components/ui/tooltip";
15
16 interface HeaderProps {
17 title?: string;
18 isAdmin?: boolean;
19 onAuthChange?: () => void | Promise<void>;
20 showQuickStartLink?: boolean;
21 }
22
23 const repoURL = "https://github.com/gosuda/portal-tunnel";
24
25 export function Header({
26 title = "PORTAL",
27 isAdmin,
28 onAuthChange,
29 showQuickStartLink = true,
30 }: HeaderProps) {
31 const [releaseVersion, setReleaseVersion] = useState("");
32 const [ensVerified, setENSVerified] = useState(false);
33 const {
34 isAuthenticated,
35 isLoading,
36 login,
37 logout,
38 } = useAuth();
39 const [adminToken, setAdminToken] = useState("");
40 const [authError, setAuthError] = useState("");
41
42 const handleAdminLogin = async () => {
43 setAuthError("");
44 const result = await login(adminToken);
45 if (!result.success) {
46 setAuthError(result.error || "Admin login failed.");
47 return;
48 }
49 setAdminToken("");
50 await onAuthChange?.();
51 };
52
53 const handleLogout = async () => {
54 setAuthError("");
55 await logout();
56 await onAuthChange?.();
57 };
58
59 useEffect(() => {
60 let cancelled = false;
61
62 void (async () => {
63 try {
64 const status = await apiClient.get<DomainResponse>(
65 BROWSER_API_PATHS.sdk.domain
66 );
67 if (!cancelled) {
68 setReleaseVersion(
69 typeof status?.release_version === "string"
70 ? status.release_version.trim()
71 : ""
72 );
73 setENSVerified(status?.ens?.verified === true);
74 }
75 } catch {
76 if (!cancelled) {
77 setReleaseVersion("");
78 setENSVerified(false);
79 }
80 }
81 })();
82
83 return () => {
84 cancelled = true;
85 };
86 }, []);
87
88 return (
89 <header className="flex flex-wrap items-center justify-between gap-x-4 gap-y-3 py-2 lg:flex-nowrap">
90 <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-foreground sm:gap-x-6 lg:gap-x-7">
91 <div className="min-w-0 text-foreground">
92 <div className="flex min-w-0 items-center gap-1.5 sm:gap-2">
93 <div className="flex h-10 w-10 shrink-0 items-center justify-center">
94 <svg
95 xmlns="http://www.w3.org/2000/svg"
96 width="27"
97 height="27"
98 viewBox="0 0 906.26 1457.543"
99 className="h-6 w-6 text-primary"
100 >
101 <path
102 fill="currentColor"
103 d="M254.854 137.158c-34.46 84.407-88.363 149.39-110.934 245.675 90.926-187.569 308.397-483.654 554.729-348.685 135.487 74.216 194.878 270.78 206.058 467.566 21.924 385.996-190.977 853.604-467.585 943.057-174.879 56.543-307.375-86.447-364.527-198.115-176.498-344.82 2.041-910.077 182.259-1109.498zm198.13 7.918C202.61 280.257 4.622 968.542 207.322 1270.414c51.713 77.029 194.535 160.648 285.294 71.318-209.061 31.529-288.389-176.143-301.145-340.765 31.411 147.743 139.396 326.12 309.075 253.588 251.957-107.723 376.778-648.46 269.433-966.817 22.394 134.616 15.572 317.711-47.551 412.087 86.655-230.615 7.903-704.478-269.444-554.749z"
104 />
105 </svg>
106 </div>
107
108 <div className="flex min-w-0 flex-wrap items-center gap-2.5">
109 <h2 className="min-w-0 wrap-break-word text-xl leading-none font-extrabold tracking-normal text-foreground sm:text-2xl">
110 {title}
111 </h2>
112 {releaseVersion && (
113 <span className="inline-flex h-6 items-center rounded-md bg-secondary px-2.5 text-xs font-semibold text-text-muted">
114 {releaseVersion}
115 </span>
116 )}
117 {ensVerified && (
118 <span className="inline-flex h-6 items-center rounded-md bg-primary/12 px-2.5 text-xs font-semibold text-primary ring-1 ring-primary/20">
119 ENS verified
120 </span>
121 )}
122 </div>
123 </div>
124 </div>
125 {!isAdmin && (
126 <nav className="hidden items-center gap-4 pl-2 text-sm font-semibold text-text-muted xl:flex xl:pl-3 2xl:gap-6 2xl:text-base">
127 {showQuickStartLink && (
128 <a
129 href="#quick-start"
130 className="whitespace-nowrap transition-colors hover:text-foreground"
131 >
132 Quick Start
133 </a>
134 )}
135 <a
136 href="#live-servers"
137 className="whitespace-nowrap transition-colors hover:text-foreground"
138 >
139 Live apps
140 </a>
141 <a
142 href="#public-relays"
143 className="whitespace-nowrap transition-colors hover:text-foreground"
144 >
145 Public relays
146 </a>
147 </nav>
148 )}
149 </div>
150
151 <div className="flex shrink-0 flex-wrap items-center gap-2 sm:gap-3">
152 {!isAdmin && (
153 <a
154 href={repoURL}
155 target="_blank"
156 rel="noopener noreferrer"
157 className="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-md border border-border/70 bg-background/90 text-foreground shadow-sm transition-colors hover:border-primary/40 hover:text-primary"
158 aria-label="View source on GitHub"
159 >
160 <svg
161 height="22"
162 width="22"
163 viewBox="0 0 24 24"
164 fill="currentColor"
165 className="opacity-85 transition-opacity hover:opacity-100"
166 >
167 <path d="M12 1C5.923 1 1 5.923 1 12c0 4.867 3.149 8.979 7.521 10.436.55.096.756-.233.756-.522 0-.262-.013-1.128-.013-2.049-2.764.509-3.479-.674-3.699-1.292-.124-.317-.66-1.293-1.127-1.554-.385-.207-.936-.715-.014-.729.866-.014 1.485.797 1.691 1.128.99 1.663 2.571 1.196 3.204.907.096-.715.385-1.196.701-1.471-2.448-.275-5.005-1.224-5.005-5.432 0-1.196.426-2.186 1.128-2.956-.111-.275-.496-1.402.11-2.915 0 0 .921-.288 3.024 1.128a10.193 10.193 0 0 1 2.75-.371c.936 0 1.871.123 2.75.371 2.104-1.43 3.025-1.128 3.025-1.128.605 1.513.221 2.64.111 2.915.701.77 1.127 1.747 1.127 2.956 0 4.222-2.571 5.157-5.019 5.432.399.344.743 1.004.743 2.035 0 1.471-.014 2.654-.014 3.025 0 .289.206.632.756.522C19.851 20.979 23 16.854 23 12c0-6.077-4.922-11-11-11Z" />
168 </svg>
169 </a>
170 )}
171
172 <ThemeToggleButton className="inline-flex shrink-0" />
173
174 {isAdmin && (
175 <div className="flex max-w-full flex-wrap items-center justify-end gap-2">
176 {authError && (
177 <span className="max-w-64 text-right text-xs font-medium text-destructive">
178 {authError}
179 </span>
180 )}
181
182 {!isAuthenticated ? (
183 <form
184 className="flex max-w-full flex-wrap items-center justify-end gap-2"
185 onSubmit={(event) => {
186 event.preventDefault();
187 void handleAdminLogin();
188 }}
189 >
190 <input
191 type="password"
192 value={adminToken}
193 onChange={(event) => setAdminToken(event.target.value)}
194 placeholder="Admin token"
195 autoComplete="current-password"
196 className="h-12 w-44 rounded-md border border-border/70 bg-background/90 px-4 text-sm text-foreground shadow-sm outline-none transition focus:border-primary/50 sm:w-56"
197 disabled={isLoading}
198 />
199 <Button
200 type="submit"
201 variant="outline"
202 disabled={isLoading}
203 className="h-12 rounded-md border-border/70 bg-background/90 px-4 text-foreground shadow-sm transition-colors hover:border-primary/40 hover:text-primary disabled:cursor-not-allowed"
204 >
205 {isLoading ? (
206 <Loader2 className="h-5 w-5 animate-spin" />
207 ) : (
208 <KeyRound className="h-5 w-5" />
209 )}
210 <span className="text-sm font-semibold">Login</span>
211 </Button>
212 </form>
213 ) : (
214 <TooltipProvider>
215 <Tooltip>
216 <TooltipTrigger asChild>
217 <Button
218 variant="outline"
219 size="icon"
220 onClick={handleLogout}
221 className="h-12 w-12 cursor-pointer rounded-md border-border/70 bg-background/90 text-foreground shadow-sm transition-colors hover:border-destructive/40 hover:bg-background hover:text-destructive"
222 aria-label="Logout"
223 >
224 <LogOut className="h-5 w-5" />
225 </Button>
226 </TooltipTrigger>
227 <TooltipContent>
228 <p>Logout</p>
229 </TooltipContent>
230 </Tooltip>
231 </TooltipProvider>
232 )}
233 </div>
234 )}
235 </div>
236 </header>
237 );
238 }