Files
SpaceWanderer/components/local-system.tsx
T
kamilkrzywda 85627201f8 feat(ui): layout pass — CARGO HOLD to right column, wider panes, drop placeholders
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.
2026-09-29 06:48:20 +00:00

139 lines
5.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
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. 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.
interface LocalSystemPanelProps {
system: SystemState | null
onChanged: (next: SystemState) => void
}
export function LocalSystemPanel({ system, onChanged }: LocalSystemPanelProps) {
const [lastOutcome, setLastOutcome] = useState<FoundItem[] | null>(null)
const [error, setError] = useState<string | null>(null)
const [exploringId, setExploringId] = useState<number | null>(null)
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]
)
if (!system) {
return (
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.75rem' }}>
<span style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.12em' }}>SCANNING SYSTEM…</span>
</div>
)
}
const badgeFor = (explored: boolean, hazard: boolean) =>
explored ? { label: 'SCANNED', variant: 'ACTIVE' as const } : hazard ? { label: 'HAZARD', variant: 'WARNING' as const } : { label: 'UNEXPLORED', variant: 'SCANNING' as const }
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', overflow: 'hidden' }}>
<div
style={{
padding: '0.4rem 0.75rem',
borderBottom: '1px solid var(--border)',
background: 'var(--surface-raised)',
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}
>
EXPLORABLE WORLDS // {system.systemName.toUpperCase()} — TURN {system.turn}
</div>
{system.planets.map((row, i) => {
const depth = row.parent !== null ? 1 : 0
const badge = badgeFor(row.explored, row.hazard)
return (
<div
key={row.id}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '0.5rem',
padding: depth ? '0.6rem 0.75rem 0.6rem 1.75rem' : '0.6rem 0.75rem',
borderBottom: i < system.planets.length - 1 ? '1px solid var(--border)' : 'none',
}}
>
<div style={{ overflow: 'hidden' }}>
<div
style={{
fontSize: '0.68rem',
color: depth ? 'var(--text-muted)' : 'var(--text-secondary)',
letterSpacing: '0.04em',
textTransform: 'uppercase',
}}
>
{row.name}
</div>
<div style={{ fontSize: '0.57rem', color: 'var(--text-muted)', letterSpacing: '0.06em', marginTop: '2px' }}>
{depth ? `MOON OF ${system.planets.find((p) => p.id === row.parent)?.name ?? '?'} // ${row.type}` : row.type}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Badge variant={badge.variant}>{badge.label}</Badge>
{!row.explored && (
<Button
variant="OUTLINE"
size="SM"
disabled={exploringId !== null}
onClick={() => void explore(row.id)}
>
{exploringId === row.id ? 'SCANNING…' : `${row.name.toUpperCase()} // EXPLORE`}
</Button>
)}
</div>
</div>
)
})}
</div>
{lastOutcome !== null && (
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.5rem 0.75rem' }}>
<span style={{ fontSize: '0.62rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
SCAN RESULT //{' '}
{lastOutcome.length > 0 ? lastOutcome.map((i) => `${i.qty}× ${i.name.toUpperCase()}`).join(', ') : 'NOTHING FOUND'}
</span>
</div>
)}
{error && (
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.5rem 0.75rem' }}>
<span style={{ fontSize: '0.62rem', color: 'var(--color-red)', letterSpacing: '0.08em' }}>SCAN FAILED // {error.toUpperCase()}</span>
</div>
)}
</div>
)
}