Spaces:
Runtime error
Runtime error
| 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> | |
| ); | |
| } | |