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:
Erik 2026-04-12 15:38:14 +02:00
parent 3791c01bf3
commit 2c4b8d3afb
16 changed files with 995 additions and 151 deletions

View file

@ -0,0 +1,47 @@
import React, { createContext, useContext, useReducer, type Dispatch } from 'react';
interface MapTransform {
scale: number;
offX: number;
offY: number;
}
type Action =
| { type: 'SET'; scale: number; offX: number; offY: number }
| { type: 'ZOOM'; factor: number; cx: number; cy: number }
| { type: 'PAN'; dx: number; dy: number };
const MAX_ZOOM = 20;
const MIN_ZOOM = 0.3;
function reducer(state: MapTransform, action: Action): MapTransform {
switch (action.type) {
case 'SET':
return { scale: action.scale, offX: action.offX, offY: action.offY };
case 'ZOOM': {
const newScale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, state.scale * action.factor));
const ratio = newScale / state.scale;
return {
scale: newScale,
offX: action.cx - (action.cx - state.offX) * ratio,
offY: action.cy - (action.cy - state.offY) * ratio,
};
}
case 'PAN':
return { ...state, offX: state.offX + action.dx, offY: state.offY + action.dy };
default:
return state;
}
}
const Ctx = createContext<{ transform: MapTransform; dispatch: Dispatch<Action> }>({
transform: { scale: 1, offX: 0, offY: 0 },
dispatch: () => {},
});
export const MapTransformProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [transform, dispatch] = useReducer(reducer, { scale: 1, offX: 0, offY: 0 });
return <Ctx.Provider value={{ transform, dispatch }}>{children}</Ctx.Provider>;
};
export const useMapTransform = () => useContext(Ctx);