143 lines
6.3 KiB
TypeScript
143 lines
6.3 KiB
TypeScript
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>
|
||
);
|
||
};
|