hmc-rag / frontend /src /components /PasswordGate.tsx
webmuppet
Refactor: replace Streamlit with React+Vite SPA + FastAPI server
b5c0df4
Raw
History Blame Contribute Delete
4.54 kB
import { useState, type FormEvent } from "react";
import { useAuth } from "../hooks/useAuth";
/**
* Full-screen password gate rendered when no valid session token is present.
*
* Visual style:
* - Violet gradient background: #3500B0 → #0F0027
* - Instrument Sans typeface (loaded globally via index.html)
* - rgba(255,255,255,0.06) input surface
*
* On submit: calls useAuth.login(); shows inline error on 401.
*
* Requirements: 3.1, 3.2, 3.3, 3.5
*/
export function PasswordGate() {
const { login } = useAuth();
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
setIsSubmitting(true);
try {
await login(password);
// On success, useAuth sets isAuthenticated = true and the parent
// (App.tsx) will unmount this gate and render the main UI.
} catch (err: unknown) {
// api.ts throws Error("Invalid password") on HTTP 401
if (
err instanceof Error &&
(err.message === "Invalid password" || err.message.includes("401"))
) {
setError("Incorrect password. Please try again.");
} else {
setError("Something went wrong. Please try again.");
}
} finally {
setIsSubmitting(false);
}
}
return (
<div
className="min-h-screen flex items-center justify-center font-sans"
style={{
background: "linear-gradient(to bottom, #3500B0 0%, #0F0027 100%)",
}}
>
<div className="w-full max-w-sm px-6">
{/* Hero title */}
<div className="mb-10 text-center">
<h1
className="text-3xl font-semibold tracking-tight"
style={{
background: "linear-gradient(135deg, #FFFFFF 0%, #B89BFF 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
>
Health Marketing Compliance
</h1>
<p className="mt-2 text-sm text-white/50">
Enter your access password to continue.
</p>
</div>
{/* Auth card */}
<form
onSubmit={handleSubmit}
noValidate
className="rounded-2xl p-8 flex flex-col gap-5"
style={{
background: "rgba(255,255,255,0.06)",
border: "1px solid rgba(255,255,255,0.10)",
backdropFilter: "blur(12px)",
}}
>
<div className="flex flex-col gap-2">
<label
htmlFor="password"
className="text-xs font-medium uppercase tracking-widest text-white/60"
>
Password
</label>
<input
id="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isSubmitting}
placeholder="••••••••"
className="w-full rounded-lg px-4 py-3 text-sm text-white placeholder-white/30
outline-none transition-all duration-150
focus:ring-2 focus:ring-white/30
disabled:opacity-50 disabled:cursor-not-allowed"
style={{
background: "rgba(255,255,255,0.06)",
border: "1px solid rgba(255,255,255,0.12)",
}}
/>
</div>
{/* Inline error — shown on 401 */}
{error && (
<p
role="alert"
className="text-sm text-red-300 text-center -mt-1"
>
{error}
</p>
)}
<button
type="submit"
disabled={isSubmitting || password.length === 0}
className="w-full rounded-lg py-3 text-sm font-semibold text-white
transition-all duration-150
disabled:opacity-40 disabled:cursor-not-allowed"
style={{
background: isSubmitting
? "rgba(83,46,232,0.6)"
: "linear-gradient(135deg, #542FE8 0%, #1E3798 100%)",
}}
>
{isSubmitting ? "Verifying…" : "Continue"}
</button>
</form>
</div>
</div>
);
}