Files
SREBOT/web/public/js/player-details-modal.js
T
FURRO404 2b399fdb81 add SREBOT, SHARED, TSSBOT contents (fixup for #1223)
PR #1223 only staged the deletions of the old paths because the new
top-level directories were still untracked when the commit was authored.
This commit adds the actual restructured tree: SREBOT/ (existing bot),
SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES,
update_game_files), and TSSBOT/ (skeleton).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:17:02 -07:00

828 lines
33 KiB
JavaScript

// Player Details Modal - Quick-peek popup for player stats
(function () {
let modalInjected = false;
let currentTab = 'overview';
function T(key) {
return (window.__t && window.__t(key)) || key;
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatNumber(n) {
return (n || 0).toLocaleString();
}
function getWinRateColor(wr) {
if (wr >= 70) return '#90EE90';
if (wr >= 60) return '#A8E6CF';
if (wr >= 50) return '#FFD700';
if (wr >= 40) return '#FFA500';
return '#FF6B6B';
}
function getKDRColor(kdr) {
if (kdr >= 3) return '#90EE90';
if (kdr >= 2) return '#A8E6CF';
if (kdr >= 1.5) return '#FFD700';
if (kdr >= 1) return '#FFA500';
return '#FF6B6B';
}
function injectModal() {
if (modalInjected) return;
modalInjected = true;
const style = document.createElement('style');
style.textContent = `
.pdm-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(4px);
z-index: 9999;
display: flex; align-items: center; justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
}
.pdm-overlay.pdm-visible { opacity: 1; }
.pdm-modal {
background: #1e1e1e;
border: 1px solid rgba(144,238,144,0.15);
border-radius: 12px;
max-width: 700px; width: 90%;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
transform: scale(0.95);
transition: transform 0.2s ease;
}
.pdm-overlay.pdm-visible .pdm-modal { transform: scale(1); }
.pdm-header {
display: flex; align-items: center; justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid rgba(144,238,144,0.1);
position: sticky; top: 0;
background: #1e1e1e;
z-index: 1;
border-radius: 12px 12px 0 0;
}
.pdm-header-left {
display: flex; align-items: center; gap: 0.75rem;
min-width: 0;
}
.pdm-player-name {
font-size: 1.1rem; font-weight: 700;
color: #F5F5DC;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pdm-profile-link {
font-size: 0.75rem;
color: #90EE90;
text-decoration: none;
white-space: nowrap;
opacity: 0.8;
transition: opacity 0.2s;
}
.pdm-profile-link:hover { opacity: 1; text-decoration: underline; }
.pdm-close {
background: none; border: none;
color: rgba(255,255,255,0.5);
font-size: 1.4rem; cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 6px;
transition: color 0.2s, background 0.2s;
flex-shrink: 0;
}
.pdm-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
.pdm-tabs {
display: flex; justify-content: center;
padding: 0.75rem 1.25rem 0;
}
.pdm-tab-group {
position: relative;
display: inline-flex;
background: rgba(255,255,255,0.05);
border-radius: 2rem;
padding: 3px;
border: 1px solid rgba(144,238,144,0.2);
}
.pdm-tab-slider {
position: absolute;
top: 3px; left: 3px;
height: calc(100% - 6px);
background: #90EE90;
border-radius: calc(2rem - 2px);
transition: left 0.25s cubic-bezier(0.4,0,0.2,1), width 0.25s cubic-bezier(0.4,0,0.2,1);
pointer-events: none;
}
.pdm-tab {
position: relative; z-index: 1;
background: transparent; border: none;
color: rgba(255,255,255,0.45);
padding: 0.4rem 1.3rem;
border-radius: calc(2rem - 2px);
cursor: pointer;
font-size: 0.82rem; font-weight: 600;
transition: color 0.25s ease;
white-space: nowrap;
}
.pdm-tab.active { color: #1b1b1b; }
.pdm-tab:hover:not(.active) { color: rgba(255,255,255,0.8); }
.pdm-body { padding: 1rem 1.25rem 1.25rem; }
.pdm-loading {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 3rem; color: rgba(255,255,255,0.5);
gap: 0.75rem;
}
.pdm-spinner {
width: 32px; height: 32px;
border: 3px solid rgba(144,238,144,0.2);
border-top-color: #90EE90;
border-radius: 50%;
animation: pdm-spin 0.8s linear infinite;
}
@keyframes pdm-spin { to { transform: rotate(360deg); } }
.pdm-error {
text-align: center; padding: 2rem;
color: #ff6b6b;
}
.pdm-error i { font-size: 2rem; margin-bottom: 0.5rem; }
/* Overview tab */
.pdm-stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.6rem;
}
.pdm-stats-grid-last-row {
display: flex;
justify-content: center;
gap: 0.6rem;
margin-top: 0.6rem;
}
.pdm-stats-grid-last-row .pdm-stat-card {
flex: 0 1 calc(33.333% - 0.4rem);
}
.pdm-stat-card {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(144,238,144,0.08);
border-radius: 8px;
padding: 0.6rem 0.75rem;
text-align: center;
}
.pdm-stat-value {
font-size: 1.1rem; font-weight: 700;
color: #90EE90;
}
.pdm-stat-toggle {
display: flex;
justify-content: center;
gap: 0.3rem;
margin-top: 0.15rem;
}
.pdm-stat-toggle span {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: color 0.2s;
}
.pdm-stat-toggle span.pdm-toggle-active {
color: #90EE90;
font-weight: 700;
}
.pdm-stat-toggle span.pdm-toggle-inactive {
color: rgba(255,255,255,0.3);
}
.pdm-stat-toggle span.pdm-toggle-inactive:hover {
color: rgba(255,255,255,0.55);
}
.pdm-stat-toggle .pdm-toggle-sep {
color: rgba(255,255,255,0.15);
cursor: default;
}
.pdm-stat-label {
font-size: 0.7rem;
color: rgba(255,255,255,0.5);
margin-top: 0.15rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Mini chart card */
.pdm-chart-card {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(144,238,144,0.08);
border-radius: 8px;
padding: 0.5rem 0.6rem;
margin-top: 0.6rem;
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color 0.2s;
}
.pdm-chart-card:hover {
border-color: rgba(144,238,144,0.25);
}
.pdm-chart-label {
font-size: 0.65rem;
color: rgba(255,255,255,0.5);
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: center;
margin-bottom: 0.2rem;
}
.pdm-chart-hint {
font-size: 0.55rem;
color: rgba(255,255,255,0.25);
text-align: center;
margin-top: 0.15rem;
}
.pdm-mini-canvas-wrap {
position: relative;
width: 100%;
height: 60px;
}
.pdm-mini-canvas {
width: 100%;
height: 100%;
display: block;
}
/* Vehicles tab */
.pdm-vehicles-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
max-height: 55vh;
overflow-y: auto;
}
.pdm-vehicle-card {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(144,238,144,0.08);
border-radius: 8px;
padding: 0.6rem 0.75rem;
}
.pdm-vehicle-card.pdm-crown-card {
border-color: rgba(255,215,0,0.3);
background: rgba(255,215,0,0.03);
}
.pdm-vehicle-name {
font-weight: 600; font-size: 0.85rem;
color: #F5F5DC;
margin-bottom: 0.4rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pdm-crown {
color: #FFD700;
margin-right: 0.3rem;
font-size: 0.75rem;
}
.pdm-vehicle-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.15rem 0.5rem;
font-size: 0.72rem;
color: rgba(255,255,255,0.6);
}
.pdm-vehicle-stats span {
white-space: nowrap;
}
.pdm-vs-val { font-weight: 600; }
/* Sessions tab */
.pdm-sessions-wrap {
max-height: 55vh;
overflow-y: auto;
}
.pdm-sessions-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
}
.pdm-sessions-table th {
text-align: left;
padding: 0.55rem 0.6rem;
color: rgba(255,255,255,0.5);
font-weight: 600;
border-bottom: 1px solid rgba(144,238,144,0.1);
white-space: nowrap;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.pdm-sessions-table td {
padding: 0.55rem 0.6rem;
color: rgba(255,255,255,0.75);
border-bottom: 1px solid rgba(255,255,255,0.03);
white-space: nowrap;
}
.pdm-sessions-table tr:hover td {
background: rgba(255,255,255,0.03);
}
.pdm-result-badge {
display: inline-block;
padding: 0.2rem 0.55rem;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
}
.pdm-result-win { background: rgba(144,238,144,0.15); color: #90EE90; }
.pdm-result-loss { background: rgba(255,107,107,0.15); color: #ff6b6b; }
.pdm-no-data {
text-align: center; padding: 2rem;
color: rgba(255,255,255,0.4);
}
/* Details button */
.pdm-details-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid rgba(144,238,144,0.15);
color: rgba(144,238,144,0.5);
width: 22px; height: 22px;
border-radius: 4px;
cursor: pointer;
font-size: 0.65rem;
margin-left: 0.4rem;
vertical-align: middle;
transition: all 0.2s;
padding: 0;
line-height: 1;
flex-shrink: 0;
}
.pdm-details-btn:hover {
background: rgba(144,238,144,0.12);
color: #90EE90;
border-color: rgba(144,238,144,0.4);
}
/* Scrollbar */
.pdm-modal::-webkit-scrollbar,
.pdm-vehicles-grid::-webkit-scrollbar,
.pdm-sessions-wrap::-webkit-scrollbar {
width: 6px;
}
.pdm-modal::-webkit-scrollbar-track,
.pdm-vehicles-grid::-webkit-scrollbar-track,
.pdm-sessions-wrap::-webkit-scrollbar-track {
background: transparent;
}
.pdm-modal::-webkit-scrollbar-thumb,
.pdm-vehicles-grid::-webkit-scrollbar-thumb,
.pdm-sessions-wrap::-webkit-scrollbar-thumb {
background: rgba(144,238,144,0.15);
border-radius: 3px;
}
@media (max-width: 640px) {
.pdm-stats-grid { grid-template-columns: repeat(2, 1fr); }
.pdm-vehicles-grid { grid-template-columns: 1fr; }
.pdm-sessions-table { font-size: 0.75rem; }
.pdm-sessions-table th,
.pdm-sessions-table td { padding: 0.45rem 0.4rem; }
}
`;
document.head.appendChild(style);
const overlay = document.createElement('div');
overlay.id = 'pdm-overlay';
overlay.className = 'pdm-overlay';
overlay.style.display = 'none';
overlay.innerHTML = `
<div class="pdm-modal" id="pdm-modal">
<div class="pdm-header">
<div class="pdm-header-left">
<span class="pdm-player-name" id="pdm-player-name"></span>
<a class="pdm-profile-link" id="pdm-profile-link" href="#">${T('playerModal.viewFullProfile')}</a>
</div>
<button class="pdm-close" id="pdm-close" title="${T('playerModal.close')}">&times;</button>
</div>
<div class="pdm-tabs">
<div class="pdm-tab-group" id="pdm-tab-group">
<div class="pdm-tab-slider" id="pdm-tab-slider"></div>
<button class="pdm-tab active" data-tab="overview">${T('playerModal.overview')}</button>
<button class="pdm-tab" data-tab="vehicles">${T('playerModal.vehicles')}</button>
<button class="pdm-tab" data-tab="sessions">${T('playerModal.sessions')}</button>
</div>
</div>
<div class="pdm-body" id="pdm-body">
<div class="pdm-loading">
<div class="pdm-spinner"></div>
<span>${T('playerModal.loadingPlayerData')}</span>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
// Events
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
document.getElementById('pdm-close').addEventListener('click', closeModal);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && overlay.style.display !== 'none') closeModal();
});
// Tab switching
document.getElementById('pdm-tab-group').addEventListener('click', function (e) {
const btn = e.target.closest('.pdm-tab');
if (!btn || btn.classList.contains('active')) return;
document.querySelectorAll('.pdm-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentTab = btn.dataset.tab;
updateSlider();
renderTab();
});
// Prevent modal scroll from propagating
document.getElementById('pdm-modal').addEventListener('click', function (e) {
e.stopPropagation();
});
}
function updateSlider() {
const group = document.getElementById('pdm-tab-group');
const slider = document.getElementById('pdm-tab-slider');
const active = group.querySelector('.pdm-tab.active');
if (!active || !slider) return;
slider.style.left = active.offsetLeft + 'px';
slider.style.width = active.offsetWidth + 'px';
}
let playerDataCache = null;
let gamesDataCache = null;
let historyDataCache = null;
let miniChart = null;
let miniChartMetric = 'kdr';
let currentKdrKps = 'kdr';
const miniChartMetrics = ['kdr', 'win_rate', 'battles'];
const miniChartConfig = {
kdr: { label: T('playerModal.kdr'), color: '#64b5f6', suffix: '' },
win_rate: { label: T('playerModal.winRate'), color: '#90EE90', suffix: '%' },
battles: { label: T('playerModal.battles'), color: '#ffb74d', suffix: '' }
};
function closeModal() {
const overlay = document.getElementById('pdm-overlay');
overlay.classList.remove('pdm-visible');
setTimeout(() => {
overlay.style.display = 'none';
document.body.style.overflow = '';
}, 200);
// Destroy mini chart to prevent canvas reuse issues
if (miniChart) {
miniChart.destroy();
miniChart = null;
}
}
function renderTab() {
const body = document.getElementById('pdm-body');
if (!playerDataCache) return;
// Destroy old mini chart before re-rendering
if (miniChart) {
miniChart.destroy();
miniChart = null;
}
if (currentTab === 'overview') {
body.innerHTML = renderOverview(playerDataCache);
initMiniChart();
} else if (currentTab === 'vehicles') {
body.innerHTML = renderVehicles(playerDataCache);
initVehicleKdrToggle();
} else if (currentTab === 'sessions') {
body.innerHTML = renderSessions(gamesDataCache);
}
}
function renderOverview(data) {
const vehicles = (data.vehicles || []).filter(v => v.vehicle !== 'DISCONNECTED');
let totalBattles = 0, wins = 0, groundKills = 0, airKills = 0, assists = 0, deaths = 0, captures = 0;
vehicles.forEach(v => {
const s = v.stats;
totalBattles += s.total_battles || 0;
wins += s.wins || 0;
groundKills += s.ground_kills || 0;
airKills += s.air_kills || 0;
assists += s.assists || 0;
deaths += s.deaths || 0;
captures += s.captures || 0;
});
const totalKills = groundKills + airKills;
const winRate = totalBattles > 0 ? ((wins / totalBattles) * 100).toFixed(1) + '%' : '0.0%';
const kdr = deaths > 0 ? (totalKills / deaths).toFixed(2) : totalKills.toFixed(2);
const kps = totalBattles > 0 ? (totalKills / totalBattles).toFixed(2) : '0.00';
const stats = [
[T('playerModal.totalBattles'), formatNumber(totalBattles)],
[T('playerModal.wins'), formatNumber(wins)],
[T('playerModal.winRate'), winRate],
[T('playerModal.totalKills'), formatNumber(totalKills)],
[T('playerModal.kdr'), kdr],
[T('playerModal.kps'), kps],
[T('playerModal.airKills'), formatNumber(airKills)],
[T('playerModal.groundKills'), formatNumber(groundKills)],
[T('playerModal.assists'), formatNumber(assists)],
[T('playerModal.deaths'), formatNumber(deaths)],
[T('playerModal.captures'), formatNumber(captures)],
];
const cfg = miniChartConfig[miniChartMetric];
const chartCard = `<div class="pdm-chart-card" id="pdm-chart-card" title="${T('playerModal.clickToSwitchMetric')}">
<div class="pdm-chart-label" id="pdm-chart-metric-label">${cfg.label}</div>
<div class="pdm-mini-canvas-wrap"><canvas class="pdm-mini-canvas" id="pdm-mini-canvas"></canvas></div>
<div class="pdm-chart-hint">${T('playerModal.clickToCycle')}</div>
</div>`;
const fullRows = stats.slice(0, Math.floor(stats.length / 3) * 3);
const remainder = stats.slice(fullRows.length);
const cardHtml = s => `<div class="pdm-stat-card"><div class="pdm-stat-value">${s[1]}</div><div class="pdm-stat-label">${s[0]}</div></div>`;
const lastRow = remainder.length ? `<div class="pdm-stats-grid-last-row">${remainder.map(cardHtml).join('')}</div>` : '';
return `<div class="pdm-stats-grid">${fullRows.map(cardHtml).join('')}</div>${lastRow}${chartCard}`;
}
function initVehicleKdrToggle() {
const toggle = document.getElementById('pdm-veh-kdr-toggle');
if (!toggle) return;
toggle.addEventListener('click', function (e) {
const span = e.target.closest('[data-mode]');
if (!span || span.classList.contains('pdm-toggle-active')) return;
currentKdrKps = span.dataset.mode;
renderTab();
});
}
function initMiniChart() {
const card = document.getElementById('pdm-chart-card');
if (!card) return;
card.addEventListener('click', function () {
const idx = miniChartMetrics.indexOf(miniChartMetric);
miniChartMetric = miniChartMetrics[(idx + 1) % miniChartMetrics.length];
renderMiniChart();
});
// Load history data if not cached, then render
if (historyDataCache) {
renderMiniChart();
} else if (currentUid) {
window.apiClient.request('/api/player/' + currentUid + '/history').then(data => {
historyDataCache = data;
renderMiniChart();
}).catch(() => {
const canvas = document.getElementById('pdm-mini-canvas');
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(T('playerModal.noChartData'), canvas.width / 2, canvas.height / 2);
}
});
}
}
function renderMiniChart() {
if (!historyDataCache || !historyDataCache.history || !historyDataCache.history.length) return;
if (typeof Chart === 'undefined') return;
const canvas = document.getElementById('pdm-mini-canvas');
const label = document.getElementById('pdm-chart-metric-label');
if (!canvas) return;
const cfg = miniChartConfig[miniChartMetric];
if (label) label.textContent = cfg.label;
const history = historyDataCache.history;
const dataPoints = history
.filter(d => d[miniChartMetric] != null)
.map(d => ({ x: new Date(d.period + 'T00:00:00Z').getTime(), y: d[miniChartMetric] }));
if (!dataPoints.length) return;
if (miniChart) {
miniChart.data.datasets[0].data = dataPoints;
miniChart.data.datasets[0].borderColor = cfg.color;
miniChart.data.datasets[0].backgroundColor = cfg.color + '18';
miniChart.data.datasets[0].pointBackgroundColor = cfg.color;
miniChart.options.plugins.tooltip.borderColor = cfg.color;
miniChart.options.plugins.tooltip.bodyColor = cfg.color;
miniChart.options.plugins.tooltip.callbacks.label = ctx => `${cfg.label}: ${ctx.parsed.y}${cfg.suffix}`;
miniChart.update();
return;
}
miniChart = new Chart(canvas.getContext('2d'), {
type: 'line',
data: {
datasets: [{
data: dataPoints,
borderColor: cfg.color,
backgroundColor: cfg.color + '18',
borderWidth: 1.5,
pointBackgroundColor: cfg.color,
pointRadius: 0,
pointHoverRadius: 3,
tension: 0.15,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 300 },
plugins: {
legend: { display: false },
tooltip: {
enabled: true,
backgroundColor: 'rgba(15,15,15,0.92)',
borderColor: cfg.color,
borderWidth: 1,
titleColor: 'rgba(255,255,255,0.75)',
bodyColor: cfg.color,
padding: 6,
displayColors: false,
callbacks: {
title: () => '',
label: ctx => `${cfg.label}: ${ctx.parsed.y}${cfg.suffix}`
}
}
},
scales: {
x: { type: 'linear', display: false, min: (function() { var y = new Date(history[0].period + 'T00:00:00Z').getUTCFullYear(); return Date.UTC(y, 0, 1); })() },
y: { display: false, beginAtZero: true }
}
}
});
}
let currentUid = null;
function renderVehicles(data) {
const vehicles = (data.vehicles || []).filter(v => v.vehicle !== 'DISCONNECTED');
if (!vehicles.length) return `<div class="pdm-no-data"><i class="fas fa-box-open" style="font-size:2rem;margin-bottom:0.5rem;display:block;"></i>${T('playerModal.noVehicleData')}</div>`;
// Sort by total battles desc
vehicles.sort((a, b) => (b.stats.total_battles || 0) - (a.stats.total_battles || 0));
// Find best WR vehicle with >15 games
let bestWrVehicle = null;
let bestWr = -1;
vehicles.forEach(v => {
const battles = v.stats.total_battles || 0;
if (battles > 15) {
const wr = (v.stats.wins || 0) / battles;
if (wr > bestWr) {
bestWr = wr;
bestWrVehicle = v.vehicle;
}
}
});
const mode = currentKdrKps || 'kdr';
const kdrCls = mode === 'kdr' ? 'pdm-toggle-active' : 'pdm-toggle-inactive';
const kpsCls = mode === 'kps' ? 'pdm-toggle-active' : 'pdm-toggle-inactive';
const toggleHeader = `<div class="pdm-stat-toggle" id="pdm-veh-kdr-toggle" style="margin-bottom:0.5rem;">
<span class="${kdrCls}" data-mode="kdr">KDR</span>
<span class="pdm-toggle-sep">/</span>
<span class="${kpsCls}" data-mode="kps">KPS</span>
</div>`;
return `${toggleHeader}<div class="pdm-vehicles-grid">${vehicles.map(v => {
const s = v.stats;
const totalKills = (s.ground_kills || 0) + (s.air_kills || 0);
const battles = s.total_battles || 0;
const wins = s.wins || 0;
const wrNum = battles > 0 ? (wins / battles) * 100 : 0;
const wr = wrNum.toFixed(1) + '%';
const kdrNum = s.deaths > 0 ? totalKills / s.deaths : totalKills;
const kdr = kdrNum.toFixed(2);
const kpsNum = battles > 0 ? totalKills / battles : 0;
const kps = kpsNum.toFixed(2);
const isCrown = v.vehicle === bestWrVehicle;
const crownClass = isCrown ? ' pdm-crown-card' : '';
const crownIcon = isCrown ? '<i class="fas fa-crown pdm-crown"></i>' : '';
const wrColor = getWinRateColor(wrNum);
const activeVal = mode === 'kdr' ? kdr : kps;
const activeNum = mode === 'kdr' ? kdrNum : kpsNum;
const activeColor = getKDRColor(activeNum);
const activeLabel = mode === 'kdr' ? T('playerModal.kdr') : T('playerModal.kps');
return `<div class="pdm-vehicle-card${crownClass}">
<div class="pdm-vehicle-name" title="${escapeHtml(v.vehicle)}">${crownIcon}${escapeHtml(v.vehicle)}</div>
<div class="pdm-vehicle-stats">
<span>${T('playerModal.battles')}: <span class="pdm-vs-val" style="color:#90EE90">${battles}</span></span>
<span>${T('playerModal.wins')}: <span class="pdm-vs-val" style="color:#90EE90">${wins}</span></span>
<span>${T('playerModal.winRate')}: <span class="pdm-vs-val" style="color:${wrColor}">${wr}</span></span>
<span>${activeLabel}: <span class="pdm-vs-val" style="color:${activeColor}">${activeVal}</span></span>
<span>${T('playerModal.ground')}: <span class="pdm-vs-val" style="color:#90EE90">${s.ground_kills || 0}</span></span>
<span>${T('playerModal.air')}: <span class="pdm-vs-val" style="color:#90EE90">${s.air_kills || 0}</span></span>
<span>${T('playerModal.assists')}: <span class="pdm-vs-val" style="color:#90EE90">${s.assists || 0}</span></span>
<span>${T('playerModal.deaths')}: <span class="pdm-vs-val" style="color:#90EE90">${s.deaths || 0}</span></span>
</div>
</div>`;
}).join('')}</div>`;
}
function renderSessions(gamesData) {
if (!gamesData || !gamesData.games || !gamesData.games.length) {
return `<div class="pdm-no-data"><i class="fas fa-history" style="font-size:2rem;margin-bottom:0.5rem;display:block;"></i>${T('playerModal.noSessionData')}</div>`;
}
const games = gamesData.games
.slice()
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
.slice(0, 50);
const rows = games.map(g => {
const date = new Date(g.timestamp * 1000);
const fmt = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const result = g.result || T('playerModal.unknown');
const isWin = result.toLowerCase() === 'win';
const badgeClass = isWin ? 'pdm-result-win' : 'pdm-result-loss';
return `<tr>
<td>${fmt}</td>
<td>${escapeHtml(g.vehicle || g.vehicle_internal || T('playerModal.unknown'))}</td>
<td>${g.stats.ground_kills || 0}</td>
<td>${g.stats.air_kills || 0}</td>
<td>${g.stats.assists || 0}</td>
<td>${g.stats.deaths || 0}</td>
<td><span class="pdm-result-badge ${badgeClass}">${result}</span></td>
</tr>`;
}).join('');
return `<div class="pdm-sessions-wrap"><table class="pdm-sessions-table">
<thead><tr>
<th>${T('playerModal.date')}</th><th>${T('playerModal.vehicle')}</th><th>${T('playerModal.ground')}</th><th>${T('playerModal.air')}</th><th>${T('playerModal.assists')}</th><th>${T('playerModal.deaths')}</th><th>${T('playerModal.result')}</th>
</tr></thead>
<tbody>${rows}</tbody>
</table></div>`;
}
window.openPlayerDetailsModal = async function (uid, nick) {
injectModal();
const overlay = document.getElementById('pdm-overlay');
const body = document.getElementById('pdm-body');
const nameEl = document.getElementById('pdm-player-name');
const linkEl = document.getElementById('pdm-profile-link');
// Reset state
currentTab = 'overview';
miniChartMetric = 'kdr';
currentKdrKps = 'kdr';
playerDataCache = null;
gamesDataCache = null;
historyDataCache = null;
currentUid = uid;
if (miniChart) {
miniChart.destroy();
miniChart = null;
}
document.querySelectorAll('.pdm-tab').forEach(b => b.classList.remove('active'));
document.querySelector('.pdm-tab[data-tab="overview"]').classList.add('active');
nameEl.textContent = nick || uid;
linkEl.href = '/players/' + encodeURIComponent(uid);
body.innerHTML = `<div class="pdm-loading"><div class="pdm-spinner"></div><span>${T('playerModal.loadingPlayerData')}</span></div>`;
overlay.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Trigger animation
requestAnimationFrame(() => {
overlay.classList.add('pdm-visible');
updateSlider();
});
try {
const [playerData, gamesData] = await Promise.all([
window.apiClient.getPlayer(uid),
window.apiClient.getPlayerGames(uid)
]);
playerDataCache = playerData;
gamesDataCache = gamesData;
renderTab();
} catch (err) {
console.error('[Player Details Modal] Error:', err);
body.innerHTML = `<div class="pdm-error"><i class="fas fa-exclamation-triangle" style="display:block;margin-bottom:0.5rem;"></i>${T('playerModal.failedToLoadPlayerData')}</div>`;
}
};
})();