// 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 = `
${T('playerModal.loadingPlayerData')}
`; 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 = `
${cfg.label}
${T('playerModal.clickToCycle')}
`; const fullRows = stats.slice(0, Math.floor(stats.length / 3) * 3); const remainder = stats.slice(fullRows.length); const cardHtml = s => `
${s[1]}
${s[0]}
`; const lastRow = remainder.length ? `
${remainder.map(cardHtml).join('')}
` : ''; return `
${fullRows.map(cardHtml).join('')}
${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 `
${T('playerModal.noVehicleData')}
`; // 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 = `
KDR / KPS
`; return `${toggleHeader}
${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 ? '' : ''; 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 `
${crownIcon}${escapeHtml(v.vehicle)}
${T('playerModal.battles')}: ${battles} ${T('playerModal.wins')}: ${wins} ${T('playerModal.winRate')}: ${wr} ${activeLabel}: ${activeVal} ${T('playerModal.ground')}: ${s.ground_kills || 0} ${T('playerModal.air')}: ${s.air_kills || 0} ${T('playerModal.assists')}: ${s.assists || 0} ${T('playerModal.deaths')}: ${s.deaths || 0}
`; }).join('')}
`; } function renderSessions(gamesData) { if (!gamesData || !gamesData.games || !gamesData.games.length) { return `
${T('playerModal.noSessionData')}
`; } 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 ` ${fmt} ${escapeHtml(g.vehicle || g.vehicle_internal || T('playerModal.unknown'))} ${g.stats.ground_kills || 0} ${g.stats.air_kills || 0} ${g.stats.assists || 0} ${g.stats.deaths || 0} ${result} `; }).join(''); return `
${rows}
${T('playerModal.date')}${T('playerModal.vehicle')}${T('playerModal.ground')}${T('playerModal.air')}${T('playerModal.assists')}${T('playerModal.deaths')}${T('playerModal.result')}
`; } 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 = `
${T('playerModal.loadingPlayerData')}
`; 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 = `
${T('playerModal.failedToLoadPlayerData')}
`; } }; })();