iv: wire Shell into App.jsx, replace tab navigation with context dropdown
This commit is contained in:
150
src/App.jsx
150
src/App.jsx
@@ -2,18 +2,21 @@ import { useState, useEffect } from 'react'
|
||||
import { generateToken, loadState, saveState } from './api.js'
|
||||
import { createState } from './gameState.js'
|
||||
import { BACKGROUNDS } from './constants.js'
|
||||
import Ledger from './screens/Ledger.jsx'
|
||||
import Shell from './components/Shell.jsx'
|
||||
import Actor from './screens/Actor.jsx'
|
||||
import Forum from './screens/Forum.jsx'
|
||||
import Map from './screens/Map.jsx'
|
||||
import Prologue from './screens/Prologue.jsx'
|
||||
import './App.css'
|
||||
|
||||
import contextsJson from './config/contexts.json'
|
||||
|
||||
const TOKEN_KEY = 'otivm_token'
|
||||
|
||||
export default function App() {
|
||||
const [state, setState] = useState(null)
|
||||
const [token, setToken] = useState(null)
|
||||
const [state, setState] = useState(null)
|
||||
const [token, setToken] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [screen, setScreen] = useState('prologue')
|
||||
const [context, setContext] = useState('actor')
|
||||
|
||||
useEffect(() => {
|
||||
async function bootstrap() {
|
||||
@@ -26,13 +29,12 @@ export default function App() {
|
||||
const saved = await loadState(tok)
|
||||
if (saved) {
|
||||
setState(saved)
|
||||
// If background already chosen, open on ledger
|
||||
setScreen(saved.background_id ? 'ledger' : 'prologue')
|
||||
setContext(saved.background_id && saved.background_id !== 'unknown' ? 'forum' : 'actor')
|
||||
} else {
|
||||
const fresh = createState(tok)
|
||||
setState(fresh)
|
||||
await saveState(tok, fresh)
|
||||
setScreen('prologue')
|
||||
setContext('actor')
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -44,8 +46,6 @@ export default function App() {
|
||||
await saveState(token, newState)
|
||||
}
|
||||
|
||||
// Called from Prologue when player confirms a background.
|
||||
// Seeds background_id into state, saves, switches to Ledger.
|
||||
async function onSelectBackground(backgroundId) {
|
||||
const bg = BACKGROUNDS.find(b => b.id === backgroundId)
|
||||
if (!bg) return
|
||||
@@ -56,15 +56,9 @@ export default function App() {
|
||||
}
|
||||
setState(updated)
|
||||
await saveState(token, updated)
|
||||
setScreen('ledger')
|
||||
setContext('forum')
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
@@ -82,53 +76,93 @@ export default function App() {
|
||||
const fresh = createState(newTok)
|
||||
setState(fresh)
|
||||
await saveState(newTok, fresh)
|
||||
setScreen('prologue')
|
||||
setContext('actor')
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading">
|
||||
<span>Consulting the ledger...</span>
|
||||
</div>
|
||||
)
|
||||
return <div className="otivm-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 === 'prologue' ? ' active' : ''}`}
|
||||
onClick={() => setScreen('prologue')}
|
||||
>
|
||||
Prologue
|
||||
</button>
|
||||
<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 === 'prologue' ? 'block' : 'none' }}>
|
||||
<Prologue state={state} onSelectBackground={onSelectBackground} />
|
||||
</div>
|
||||
<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>
|
||||
// Active context config for Shell
|
||||
const activeCtx = contextsJson.find(c => c.id === context) || contextsJson[0]
|
||||
const subitems = activeCtx.subitems || []
|
||||
|
||||
// Context header text
|
||||
const ctxHeader = (
|
||||
<div className="otivm-ctx-header">
|
||||
<div>
|
||||
<span className="otivm-ctx-eyebrow">Context</span>
|
||||
<span className="otivm-ctx-name">{activeCtx.name} — {activeCtx.subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Screen content
|
||||
let screen
|
||||
if (context === 'actor') {
|
||||
screen = (
|
||||
<Actor
|
||||
state={state}
|
||||
onSelectBackground={onSelectBackground}
|
||||
/>
|
||||
)
|
||||
} else if (context === 'forum') {
|
||||
screen = (
|
||||
<Forum
|
||||
state={state}
|
||||
onStateChange={onStateChange}
|
||||
onNewGame={onNewGame}
|
||||
/>
|
||||
)
|
||||
} else if (context === 'map') {
|
||||
screen = <Map state={state} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell
|
||||
contexts={contextsJson}
|
||||
activeId={context}
|
||||
onContext={setContext}
|
||||
subitems={subitems}
|
||||
layout={activeCtx.layout}
|
||||
token={token}
|
||||
onNewGame={onNewGame}
|
||||
>
|
||||
{ctxHeader}
|
||||
<div className="otivm-layout-wrap">
|
||||
{context === 'actor' && (
|
||||
<div className={activeCtx.layout === 'three-col' ? 'otivm-layout-three-col' : ''}>
|
||||
{activeCtx.layout === 'three-col' ? (
|
||||
<>
|
||||
<div>{filterCol(screen, 'left')}</div>
|
||||
<div>{filterCol(screen, 'center')}</div>
|
||||
<div>{filterCol(screen, 'right')}</div>
|
||||
</>
|
||||
) : screen}
|
||||
</div>
|
||||
)}
|
||||
{context === 'forum' && (
|
||||
<div className={activeCtx.layout === 'two-col' ? 'otivm-layout-two-col' : ''}>
|
||||
{activeCtx.layout === 'two-col' ? (
|
||||
<>
|
||||
<div>{filterCol(screen, 'left')}</div>
|
||||
<div>{filterCol(screen, 'right')}</div>
|
||||
</>
|
||||
) : screen}
|
||||
</div>
|
||||
)}
|
||||
{context === 'map' && screen}
|
||||
</div>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
|
||||
// Extract children from a screen component by their col prop.
|
||||
// Actor and Forum return arrays of Section elements with col props.
|
||||
function filterCol(screen, col) {
|
||||
if (!screen) return null
|
||||
const children = screen.props?.children
|
||||
if (!children) return null
|
||||
const arr = Array.isArray(children) ? children : [children]
|
||||
return arr.filter(c => c?.props?.col === col)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user