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 { generateToken, loadState, saveState } from './api.js'
|
||||||
import { createState } from './gameState.js'
|
import { createState } from './gameState.js'
|
||||||
import { BACKGROUNDS } from './constants.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 Map from './screens/Map.jsx'
|
||||||
import Prologue from './screens/Prologue.jsx'
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
|
import contextsJson from './config/contexts.json'
|
||||||
|
|
||||||
const TOKEN_KEY = 'otivm_token'
|
const TOKEN_KEY = 'otivm_token'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [state, setState] = useState(null)
|
const [state, setState] = useState(null)
|
||||||
const [token, setToken] = useState(null)
|
const [token, setToken] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [screen, setScreen] = useState('prologue')
|
const [context, setContext] = useState('actor')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
@@ -26,13 +29,12 @@ export default function App() {
|
|||||||
const saved = await loadState(tok)
|
const saved = await loadState(tok)
|
||||||
if (saved) {
|
if (saved) {
|
||||||
setState(saved)
|
setState(saved)
|
||||||
// If background already chosen, open on ledger
|
setContext(saved.background_id && saved.background_id !== 'unknown' ? 'forum' : 'actor')
|
||||||
setScreen(saved.background_id ? 'ledger' : 'prologue')
|
|
||||||
} else {
|
} else {
|
||||||
const fresh = createState(tok)
|
const fresh = createState(tok)
|
||||||
setState(fresh)
|
setState(fresh)
|
||||||
await saveState(tok, fresh)
|
await saveState(tok, fresh)
|
||||||
setScreen('prologue')
|
setContext('actor')
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -44,8 +46,6 @@ export default function App() {
|
|||||||
await saveState(token, newState)
|
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) {
|
async function onSelectBackground(backgroundId) {
|
||||||
const bg = BACKGROUNDS.find(b => b.id === backgroundId)
|
const bg = BACKGROUNDS.find(b => b.id === backgroundId)
|
||||||
if (!bg) return
|
if (!bg) return
|
||||||
@@ -56,15 +56,9 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
setState(updated)
|
setState(updated)
|
||||||
await saveState(token, 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() {
|
async function onNewGame() {
|
||||||
if (state && token) {
|
if (state && token) {
|
||||||
const abandoned = {
|
const abandoned = {
|
||||||
@@ -82,53 +76,93 @@ export default function App() {
|
|||||||
const fresh = createState(newTok)
|
const fresh = createState(newTok)
|
||||||
setState(fresh)
|
setState(fresh)
|
||||||
await saveState(newTok, fresh)
|
await saveState(newTok, fresh)
|
||||||
setScreen('prologue')
|
setContext('actor')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <div className="otivm-loading"><span>Consulting the ledger...</span></div>
|
||||||
<div className="loading">
|
|
||||||
<span>Consulting the ledger...</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Active context config for Shell
|
||||||
<div>
|
const activeCtx = contextsJson.find(c => c.id === context) || contextsJson[0]
|
||||||
<nav className="main-nav">
|
const subitems = activeCtx.subitems || []
|
||||||
<span className="nav-title">OTIVM</span>
|
|
||||||
<div className="nav-links">
|
// Context header text
|
||||||
<button
|
const ctxHeader = (
|
||||||
className={`nav-btn${screen === 'prologue' ? ' active' : ''}`}
|
<div className="otivm-ctx-header">
|
||||||
onClick={() => setScreen('prologue')}
|
<div>
|
||||||
>
|
<span className="otivm-ctx-eyebrow">Context</span>
|
||||||
Prologue
|
<span className="otivm-ctx-name">{activeCtx.name} — {activeCtx.subtitle}</span>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</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