sitcod3 commited on
Commit
bf27873
·
verified ·
1 Parent(s): cd94c67

Upload src/pages/Gate.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/pages/Gate.tsx +262 -0
src/pages/Gate.tsx ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
3
+ import { useApiKey } from "../context/ApiKeyContext";
4
+ import "./gate.css";
5
+
6
+ type Phase = "idle" | "handshake" | "cleared";
7
+
8
+ const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
9
+
10
+ function delay(ms: number) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+
14
+ function maskKey(key: string): string {
15
+ const trimmed = key.trim();
16
+ if (trimmed.length <= 8) return "••••";
17
+ return `${trimmed.slice(0, 4)}••••${trimmed.slice(-4)}`;
18
+ }
19
+
20
+ function stampDate(): string {
21
+ return new Date()
22
+ .toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })
23
+ .toUpperCase();
24
+ }
25
+
26
+ export default function Gate() {
27
+ const { apiKeys, activeKeyId, addApiKey, setActiveKeyId } = useApiKey();
28
+ const navigate = useNavigate();
29
+
30
+ const prefersReducedMotion = useMemo(
31
+ () => window.matchMedia(REDUCED_MOTION_QUERY).matches,
32
+ []
33
+ );
34
+
35
+ const hasStoredKeys = apiKeys.length > 0;
36
+ const [enterMode, setEnterMode] = useState<"resume" | "fresh">(
37
+ hasStoredKeys ? "resume" : "fresh"
38
+ );
39
+
40
+ const [workspaceName, setWorkspaceName] = useState("");
41
+ const [apiKey, setApiKey] = useState("");
42
+ const [showKey, setShowKey] = useState(false);
43
+ const [resumeKeyId, setResumeKeyId] = useState(activeKeyId ?? apiKeys[0]?.id ?? "");
44
+ const [error, setError] = useState<string | null>(null);
45
+ const [phase, setPhase] = useState<Phase>("idle");
46
+
47
+ const resumeKey = apiKeys.find((k) => k.id === resumeKeyId);
48
+
49
+ const aliveRef = useRef(true);
50
+ const timersRef = useRef<number[]>([]);
51
+ useEffect(() => {
52
+ aliveRef.current = true;
53
+ return () => {
54
+ aliveRef.current = false;
55
+ timersRef.current.forEach((t) => clearTimeout(t));
56
+ };
57
+ }, []);
58
+
59
+ function finish(opts: { name: string; key: string; resumeId?: string }) {
60
+ if (!aliveRef.current) return;
61
+ if (opts.resumeId) {
62
+ setActiveKeyId(opts.resumeId);
63
+ } else {
64
+ addApiKey(opts.name, opts.key, true);
65
+ }
66
+ navigate("/", { replace: true });
67
+ }
68
+
69
+ async function runHandshake(opts: { name: string; key: string; resumeId?: string }) {
70
+ setPhase("handshake");
71
+ setError(null);
72
+
73
+ if (prefersReducedMotion) {
74
+ finish(opts);
75
+ return;
76
+ }
77
+
78
+ await delay(620);
79
+ if (!aliveRef.current) return;
80
+ setPhase("cleared");
81
+ const t = window.setTimeout(() => {
82
+ finish(opts);
83
+ }, 900);
84
+ timersRef.current.push(t);
85
+ }
86
+
87
+ function handleSubmit(event: FormEvent<HTMLFormElement>) {
88
+ event.preventDefault();
89
+ if (phase !== "idle") return;
90
+
91
+ if (enterMode === "resume" && resumeKey) {
92
+ void runHandshake({
93
+ name: resumeKey.name,
94
+ key: resumeKey.key,
95
+ resumeId: resumeKey.id,
96
+ });
97
+ return;
98
+ }
99
+
100
+ const name = workspaceName.trim();
101
+ const key = apiKey.trim();
102
+ if (!name) {
103
+ setError("Name your workspace — anything you will recognize later.");
104
+ return;
105
+ }
106
+ if (!key) {
107
+ setError("Paste your Smartlead API key to open the module.");
108
+ return;
109
+ }
110
+ void runHandshake({ name, key });
111
+ }
112
+
113
+ function enterDemo() {
114
+ if (phase !== "idle") return;
115
+ setEnterMode("fresh");
116
+ setWorkspaceName("Demo Workspace");
117
+ setApiKey("demo_bridge_e");
118
+ setError(null);
119
+ void runHandshake({ name: "Demo Workspace", key: "demo_bridge_e" });
120
+ }
121
+
122
+ const busy = phase !== "idle";
123
+ const buttonLabel =
124
+ phase === "idle"
125
+ ? "OPEN MODULE"
126
+ : phase === "handshake"
127
+ ? "OPENING…"
128
+ : "CLEARED";
129
+
130
+ return (
131
+ <div className="gate">
132
+ <h1 className="gate-sr">BRIDGE Modul E — connect a Smartlead workspace</h1>
133
+
134
+ <span className="gate-cropmark tl" aria-hidden="true" />
135
+ <span className="gate-cropmark tr" aria-hidden="true" />
136
+ <span className="gate-cropmark bl" aria-hidden="true" />
137
+ <span className="gate-cropmark br" aria-hidden="true" />
138
+
139
+ <div className="gate-formwrap">
140
+ {phase === "cleared" && (
141
+ <svg className="gate-stamp" viewBox="0 0 120 120" aria-hidden="true">
142
+ <circle cx="60" cy="60" r="56" fill="none" stroke="currentColor" strokeWidth="2.5" />
143
+ <circle cx="60" cy="60" r="47" fill="none" stroke="currentColor" strokeWidth="1.2" />
144
+ <path id="gate-stamp-arc" d="M 24 60 A 36 36 0 0 1 96 60" fill="none" />
145
+ <text fontSize="10.5" fontWeight="700" letterSpacing="2.5" fill="currentColor">
146
+ <textPath href="#gate-stamp-arc" startOffset="50%" textAnchor="middle">
147
+ CLEARED FOR DISPATCH
148
+ </textPath>
149
+ </text>
150
+ <text x="60" y="63" textAnchor="middle" fontSize="15" fontWeight="800" letterSpacing="1.5" fill="currentColor">
151
+ MODUL·E
152
+ </text>
153
+ <text x="60" y="78" textAnchor="middle" fontSize="8.5" fontWeight="600" letterSpacing="2" fill="currentColor">
154
+ {stampDate()}
155
+ </text>
156
+ </svg>
157
+ )}
158
+
159
+ <div className="gate-card">
160
+ <form onSubmit={handleSubmit} noValidate>
161
+ {enterMode === "resume" && hasStoredKeys ? (
162
+ <div className="gate-field">
163
+ <label htmlFor="gate-resume">Stored workspace</label>
164
+ <select
165
+ id="gate-resume"
166
+ value={resumeKeyId}
167
+ onChange={(e) => setResumeKeyId(e.target.value)}
168
+ disabled={busy}
169
+ >
170
+ {apiKeys.map((k) => (
171
+ <option key={k.id} value={k.id}>
172
+ {k.name} — {maskKey(k.key)}
173
+ </option>
174
+ ))}
175
+ </select>
176
+ </div>
177
+ ) : (
178
+ <>
179
+ <div className="gate-field">
180
+ <label htmlFor="gate-name">Workspace name</label>
181
+ <input
182
+ id="gate-name"
183
+ type="text"
184
+ value={workspaceName}
185
+ onChange={(e) => setWorkspaceName(e.target.value)}
186
+ placeholder="e.g. Acme growth pod"
187
+ autoComplete="off"
188
+ disabled={busy}
189
+ />
190
+ </div>
191
+ <div className="gate-field">
192
+ <label htmlFor="gate-key">Smartlead API key</label>
193
+ <div className="gate-key-row">
194
+ <input
195
+ id="gate-key"
196
+ type={showKey ? "text" : "password"}
197
+ value={apiKey}
198
+ onChange={(e) => setApiKey(e.target.value)}
199
+ placeholder="Paste your Smartlead key"
200
+ autoComplete="off"
201
+ spellCheck={false}
202
+ disabled={busy}
203
+ className="gate-key-input"
204
+ />
205
+ <button
206
+ type="button"
207
+ className="gate-peek"
208
+ onClick={() => setShowKey((v) => !v)}
209
+ disabled={busy || apiKey.length === 0}
210
+ >
211
+ {showKey ? "Hide" : "Show"}
212
+ </button>
213
+ </div>
214
+ </div>
215
+ </>
216
+ )}
217
+
218
+ {error && (
219
+ <p className="gate-error" role="alert">
220
+ {error}
221
+ </p>
222
+ )}
223
+
224
+ <button
225
+ type="submit"
226
+ className="gate-cta"
227
+ data-phase={phase}
228
+ disabled={busy}
229
+ >
230
+ {buttonLabel}
231
+ </button>
232
+
233
+ {enterMode === "resume" ? (
234
+ <button
235
+ type="button"
236
+ className="gate-switch"
237
+ onClick={() => {
238
+ setEnterMode("fresh");
239
+ setError(null);
240
+ }}
241
+ disabled={busy}
242
+ >
243
+ Use a different key
244
+ </button>
245
+ ) : (
246
+ <>
247
+ <div className="gate-or">
248
+ <span aria-hidden="true" />
249
+ OR
250
+ <span aria-hidden="true" />
251
+ </div>
252
+ <button type="button" className="gate-demo" onClick={enterDemo} disabled={busy}>
253
+ Enter with demo data
254
+ </button>
255
+ </>
256
+ )}
257
+ </form>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ );
262
+ }