MosswartOverlord/frontend/src/components/CharacterCard.tsx
Erik e58c05c895 feat: v2 dashboard — React + Vite parallel implementation
New modern dashboard at /v2 running alongside the existing UI at /.
Same backend, same APIs, same WebSocket — zero backend changes.

Stack: React 19 + Vite + TypeScript + Recharts
Source: frontend/ — build output: static/v2/

Phase 1 delivers:
- Character overview cards in a responsive CSS Grid
  - Live HP/Stamina/Mana bars via WebSocket vitals
  - Kills/hr, total kills, deaths, session uptime
  - VTank state badge (Combat/Nav/Idle)
  - Location coordinates
  - Click to expand: combat stats, prismatic count, CPU/RAM
- Global stats header: active chars, total kills, total rares, server health
- WebSocket hook with auto-reconnect
- HTTP poll fallback for initial load + server health
- Mobile responsive (single column on narrow screens)
- Dark theme matching the MosswartOverlord palette

Build: cd frontend && npm run build
Access: /v2 (served by existing NoCacheStaticFiles mount)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:07:11 +02:00

89 lines
3.3 KiB
TypeScript

import React, { useState } from 'react';
import { VitalBar } from './VitalBar';
import type { CharacterState } from '../types';
interface Props {
character: CharacterState;
}
const vtankBadge = (state: string) => {
const s = (state || 'idle').toLowerCase();
if (s === 'combat') return { label: 'Combat', cls: 'badge-combat' };
if (s === 'nav' || s === 'navigation') return { label: 'Nav', cls: 'badge-nav' };
return { label: 'Idle', cls: 'badge-idle' };
};
export const CharacterCard: React.FC<Props> = React.memo(({ character }) => {
const [expanded, setExpanded] = useState(false);
const { telemetry: t, vitals: v, combat: c } = character;
const badge = vtankBadge(t?.vt_state ?? '');
return (
<div className="char-card" onClick={() => setExpanded(!expanded)}>
<div className="char-header">
<span className="char-name">{character.name}</span>
<span className={`char-badge ${badge.cls}`}>{badge.label}</span>
</div>
{v ? (
<div className="char-vitals">
<VitalBar label="HP" current={v.health_current} max={v.health_max}
color="linear-gradient(90deg, #ff4444, #ff6666)" bgColor="#330000" />
<VitalBar label="ST" current={v.stamina_current} max={v.stamina_max}
color="linear-gradient(90deg, #ffaa00, #ffcc44)" bgColor="#331a00" />
<VitalBar label="MN" current={v.mana_current} max={v.mana_max}
color="linear-gradient(90deg, #4488ff, #66aaff)" bgColor="#001433" />
</div>
) : (
<div className="char-vitals-placeholder">Awaiting vitals...</div>
)}
<div className="char-stats-row">
<div className="stat">
<span className="stat-value">{t?.kills_per_hour ?? '--'}</span>
<span className="stat-label">kills/hr</span>
</div>
<div className="stat">
<span className="stat-value">{t?.kills?.toLocaleString() ?? '--'}</span>
<span className="stat-label">kills</span>
</div>
<div className="stat">
<span className="stat-value">{t?.deaths ?? '0'}</span>
<span className="stat-label">deaths</span>
</div>
<div className="stat">
<span className="stat-value">{t?.onlinetime?.replace(/^00\./, '') ?? '--'}</span>
<span className="stat-label">uptime</span>
</div>
</div>
{t && (
<div className="char-location">
{t.ns?.toFixed(1)}N, {t.ew?.toFixed(1)}E
</div>
)}
{expanded && (
<div className="char-expanded">
{v?.vitae ? <div className="vitae-warn">Vitae: {v.vitae}%</div> : null}
<div className="expanded-row">
<span>Prismatics: {t?.prismatic_taper_count ?? '--'}</span>
<span>Total Deaths: {t?.total_deaths ?? '--'}</span>
</div>
{c?.session && (
<div className="expanded-row">
<span>Session Dmg: {c.session.total_damage_given?.toLocaleString()}</span>
<span>Session Kills: {c.session.total_kills}</span>
</div>
)}
<div className="expanded-row">
<span>RAM: {t?.mem_mb ? (t.mem_mb / 1048576).toFixed(0) + ' MB' : '--'}</span>
<span>CPU: {t?.cpu_pct?.toFixed(1) ?? '--'}%</span>
</div>
</div>
)}
</div>
);
});
CharacterCard.displayName = 'CharacterCard';