MosswartOverlord/frontend/src/components/map/Sidebar.tsx

143 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useMemo, useDeferredValue } from 'react';
import { PlayerList } from '../sidebar/PlayerList';
import { SortButtons, type SortKey } from '../sidebar/SortButtons';
import { SidebarWindowButtons } from '../sidebar/SidebarWindowButtons';
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;
showHeatmap: boolean;
showPortals: boolean;
onToggleHeatmap: (v: boolean) => void;
onTogglePortals: (v: boolean) => void;
version?: string;
selectedPlayer?: string | null;
}
export const Sidebar: React.FC<Props> = ({
players, vitals, serverHealth, totalRares, totalKills, getColor, onSelectPlayer,
showHeatmap, showPortals, onToggleHeatmap, onTogglePortals, version, selectedPlayer,
}: Props) => {
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';
// Defer player list rendering — sidebar stats don't need real-time updates
const deferredPlayers = useDeferredValue(players);
const deferredVitals = useDeferredValue(vitals);
const sorted = useMemo(() => {
let list = [...deferredPlayers];
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;
}, [deferredPlayers, sortKey, filter]);
return (
<div className="ml-sidebar">
{version && <div className="ml-version">v{version}</div>}
<div className="ml-sidebar-header">
<span className="ml-sidebar-title" style={{ cursor: 'pointer' }} onClick={() => {
// 🐸 Små grodorna hop — bounce the whole layout and send frogs
// leaping up the screen. Replaces the old rickroll.
const layout = document.querySelector('.ml-layout') as HTMLElement | null;
if (layout) {
layout.classList.remove('ms-hop');
void layout.offsetWidth; // force reflow so the animation restarts
layout.classList.add('ms-hop');
}
const frogs = document.createElement('div');
frogs.className = 'ms-hop-frogs';
for (let i = 0; i < 9; i++) {
const f = document.createElement('span');
f.textContent = '🐸';
f.style.left = (i * 11 + 3) + 'vw';
f.style.animationDelay = (i * 0.07).toFixed(2) + 's';
frogs.appendChild(f);
}
document.body.appendChild(frogs);
window.setTimeout(() => {
layout?.classList.remove('ms-hop');
frogs.remove();
}, 2600);
}}>Active Mosswart Enjoyers ({players.length})</span>
</div>
<div className="ml-server-status">
<span className={`ml-status-dot ${isOnline ? 'online' : 'offline'}`} />
<span className="ml-status-text">Coldeve {isOnline ? 'Online' : 'Offline'}</span>
{serverHealth?.player_count != null && <span className="ml-status-detail">👥 {serverHealth.player_count}</span>}
{serverHealth?.latency_ms != null && <span className="ml-status-detail">{Math.round(serverHealth.latency_ms)}ms</span>}
{serverHealth?.uptime_seconds != null && (
<span className="ml-status-detail">Up: {Math.floor(serverHealth.uptime_seconds / 3600)}h</span>
)}
</div>
<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>
{/* Tool links */}
<div className="ml-tool-links">
<a href="/inventory.html" target="_blank" className="ml-tool-link">🔍 Inv Search</a>
<a href="/suitbuilder.html" target="_blank" className="ml-tool-link">🛡 Suitbuilder</a>
<a href="/debug.html" target="_blank" className="ml-tool-link">🐛 Debug</a>
</div>
<SidebarWindowButtons />
{/* Map toggles */}
<div className="ml-toggles">
<label className="ml-toggle-label">
<input type="checkbox" checked={showHeatmap} onChange={e => onToggleHeatmap(e.target.checked)} />
<span>Spawn Heatmap</span>
</label>
<label className="ml-toggle-label">
<input type="checkbox" checked={showPortals} onChange={e => onTogglePortals(e.target.checked)} />
<span>Portals</span>
</label>
</div>
<div style={{ borderTop: '1px solid #333', marginTop: 4, paddingTop: 4 }} />
<SortButtons value={sortKey} onChange={setSortKey} />
<input
className="ml-filter"
type="text"
placeholder="Filter players..."
value={filter}
onChange={e => setFilter(e.target.value)}
/>
<PlayerList
players={sorted}
vitals={deferredVitals}
getColor={getColor}
onSelect={onSelectPlayer}
selectedPlayer={selectedPlayer}
/>
</div>
);
};