106 lines
3.0 KiB
JavaScript
106 lines
3.0 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { generateToken, loadState, saveState } from './api.js'
|
|
import { createState } from './gameState.js'
|
|
import Ledger from './screens/Ledger.jsx'
|
|
import Map from './screens/Map.jsx'
|
|
import './App.css'
|
|
|
|
const TOKEN_KEY = 'otivm_token'
|
|
|
|
export default function App() {
|
|
const [state, setState] = useState(null)
|
|
const [token, setToken] = useState(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [screen, setScreen] = useState('ledger')
|
|
|
|
useEffect(() => {
|
|
async function bootstrap() {
|
|
let tok = localStorage.getItem(TOKEN_KEY)
|
|
if (!tok) {
|
|
tok = generateToken()
|
|
localStorage.setItem(TOKEN_KEY, tok)
|
|
}
|
|
setToken(tok)
|
|
const saved = await loadState(tok)
|
|
if (saved) {
|
|
setState(saved)
|
|
} else {
|
|
const fresh = createState(tok)
|
|
setState(fresh)
|
|
await saveState(tok, fresh)
|
|
}
|
|
setLoading(false)
|
|
}
|
|
bootstrap()
|
|
}, [])
|
|
|
|
async function onStateChange(newState) {
|
|
setState(newState)
|
|
await saveState(token, newState)
|
|
}
|
|
|
|
// Session abandonment — forward-looking lifecycle handler.
|
|
// Does not delete the old save. Appends a terminal event so the record
|
|
// is complete. The old save becomes a historical artefact on disk.
|
|
// In the Simulator this event will have social and ecological consequences
|
|
// for the clan — a Constructor who stops participating leaves a gap.
|
|
// For now: mark abandoned, generate new token, bootstrap fresh in-place.
|
|
async function onNewGame() {
|
|
if (state && token) {
|
|
const abandoned = {
|
|
...state,
|
|
events: [
|
|
...(state.events || []),
|
|
{ type: 'session_abandoned', route_id: null, timestamp_utc: new Date().toISOString() },
|
|
],
|
|
}
|
|
await saveState(token, abandoned)
|
|
}
|
|
const newTok = generateToken()
|
|
localStorage.setItem(TOKEN_KEY, newTok)
|
|
setToken(newTok)
|
|
const fresh = createState(newTok)
|
|
setState(fresh)
|
|
await saveState(newTok, fresh)
|
|
setScreen('ledger')
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="loading">
|
|
<span>Consulting the ledger...</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<nav className="main-nav">
|
|
<span className="nav-title">OTIVM</span>
|
|
<div className="nav-links">
|
|
<button
|
|
className={`nav-btn${screen === 'ledger' ? ' active' : ''}`}
|
|
onClick={() => setScreen('ledger')}
|
|
>
|
|
Ledger
|
|
</button>
|
|
<button
|
|
className={`nav-btn${screen === 'map' ? ' active' : ''}`}
|
|
onClick={() => setScreen('map')}
|
|
>
|
|
Map
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
<div className="screen-wrap">
|
|
<div style={{ display: screen === 'ledger' ? 'block' : 'none' }}>
|
|
<Ledger state={state} onStateChange={onStateChange} onNewGame={onNewGame} />
|
|
</div>
|
|
<div style={{ display: screen === 'map' ? 'block' : 'none' }}>
|
|
<Map state={state} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|