feat(v2): Phase 1 — map-first layout matching v1
Rebuilds the v1 map-centric experience in React: Layout: - 400px sidebar on left, interactive map on right (flex, 100vh) - Exact same proportions and dark theme as v1 Sidebar (top→bottom): - Header with active player count + Dashboard toggle button - Server status dot (Coldeve online/offline with pulse) - Aggregate counters: Rares (gold), Server KPH (blue glow), Kills (red) - 6 sort buttons (Name, KPH, S.Kills, S.Rares, T.Kills, KPR) - Player name filter - Scrollable player list with per-row: - Name + coordinates - HP/Stamina/Mana vital bars (red/orange/blue gradients) - Session kills, total kills, KPH - Session rares, total rares, VTank meta state pill - Online time, deaths, prismatic tapers - Color-coded left border per player Map: - dereth.png with CSS transform pan (drag) + zoom (wheel, 1.1x factor, max 20x) - Player dots (6px circles, color-matched to sidebar) - Hover tooltip (name, coords, kph, kills) - World coordinate display at cursor position - Fit-to-window on first load View toggle: Map View ↔ Dashboard with localStorage persistence. All v1 CSS ported under ml-* prefix, scoped via map-layout.css. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3791c01bf3
commit
2c4b8d3afb
16 changed files with 995 additions and 151 deletions
24
frontend/src/components/sidebar/PlayerList.tsx
Normal file
24
frontend/src/components/sidebar/PlayerList.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { PlayerRow } from './PlayerRow';
|
||||
import type { TelemetrySnapshot, VitalsMessage } from '../../types';
|
||||
|
||||
interface Props {
|
||||
players: TelemetrySnapshot[];
|
||||
vitals: Map<string, VitalsMessage>;
|
||||
getColor: (name: string) => string;
|
||||
onSelect: (name: string) => void;
|
||||
}
|
||||
|
||||
export const PlayerList: React.FC<Props> = ({ players, vitals, getColor, onSelect }) => (
|
||||
<ul className="ml-player-list">
|
||||
{players.map(p => (
|
||||
<PlayerRow
|
||||
key={p.character_name}
|
||||
player={p}
|
||||
vitals={vitals.get(p.character_name) ?? null}
|
||||
color={getColor(p.character_name)}
|
||||
onSelect={() => onSelect(p.character_name)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
59
frontend/src/components/sidebar/PlayerRow.tsx
Normal file
59
frontend/src/components/sidebar/PlayerRow.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import { formatCoord } from '../../utils/coordinates';
|
||||
import type { TelemetrySnapshot, VitalsMessage } from '../../types';
|
||||
|
||||
interface Props {
|
||||
player: TelemetrySnapshot;
|
||||
vitals: VitalsMessage | null;
|
||||
color: string;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
export const PlayerRow: React.FC<Props> = React.memo(({ player: p, vitals: v, color, onSelect }) => {
|
||||
const vtState = (p.vt_state || 'idle').toLowerCase();
|
||||
const isActive = vtState === 'combat' || vtState === 'hunt';
|
||||
|
||||
return (
|
||||
<li className="ml-player-row" style={{ borderLeftColor: color }} onClick={onSelect}>
|
||||
{/* Row 1: Name + coords */}
|
||||
<div className="ml-pr-name">{p.character_name}</div>
|
||||
<div className="ml-pr-coords">{formatCoord(p.ns, p.ew)}</div>
|
||||
|
||||
{/* Row 2: Vital bars */}
|
||||
<div className="ml-pr-vitals">
|
||||
<div className="ml-vital-bar hp">
|
||||
<div className="ml-vital-fill" style={{ width: `${v?.health_percentage ?? 0}%` }} />
|
||||
</div>
|
||||
<div className="ml-vital-bar sta">
|
||||
<div className="ml-vital-fill" style={{ width: `${v?.stamina_percentage ?? 0}%` }} />
|
||||
</div>
|
||||
<div className="ml-vital-bar mana">
|
||||
<div className="ml-vital-fill" style={{ width: `${v?.mana_percentage ?? 0}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Stats */}
|
||||
<div className="ml-pr-stats">
|
||||
<span className="ml-stat" title="Session kills">{p.kills?.toLocaleString() ?? 0}</span>
|
||||
<span className="ml-stat" title="Total kills">{(p.total_kills ?? 0).toLocaleString()}</span>
|
||||
<span className="ml-stat" title="Kills per hour">{p.kills_per_hour ?? '0'} kph</span>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Rares + meta */}
|
||||
<div className="ml-pr-stats">
|
||||
<span className="ml-stat" title="Session rares">{p.session_rares ?? 0}r</span>
|
||||
<span className="ml-stat" title="Total rares">{p.total_rares ?? 0}r</span>
|
||||
<span className={`ml-meta-pill ${isActive ? 'active' : ''}`}>{p.vt_state || 'idle'}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 5: Time + deaths + tapers */}
|
||||
<div className="ml-pr-stats">
|
||||
<span className="ml-stat" title="Online time">{p.onlinetime?.replace(/^00\./, '') ?? '--'}</span>
|
||||
<span className="ml-stat" title="Deaths">{p.deaths ?? '0'}d</span>
|
||||
<span className="ml-stat" title="Prismatic tapers">{p.prismatic_taper_count ?? '0'}t</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
PlayerRow.displayName = 'PlayerRow';
|
||||
31
frontend/src/components/sidebar/SortButtons.tsx
Normal file
31
frontend/src/components/sidebar/SortButtons.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
export type SortKey = 'name' | 'kph' | 'skills' | 'srares' | 'tkills' | 'kpr';
|
||||
|
||||
const SORTS: { key: SortKey; label: string }[] = [
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'kph', label: 'KPH' },
|
||||
{ key: 'skills', label: 'S.Kills' },
|
||||
{ key: 'srares', label: 'S.Rares' },
|
||||
{ key: 'tkills', label: 'T.Kills' },
|
||||
{ key: 'kpr', label: 'KPR' },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
value: SortKey;
|
||||
onChange: (key: SortKey) => void;
|
||||
}
|
||||
|
||||
export const SortButtons: React.FC<Props> = ({ value, onChange }) => (
|
||||
<div className="ml-sort-buttons">
|
||||
{SORTS.map(s => (
|
||||
<button
|
||||
key={s.key}
|
||||
className={`ml-sort-btn ${value === s.key ? 'active' : ''}`}
|
||||
onClick={() => onChange(s.key)}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue