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
88
frontend/src/components/map/Sidebar.tsx
Normal file
88
frontend/src/components/map/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { PlayerList } from '../sidebar/PlayerList';
|
||||
import { SortButtons, type SortKey } from '../sidebar/SortButtons';
|
||||
import type { TelemetrySnapshot, VitalsMessage, ServerHealth } from '../../types';
|
||||
|
||||
interface Props {
|
||||
players: TelemetrySnapshot[];
|
||||
vitals: Map<string, VitalsMessage>;
|
||||
serverHealth: ServerHealth | null;
|
||||
totalRares: number;
|
||||
totalKills: number;
|
||||
getColor: (name: string) => string;
|
||||
onSelectPlayer: (name: string) => void;
|
||||
onViewToggle: () => void;
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<Props> = ({
|
||||
players, vitals, serverHealth, totalRares, totalKills, getColor, onSelectPlayer, onViewToggle,
|
||||
}) => {
|
||||
const [sortKey, setSortKey] = useState<SortKey>('name');
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const serverKph = useMemo(() =>
|
||||
players.reduce((sum, p) => sum + (parseInt(p.kills_per_hour) || 0), 0),
|
||||
[players]);
|
||||
|
||||
const isOnline = serverHealth?.status?.toLowerCase() === 'online' || serverHealth?.status?.toLowerCase() === 'up';
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
let list = [...players];
|
||||
if (filter) list = list.filter(p => p.character_name.toLowerCase().startsWith(filter.toLowerCase()));
|
||||
switch (sortKey) {
|
||||
case 'kph': list.sort((a, b) => (parseInt(b.kills_per_hour) || 0) - (parseInt(a.kills_per_hour) || 0)); break;
|
||||
case 'skills': list.sort((a, b) => (b.kills || 0) - (a.kills || 0)); break;
|
||||
case 'srares': list.sort((a, b) => (b.session_rares ?? 0) - (a.session_rares ?? 0)); break;
|
||||
case 'tkills': list.sort((a, b) => (b.total_kills ?? 0) - (a.total_kills ?? 0)); break;
|
||||
case 'kpr': list.sort((a, b) => {
|
||||
const ar = (a.total_kills ?? 0) / Math.max(1, a.total_rares ?? 1);
|
||||
const br = (b.total_kills ?? 0) / Math.max(1, b.total_rares ?? 1);
|
||||
return ar - br;
|
||||
}); break;
|
||||
default: list.sort((a, b) => a.character_name.localeCompare(b.character_name));
|
||||
}
|
||||
return list;
|
||||
}, [players, sortKey, filter]);
|
||||
|
||||
return (
|
||||
<div className="ml-sidebar">
|
||||
{/* Header */}
|
||||
<div className="ml-sidebar-header">
|
||||
<span className="ml-sidebar-title">Active Mosswart Enjoyers ({players.length})</span>
|
||||
<button className="ml-view-toggle" onClick={onViewToggle}>Dashboard</button>
|
||||
</div>
|
||||
|
||||
{/* Server status */}
|
||||
<div className="ml-server-status">
|
||||
<span className={`ml-status-dot ${isOnline ? 'online' : 'offline'}`} />
|
||||
<span className="ml-status-text">Coldeve</span>
|
||||
{serverHealth?.latency_ms != null && <span className="ml-status-latency">{serverHealth.latency_ms}ms</span>}
|
||||
</div>
|
||||
|
||||
{/* Aggregate counters */}
|
||||
<div className="ml-counters">
|
||||
<div className="ml-counter rares"><span className="ml-counter-val">{totalRares}</span><span className="ml-counter-lbl">Rares</span></div>
|
||||
<div className={`ml-counter kph ${serverKph > 5000 ? 'ultra' : ''}`}><span className="ml-counter-val">{serverKph.toLocaleString()}</span><span className="ml-counter-lbl">Server KPH</span></div>
|
||||
<div className="ml-counter kills"><span className="ml-counter-val">{totalKills.toLocaleString()}</span><span className="ml-counter-lbl">Kills</span></div>
|
||||
</div>
|
||||
|
||||
{/* Sort + filter */}
|
||||
<SortButtons value={sortKey} onChange={setSortKey} />
|
||||
<input
|
||||
className="ml-filter"
|
||||
type="text"
|
||||
placeholder="Filter players..."
|
||||
value={filter}
|
||||
onChange={e => setFilter(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Player list */}
|
||||
<PlayerList
|
||||
players={sorted}
|
||||
vitals={vitals}
|
||||
getColor={getColor}
|
||||
onSelect={onSelectPlayer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue