modul-e / src /pages /Gate.tsx
sitcod3's picture
Upload src/pages/Gate.tsx with huggingface_hub
bf27873 verified
Raw History Blame Contribute Delete
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>
);
}