From 85627201f89c4f2548b83d52460bb70753c0ddfe Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 29 Sep 2026 06:48:20 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(ui):=20layout=20pass=20=E2=80=94=20CAR?= =?UTF-8?q?GO=20HOLD=20to=20right=20column,=20wider=20panes,=20drop=20plac?= =?UTF-8?q?eholders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CARGO HOLD // INVENTORY moves out of the LOCAL SYSTEM tab into a persistent right-column panel; UPCOMING ENCOUNTERS removed; SHIP DATA folded into VESSEL METRICS (SECTOR + TURN kept with real values, filler dropped). Grid goes 260px/1fr/240px -> 320px/1fr/360px. Cargo state uses the lifted-state pattern: MissionControl owns /api/system and LocalSystemPanel reports each scan's response up via onChanged (ShipSystemsPanel style) — no polling. --- components/local-system.tsx | 123 ++++++-------------- components/mission-control.tsx | 201 ++++++++++++--------------------- 2 files changed, 108 insertions(+), 216 deletions(-) diff --git a/components/local-system.tsx b/components/local-system.tsx index d1c413a..e8745cb 100644 --- a/components/local-system.tsx +++ b/components/local-system.tsx @@ -1,69 +1,50 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useState } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import type { ExploreResult, SystemState } from '@/lib/game/save' import type { FoundItem } from '@/lib/game/world' // LOCAL SYSTEM tab — the bodies of the current system (planets + staggered moons) with an -// EXPLORE button per unexplored row. Fetches /api/system on mount; explore responses are -// applied directly (no polling). Turn-based: one scan costs a turn, each body once. +// EXPLORE button per unexplored row. The parent (MissionControl) owns the /api/system state so +// the right column's CARGO HOLD mirrors scans immediately; this panel reports each scan's +// response up via onChanged and keeps only its own UI bits locally (no polling). Turn-based: +// one scan costs a turn, each body once. -export function LocalSystemPanel() { - const [state, setState] = useState(null) +interface LocalSystemPanelProps { + system: SystemState | null + onChanged: (next: SystemState) => void +} + +export function LocalSystemPanel({ system, onChanged }: LocalSystemPanelProps) { const [lastOutcome, setLastOutcome] = useState(null) const [error, setError] = useState(null) const [exploringId, setExploringId] = useState(null) - useEffect(() => { - let cancelled = false - fetch('/api/system') - .then(async (res) => { - if (!res.ok) throw new Error(`HTTP ${res.status}`) - return (await res.json()) as SystemState - }) - .then((s) => { - if (!cancelled) setState(s) - }) - .catch((err: unknown) => { - if (!cancelled) setError(err instanceof Error ? err.message : String(err)) - }) - return () => { - cancelled = true - } - }, []) + const explore = useCallback( + async (planetId: number) => { + setExploringId(planetId) + try { + const res = await fetch('/api/explore', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ planetId }), + }) + const body = (await res.json()) as ExploreResult & { error?: string } + if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`) + onChanged(body) // fresh state straight from the response — parent mirrors it to CARGO HOLD + setLastOutcome(body.outcome) + } catch (err: unknown) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setExploringId(null) + } + }, + [onChanged] + ) - const explore = useCallback(async (planetId: number) => { - setExploringId(planetId) - try { - const res = await fetch('/api/explore', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ planetId }), - }) - const body = (await res.json()) as ExploreResult & { error?: string } - if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`) - setState(body) // fresh state straight from the response — no refetch needed - setLastOutcome(body.outcome) - } catch (err: unknown) { - setError(err instanceof Error ? err.message : String(err)) - } finally { - setExploringId(null) - } - }, []) - - if (error && !state) { - return ( -
- - SYSTEM SCAN OFFLINE // {error.toUpperCase()} - -
- ) - } - - if (!state) { + if (!system) { return (
SCANNING SYSTEM… @@ -88,9 +69,9 @@ export function LocalSystemPanel() { textTransform: 'uppercase', }} > - EXPLORABLE WORLDS // {state.systemName.toUpperCase()} — TURN {state.turn} + EXPLORABLE WORLDS // {system.systemName.toUpperCase()} — TURN {system.turn}
- {state.planets.map((row, i) => { + {system.planets.map((row, i) => { const depth = row.parent !== null ? 1 : 0 const badge = badgeFor(row.explored, row.hazard) return ( @@ -102,7 +83,7 @@ export function LocalSystemPanel() { alignItems: 'center', gap: '0.5rem', padding: depth ? '0.6rem 0.75rem 0.6rem 1.75rem' : '0.6rem 0.75rem', - borderBottom: i < state.planets.length - 1 ? '1px solid var(--border)' : 'none', + borderBottom: i < system.planets.length - 1 ? '1px solid var(--border)' : 'none', }} >
@@ -117,7 +98,7 @@ export function LocalSystemPanel() { {row.name}
- {depth ? `MOON OF ${state.planets.find((p) => p.id === row.parent)?.name ?? '?'} // ${row.type}` : row.type} + {depth ? `MOON OF ${system.planets.find((p) => p.id === row.parent)?.name ?? '?'} // ${row.type}` : row.type}
@@ -147,36 +128,6 @@ export function LocalSystemPanel() {
)} -
-
- CARGO HOLD // INVENTORY -
- {state.inventory.length === 0 ? ( -
- EMPTY — EXPLORE WORLDS TO FIND THINGS -
- ) : ( - state.inventory.map((item) => ( -
- - {item.name} // {item.kind} - - ×{item.qty} -
- )) - )} -
- {error && (
SCAN FAILED // {error.toUpperCase()} diff --git a/components/mission-control.tsx b/components/mission-control.tsx index 957a8fc..91f2234 100644 --- a/components/mission-control.tsx +++ b/components/mission-control.tsx @@ -15,14 +15,15 @@ import { ShipSystemsPanel } from '@/components/ship-systems' import { SkillTreePanel } from '@/components/skill-tree' import { WorldMapPanel } from '@/components/world-map' import { CREW_CAPACITY } from '@/lib/game/ship-costs' -import type { CrewDto } from '@/lib/game/save' +import type { CrewDto, SystemState } from '@/lib/game/save' import type { ShipState } from '@/lib/game/ship' import type { TechDto } from '@/lib/game/tech' // Interface base: layout + visual treatment rebuilt from the scificn-ui Mission Control -// showcase (src/app/_pages/showcase/mission-control-content.tsx in baxy5/scificn-ui). The -// All seven tabs render live data (/api/world, /api/system, /api/crew, /api/ship, /api/tech, -// /api/missions, /api/events). Single player, no auth, by design. +// showcase (src/app/_pages/showcase/mission-control-content.tsx in baxy5/scificn-ui). All seven +// tabs render live data (/api/world, /api/system, /api/crew, /api/ship, /api/tech, /api/missions, +// /api/events); the right column's CARGO HOLD renders from the lifted /api/system state. Single +// player, no auth, by design. function useNarrow(threshold = 900): boolean { const [narrow, setNarrow] = useState( @@ -36,31 +37,15 @@ function useNarrow(threshold = 900): boolean { return narrow } -// ── Placeholder content (no game logic — real values land with the turn engine) ── - -const encounters = [ - { name: 'DERELICT FREIGHTER', when: 'T-07:14', id: '#E-102' }, - { name: 'PIRATE SKIRMISH', when: 'T-22:40', id: '#E-103' }, - { name: 'ANOMALY FIELD', when: 'T-48:12', id: '#E-104' }, -] - -const shipData = [ - { label: 'TOTAL MODULES', value: '34' }, - { label: 'HULL INTEGRITY', value: '87%' }, - { label: 'REACTOR OUTPUT', value: '94.2%' }, - { label: 'CREW ACTIVE', value: '3 / 6' }, - { label: 'SECTOR', value: '7G // DRIFT LANE' }, - { label: 'DRIFT VELOCITY', value: '0.4 KM/S' }, - { label: 'TURN', value: '12' }, -] - export function MissionControl() { const narrow = useNarrow() const [tick, setTick] = useState(0) - // Ship state is owned here (not in the tab panel) so the left column mirrors ship actions - // immediately; the SHIP SYSTEMS tab applies each action's response via setShip. Crew count - // feeds the roster bar — refetched on page load, like the other panels. + // Ship + system state is owned here (not in the tab panels): the SHIP SYSTEMS tab mirrors ship + // actions via setShip, and the right column's CARGO HOLD renders from the lifted inventory — + // the LOCAL SYSTEM tab reports each scan's response up via setSystem. Crew count feeds the + // roster bar — refetched on page load, like the other panels. const [ship, setShip] = useState(null) + const [system, setSystem] = useState(null) const [techs, setTechs] = useState(null) const [crewCount, setCrewCount] = useState(null) @@ -75,6 +60,15 @@ export function MissionControl() { if (!cancelled) setShip(s) }) .catch(() => {}) // left column + tab keep their loading state on failure + fetch('/api/system') + .then(async (res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return (await res.json()) as SystemState + }) + .then((s) => { + if (!cancelled) setSystem(s) + }) + .catch(() => {}) fetch('/api/tech') .then(async (res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) @@ -249,7 +243,7 @@ export function MissionControl() { style={{ flex: 1, display: 'grid', - gridTemplateColumns: narrow ? '1fr' : '260px 1fr 240px', + gridTemplateColumns: narrow ? '1fr' : '320px 1fr 360px', gap: '1px', background: 'var(--border)', }} @@ -297,11 +291,34 @@ export function MissionControl() { {ship ? ( // Bars driven by real values: hull/reactor from system levels (level × 20, capped), - // crew roster vs the display cap. + // crew roster vs the display cap; SECTOR/TURN rows folded in from the old SHIP DATA + // placeholder (the rest of it duplicated these bars or had no game equivalent).
s.type === 'HULL')?.level ?? 0) * 20)} /> s.type === 'REACTOR')?.level ?? 0) * 20)} /> +
+ {[ + { label: 'SECTOR', value: (system?.systemName ?? '—').toUpperCase() }, + { label: 'TURN', value: String(ship.turn) }, + ].map((item, i) => ( +
+ {item.label} + + {item.value} + +
+ ))} +
) : ( LOADING… @@ -352,9 +369,9 @@ export function MissionControl() { - {/* LOCAL SYSTEM TAB — live data from /api/system */} + {/* LOCAL SYSTEM TAB — live data (state owned by MissionControl; scans report up) */} - + {/* CREW TAB — live data from /api/crew */} @@ -386,7 +403,7 @@ export function MissionControl() {
- {/* ── RIGHT: UPCOMING ENCOUNTERS + SHIP DATA ── */} + {/* ── RIGHT: CARGO HOLD (persistent — inventory lifted from /api/system) ── */}
- UPCOMING ENCOUNTERS -
- {encounters.length} QUEUED -
+ CARGO HOLD // INVENTORY + {system && ( +
+ 0 ? 'ACTIVE' : 'OFFLINE'}> + {system.inventory.reduce((n, i) => n + i.qty, 0)} UNITS + +
+ )}
-
- {encounters.map((enc, i) => { - const isNext = i === 0 - const isLast = i === encounters.length - 1 - return ( -
-
-
-
- {enc.name} -
-
- {enc.when} // {enc.id} -
-
- - {isNext ? 'NEXT' : 'SCHED'} - -
-
- ) - })} -
-
-
- - - - SHIP DATA - - -
- {shipData.map((item, i) => ( -
- - {item.label} - - - {item.value} + {!system ? ( +
+ LOADING… +
+ ) : system.inventory.length === 0 ? ( +
+ EMPTY — EXPLORE WORLDS TO FIND THINGS +
+ ) : ( + system.inventory.map((item) => ( +
+ + {item.name} // {item.kind} + ×{item.qty}
- ))} -
+ )) + )}
From b05ac32da69f1396c31badb5d2109223e4157350 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 29 Sep 2026 06:48:20 +0000 Subject: [PATCH 2/2] release v0.9.1 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10c043..b15613e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.9.1 — 2026-09-29 06:47 +- UI layout pass (visual only — no game logic or API changes): CARGO HOLD // INVENTORY moved out of the LOCAL SYSTEM tab into a persistent panel in the right column; the UPCOMING ENCOUNTERS placeholder was removed entirely; the SHIP DATA placeholder was merged into VESSEL METRICS. +- Grid: `260px / 1fr / 240px` → `320px / 1fr / 360px` (left pane ~+23%, right pane ~50% wider). +- Cargo state wiring follows the existing lifted-state pattern: MissionControl fetches `/api/system` on mount and owns the SystemState; LocalSystemPanel receives it as a prop, keeps its planet list + scan result + explore action, and reports each scan's response up via `onChanged` (same as ShipSystemsPanel) — so CARGO HOLD mirrors scans live with no polling. +- VESSEL METRICS now carries the two SHIP DATA rows that had real values: SECTOR (current system name from the lifted state) and TURN (`ship.turn`). The rest was dropped — HULL INTEGRITY / REACTOR OUTPUT / CREW ACTIVE duplicate the existing progress bars, TOTAL MODULES duplicates the left column's "N ONLINE" badge, DRIFT VELOCITY has no game equivalent. +- e2e: no assertion changes needed — the LOCAL SYSTEM tests only assert panel internals (header, EXPLORE buttons, SCAN RESULT) and API state; CARGO HOLD / UPCOMING ENCOUNTERS / SHIP DATA were never referenced by any spec. All 15 specs compile unchanged. + ## v0.9.0 — 2026-09-29 02:47 - EVENT LOG tab — the last of the seven tabs to go live (issue #11): the terminal now renders the save's real game events instead of a placeholder feed. Every action already appends an event row (new game, jump, scan, research, ship upgrade/expand/build, mission accept + completion); this issue adds the read side and the rendering. - New endpoint: `GET /api/events` → latest N events, newest first (`{id, turn, type, message, createdAt}`), auto-initializes like all other reads. Optional `?limit=` (default 50, cap 200; non-integers or out-of-range values → 400). OpenAPI spec in code updated (14 → 15 paths) with the Event schema enumerating the eight event types. diff --git a/package.json b/package.json index eb1a4a8..72ef9b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "space-wanderer", - "version": "0.9.0", + "version": "0.9.1", "private": true, "scripts": { "dev": "next dev -p 3000",