refactor(web): split dashboard into combat stats and reference stats
Hull and Armor current are always editable with steppers at the top of the dashboard — these are the values that change every combat round. All other stats (AC, Con, Speed, Class, Size, Max values, Crew, Hardpoints, Cargo) are shown in a compact read-only reference grid with an Edit button to toggle into editable mode when needed. Removes separate MobilitySection and CrewSection in favor of a unified ShipStatsSection that combines all reference stats in one place. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
import NumericStepper from '../ui/NumericStepper';
|
||||
import type { Ship } from '../../types/ship';
|
||||
|
||||
interface Props {
|
||||
ship: Ship;
|
||||
onUpdate: (patch: Partial<Ship>) => void;
|
||||
editing: boolean;
|
||||
}
|
||||
|
||||
export default function CrewSection({ ship, onUpdate, editing }: Props) {
|
||||
return (
|
||||
<section className="dashboard-section">
|
||||
<h3 className="section-title">Crew & Cargo</h3>
|
||||
{editing ? (
|
||||
<div className="stat-grid">
|
||||
<NumericStepper
|
||||
label="Crew Required"
|
||||
value={ship.crew_req}
|
||||
onChange={(v) => onUpdate({ crew_req: v })}
|
||||
/>
|
||||
<NumericStepper
|
||||
label="Hardpoints"
|
||||
value={ship.hardpoints}
|
||||
onChange={(v) => onUpdate({ hardpoints: v })}
|
||||
/>
|
||||
<NumericStepper
|
||||
label="Cargo (tons)"
|
||||
value={ship.cargo}
|
||||
onChange={(v) => onUpdate({ cargo: v })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="stat-grid-readonly">
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Crew</span>
|
||||
<span className="stat-ro-value">{ship.crew_req}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Hardpoints</span>
|
||||
<span className="stat-ro-value">{ship.hardpoints}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Cargo</span>
|
||||
<span className="stat-ro-value">{ship.cargo}t</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import NumericStepper from '../ui/NumericStepper';
|
||||
import EnumDropdown from '../ui/EnumDropdown';
|
||||
import type { Ship } from '../../types/ship';
|
||||
|
||||
const MANEUVER_CLASSES = ['S', 'A', 'B', 'C', 'D', 'E', 'F'];
|
||||
const SIZE_CATEGORIES = ['Tiny', 'Small', 'Medium', 'Large', 'Huge', 'Gargantuan'];
|
||||
|
||||
interface Props {
|
||||
ship: Ship;
|
||||
onUpdate: (patch: Partial<Ship>) => void;
|
||||
editing: boolean;
|
||||
}
|
||||
|
||||
export default function MobilitySection({ ship, onUpdate, editing }: Props) {
|
||||
return (
|
||||
<section className="dashboard-section">
|
||||
<h3 className="section-title">Mobility</h3>
|
||||
{editing ? (
|
||||
<div className="stat-grid">
|
||||
<NumericStepper
|
||||
label="Speed"
|
||||
value={ship.speed ?? 0}
|
||||
onChange={(v) => onUpdate({ speed: v })}
|
||||
/>
|
||||
<EnumDropdown
|
||||
label="Maneuver Class"
|
||||
value={ship.maneuver_class}
|
||||
options={MANEUVER_CLASSES}
|
||||
onChange={(v) => onUpdate({ maneuver_class: v })}
|
||||
/>
|
||||
<EnumDropdown
|
||||
label="Size Category"
|
||||
value={ship.size_category}
|
||||
options={SIZE_CATEGORIES}
|
||||
onChange={(v) => onUpdate({ size_category: v })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="stat-grid-readonly">
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Speed</span>
|
||||
<span className="stat-ro-value">{ship.speed ?? 0}ft</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Class</span>
|
||||
<span className="stat-ro-value">{ship.maneuver_class ?? '—'}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Size</span>
|
||||
<span className="stat-ro-value">{ship.size_category ?? '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
89
web/src/components/dashboard/ShipStatsSection.tsx
Normal file
89
web/src/components/dashboard/ShipStatsSection.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useState } from 'react';
|
||||
import NumericStepper from '../ui/NumericStepper';
|
||||
import EnumDropdown from '../ui/EnumDropdown';
|
||||
import type { Ship } from '../../types/ship';
|
||||
|
||||
const MANEUVER_CLASSES = ['S', 'A', 'B', 'C', 'D', 'E', 'F'];
|
||||
const SIZE_CATEGORIES = ['Tiny', 'Small', 'Medium', 'Large', 'Huge', 'Gargantuan'];
|
||||
|
||||
interface Props {
|
||||
ship: Ship;
|
||||
onUpdate: (patch: Partial<Ship>) => void;
|
||||
}
|
||||
|
||||
export default function ShipStatsSection({ ship, onUpdate }: Props) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
return (
|
||||
<section className="dashboard-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">Ship Stats</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary btn-sm"
|
||||
onClick={() => setEditing(!editing)}
|
||||
>
|
||||
{editing ? 'Done' : 'Edit'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div className="stat-grid">
|
||||
<NumericStepper label="Hull Max" value={ship.hull_max} min={ship.hull_current} onChange={(v) => onUpdate({ hull_max: v })} />
|
||||
<NumericStepper label="Armor Max" value={ship.armor_max} min={ship.armor_current} onChange={(v) => onUpdate({ armor_max: v })} />
|
||||
<NumericStepper label="AC" value={ship.ac} onChange={(v) => onUpdate({ ac: v })} />
|
||||
<NumericStepper label="Con Save" value={ship.con_save ?? 0} onChange={(v) => onUpdate({ con_save: v })} />
|
||||
<NumericStepper label="Speed (ft)" value={ship.speed ?? 0} onChange={(v) => onUpdate({ speed: v })} />
|
||||
<EnumDropdown label="Maneuver Class" value={ship.maneuver_class} options={MANEUVER_CLASSES} onChange={(v) => onUpdate({ maneuver_class: v })} />
|
||||
<EnumDropdown label="Size" value={ship.size_category} options={SIZE_CATEGORIES} onChange={(v) => onUpdate({ size_category: v })} />
|
||||
<NumericStepper label="Crew Required" value={ship.crew_req} onChange={(v) => onUpdate({ crew_req: v })} />
|
||||
<NumericStepper label="Hardpoints" value={ship.hardpoints} onChange={(v) => onUpdate({ hardpoints: v })} />
|
||||
<NumericStepper label="Cargo (tons)" value={ship.cargo} onChange={(v) => onUpdate({ cargo: v })} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="stat-grid-readonly">
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">AC</span>
|
||||
<span className="stat-ro-value">{ship.ac}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Con</span>
|
||||
<span className="stat-ro-value">{ship.con_save != null ? `+${ship.con_save}` : '—'}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Hull Max</span>
|
||||
<span className="stat-ro-value">{ship.hull_max}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Armor Max</span>
|
||||
<span className="stat-ro-value">{ship.armor_max}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Speed</span>
|
||||
<span className="stat-ro-value">{ship.speed ?? 0}ft</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Class</span>
|
||||
<span className="stat-ro-value">{ship.maneuver_class ?? '—'}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Size</span>
|
||||
<span className="stat-ro-value">{ship.size_category ?? '—'}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Crew</span>
|
||||
<span className="stat-ro-value">{ship.crew_req}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Hardpoints</span>
|
||||
<span className="stat-ro-value">{ship.hardpoints}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Cargo</span>
|
||||
<span className="stat-ro-value">{ship.cargo}t</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -4,70 +4,29 @@ import type { Ship } from '../../types/ship';
|
||||
interface Props {
|
||||
ship: Ship;
|
||||
onUpdate: (patch: Partial<Ship>) => void;
|
||||
editing: boolean;
|
||||
}
|
||||
|
||||
export default function VitalsSection({ ship, onUpdate, editing }: Props) {
|
||||
export default function VitalsSection({ ship, onUpdate }: Props) {
|
||||
return (
|
||||
<section className="dashboard-section">
|
||||
<h3 className="section-title">Vitals</h3>
|
||||
{editing ? (
|
||||
<div className="stat-grid">
|
||||
<div className="combat-stats">
|
||||
<div className="combat-stat">
|
||||
<NumericStepper
|
||||
label="Hull"
|
||||
label={`Hull (/${ship.hull_max})`}
|
||||
value={ship.hull_current}
|
||||
max={ship.hull_max}
|
||||
onChange={(v) => onUpdate({ hull_current: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="combat-stat">
|
||||
<NumericStepper
|
||||
label="Hull Max"
|
||||
value={ship.hull_max}
|
||||
min={ship.hull_current}
|
||||
onChange={(v) => onUpdate({ hull_max: v })}
|
||||
/>
|
||||
<NumericStepper
|
||||
label="Armor"
|
||||
label={`Armor (/${ship.armor_max})`}
|
||||
value={ship.armor_current}
|
||||
max={ship.armor_max}
|
||||
onChange={(v) => onUpdate({ armor_current: v })}
|
||||
/>
|
||||
<NumericStepper
|
||||
label="Armor Max"
|
||||
value={ship.armor_max}
|
||||
min={ship.armor_current}
|
||||
onChange={(v) => onUpdate({ armor_max: v })}
|
||||
/>
|
||||
<NumericStepper
|
||||
label="AC"
|
||||
value={ship.ac}
|
||||
onChange={(v) => onUpdate({ ac: v })}
|
||||
/>
|
||||
<NumericStepper
|
||||
label="Con Save"
|
||||
value={ship.con_save ?? 0}
|
||||
onChange={(v) => onUpdate({ con_save: v })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="stat-grid-readonly">
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Hull</span>
|
||||
<span className="stat-ro-value">{ship.hull_current}/{ship.hull_max}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">AC</span>
|
||||
<span className="stat-ro-value">{ship.ac}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Armor</span>
|
||||
<span className="stat-ro-value">{ship.armor_current}/{ship.armor_max}</span>
|
||||
</div>
|
||||
<div className="stat-ro">
|
||||
<span className="stat-ro-label">Con</span>
|
||||
<span className="stat-ro-value">{ship.con_save != null ? `+${ship.con_save}` : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -321,10 +321,17 @@ button:disabled {
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.dashboard-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: var(--spacing-md);
|
||||
.combat-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.combat-stat {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import TopBar from '../components/layout/TopBar';
|
||||
import PageContainer from '../components/layout/PageContainer';
|
||||
import VitalsSection from '../components/dashboard/VitalsSection';
|
||||
import MobilitySection from '../components/dashboard/MobilitySection';
|
||||
import CrewSection from '../components/dashboard/CrewSection';
|
||||
import ShipStatsSection from '../components/dashboard/ShipStatsSection';
|
||||
import WeaponsSection from '../components/dashboard/WeaponsSection';
|
||||
import NotesSection from '../components/dashboard/NotesSection';
|
||||
import { useShipStore } from '../store/use-ship-store';
|
||||
@@ -12,7 +11,6 @@ import { useShipStore } from '../store/use-ship-store';
|
||||
export default function ShipDashboardPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { ship, weapons, loading, joinShip, leaveShip, updateShip, createWeapon, updateWeapon, deleteWeapon } = useShipStore();
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) joinShip(id);
|
||||
@@ -34,18 +32,8 @@ export default function ShipDashboardPage() {
|
||||
<>
|
||||
<TopBar title={ship.name} />
|
||||
<PageContainer>
|
||||
<div className="dashboard-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
className={editing ? 'btn-primary btn-sm' : 'btn-secondary btn-sm'}
|
||||
onClick={() => setEditing(!editing)}
|
||||
>
|
||||
{editing ? 'Done' : 'Edit'}
|
||||
</button>
|
||||
</div>
|
||||
<VitalsSection ship={ship} onUpdate={updateShip} editing={editing} />
|
||||
<MobilitySection ship={ship} onUpdate={updateShip} editing={editing} />
|
||||
<CrewSection ship={ship} onUpdate={updateShip} editing={editing} />
|
||||
<VitalsSection ship={ship} onUpdate={updateShip} />
|
||||
<ShipStatsSection ship={ship} onUpdate={updateShip} />
|
||||
<WeaponsSection
|
||||
weapons={weapons}
|
||||
onCreateWeapon={createWeapon}
|
||||
|
||||
Reference in New Issue
Block a user