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
This commit is contained in:
Indiana
2026-07-20 21:11:49 +00:00
parent 1ec9ea5863
commit 6edbbbbc2a
48 changed files with 10494 additions and 168 deletions

View File

@@ -0,0 +1,209 @@
// Codex dossier — a single spirit's occult record at /codex/:id.
// Fetches GET /api/codex/:id; 404s render a themed "not reached" state.
import { useEffect, useState, type CSSProperties } from 'react'
import { Link, useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import type { CodexEntityDetail } from '../lib/types'
import './CodexEntityPage.css'
type DetailState =
| { status: 'loading' }
| { status: 'error' }
| { status: 'notfound' }
| { status: 'ready'; entity: CodexEntityDetail }
function formatDate(iso: string, locale: string): string {
const date = new Date(iso)
if (Number.isNaN(date.getTime())) return iso
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date)
}
/** Voice profiles are loose records — render whatever the veil preserved. */
function formatVoiceValue(value: unknown): string {
if (value === null || value === undefined) return '—'
if (typeof value === 'number') {
return Number.isInteger(value) ? String(value) : value.toFixed(2)
}
if (typeof value === 'object') return JSON.stringify(value)
return String(value)
}
export function CodexEntityPage() {
const { t, i18n } = useTranslation()
const { id } = useParams<{ id: string }>()
const [state, setState] = useState<DetailState>({ status: 'loading' })
useEffect(() => {
if (!id) {
setState({ status: 'notfound' })
return
}
const controller = new AbortController()
setState({ status: 'loading' })
fetch(`/api/codex/${encodeURIComponent(id)}`, {
credentials: 'include',
signal: controller.signal,
})
.then(async (res) => {
if (res.status === 404) {
setState({ status: 'notfound' })
return
}
if (!res.ok) throw new Error(`the registry answered ${res.status}`)
const entity = (await res.json()) as CodexEntityDetail
setState({ status: 'ready', entity })
})
.catch(() => {
if (!controller.signal.aborted) setState({ status: 'error' })
})
return () => controller.abort()
}, [id])
return (
<div className="codex-detail-page">
<div className="codex-detail-overlay" aria-hidden="true" />
<nav className="codex-detail-nav">
<Link to="/codex" className="codex-detail-back">
{t('codex.back')}
</Link>
</nav>
{state.status === 'loading' && (
<p className="codex-detail-state">
{t('codex.detail.loading', 'summoning the dossier…')}
</p>
)}
{state.status === 'error' && (
<p className="codex-detail-state codex-detail-state-error" role="alert">
{t('common.error')}
</p>
)}
{state.status === 'notfound' && (
<div className="codex-detail-missing">
<div className="codex-detail-missing-sigil" aria-hidden="true" />
<h1 className="codex-detail-missing-title">
{t('codex.detail.notReached', 'this spirit has not been reached')}
</h1>
<Link to="/codex" className="codex-detail-cta codex-detail-cta-ghost">
{t('codex.back')}
</Link>
</div>
)}
{state.status === 'ready' && (
<Dossier entity={state.entity} locale={i18n.language} />
)}
</div>
)
}
function Dossier({ entity, locale }: { entity: CodexEntityDetail; locale: string }) {
const { t } = useTranslation()
const discoverer = entity.discovered_by ?? t('codex.detail.unknownMedium', 'unknown medium')
const stats: Array<{ label: string; value: string }> = [
{
label: t('codex.detail.stats.sightings', 'sightings'),
value: String(entity.sightings),
},
{
label: t('codex.detail.stats.contacts', 'contacts'),
value: String(entity.contact_count),
},
{
label: t('codex.detail.stats.discoveredBy', 'discovered by'),
value: discoverer,
},
{
label: t('codex.detail.stats.discoveredAt', 'entered the registry'),
value: formatDate(entity.discovered_at, locale),
},
]
return (
<article className="codex-dossier" data-rarity={entity.rarity}>
<header className="codex-dossier-header">
<div
className="codex-dossier-glyph"
style={{ '--glyph-hue': entity.visual.hue } as CSSProperties}
aria-hidden="true"
/>
<span className="codex-dossier-form">
{t(`codex.form.${entity.visual.form}`, entity.visual.form)}
</span>
<h1 className="codex-dossier-name">{entity.name}</h1>
<p className="codex-dossier-epithet">{entity.epithet}</p>
<span className="codex-dossier-rarity" data-rarity={entity.rarity}>
{t(`codex.rarity.${entity.rarity}`)}
</span>
</header>
<section className="codex-dossier-section">
<h2 className="codex-dossier-heading">{t('codex.persona')}</h2>
<p className="codex-dossier-persona">{entity.persona}</p>
</section>
<section className="codex-dossier-section">
<h2 className="codex-dossier-heading">
{t('codex.detail.utterances', 'known utterances')}
</h2>
{entity.quotes.length > 0 ? (
<ul className="codex-dossier-quotes">
{entity.quotes.map((quote, i) => (
<li key={i} className="codex-dossier-quote">
“{quote}”
</li>
))}
</ul>
) : (
<p className="codex-dossier-none">
{t('codex.detail.noUtterances', 'no utterances recorded yet')}
</p>
)}
</section>
<section className="codex-dossier-section">
<dl className="codex-dossier-stats">
{stats.map((stat) => (
<div key={stat.label} className="codex-dossier-stat">
<dt>{stat.label}</dt>
<dd>{stat.value}</dd>
</div>
))}
</dl>
</section>
<section className="codex-dossier-section">
<h2 className="codex-dossier-heading">
{t('codex.detail.voiceSignature', 'voice signature')}
</h2>
<table className="codex-dossier-voice">
<tbody>
{Object.entries(entity.voice).map(([key, value]) => (
<tr key={key}>
<th scope="row">{key.replace(/_/g, ' ')}</th>
<td>{formatVoiceValue(value)}</td>
</tr>
))}
</tbody>
</table>
</section>
<footer className="codex-dossier-footer">
<Link to="/seance" className="codex-detail-cta">
{t('codex.detail.attemptContact', 'attempt contact')}
</Link>
</footer>
</article>
)
}