ui: CREW tab renders real roster cards with traits + origin (real data from /api/crew)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user