Files
Ymir/apps/web/src/features/shell/AppShell.tsx
T
vinihcrosaandClaude Opus 4.8 e2b50cde87 feat(web): scenario list screen + routing (Meus cenários)
Add react-router-dom with two routes: "/" → MyScenariosPage (scenario grid with
search, create, open, delete against the scenarios API) and "/editor" → AppShell.
"Sair do cenário" returns to the list. New: use-scenarios hook, ScenarioCard,
MyScenariosPage (+ tests, 235 web tests green).

API: backfill legacy initialConditions rows missing instanceId so GET /scenarios
serializes under the updated schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:19:20 -03:00

123 lines
4.8 KiB
TypeScript
Raw 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.
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { TopBar } from '../../ui/TopBar'
import { IconButton } from '../../ui/IconButton'
import { Button } from '../../ui/Button'
import { SimulationControl, type SimulationState } from '../../ui/SimulationControl'
import { MapActions } from '../../ui/MapActions'
import { AlertDialog } from '../../ui/Modal'
import { Sidebar } from '../scenario-creator/components/Sidebar'
import { AreaMapView } from '../scenario-creator/components/AreaMapView'
import { VesselPanel } from '../scenario-creator/components/VesselPanel'
import { useScenarioStore } from '../scenario-creator/store'
import { useSimulationStore } from '../../stores/simulationStore'
import { useMapStore } from '../../stores/mapStore'
import { tokens } from '../../theme/tokens'
const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:3000'
function pillState(status: string): SimulationState {
if (status === 'running') return 'running'
if (status === 'loading') return 'building'
if (status === 'ready') return 'ready'
return 'idle'
}
/**
* Instructor workspace shell (Figma "Layout do instrutor"): a top bar over a
* full-bleed map, with a floating Build/Play/Stop pill and a zoom control. The
* scenario-setup sidebar and the per-vessel panel dock on top of the map.
*/
export function AppShell() {
const { name, vessels, toCreateScenarioDTO } = useScenarioStore()
const { status, start, stop, loadScenario } = useSimulationStore()
const { zoomIn, zoomOut } = useMapStore()
const [confirmNoOwnship, setConfirmNoOwnship] = useState(false)
const navigate = useNavigate()
async function saveScenario() {
try {
const res = await fetch(`${API_BASE}/scenarios`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(toCreateScenarioDTO()),
})
if (!res.ok) console.error('Failed to save scenario:', res.status)
} catch (err) {
console.error('Network error saving scenario:', err)
}
}
function handleSave() {
if (vessels.length === 0) {
setConfirmNoOwnship(true)
return
}
void saveScenario()
}
function handleBuild() {
void saveScenario()
loadScenario(vessels)
start()
}
const logo = (
<div style={{ display: 'flex', alignItems: 'center', gap: tokens.space.md }}>
<div style={{ display: 'flex', alignItems: 'center', gap: tokens.space.xs, fontSize: tokens.fontSize.title }} aria-hidden="true">
▣ <span style={{ fontSize: tokens.fontSize.sm, color: tokens.color.textSubtle }}>▾</span>
</div>
<nav aria-label="Caminho do cenário" style={{ display: 'flex', alignItems: 'center', gap: tokens.space.sm, fontSize: tokens.fontSize.body, color: tokens.color.textSubtle }}>
<span>Pasta pai</span>
<span aria-hidden="true">›</span>
<span style={{ color: tokens.color.textPrimary, fontWeight: tokens.fontWeight.medium }}>{name}</span>
</nav>
</div>
)
const center = (
<div style={{ display: 'flex', alignItems: 'center', gap: tokens.space.sm }}>
<IconButton icon="+" label="Adicionar embarcação" variant="accent" />
<IconButton icon="ⓘ" label="Informações do cenário" />
<IconButton icon="☁" label="Sincronizar" />
<IconButton icon="⚙" label="Configurações" />
</div>
)
const right = (
<div style={{ display: 'flex', alignItems: 'center', gap: tokens.space.sm }}>
<Button variant="secondary" size="sm" onClick={() => navigate('/')}>Sair do cenário</Button>
<Button variant="dark" size="sm" icon="💾" onClick={handleSave}>Salvar cenário</Button>
</div>
)
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
<TopBar left={logo} center={center} right={right} actionsDisabled={vessels.length === 0 ? 'center' : 'none'} />
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
<Sidebar />
<div style={{ flex: 1, position: 'relative' }}>
<AreaMapView />
<VesselPanel />
<MapActions onZoomIn={zoomIn} onZoomOut={zoomOut} />
<SimulationControl
state={pillState(status)}
onBuild={handleBuild}
onPlay={start}
onStop={stop}
/>
</div>
</div>
<AlertDialog
open={confirmNoOwnship}
title="Seu cenário não possui um ownship"
body="Seu cenário não possui um ownship, portanto não será possível utilizá-lo em uma cabine. Tem certeza que deseja salvar?"
cancelLabel="Cancelar"
confirmLabel="Salvar mesmo assim"
onCancel={() => setConfirmNoOwnship(false)}
onConfirm={() => { setConfirmNoOwnship(false); void saveScenario() }}
/>
</div>
)
}