Files
qtalker---/frontend/src/pages/EnterPage.tsx
Indiana 6edbbbbc2a feat: complete Quantumancy web app — full frontend + docs
Frontend (React 18 + TS + Vite):
- Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits
- Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive),
  Ouija planchette board spelling utterances, transcript with TTS replay,
  entity dossier, direct contact streaming, passive/active listening
- Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB
  RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact
- Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete
- State: VeilSocket (reconnect/backoff), seance reducer, auth context
- 72 vitest tests green; served by FastAPI at :7777

Docs: README + as-built plans 3-7
2026-07-20 21:11:49 +00:00

130 lines
3.9 KiB
TypeScript

// Enter page: the threshold. Gothic auth panel floating in CSS fog —
// toggles between Reconnect (login) and First contact (register), and
// steps the medium through to the séance on success.
import { useState } from 'react'
import type { FormEvent } from 'react'
import { Navigate, useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useAuth } from '../state/auth'
import './EnterPage.css'
type EnterMode = 'login' | 'register'
export function EnterPage() {
const { t } = useTranslation()
const { user, login, register } = useAuth()
const navigate = useNavigate()
const [mode, setMode] = useState<EnterMode>('login')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [busy, setBusy] = useState(false)
// Already seated at the table — no need to knock again.
if (user) {
return <Navigate to="/seance" replace />
}
const switchMode = (next: EnterMode) => {
setMode(next)
setError(null)
}
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (busy) return
setError(null)
setBusy(true)
try {
if (mode === 'register') {
await register(username, password)
} else {
await login(username, password)
}
navigate('/seance')
} catch (err) {
setError(err instanceof Error ? err.message : String(err))
setBusy(false)
}
}
return (
<div className="enter-page">
<div className="enter-fog" aria-hidden="true" />
<div className="enter-scanlines" aria-hidden="true" />
<main className="enter-panel">
<h1 className="enter-title">{t('nav.home')}</h1>
<p className="enter-sub">
{mode === 'login' ? t('enter.reconnectSub') : t('enter.firstContactSub')}
</p>
<div className="enter-tabs" role="tablist">
<button
type="button"
role="tab"
aria-selected={mode === 'login'}
className={`enter-tab${mode === 'login' ? ' active' : ''}`}
onClick={() => switchMode('login')}
>
{t('enter.reconnect')}
</button>
<button
type="button"
role="tab"
aria-selected={mode === 'register'}
className={`enter-tab${mode === 'register' ? ' active' : ''}`}
onClick={() => switchMode('register')}
>
{t('enter.firstContact')}
</button>
</div>
<form className="enter-form" onSubmit={handleSubmit}>
<label className="enter-field">
<span>{t('enter.username')}</span>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
minLength={3}
autoComplete="username"
autoFocus
/>
</label>
<label className="enter-field">
<span>{t('enter.password')}</span>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
/>
</label>
{error && (
<p className="enter-error" role="alert">
{t('enter.failed', { message: error })}
</p>
)}
<button type="submit" className="enter-submit" disabled={busy}>
{mode === 'login' ? t('enter.submitLogin') : t('enter.submitRegister')}
</button>
</form>
<p className="enter-smallprint">
{t('enter.smallPrint', {
defaultValue: 'your medium handle is only ever stored on this server',
})}
</p>
</main>
</div>
)
}