102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import type { CrewDto } from '@/lib/game/save'
|
|
|
|
// CREW tab — the ship's roster: starting crew plus members found while exploring. Fetches
|
|
// /api/crew on mount; Radix unmounts inactive tabs, so opening this tab always refetches
|
|
// (no polling needed — turn-based game). Display-only for now: no dismiss/assign actions yet.
|
|
|
|
interface CrewPanelProps {
|
|
narrow?: boolean
|
|
}
|
|
|
|
export function CrewPanel({ narrow }: CrewPanelProps) {
|
|
const [roster, setRoster] = useState<CrewDto[] | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
fetch('/api/crew')
|
|
.then(async (res) => {
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
return (await res.json()) as CrewDto[]
|
|
})
|
|
.then((r) => {
|
|
if (!cancelled) setRoster(r)
|
|
})
|
|
.catch((err: unknown) => {
|
|
if (!cancelled) setError(err instanceof Error ? err.message : String(err))
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
if (error && !roster) {
|
|
return (
|
|
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', padding: '0.75rem' }}>
|
|
<span style={{ fontSize: '0.62rem', color: 'var(--color-red)', letterSpacing: '0.08em' }}>
|
|
CREW MANIFEST OFFLINE // {error.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!roster) {
|
|
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' }}>LOADING CREW MANIFEST…</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: narrow ? '1fr' : 'repeat(2, 1fr)',
|
|
gap: '0.75rem',
|
|
}}
|
|
>
|
|
{roster.map((member) => (
|
|
<Card key={member.id}>
|
|
<CardHeader>
|
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
<CardTitle>{member.name}</CardTitle>
|
|
<Badge variant="ACTIVE" className="ml-auto">
|
|
ACTIVE
|
|
</Badge>
|
|
</div>
|
|
<p
|
|
style={{
|
|
fontSize: '0.6rem',
|
|
color: 'var(--text-muted)',
|
|
letterSpacing: '0.1em',
|
|
textTransform: 'uppercase',
|
|
}}
|
|
>
|
|
{member.role}
|
|
</p>
|
|
</CardHeader>
|
|
<CardContent style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
|
<div style={{ fontSize: '0.7rem', letterSpacing: '0.05em' }}>
|
|
<span style={{ color: 'var(--color-green)' }}>+ </span>
|
|
<span style={{ color: 'var(--text-secondary)' }}>{member.traitGood}</span>
|
|
</div>
|
|
<div style={{ fontSize: '0.7rem', letterSpacing: '0.05em' }}>
|
|
<span style={{ color: 'var(--color-red)' }}>- </span>
|
|
<span style={{ color: 'var(--text-secondary)' }}>{member.traitBad}</span>
|
|
</div>
|
|
<p style={{ fontSize: '0.6rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
|
|
ORIGIN // {member.origin.toUpperCase()}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|