Download src/pages/Gate.tsx from sitcod3/modul-e: direct link, hf CLI and curl.
- Browser
- Download file 8.51 kB
-
https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Gate.tsx
- Command line
-
hf download hf://spaces/sitcod3/modul-e/src/pages/Gate.tsx
-
curl -L -o Gate.tsx https://huggingface.co/spaces/sitcod3/modul-e/resolve/main/src/pages/Gate.tsx
8.51 kB
| import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; | |
| import { useNavigate } from "react-router-dom"; | |
| import { useApiKey } from "../context/ApiKeyContext"; | |
| import "./gate.css"; | |
| type Phase = "idle" | "handshake" | "cleared"; | |
| const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)"; | |
| function delay(ms: number) { | |
| return new Promise((resolve) => setTimeout(resolve, ms)); | |
| } | |
| function maskKey(key: string): string { | |
| const trimmed = key.trim(); | |
| if (trimmed.length <= 8) return "••••"; | |
| return `${trimmed.slice(0, 4)}••••${trimmed.slice(-4)}`; | |
| } | |
| function stampDate(): string { | |
| return new Date() | |
| .toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" }) | |
| .toUpperCase(); | |
| } | |
| export default function Gate() { | |
| const { apiKeys, activeKeyId, addApiKey, setActiveKeyId } = useApiKey(); | |
| const navigate = useNavigate(); | |
| const prefersReducedMotion = useMemo( | |
| () => window.matchMedia(REDUCED_MOTION_QUERY).matches, | |
| [] | |
| ); | |
| const hasStoredKeys = apiKeys.length > 0; | |
| const [enterMode, setEnterMode] = useState<"resume" | "fresh">( | |
| hasStoredKeys ? "resume" : "fresh" | |
| ); | |
| const [workspaceName, setWorkspaceName] = useState(""); | |
| const [apiKey, setApiKey] = useState(""); | |
| const [showKey, setShowKey] = useState(false); | |
| const [resumeKeyId, setResumeKeyId] = useState(activeKeyId ?? apiKeys[0]?.id ?? ""); | |
| const [error, setError] = useState<string | null>(null); | |
| const [phase, setPhase] = useState<Phase>("idle"); | |
| const resumeKey = apiKeys.find((k) => k.id === resumeKeyId); | |
| const aliveRef = useRef(true); | |
| const timersRef = useRef<number[]>([]); | |
| useEffect(() => { | |
| aliveRef.current = true; | |
| return () => { | |
| aliveRef.current = false; | |
| timersRef.current.forEach((t) => clearTimeout(t)); | |
| }; | |
| }, []); | |
| function finish(opts: { name: string; key: string; resumeId?: string }) { | |
| if (!aliveRef.current) return; | |
| if (opts.resumeId) { | |
| setActiveKeyId(opts.resumeId); | |
| } else { | |
| addApiKey(opts.name, opts.key, true); | |
| } | |
| navigate("/", { replace: true }); | |
| } | |
| async function runHandshake(opts: { name: string; key: string; resumeId?: string }) { | |
| setPhase("handshake"); | |
| setError(null); | |
| if (prefersReducedMotion) { | |
| finish(opts); | |
| return; | |
| } | |
| await delay(620); | |
| if (!aliveRef.current) return; | |
| setPhase("cleared"); | |
| const t = window.setTimeout(() => { | |
| finish(opts); | |
| }, 900); | |
| timersRef.current.push(t); | |
| } | |
| function handleSubmit(event: FormEvent<HTMLFormElement>) { | |
| event.preventDefault(); | |
| if (phase !== "idle") return; | |
| if (enterMode === "resume" && resumeKey) { | |
| void runHandshake({ | |
| name: resumeKey.name, | |
| key: resumeKey.key, | |
| resumeId: resumeKey.id, | |
| }); | |
| return; | |
| } | |
| const name = workspaceName.trim(); | |
| const key = apiKey.trim(); | |
| if (!name) { | |
| setError("Name your workspace — anything you will recognize later."); | |
| return; | |
| } | |
| if (!key) { | |
| setError("Paste your Smartlead API key to open the module."); | |
| return; | |
| } | |
| void runHandshake({ name, key }); | |
| } | |
| function enterDemo() { | |
| if (phase !== "idle") return; | |
| setEnterMode("fresh"); | |
| setWorkspaceName("Demo Workspace"); | |
| setApiKey("demo_bridge_e"); | |
| setError(null); | |
| void runHandshake({ name: "Demo Workspace", key: "demo_bridge_e" }); | |
| } | |
| const busy = phase !== "idle"; | |
| const buttonLabel = | |
| phase === "idle" | |
| ? "OPEN MODULE" | |
| : phase === "handshake" | |
| ? "OPENING…" | |
| : "CLEARED"; | |
| return ( | |
| <div className="gate"> | |
| <h1 className="gate-sr">BRIDGE Modul E — connect a Smartlead workspace</h1> | |
| <span className="gate-cropmark tl" aria-hidden="true" /> | |
| <span className="gate-cropmark tr" aria-hidden="true" /> | |
| <span className="gate-cropmark bl" aria-hidden="true" /> | |
| <span className="gate-cropmark br" aria-hidden="true" /> | |
| <div className="gate-formwrap"> | |
| {phase === "cleared" && ( | |
| <svg className="gate-stamp" viewBox="0 0 120 120" aria-hidden="true"> | |
| <circle cx="60" cy="60" r="56" fill="none" stroke="currentColor" strokeWidth="2.5" /> | |
| <circle cx="60" cy="60" r="47" fill="none" stroke="currentColor" strokeWidth="1.2" /> | |
| <path id="gate-stamp-arc" d="M 24 60 A 36 36 0 0 1 96 60" fill="none" /> | |
| <text fontSize="10.5" fontWeight="700" letterSpacing="2.5" fill="currentColor"> | |
| <textPath href="#gate-stamp-arc" startOffset="50%" textAnchor="middle"> | |
| CLEARED FOR DISPATCH | |
| </textPath> | |
| </text> | |
| <text x="60" y="63" textAnchor="middle" fontSize="15" fontWeight="800" letterSpacing="1.5" fill="currentColor"> | |
| MODUL·E | |
| </text> | |
| <text x="60" y="78" textAnchor="middle" fontSize="8.5" fontWeight="600" letterSpacing="2" fill="currentColor"> | |
| {stampDate()} | |
| </text> | |
| </svg> | |
| )} | |
| <div className="gate-card"> | |
| <form onSubmit={handleSubmit} noValidate> | |
| {enterMode === "resume" && hasStoredKeys ? ( | |
| <div className="gate-field"> | |
| <label htmlFor="gate-resume">Stored workspace</label> | |
| <select | |
| id="gate-resume" | |
| value={resumeKeyId} | |
| onChange={(e) => setResumeKeyId(e.target.value)} | |
| disabled={busy} | |
| > | |
| {apiKeys.map((k) => ( | |
| <option key={k.id} value={k.id}> | |
| {k.name} — {maskKey(k.key)} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| ) : ( | |
| <> | |
| <div className="gate-field"> | |
| <label htmlFor="gate-name">Workspace name</label> | |
| <input | |
| id="gate-name" | |
| type="text" | |
| value={workspaceName} | |
| onChange={(e) => setWorkspaceName(e.target.value)} | |
| placeholder="e.g. Acme growth pod" | |
| autoComplete="off" | |
| disabled={busy} | |
| /> | |
| </div> | |
| <div className="gate-field"> | |
| <label htmlFor="gate-key">Smartlead API key</label> | |
| <div className="gate-key-row"> | |
| <input | |
| id="gate-key" | |
| type={showKey ? "text" : "password"} | |
| value={apiKey} | |
| onChange={(e) => setApiKey(e.target.value)} | |
| placeholder="Paste your Smartlead key" | |
| autoComplete="off" | |
| spellCheck={false} | |
| disabled={busy} | |
| className="gate-key-input" | |
| /> | |
| <button | |
| type="button" | |
| className="gate-peek" | |
| onClick={() => setShowKey((v) => !v)} | |
| disabled={busy || apiKey.length === 0} | |
| > | |
| {showKey ? "Hide" : "Show"} | |
| </button> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| {error && ( | |
| <p className="gate-error" role="alert"> | |
| {error} | |
| </p> | |
| )} | |
| <button | |
| type="submit" | |
| className="gate-cta" | |
| data-phase={phase} | |
| disabled={busy} | |
| > | |
| {buttonLabel} | |
| </button> | |
| {enterMode === "resume" ? ( | |
| <button | |
| type="button" | |
| className="gate-switch" | |
| onClick={() => { | |
| setEnterMode("fresh"); | |
| setError(null); | |
| }} | |
| disabled={busy} | |
| > | |
| Use a different key | |
| </button> | |
| ) : ( | |
| <> | |
| <div className="gate-or"> | |
| <span aria-hidden="true" /> | |
| OR | |
| <span aria-hidden="true" /> | |
| </div> | |
| <button type="button" className="gate-demo" onClick={enterDemo} disabled={busy}> | |
| Enter with demo data | |
| </button> | |
| </> | |
| )} | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |