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; 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 = ({ players, vitals, serverHealth, totalRares, totalKills, getColor, onSelectPlayer, showHeatmap, showPortals, onToggleHeatmap, onTogglePortals, version, selectedPlayer, }: Props) => { const [sortKey, setSortKey] = useState('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 (
{version &&
v{version}
}
{ // 🐸 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})
Coldeve {isOnline ? 'Online' : 'Offline'} {serverHealth?.player_count != null && 👥 {serverHealth.player_count}} {serverHealth?.latency_ms != null && {Math.round(serverHealth.latency_ms)}ms} {serverHealth?.uptime_seconds != null && ( Up: {Math.floor(serverHealth.uptime_seconds / 3600)}h )}
{totalRares}Rares
5000 ? 'ultra' : ''}`}>{serverKph.toLocaleString()}Server KPH
{totalKills.toLocaleString()}Kills
{/* Tool links */}
🔍 Inv Search 🛡️ Suitbuilder 🐛 Debug
{/* Map toggles */}
setFilter(e.target.value)} />
); };