Files
SREBOT-web/views/player.ejs
T

2811 lines
127 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="<%= lang %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title><%= playerData.nick %> - Player Stats | <%= botName %></title>
<meta name="description" content="View <%= playerData.nick %>'s War Thunder vehicle statistics and performance data.">
<link rel="icon" type="image/png" href="/images/transparent_toothlessssss.png">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link rel="preload" href="/Fonts/symbols_skyquake.ttf" as="font" type="font/ttf" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Tailwind CSS -->
<link rel="stylesheet" href="/css/output.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" media="print" onload="this.media='all'">
<style>
@font-face {
font-family: 'skyquakesymbols';
src: url('/Fonts/symbols_skyquake.ttf') format('truetype');
font-display: swap;
}
body {
background: #1b1b1b;
min-height: 100vh;
}
.text-accent { color: #F5F5DC; }
.text-muted { color: #90EE90; }
.btn-primary {
background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%);
box-shadow: 0 4px 20px rgba(245, 245, 220, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.4);
color: #1E1E1E;
font-weight: 700;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-primary:hover {
box-shadow: 0 8px 25px rgba(245, 245, 220, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.4);
transform: translateY(-2px);
}
.player-container {
max-width: none;
margin: 0 auto;
padding: 6rem 1rem 2rem;
min-height: calc(100vh - 200px);
width: 100%;
}
.player-header {
background: linear-gradient(135deg, rgba(30, 30, 30, 0.8), rgba(40, 40, 40, 0.8));
border-radius: 1rem;
padding: 2rem;
margin-bottom: 2rem;
color: white;
text-align: center;
border: 1px solid rgba(144, 238, 144, 0.1);
}
.player-title-section {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.player-nick {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.player-squadron-tag {
font-family: 'skyquakesymbols', 'Inter', sans-serif !important;
font-size: 2rem;
font-weight: 600;
color: #90EE90;
margin-right: 1rem;
text-shadow: 0 0 15px rgba(144, 238, 144, 0.5);
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
display: inline-block;
}
.player-squadron-tag:hover {
color: #A8E6CF;
text-shadow: 0 0 20px rgba(144, 238, 144, 0.8);
transform: translateY(-2px);
}
.player-squadron-tag:active {
transform: translateY(0);
}
.player-uid {
font-size: 1.1rem;
opacity: 0.8;
margin-bottom: 1rem;
}
.player-stats-summary {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 0.75rem;
margin-top: 1.5rem;
}
@media (max-width: 1200px) {
.player-stats-summary {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.player-stats-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.stats-row-1 {
display: contents;
}
.stats-row-2 {
display: contents;
}
.stat-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
padding: 0.75rem;
text-align: center;
backdrop-filter: blur(10px);
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 78px;
}
.stat-number {
font-size: 1.3rem;
font-weight: 600;
display: block;
}
.stat-label {
font-size: 0.8rem;
opacity: 0.8;
margin-top: 0.2rem;
}
.vehicles-section {
background: rgba(30, 30, 30, 0.6);
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(144, 238, 144, 0.1);
}
.section-title {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.vehicles-table {
width: 100%;
border-collapse: collapse;
background: rgba(30, 30, 30, 0.8);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(144, 238, 144, 0.1);
table-layout: auto;
}
.vehicles-table th {
background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(144, 238, 144, 0.05));
color: #F5F5DC;
padding: 0.75rem;
text-align: center;
font-weight: 600;
border-bottom: 1px solid rgba(144, 238, 144, 0.2);
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 0.5px;
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.vehicles-table th:hover {
background: linear-gradient(135deg, rgba(144, 238, 144, 0.25), rgba(144, 238, 144, 0.1));
text-shadow: 0 0 15px rgba(144, 238, 144, 0.6);
}
.vehicles-table th.sortable::after {
content: '';
font-size: 0.8rem;
opacity: 0.6;
}
.vehicles-table th.sorted-asc::after {
content: ' ↑';
color: #90EE90;
opacity: 1;
}
.vehicles-table th.sorted-desc::after {
content: ' ↓';
color: #90EE90;
opacity: 1;
}
.vehicles-table th:first-child {
text-align: left;
}
.vehicles-table td {
padding: 0.625rem;
border-bottom: 1px solid rgba(144, 238, 144, 0.1);
background: rgba(30, 30, 30, 0.4);
color: #ffffff;
transition: all 0.3s ease;
}
.vehicles-table tr:last-child td {
border-bottom: none;
}
.vehicle-name {
font-family: 'skyquakesymbols', 'Inter', sans-serif !important;
font-weight: 600;
color: #90EE90;
font-size: 1.1rem;
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
white-space: nowrap;
min-width: max-content;
width: auto;
}
.vehicles-table th:first-child,
.vehicles-table td:first-child {
white-space: nowrap;
}
.stat-cell {
text-align: center;
font-weight: 600;
font-size: 1.1rem;
transition: all 0.3s ease;
}
.stat-cell.kills {
color: #ff4757;
text-shadow: 0 0 10px rgba(255, 71, 87, 0.5);
}
.stat-cell.air-kills {
color: #3742fa;
text-shadow: 0 0 10px rgba(55, 66, 250, 0.5);
}
.stat-cell.assists {
color: #ffa502;
text-shadow: 0 0 10px rgba(255, 165, 2, 0.5);
}
.stat-cell.captures {
color: #90EE90;
text-shadow: 0 0 10px rgba(144, 238, 144, 0.5);
}
.stat-cell.kdr {
font-weight: 700;
}
.stat-cell.deaths {
color: #ff3838;
text-shadow: 0 0 10px rgba(255, 56, 56, 0.5);
}
.stat-cell.battles {
color: #a855f7;
text-shadow: 0 0 10px rgba(168, 85, 247, 0.5);
}
.stat-cell.wins {
color: #10b981;
text-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
}
.stat-cell.win-rate {
color: #f59e0b;
text-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
font-weight: 700;
}
.no-vehicles {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
}
.back-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
margin-bottom: 2rem;
transition: color 0.2s;
}
.back-link:hover {
color: var(--accent-color);
}
.responsive-table {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
width: 100%;
border-radius: 0.75rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
/* View Switcher Styles */
.section-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
}
.section-header-row .section-title {
margin-bottom: 0;
}
.view-toggle-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);
}
.view-toggle-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;
}
.view-toggle {
position: relative;
z-index: 1;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.45);
padding: 0.35rem 1.1rem;
border-radius: calc(2rem - 2px);
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
transition: color 0.25s ease;
white-space: nowrap;
}
.view-toggle.active {
color: #1b1b1b;
}
.view-toggle:hover:not(.active) {
color: rgba(255, 255, 255, 0.8);
}
.player-chart-section {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(144, 238, 144, 0.12);
}
.chart-collapse {
display: flex;
justify-content: center;
align-items: center;
margin-top: 0.75rem;
cursor: pointer;
color: rgba(144, 238, 144, 0.35);
transition: color 0.2s;
padding: 0.2rem;
}
.chart-collapse:hover {
color: rgba(144, 238, 144, 0.75);
}
/* Games Section Styles */
.games-section {
background: rgba(30, 30, 30, 0.6);
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(144, 238, 144, 0.2);
}
.games-loading, .games-error, .no-games {
text-align: center;
padding: 3rem;
color: #ffffff;
}
.games-loading i {
font-size: 3rem;
color: #90EE90;
margin-bottom: 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.games-stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
padding: 1.5rem;
background: rgba(30, 30, 30, 0.8);
border-radius: 0.75rem;
border: 1px solid rgba(144, 238, 144, 0.1);
}
.games-stat-card {
text-align: center;
padding: 1rem;
background: rgba(144, 238, 144, 0.05);
border-radius: 0.5rem;
border: 1px solid rgba(144, 238, 144, 0.1);
}
.games-stat-number {
font-size: 1.5rem;
font-weight: 700;
color: #90EE90;
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
display: block;
}
.games-stat-label {
font-size: 0.9rem;
color: #ffffff;
opacity: 0.8;
margin-top: 0.25rem;
}
.games-table {
width: 100%;
min-width: 1000px;
border-collapse: collapse;
background: rgba(30, 30, 30, 0.8);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(144, 238, 144, 0.1);
table-layout: fixed;
}
.games-table tr[data-session-id]:hover {
background: rgba(144, 238, 144, 0.08);
}
.games-table th {
background: linear-gradient(135deg, rgba(144, 238, 144, 0.2), rgba(0, 255, 107, 0.2));
color: #90EE90;
padding: 0.75rem;
text-align: center;
font-weight: 600;
border-bottom: 1px solid rgba(144, 238, 144, 0.2);
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 0.5px;
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
cursor: pointer;
transition: all 0.3s ease;
}
.games-table th:hover {
background: linear-gradient(135deg, rgba(144, 238, 144, 0.3), rgba(0, 255, 107, 0.3));
text-shadow: 0 0 15px rgba(144, 238, 144, 0.6);
}
.games-table th.sortable::after {
content: '';
font-size: 0.8rem;
opacity: 0.6;
}
.games-table th.sorted-asc::after {
content: ' ↑';
color: #90EE90;
opacity: 1;
}
.games-table th.sorted-desc::after {
content: ' ↓';
color: #90EE90;
opacity: 1;
}
.games-table th:first-child {
text-align: left;
}
/* Games table column widths */
.games-table th:nth-child(1),
.games-table td:nth-child(1) {
width: 15%;
}
.games-table th:nth-child(2),
.games-table td:nth-child(2) {
width: 25%;
}
.games-table th:nth-child(3),
.games-table td:nth-child(3) {
width: 12%;
}
.games-table th:nth-child(4),
.games-table td:nth-child(4) {
width: 12%;
}
.games-table th:nth-child(5),
.games-table td:nth-child(5) {
width: 10%;
}
.games-table th:nth-child(6),
.games-table td:nth-child(6) {
width: 12%;
}
.games-table th:nth-child(7),
.games-table td:nth-child(7) {
width: 8%;
}
.games-table th:nth-child(8),
.games-table td:nth-child(8) {
width: 10%;
}
.games-table td {
padding: 0.625rem;
border-bottom: 1px solid rgba(144, 238, 144, 0.1);
background: rgba(30, 30, 30, 0.4);
color: #ffffff;
transition: all 0.3s ease;
text-align: center;
}
.games-table td:first-child {
text-align: left;
}
.games-table tr:last-child td {
border-bottom: none;
}
.games-table tr:hover td {
background: rgba(144, 238, 144, 0.05);
}
.game-date {
font-weight: 600;
color: #90EE90;
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
}
.game-vehicle {
font-family: 'skyquakesymbols', 'Inter', sans-serif !important;
font-weight: 600;
color: #A8E6CF;
text-shadow: 0 0 10px rgba(0, 255, 107, 0.3);
}
.mobile-vehicle-name {
font-family: 'skyquakesymbols', 'Inter', sans-serif !important;
}
.game-result {
font-weight: 700;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.9rem;
}
.game-result.win {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.game-result.loss {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.game-result.draw {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
border: 1px solid rgba(245, 158, 11, 0.3);
}
/* Date Filters Styles */
.games-filters,
.vehicles-filters {
background: rgba(30, 30, 30, 0.9);
border-radius: 1rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid rgba(144, 238, 144, 0.15);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
position: relative;
}
/* Primary filter gets special styling */
.filter-group:first-child {
padding-right: 1rem;
border-right: 1px solid rgba(144, 238, 144, 0.2);
}
.filter-group label {
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
white-space: nowrap;
font-size: 0.9rem;
}
.filter-group select,
.filter-group input[type="date"],
.filter-group input[type="text"] {
background: rgba(30, 30, 30, 0.9);
border: 1px solid rgba(144, 238, 144, 0.25);
border-radius: 0.5rem;
padding: 0.6rem 0.9rem;
color: #ffffff;
font-size: 0.9rem;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 150px;
cursor: pointer;
}
/* Vehicle filters echo back text containing skyquake glyphs. */
#cumulative-vehicle-search, #vehicle-search {
font-family: 'skyquakesymbols', 'Inter', sans-serif !important;
}
.filter-group select:hover,
.filter-group input[type="date"]:hover,
.filter-group input[type="text"]:hover {
border-color: rgba(144, 238, 144, 0.4);
background: rgba(30, 30, 30, 1);
}
.filter-group select:focus,
.filter-group input[type="date"]:focus,
.filter-group input[type="text"]:focus {
outline: none;
border-color: #90EE90;
box-shadow: 0 0 15px rgba(144, 238, 144, 0.3), 0 0 0 3px rgba(144, 238, 144, 0.1);
transform: translateY(-1px);
}
.filter-group input[type="text"]::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.filter-group select option {
background: rgba(30, 30, 30, 0.98);
color: #ffffff;
padding: 0.5rem;
}
.filter-reset-btn {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.15));
border: 1px solid rgba(239, 68, 68, 0.4);
color: #ff6b6b;
padding: 0.6rem 1.2rem;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-reset-btn:hover {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(220, 38, 38, 0.25));
border-color: #ef4444;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.filter-reset-btn:active {
transform: translateY(0);
}
.filter-results {
margin-left: auto;
color: #90EE90;
font-weight: 700;
font-size: 1rem;
text-shadow: 0 0 15px rgba(144, 238, 144, 0.4);
padding: 0.5rem 1rem;
background: rgba(144, 238, 144, 0.08);
border-radius: 0.5rem;
border: 1px solid rgba(144, 238, 144, 0.3);
}
.custom-range,
.specific-date {
padding: 0.5rem;
background: rgba(144, 238, 144, 0.05);
border-radius: 0.5rem;
border: 1px solid rgba(144, 238, 144, 0.1);
}
.games-table tr.filtered-out {
display: none;
}
.chart-toggle-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);
}
.chart-toggle-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;
}
.chart-toggle {
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;
}
.chart-toggle.active {
color: #1b1b1b;
}
.chart-toggle:hover:not(.active) {
color: rgba(255, 255, 255, 0.8);
}
.season-recap-modal.hidden { display: none; }
.season-recap-modal { position: fixed; inset: 0; z-index: 1000; overflow-y: auto; }
.season-recap-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); }
.season-recap-content {
position: relative;
width: min(92vw, 1100px);
margin: 2vh auto;
background: #1a1f2e;
color: #e2e8f0;
padding: 0.85rem 1.25rem 1rem;
border-radius: 0.75rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.season-recap-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.6rem; }
.season-recap-header h2 { margin: 0; font-size: 1.2rem; }
.season-recap-close { background: transparent; border: 0; color: #e2e8f0; font-size: 1.75rem; cursor: pointer; line-height: 1; }
.season-recap-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem 1.25rem; margin-bottom: 0.5rem; }
.season-recap-controls > div { display: flex; flex-direction: column; }
.season-recap-body label { display: block; margin-bottom: 0.15rem; color: #94a3b8; font-size: 0.85rem; }
.season-recap-theme-group { display: inline-flex; gap: 0.5rem; font-size: 0.9rem; }
.season-recap-theme-group label { margin: 0; color: #e2e8f0; font-size: 0.9rem; cursor: pointer; }
#season-recap-btn .fa-image { margin-right: 0.45rem; }
.season-recap-body select {
padding: 0.4rem 0.6rem;
border-radius: 0.35rem;
margin-right: 0.5rem;
background: #0f172a;
color: #e2e8f0;
border: 1px solid rgba(144,238,144,0.3);
min-width: 12rem;
font: inherit;
}
.season-recap-body select option { background: #0f172a; color: #e2e8f0; }
#season-recap-status { margin: 0.75rem 0; color: #94a3b8; font-size: 0.9rem; }
#season-recap-image-wrap { margin-top: 0.75rem; }
#season-recap-image {
display: none;
max-width: 100%;
max-height: 75vh;
object-fit: contain;
border-radius: 0.35rem;
}
.player-empty-state {
margin-top: 1rem;
padding: 0.9rem 1rem;
border-radius: 0.75rem;
border: 1px solid rgba(255, 193, 7, 0.35);
background: linear-gradient(135deg, rgba(255, 193, 7, 0.14), rgba(255, 193, 7, 0.06));
color: #fde68a;
font-weight: 600;
text-align: center;
}
</style>
</head>
<body class="text-white antialiased">
<%- include('partials/nav', { activePage: '' }) %>
<div class="player-container">
<a href="/" class="back-link">
<i class="fas fa-arrow-left"></i>
Back to Home
</a>
<div class="player-header">
<div class="player-title-section">
<% if (playerData.squadron_name) { %>
<%
const _sqHref = playerData.squadron_clan_id != null
? `/squadrons/${playerData.squadron_clan_id}`
: `/squadrons/${encodeURIComponent(playerData.squadron_long_name || playerData.squadron_name)}`;
%>
<a href="<%= _sqHref %>" class="player-squadron-tag" title="View <%= playerData.squadron_long_name || playerData.squadron_name %> squadron profile">
<%= playerData.squadron_name %>
</a>
<% } %>
<h1 class="player-nick"><%= playerData.nick %></h1>
</div>
<div class="player-uid"><%= t('player.uidLabel') %>: <%= playerData.uid %></div>
<div style="font-size: 0.9rem; opacity: 0.8; margin-top: 0.5rem; font-style: italic;">
<i class="fas fa-info-circle" style="margin-right: 0.5rem;"></i>
<%= t('common.recordingSince') %>
</div>
<% if (playerData.no_stats_yet) { %>
<div class="player-empty-state">No stats yet for this player</div>
<% } %>
<div class="player-stats-summary">
<div class="stat-card">
<span class="stat-number" id="stat-total-battles"><%= totals.totalBattles %></span>
<div class="stat-label"><%= t('player.totalBattles') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-total-wins"><%= totals.totalWins %></span>
<div class="stat-label"><%= t('player.totalWins') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-win-rate"><%= totals.overallWinRate %></span>
<div class="stat-label"><%= t('common.winRate') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-total-kills"><%= totals.totalKills %></span>
<div class="stat-label"><%= t('common.totalKills') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-kdr"><%= totals.overallKDR %></span>
<div class="stat-label"><%= t('common.kdr') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-kps"><%= totals.totalBattles > 0 ? (totals.totalKills / totals.totalBattles).toFixed(2) : '0.00' %></span>
<div class="stat-label"><%= t('common.kps') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-air-kills"><%= totals.totalAirKills %></span>
<div class="stat-label"><%= t('common.airKills') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-ground-kills"><%= totals.totalGroundKills || (totals.totalKills - totals.totalAirKills) %></span>
<div class="stat-label"><%= t('common.groundKills') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-assists"><%= totals.totalAssists %></span>
<div class="stat-label"><%= t('common.assists') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-deaths"><%= totals.totalDeaths %></span>
<div class="stat-label"><%= t('common.deaths') %></div>
</div>
<div class="stat-card">
<span class="stat-number" id="stat-captures"><%= totals.totalCaptures %></span>
<div class="stat-label"><%= t('common.captures') %></div>
</div>
<div class="stat-card" style="visibility: hidden;"></div>
</div>
<div style="margin-top: 1rem;">
<button type="button" id="season-recap-btn" class="btn btn-primary" <% if (!playerData.uid) { %>disabled title="<%= t('seasonCard.buttonDisabledTitlePlayer') %>"<% } %>>
<i class="fas fa-image"></i> <%= t('seasonCard.buttonLabelPlayer') %>
</button>
</div>
</div>
<div class="player-chart-section" id="playerChartSection" data-uid="<%= playerData.uid %>">
<div style="display:flex;justify-content:center;margin-bottom:1rem;" id="playerChartTogglesWrap">
<div class="chart-toggle-group" id="playerChartToggles">
<div class="chart-toggle-slider" id="playerChartSlider"></div>
<button class="chart-toggle" data-metric="win_rate"><%= t('common.winRate') %></button>
<button class="chart-toggle active" data-metric="kdr"><%= t('common.kdr') %></button>
<button class="chart-toggle" data-metric="battles"><%= t('common.battles') %></button>
</div>
</div>
<div style="position:relative;height:220px;" id="playerChartWrapper">
<div id="timeline-loading" style="display:flex;align-items:center;justify-content:center;height:220px;color:rgba(255,255,255,0.35);font-size:0.88rem;"><%= t('player.loadingTimeline') %></div>
<canvas id="playerChart" style="display:none;"></canvas>
<div id="timeline-error" style="display:none;text-align:center;padding:3rem;color:rgba(255,255,255,0.4);"><%= t('player.timelineUnavailable') %></div>
<div id="timeline-empty" style="display:none;text-align:center;padding:3rem;color:rgba(255,255,255,0.4);"><%= t('player.noTimelineData') %></div>
</div>
<div style="display:flex;justify-content:center;margin-top:0.5rem;">
<div class="chart-toggle-group" id="chartScaleToggles" style="margin:0;">
<div class="chart-toggle-slider" id="chartScaleSlider"></div>
<button class="chart-toggle active" data-scale="alltime"><%= t('player.allTime') %></button>
<button class="chart-toggle" data-scale="relative"><%= t('player.relative') %></button>
</div>
</div>
</div>
<div class="section-header-row" style="margin-top:1.5rem;">
<h2 class="section-title" id="viewSectionTitle"><%= t('player.vehicleStatistics') %></h2>
<div class="view-toggle-group" id="viewToggles">
<div class="view-toggle-slider" id="viewToggleSlider"></div>
<button class="view-toggle active" data-page="cumulative"><%= t('player.cumulative') %></button>
<button class="view-toggle" data-page="games"><%= t('player.individual') %></button>
</div>
</div>
<div class="player-views-wrapper">
<div id="cumulative-page">
<div class="vehicles-section">
<div class="vehicles-filters">
<div class="filter-group">
<label for="cumulative-filter-category" title="Choose how you want to filter vehicles">
<i class="fas fa-filter" style="margin-right: 0.3rem;"></i>Filter by:
</label>
<select id="cumulative-filter-category" onchange="updateCumulativeFilterCategory()" title="Select All Time, Date, Season, Week, or Session filtering">
<option value="all"><%= t('player.allTime') %></option>
<option value="date"><%= t('player.dateRange') %></option>
<option value="season"><%= t('player.season') %></option>
<option value="week"><%= t('player.week') %></option>
<option value="session"><%= t('player.session') %></option>
</select>
</div>
<!-- Date sub-filters -->
<div class="filter-group" id="cumulative-date-type-filter" style="display: none;">
<label for="cumulative-date-filter-type" title="Choose a date range">
<i class="fas fa-calendar-alt" style="margin-right: 0.3rem;"></i>Date Type:
</label>
<select id="cumulative-date-filter-type" onchange="handleCumulativeDateTypeChange()" title="Select a predefined or custom date range">
<option value="last7"><%= t('player.last7Days') %></option>
<option value="last30"><%= t('player.last30Days') %></option>
<option value="last90"><%= t('player.last90Days') %></option>
<option value="custom"><%= t('player.customRange') %></option>
</select>
</div>
<div class="filter-group custom-range" id="cumulative-custom-range-filters" style="display: none;">
<label for="cumulative-start-date"><%= t('player.from') %></label>
<input type="date" id="cumulative-start-date" onchange="applyCumulativeFilters()">
<label for="cumulative-end-date"><%= t('player.to') %></label>
<input type="date" id="cumulative-end-date" onchange="applyCumulativeFilters()">
</div>
<!-- Season filter -->
<div class="filter-group season-select-filter" id="cumulative-season-select-filter" style="display: none;">
<label for="cumulative-season"><%= t('player.selectSeason') %></label>
<select id="cumulative-season" onchange="applyCumulativeFilters()">
<option value=""><%= t('player.pleaseSelect') %></option>
</select>
</div>
<!-- Week filters -->
<div class="filter-group week-select-filter" id="cumulative-week-season-filter" style="display: none;">
<label for="cumulative-week-season-select"><%= t('player.selectSeason') %></label>
<select id="cumulative-week-season-select" onchange="updateCumulativeWeekOptions()">
<option value=""><%= t('player.pleaseSelect') %></option>
</select>
</div>
<div class="filter-group week-select-filter" id="cumulative-week-select-filter" style="display: none;">
<label for="cumulative-week"><%= t('player.selectWeek') %></label>
<select id="cumulative-week" onchange="onCumulativeWeekChange()">
<option value=""><%= t('player.pleaseSelect') %></option>
</select>
</div>
<!-- Session filter -->
<div class="filter-group" id="cumulative-session-filter" style="display: none;">
<label for="cumulative-session"><%= t('player.session') %>:</label>
<select id="cumulative-session" onchange="applyCumulativeFilters()">
<option value=""><%= t('player.pleaseSelect') %></option>
</select>
</div>
<!-- Vehicle search -->
<div class="filter-group">
<label for="cumulative-vehicle-search" title="Search for specific vehicles">
<i class="fas fa-fighter-jet" style="margin-right: 0.3rem;"></i>Vehicle:
</label>
<input type="text" id="cumulative-vehicle-search" placeholder="<%= t('player.searchVehicles') %>" onkeyup="filterCumulativeVehicles()" oninput="filterCumulativeVehicles()" title="Type to filter by vehicle name">
</div>
<div class="filter-group" id="cumulative-reset-btn-group" style="display:none;">
<button class="filter-reset-btn" onclick="resetCumulativeFilters()" title="Clear all filters and show all vehicles">
<i class="fas fa-times-circle"></i> Reset Filters
</button>
</div>
<div class="filter-results" id="cumulative-filter-results">
<span id="cumulative-filtered-count"><%= playerData.vehicles ? playerData.vehicles.length : 0 %></span> <%= t('player.vehiclesShown') %>
</div>
</div>
<div id="vehicles-loading" style="display:none; justify-content:center; align-items:center; padding: 3rem; color: rgba(255,255,255,0.5); gap: 0.5rem;">
<i class="fas fa-spinner fa-spin"></i> <%= t('common.loading') %>
</div>
<div class="mobile-view-toggle" style="display: none; margin-bottom: 1rem; text-align: center;">
<button class="btn btn-secondary" id="toggleViewBtn" onclick="toggleMobileView()" style="font-size: 0.9rem; padding: 0.75rem 1.5rem;">
<i class="fas fa-th-list"></i> <%= t('player.switchToCards') %>
</button>
</div>
<div class="mobile-card-view" id="mobileCardView">
</div>
<div class="responsive-table" id="tableView"<% if (!playerData.vehicles || playerData.vehicles.length === 0) { %> style="display:none;"<% } %>>
<table class="vehicles-table">
<thead>
<tr>
<th onclick="sortTable(0, 'string')" class="sortable"><%= t('common.vehicle') %></th>
<th onclick="sortTable(1, 'number')" class="sortable"><%= t('common.battles') %></th>
<th onclick="sortTable(2, 'number')" class="sortable"><%= t('common.wins') %></th>
<th onclick="sortTable(3, 'number')" class="sortable"><%= t('common.winRate') %></th>
<th onclick="sortTable(4, 'number')" class="sortable"><%= t('common.kdr') %></th>
<th onclick="sortTable(5, 'number')" class="sortable"><%= t('common.kps') %></th>
<th onclick="sortTable(6, 'number')" class="sortable"><%= t('common.groundKills') %></th>
<th onclick="sortTable(7, 'number')" class="sortable"><%= t('common.airKills') %></th>
<th onclick="sortTable(8, 'number')" class="sortable"><%= t('common.assists') %></th>
<th onclick="sortTable(9, 'number')" class="sortable"><%= t('common.captures') %></th>
<th onclick="sortTable(10, 'number')" class="sortable"><%= t('common.deaths') %></th>
</tr>
</thead>
<tbody>
<% if (playerData.vehicles) { playerData.vehicles.filter(v => v.vehicle !== 'DISCONNECTED').forEach(vehicle => {
const totalKills = vehicle.stats.ground_kills + vehicle.stats.air_kills;
const kdrNum = vehicle.stats.deaths > 0 ? totalKills / vehicle.stats.deaths : totalKills;
const kdr = kdrNum.toFixed(2);
const kdrColor = kdrNum >= 3 ? '#90EE90' : kdrNum >= 2 ? '#A8E6CF' : kdrNum >= 1.5 ? '#FFD700' : kdrNum >= 1 ? '#FFA500' : '#FF6B6B';
const battles = vehicle.stats.total_battles || 0;
const wins = vehicle.stats.wins || 0;
const winRate = battles > 0 ? ((wins / battles) * 100).toFixed(1) + '%' : '0.0%';
const kps = battles > 0 ? (totalKills / battles).toFixed(2) : '0.00';
%>
<tr>
<td class="vehicle-name" data-vehicle-internal="<%= vehicle.vehicle_internal || '' %>"><%= vehicle.vehicle %></td>
<td class="stat-cell battles"><%= battles %></td>
<td class="stat-cell wins"><%= wins %></td>
<td class="stat-cell win-rate"><%= winRate %></td>
<td class="stat-cell kdr" style="color: <%= kdrColor %>; text-shadow: 0 0 10px <%= kdrColor %>40;"><%= kdr %></td>
<td class="stat-cell kps"><%= kps %></td>
<td class="stat-cell kills"><%= vehicle.stats.ground_kills %></td>
<td class="stat-cell air-kills"><%= vehicle.stats.air_kills %></td>
<td class="stat-cell assists"><%= vehicle.stats.assists %></td>
<td class="stat-cell captures"><%= vehicle.stats.captures %></td>
<td class="stat-cell deaths"><%= vehicle.stats.deaths %></td>
</tr>
<% }); } %>
</tbody>
</table>
</div>
<div class="no-vehicles" id="no-vehicles-msg"<% if (playerData.vehicles && playerData.vehicles.length > 0) { %> style="display:none;"<% } %>>
<i class="fas fa-tank" style="font-size: 3rem; color: #d1d5db; margin-bottom: 1rem;"></i>
<h3><%= t('player.noVehicleData') %></h3>
<p id="no-vehicles-text"><%= t('player.noVehiclesForRange') %></p>
</div>
</div>
</div>
<div id="games-page" style="display: none;">
<div class="games-section">
<div class="games-loading" id="games-loading">
<i class="fas fa-spinner fa-spin"></i>
<p><%= t('player.loadingGameRecords') %></p>
</div>
<div class="games-content" id="games-content" style="display: none;">
<div class="games-filters">
<div class="filter-group">
<label for="date-filter-category" title="Choose how you want to filter games">
<i class="fas fa-filter" style="margin-right: 0.3rem;"></i><%= t('player.filterBy') %>
</label>
<select id="date-filter-category" onchange="updateFilterCategory()" title="Select All Time, Date, or Season filtering">
<option value="all"><%= t('player.allTime') %></option>
<option value="date"><%= t('player.dateRange') %></option>
<option value="season"><%= t('player.season') %> / <%= t('player.week') %></option>
</select>
</div>
<!-- Date sub-filters -->
<div class="filter-group" id="date-type-filter" style="display: none;">
<label for="date-filter-type" title="Choose a date range">
<i class="fas fa-calendar-alt" style="margin-right: 0.3rem;"></i><%= t('player.dateType') %>
</label>
<select id="date-filter-type" onchange="updateDateFilters()" title="Select a predefined or custom date range">
<option value="last7"><%= t('player.last7Days') %></option>
<option value="last30"><%= t('player.last30Days') %></option>
<option value="last90"><%= t('player.last90Days') %></option>
<option value="custom"><%= t('player.customRange') %></option>
<option value="specific"><%= t('player.specificDate') %></option>
</select>
</div>
<!-- Season sub-filters -->
<div class="filter-group" id="season-type-filter" style="display: none;">
<label for="season-filter-type" title="Filter by season or specific week">
<i class="fas fa-trophy" style="margin-right: 0.3rem;"></i><%= t('player.filterType') %>
</label>
<select id="season-filter-type" onchange="updateSeasonFilters()" title="View entire season or specific week stats">
<option value="full-season"><%= t('player.fullSeason') %></option>
<option value="week"><%= t('player.specificWeek') %></option>
</select>
</div>
<div class="filter-group season-select-filter" id="season-select-filter" style="display: none;">
<label for="season-select"><%= t('player.selectSeason') %></label>
<select id="season-select" onchange="applyFilters()">
<option value=""><%= t('common.loading') %></option>
</select>
</div>
<div class="filter-group week-filters" id="week-filters" style="display: none;">
<label for="week-season-select"><%= t('player.selectSeason') %></label>
<select id="week-season-select" onchange="updateWeekOptions()">
<option value=""><%= t('common.loading') %></option>
</select>
<label for="week-select"><%= t('player.selectWeek') %></label>
<select id="week-select" onchange="applyFilters()">
<option value=""><%= t('player.selectSeasonFirst') %></option>
</select>
</div>
<div class="filter-group custom-range" id="custom-range-filters" style="display: none;">
<label for="start-date"><%= t('player.from') %></label>
<input type="date" id="start-date" onchange="applyFilters()">
<label for="end-date"><%= t('player.to') %></label>
<input type="date" id="end-date" onchange="applyFilters()">
</div>
<div class="filter-group specific-date" id="specific-date-filter" style="display: none;">
<label for="specific-date"><%= t('common.date') %>:</label>
<input type="date" id="specific-date" onchange="applyFilters()">
</div>
<div class="filter-group">
<label for="vehicle-search" title="Search for specific vehicles">
<i class="fas fa-fighter-jet" style="margin-right: 0.3rem;"></i><%= t('common.vehicle') %>:
</label>
<input type="text" id="vehicle-search" placeholder="<%= t('player.searchVehicles') %>" onkeyup="applyFilters()" oninput="applyFilters()" title="Type to filter by vehicle name">
</div>
<div class="filter-group" id="games-reset-btn-group" style="display:none;">
<button class="filter-reset-btn" onclick="resetAllFilters()" title="Clear all filters and show all games">
<i class="fas fa-times-circle"></i> <%= t('player.resetFilters') %>
</button>
</div>
<div class="filter-results" id="filter-results">
<span id="filtered-count">0</span> <%= t('player.gamesShown') %>
</div>
</div>
<div class="games-stats-summary" id="games-stats-summary"></div>
<div class="responsive-table">
<table class="games-table" id="games-table">
<thead>
<tr>
<th onclick="sortGamesTable(0, 'string')" class="sortable"><%= t('common.date') %></th>
<th onclick="sortGamesTable(1, 'string')" class="sortable"><%= t('common.vehicle') %></th>
<th onclick="sortGamesTable(2, 'number')" class="sortable"><%= t('common.groundKills') %></th>
<th onclick="sortGamesTable(3, 'number')" class="sortable"><%= t('common.airKills') %></th>
<th onclick="sortGamesTable(4, 'number')" class="sortable"><%= t('common.assists') %></th>
<th onclick="sortGamesTable(5, 'number')" class="sortable"><%= t('common.captures') %></th>
<th onclick="sortGamesTable(6, 'number')" class="sortable"><%= t('common.deaths') %></th>
<th onclick="sortGamesTable(7, 'string')" class="sortable"><%= t('common.result') %></th>
</tr>
</thead>
<tbody id="games-table-body">
</tbody>
</table>
</div>
</div>
<div class="games-error" id="games-error" style="display: none;">
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; color: #f59e0b; margin-bottom: 1rem;"></i>
<h3><%= t('player.unableToLoadRecords') %></h3>
<p id="games-error-message"><%= t('player.failedToFetch') %></p>
<button class="btn btn-primary" onclick="retryLoadGamesData()"><%= t('common.retry') %></button>
</div>
<div class="no-games" id="no-games" style="display: none;">
<i class="fas fa-gamepad" style="font-size: 3rem; color: #d1d5db; margin-bottom: 1rem;"></i>
<h3><%= t('player.noGameRecords') %></h3>
<p><%= t('player.noGamesYet') %></p>
</div>
</div>
</div>
</div><!-- end player-views-wrapper -->
</div>
<!-- Footer -->
<%- include('partials/footer') %>
<script src="/js/chart.umd.min.js"></script>
<script>
window.__lang = '<%= lang %>';
window.__i18n = <%- localeJson %>;
window.__t = function(key) {
var parts = key.split('.'), obj = window.__i18n;
for (var i = 0; i < parts.length; i++) { obj = obj && obj[parts[i]]; }
return obj !== undefined ? obj : key;
};
window.switchLanguage = function(lang) {
var next = lang || (document.documentElement.lang === 'en' ? 'ru' : 'en');
if (next === document.documentElement.lang) return;
document.cookie = 'lang=' + next + ';path=/;max-age=31536000;SameSite=Lax';
window.location.reload();
};
</script>
<script src="/js/main.js?v=3"></script>
<script src="/js/api-client.js"></script>
<script src="/js/vehicle-i18n.js"></script>
<script src="/js/header-search.js?v=2"></script>
<script src="/js/seasons-filter.js"></script>
<script>
</script>
<script>
let currentSort = { column: -1, direction: 'asc' };
function sortTable(columnIndex, dataType) {
const table = document.querySelector('.vehicles-table');
const tbody = table.querySelector('tbody');
const headers = table.querySelectorAll('th');
const rows = Array.from(tbody.querySelectorAll('tr'));
headers.forEach(header => {
header.classList.remove('sorted-asc', 'sorted-desc');
});
let direction = 'asc';
if (currentSort.column === columnIndex && currentSort.direction === 'asc') {
direction = 'desc';
}
rows.sort((a, b) => {
let valueA, valueB;
if (dataType === 'number') {
valueA = parseFloat(a.cells[columnIndex].textContent) || 0;
valueB = parseFloat(b.cells[columnIndex].textContent) || 0;
} else {
valueA = a.cells[columnIndex].textContent.trim().toLowerCase();
valueB = b.cells[columnIndex].textContent.trim().toLowerCase();
}
if (direction === 'asc') {
return valueA > valueB ? 1 : valueA < valueB ? -1 : 0;
} else {
return valueA < valueB ? 1 : valueA > valueB ? -1 : 0;
}
});
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
headers[columnIndex].classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc');
currentSort = { column: columnIndex, direction: direction };
}
let isMobileView = false;
function toggleMobileView() {
const tableView = document.getElementById('tableView');
const mobileCardView = document.getElementById('mobileCardView');
const toggleBtn = document.getElementById('toggleViewBtn');
isMobileView = !isMobileView;
if (isMobileView) {
tableView.style.display = 'none';
mobileCardView.style.display = 'block';
toggleBtn.innerHTML = '<i class="fas fa-table"></i> ' + __t('player.switchToTable');
populateMobileCards();
} else {
tableView.style.display = 'block';
mobileCardView.style.display = 'none';
toggleBtn.innerHTML = '<i class="fas fa-th-list"></i> ' + __t('player.switchToCards');
}
}
function populateMobileCards() {
const table = document.querySelector('.vehicles-table tbody');
const mobileCardView = document.getElementById('mobileCardView');
if (!table || !mobileCardView) return;
const rows = Array.from(table.querySelectorAll('tr'));
const visibleRows = rows.filter(row => !row.classList.contains('filtered-out') && row.style.display !== 'none');
mobileCardView.innerHTML = '';
visibleRows.forEach(row => {
const cells = row.cells;
if (cells.length >= 10) {
const card = document.createElement('div');
card.className = 'mobile-vehicle-card';
const vehicleInternal = cells[0].getAttribute('data-vehicle-internal') || '';
const vehicleName = cells[0].textContent;
const battles = cells[1].textContent;
const wins = cells[2].textContent;
const winRate = cells[3].textContent;
const kdr = cells[4].textContent;
const kps = cells[5].textContent;
const groundKills = cells[6].textContent;
const airKills = cells[7].textContent;
const assists = cells[8].textContent;
const captures = cells[9].textContent;
const deaths = cells[10].textContent;
card.innerHTML = `
<div class="mobile-vehicle-header">
<div class="mobile-vehicle-name" data-vehicle-internal="${vehicleInternal}">${vehicleName}</div>
<div class="mobile-vehicle-battles">${battles} battles</div>
</div>
<div class="mobile-stats-grid">
<div class="mobile-stat-item">
<span class="mobile-stat-value">${wins}</span>
<div class="mobile-stat-label"><%= t('common.wins') %></div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${winRate}</span>
<div class="mobile-stat-label"><%= t('common.winRate') %></div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${kdr}</span>
<div class="mobile-stat-label">K/D Ratio</div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${kps}</span>
<div class="mobile-stat-label"><%= t('common.kps') %></div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${groundKills}</span>
<div class="mobile-stat-label"><%= t('common.groundKills') %></div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${airKills}</span>
<div class="mobile-stat-label"><%= t('common.airKills') %></div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${assists}</span>
<div class="mobile-stat-label"><%= t('common.assists') %></div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${captures}</span>
<div class="mobile-stat-label"><%= t('common.captures') %></div>
</div>
<div class="mobile-stat-item">
<span class="mobile-stat-value">${deaths}</span>
<div class="mobile-stat-label"><%= t('common.deaths') %></div>
</div>
</div>
`;
mobileCardView.appendChild(card);
}
});
}
function checkMobileToggle() {
const toggleContainer = document.querySelector('.mobile-view-toggle');
if (window.innerWidth <= 768 && toggleContainer) {
toggleContainer.style.display = 'block';
} else if (toggleContainer) {
toggleContainer.style.display = 'none';
if (isMobileView) {
toggleMobileView();
}
}
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize seasons filter on page load for cumulative stats
// Note: restoreCumulativeFilterState() is called INSIDE initializeSeasonsFilter()
// after seasons data is loaded
initializeSeasonsFilter();
const table = document.querySelector('.vehicles-table');
if (table) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
let groundKillsTotal = 0;
let airKillsTotal = 0;
let battlesTotal = 0;
rows.forEach(row => {
battlesTotal += parseFloat(row.cells[1].textContent) || 0;
groundKillsTotal += parseFloat(row.cells[6].textContent) || 0;
airKillsTotal += parseFloat(row.cells[7].textContent) || 0;
});
if (battlesTotal > 0) {
currentSort = { column: 1, direction: 'asc' };
sortTable(1, 'number');
} else if (groundKillsTotal >= airKillsTotal) {
currentSort = { column: 6, direction: 'asc' };
sortTable(6, 'number');
} else {
currentSort = { column: 7, direction: 'asc' };
sortTable(7, 'number');
}
}
initializePageNavigation();
// Games data loads on-demand when the games tab is clicked
checkMobileToggle();
window.addEventListener('resize', checkMobileToggle);
});
function initializePageNavigation() {
document.getElementById('viewToggles').addEventListener('click', (e) => {
const btn = e.target.closest('.view-toggle');
if (!btn) return;
switchToPage(btn.dataset.page);
});
requestAnimationFrame(() => {
const activeBtn = document.querySelector('.view-toggle.active');
if (activeBtn) updateViewSlider(activeBtn);
});
}
function updateViewSlider(btn) {
const slider = document.getElementById('viewToggleSlider');
const group = document.getElementById('viewToggles');
const btnRect = btn.getBoundingClientRect();
const groupRect = group.getBoundingClientRect();
slider.style.left = (btnRect.left - groupRect.left) + 'px';
slider.style.width = btnRect.width + 'px';
}
function switchToPage(pageName) {
document.getElementById('cumulative-page').style.display = pageName === 'cumulative' ? 'block' : 'none';
document.getElementById('games-page').style.display = pageName === 'games' ? 'block' : 'none';
document.querySelectorAll('.view-toggle').forEach(b => {
b.classList.toggle('active', b.dataset.page === pageName);
});
const activeBtn = document.querySelector(`.view-toggle[data-page="${pageName}"]`);
if (activeBtn) updateViewSlider(activeBtn);
document.getElementById('viewSectionTitle').textContent = pageName === 'cumulative' ? __t('player.vehicleStatistics') : __t('player.individual');
if (pageName === 'games') loadGamesData();
}
let gamesData = null;
let gamesDataLoaded = false;
let gamesDataTimestamp = null;
const CACHE_DURATION = 5 * 60 * 1000;
let currentGamesSort = { column: -1, direction: 'asc' };
async function loadGamesData() {
const now = Date.now();
const isGamesTabActive = document.getElementById('games-page').style.display !== 'none';
if (gamesDataLoaded && gamesData && gamesDataTimestamp) {
const dataAge = now - gamesDataTimestamp;
const isDataFresh = dataAge < CACHE_DURATION;
if (isDataFresh) {
const minutesOld = Math.floor(dataAge / (60 * 1000));
console.log(`Games data already loaded, using cached data (${minutesOld}m old)`);
if (isGamesTabActive) {
const loadingEl = document.getElementById('games-loading');
const contentEl = document.getElementById('games-content');
const errorEl = document.getElementById('games-error');
const noGamesEl = document.getElementById('no-games');
loadingEl.style.display = 'none';
errorEl.style.display = 'none';
if (gamesData.games && gamesData.games.length > 0) {
contentEl.style.display = 'block';
noGamesEl.style.display = 'none';
} else {
contentEl.style.display = 'none';
noGamesEl.style.display = 'block';
}
}
return;
} else {
const minutesOld = Math.floor(dataAge / (60 * 1000));
console.log(`Games data is stale (${minutesOld}m old), refreshing...`);
gamesDataLoaded = false;
gamesData = null;
gamesDataTimestamp = null;
}
}
const uid = '<%= playerData.uid %>';
const loadingEl = document.getElementById('games-loading');
const contentEl = document.getElementById('games-content');
const errorEl = document.getElementById('games-error');
const noGamesEl = document.getElementById('no-games');
if (isGamesTabActive) {
loadingEl.style.display = 'block';
contentEl.style.display = 'none';
errorEl.style.display = 'none';
noGamesEl.style.display = 'none';
}
try {
console.log('Loading games data from API...');
gamesData = await window.apiClient.getPlayerGames(uid);
if (!gamesData.games || gamesData.games.length === 0) {
if (isGamesTabActive) {
loadingEl.style.display = 'none';
noGamesEl.style.display = 'block';
}
gamesDataLoaded = true;
gamesDataTimestamp = Date.now();
return;
}
displayGamesData();
if (isGamesTabActive) {
loadingEl.style.display = 'none';
contentEl.style.display = 'block';
}
gamesDataLoaded = true;
// Initialize seasons filter
initializeSeasonsFilter();
gamesDataTimestamp = Date.now();
console.log('Games data loaded and cached successfully');
} catch (error) {
console.error('Error loading games data:', error);
if (isGamesTabActive) {
loadingEl.style.display = 'none';
errorEl.style.display = 'block';
}
const errorMessageEl = document.getElementById('games-error-message');
if (errorMessageEl) {
errorMessageEl.textContent = error.message || __t('player.failedToFetch');
}
gamesDataLoaded = false;
}
}
function retryLoadGamesData() {
console.log('Retrying games data load, clearing cache...');
gamesDataLoaded = false;
gamesData = null;
gamesDataTimestamp = null;
loadGamesData();
}
function displayGamesData() {
if (!gamesData || !gamesData.games) return;
displayGamesTable();
initializeFilters();
displayGamesSummary();
}
function displayGamesSummary() {
const summaryEl = document.getElementById('games-stats-summary');
if (!summaryEl || !gamesData.games) return;
const visibleGames = getVisibleGames();
let totalGames = visibleGames.length;
let totalWins = 0;
let totalGroundKills = 0;
let totalAirKills = 0;
let totalAssists = 0;
let totalCaptures = 0;
let totalDeaths = 0;
visibleGames.forEach(game => {
if (game.result && game.result.toLowerCase() === 'win') {
totalWins++;
}
totalGroundKills += game.stats.ground_kills || 0;
totalAirKills += game.stats.air_kills || 0;
totalAssists += game.stats.assists || 0;
totalCaptures += game.stats.captures || 0;
totalDeaths += game.stats.deaths || 0;
});
const totalKills = totalGroundKills + totalAirKills;
const winRate = totalGames > 0 ? ((totalWins / totalGames) * 100).toFixed(1) + '%' : '0.0%';
const kdr = totalDeaths > 0 ? (totalKills / totalDeaths).toFixed(2) : totalKills.toFixed(2);
const kps = totalGames > 0 ? (totalKills / totalGames).toFixed(2) : '0.00';
summaryEl.style.display = 'none';
}
function getVisibleGames() {
if (!gamesData || !gamesData.games) return [];
const table = document.getElementById('games-table');
if (!table) return gamesData.games;
const rows = table.querySelectorAll('tbody tr');
const visibleGames = [];
rows.forEach(row => {
if (!row.classList.contains('filtered-out')) {
const index = parseInt(row.dataset.gameIndex);
if (gamesData.games[index]) visibleGames.push(gamesData.games[index]);
}
});
return visibleGames;
}
function displayGamesTable() {
const tableBody = document.getElementById('games-table-body');
if (!tableBody || !gamesData.games) return;
// Pre-sort data by timestamp descending (newest first) before rendering
gamesData.games.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
// Build all HTML at once instead of 1700+ individual DOM appends
tableBody.innerHTML = gamesData.games.map((game, index) => {
const date = new Date(game.timestamp * 1000);
const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const result = game.result || 'Unknown';
const resultClass = result.toLowerCase();
const hasSession = game.session_id ? true : false;
const rowClasses = hasSession ? 'row-link' : '';
const linkHtml = hasSession ? `<a href="/games/${game.session_id}" class="row-link-overlay" aria-label="View game from ${formattedDate}"></a>` : '';
return `<tr data-game-index="${index}" class="${rowClasses}"${hasSession ? ` data-session-id="${game.session_id}"` : ''}>
<td class="game-date">${linkHtml}${formattedDate}</td>
<td class="game-vehicle" data-vehicle-internal="${game.vehicle_internal || ''}">${game.vehicle || game.vehicle_internal || 'Unknown'}</td>
<td class="stat-cell kills">${game.stats.ground_kills || 0}</td>
<td class="stat-cell air-kills">${game.stats.air_kills || 0}</td>
<td class="stat-cell assists">${game.stats.assists || 0}</td>
<td class="stat-cell captures">${game.stats.captures || 0}</td>
<td class="stat-cell deaths">${game.stats.deaths || 0}</td>
<td><span class="game-result ${resultClass}">${result}</span></td>
</tr>`;
}).join('');
currentGamesSort = { column: 0, direction: 'desc' };
const headers = document.getElementById('games-table').querySelectorAll('th');
if (headers[0]) headers[0].classList.add('sorted-desc');
}
function sortGamesTable(columnIndex, dataType) {
const table = document.getElementById('games-table');
const tbody = table.querySelector('tbody');
const headers = table.querySelectorAll('th');
const rows = Array.from(tbody.querySelectorAll('tr'));
headers.forEach(header => {
header.classList.remove('sorted-asc', 'sorted-desc');
});
let direction = 'asc';
if (currentGamesSort.column === columnIndex && currentGamesSort.direction === 'asc') {
direction = 'desc';
}
rows.sort((a, b) => {
let valueA, valueB;
if (columnIndex === 0) {
const dateA = new Date(a.cells[0].textContent);
const dateB = new Date(b.cells[0].textContent);
valueA = dateA.getTime();
valueB = dateB.getTime();
} else if (dataType === 'number') {
valueA = parseFloat(a.cells[columnIndex].textContent) || 0;
valueB = parseFloat(b.cells[columnIndex].textContent) || 0;
} else {
valueA = a.cells[columnIndex].textContent.trim().toLowerCase();
valueB = b.cells[columnIndex].textContent.trim().toLowerCase();
}
if (direction === 'asc') {
return valueA > valueB ? 1 : valueA < valueB ? -1 : 0;
} else {
return valueA < valueB ? 1 : valueA > valueB ? -1 : 0;
}
});
tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));
headers[columnIndex].classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc');
currentGamesSort = { column: columnIndex, direction: direction };
}
function updateFilterCategory() {
const category = document.getElementById('date-filter-category').value;
const dateTypeFilter = document.getElementById('date-type-filter');
const seasonTypeFilter = document.getElementById('season-type-filter');
const customRangeFilters = document.getElementById('custom-range-filters');
const specificDateFilter = document.getElementById('specific-date-filter');
const seasonSelectFilter = document.getElementById('season-select-filter');
const weekFilters = document.getElementById('week-filters');
// Hide all sub-filters
dateTypeFilter.style.display = 'none';
seasonTypeFilter.style.display = 'none';
customRangeFilters.style.display = 'none';
specificDateFilter.style.display = 'none';
seasonSelectFilter.style.display = 'none';
weekFilters.style.display = 'none';
if (category === 'date') {
dateTypeFilter.style.display = 'flex';
updateDateFilters();
} else if (category === 'season') {
seasonTypeFilter.style.display = 'flex';
updateSeasonFilters();
} else {
// All time - apply filters immediately
applyFilters();
}
updateGamesResetBtn();
}
function updateDateFilters() {
const filterType = document.getElementById('date-filter-type').value;
const customRangeFilters = document.getElementById('custom-range-filters');
const specificDateFilter = document.getElementById('specific-date-filter');
customRangeFilters.style.display = 'none';
specificDateFilter.style.display = 'none';
if (filterType === 'custom') {
customRangeFilters.style.display = 'flex';
} else if (filterType === 'specific') {
specificDateFilter.style.display = 'flex';
}
applyFilters();
}
function updateSeasonFilters() {
const seasonFilterType = document.getElementById('season-filter-type').value;
const seasonSelectFilter = document.getElementById('season-select-filter');
const weekFilters = document.getElementById('week-filters');
seasonSelectFilter.style.display = 'none';
weekFilters.style.display = 'none';
if (seasonFilterType === 'full-season') {
seasonSelectFilter.style.display = 'flex';
} else if (seasonFilterType === 'week') {
weekFilters.style.display = 'flex';
}
applyFilters();
}
function updateWeekOptions() {
const seasonSelect = document.getElementById('week-season-select');
const weekSelect = document.getElementById('week-select');
const seasonName = seasonSelect.value;
if (seasonName && window.seasonsFilter) {
window.seasonsFilter.populateWeekSelect(weekSelect, seasonName, false);
} else {
weekSelect.innerHTML = '<option value="">Select season first</option>';
}
applyFilters();
}
function applyFilters() {
if (!gamesData || !gamesData.games) return;
const category = document.getElementById('date-filter-category').value;
const vehicleSearchTerm = document.getElementById('vehicle-search').value.toLowerCase().trim();
const table = document.getElementById('games-table');
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
let startDate, endDate;
const now = new Date();
if (category === 'all') {
startDate = null;
endDate = null;
} else if (category === 'date') {
const filterType = document.getElementById('date-filter-type').value;
switch (filterType) {
case 'last7':
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
endDate = now;
break;
case 'last30':
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
endDate = now;
break;
case 'last90':
startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
endDate = now;
break;
case 'custom':
const startInput = document.getElementById('start-date').value;
const endInput = document.getElementById('end-date').value;
if (startInput) startDate = new Date(startInput);
if (endInput) {
endDate = new Date(endInput);
endDate.setHours(23, 59, 59, 999);
}
break;
case 'specific':
const specificInput = document.getElementById('specific-date').value;
if (specificInput) {
startDate = new Date(specificInput);
endDate = new Date(specificInput);
endDate.setHours(23, 59, 59, 999);
}
break;
}
} else if (category === 'season') {
const seasonFilterType = document.getElementById('season-filter-type').value;
if (seasonFilterType === 'full-season') {
const seasonSelect = document.getElementById('season-select');
const seasonName = seasonSelect.value;
if (seasonName && window.seasonsFilter) {
const range = window.seasonsFilter.getSeasonDateRange(seasonName);
if (range) {
startDate = range.startDate;
endDate = range.endDate;
}
}
} else if (seasonFilterType === 'week') {
const weekSeasonSelect = document.getElementById('week-season-select');
const weekSelect = document.getElementById('week-select');
const selectedSeason = weekSeasonSelect.value;
const selectedWeek = weekSelect.value;
if (selectedSeason && selectedWeek && window.seasonsFilter) {
const weekNum = selectedWeek === 'final' ? null : parseInt(selectedWeek);
const range = window.seasonsFilter.getWeekDateRange(selectedSeason, weekNum);
if (range) {
startDate = range.startDate;
endDate = range.endDate;
}
}
}
}
let visibleCount = 0;
rows.forEach((row, index) => {
if (gamesData.games[index]) {
const game = gamesData.games[index];
const gameDate = new Date(game.timestamp * 1000);
const gameVehicle = (game.vehicle || game.vehicle_internal || 'Unknown').toLowerCase();
let isVisible = true;
if (startDate && gameDate < startDate) {
isVisible = false;
}
if (endDate && gameDate > endDate) {
isVisible = false;
}
if (vehicleSearchTerm && !gameVehicle.includes(vehicleSearchTerm)) {
isVisible = false;
}
if (isVisible) {
row.classList.remove('filtered-out');
visibleCount++;
} else {
row.classList.add('filtered-out');
}
}
});
document.getElementById('filtered-count').textContent = visibleCount;
updateGamesResetBtn();
displayGamesSummary();
}
async function initializeSeasonsFilter() {
if (!window.seasonsFilter) return;
try {
await window.seasonsFilter.loadSeasons();
// Populate season selects for game records
const seasonSelect = document.getElementById('season-select');
const weekSeasonSelect = document.getElementById('week-season-select');
if (seasonSelect && window.seasonsFilter.loaded) {
window.seasonsFilter.populateSeasonSelect(seasonSelect, false);
}
if (weekSeasonSelect && window.seasonsFilter.loaded) {
window.seasonsFilter.populateSeasonSelect(weekSeasonSelect, false);
}
// Populate season selects for cumulative stats
const cumulativeSeasonSelect = document.getElementById('cumulative-season');
const cumulativeWeekSeasonSelect = document.getElementById('cumulative-week-season-select');
if (cumulativeSeasonSelect && window.seasonsFilter.loaded) {
window.seasonsFilter.populateSeasonSelect(cumulativeSeasonSelect, true);
}
if (cumulativeWeekSeasonSelect && window.seasonsFilter.loaded) {
window.seasonsFilter.populateSeasonSelect(cumulativeWeekSeasonSelect, true);
}
console.log('[Player] Seasons filter initialized');
// NOW restore filter state after seasons are loaded
restoreCumulativeFilterState();
} catch (error) {
console.error('[Player] Error initializing seasons filter:', error);
}
}
// Reset button visibility helpers
function updateCumulativeResetBtn() {
const active = document.getElementById('cumulative-filter-category').value !== 'all'
|| document.getElementById('cumulative-vehicle-search').value.trim() !== '';
document.getElementById('cumulative-reset-btn-group').style.display = active ? 'flex' : 'none';
}
function updateGamesResetBtn() {
const active = document.getElementById('date-filter-category').value !== 'all'
|| document.getElementById('vehicle-search').value.trim() !== '';
document.getElementById('games-reset-btn-group').style.display = active ? 'flex' : 'none';
}
// Cumulative Stats Filter Management
function updateCumulativeFilterCategory() {
const cat = document.getElementById('cumulative-filter-category').value;
// Hide all filter groups
document.getElementById('cumulative-date-type-filter').style.display = 'none';
document.getElementById('cumulative-custom-range-filters').style.display = 'none';
document.getElementById('cumulative-season-select-filter').style.display = 'none';
document.getElementById('cumulative-week-season-filter').style.display = 'none';
document.getElementById('cumulative-week-select-filter').style.display = 'none';
document.getElementById('cumulative-session-filter').style.display = 'none';
if (cat === 'date') {
document.getElementById('cumulative-date-type-filter').style.display = 'flex';
updateCumulativeDateFilters();
} else if (cat === 'season') {
document.getElementById('cumulative-season-select-filter').style.display = 'flex';
} else if (cat === 'week') {
document.getElementById('cumulative-week-season-filter').style.display = 'flex';
document.getElementById('cumulative-week-select-filter').style.display = 'flex';
} else if (cat === 'session') {
document.getElementById('cumulative-week-season-filter').style.display = 'flex';
document.getElementById('cumulative-week-select-filter').style.display = 'flex';
document.getElementById('cumulative-session-filter').style.display = 'flex';
}
updateCumulativeResetBtn();
// Apply filters when category changes
if (cat === 'all') {
clearCumulativeFilters();
} else if (cat === 'season' || cat === 'date') {
// Auto-apply the current selection
applyCumulativeFilters();
}
}
function handleCumulativeDateTypeChange() {
const filterType = document.getElementById('cumulative-date-filter-type').value;
const showCustom = filterType === 'custom';
document.getElementById('cumulative-custom-range-filters').style.display = showCustom ? 'flex' : 'none';
// Auto-apply for non-custom filters
if (filterType !== 'custom') {
applyCumulativeFilters();
}
}
function updateCumulativeDateFilters() {
const filterType = document.getElementById('cumulative-date-filter-type').value;
const showCustom = filterType === 'custom';
document.getElementById('cumulative-custom-range-filters').style.display = showCustom ? 'flex' : 'none';
}
function updateCumulativeWeekOptions() {
const seasonSelect = document.getElementById('cumulative-week-season-select');
const weekSelect = document.getElementById('cumulative-week');
const seasonName = seasonSelect.value;
if (seasonName && window.seasonsFilter) {
window.seasonsFilter.populateWeekSelect(weekSelect, seasonName, true);
} else {
weekSelect.innerHTML = '<option value="">Please select an option</option>';
}
// If in session mode, clear session dropdown
const cat = document.getElementById('cumulative-filter-category').value;
if (cat === 'session') {
document.getElementById('cumulative-session').innerHTML = '<option value="">Select session...</option>';
}
}
function populateSessionOptions() {
const cat = document.getElementById('cumulative-filter-category').value;
if (cat !== 'session') return;
const seasonSelect = document.getElementById('cumulative-week-season-select');
const weekSelect = document.getElementById('cumulative-week');
const sessionSelect = document.getElementById('cumulative-session');
const selectedSeason = seasonSelect.value;
const selectedWeek = weekSelect.value;
sessionSelect.innerHTML = '<option value="">Select session...</option>';
if (!selectedSeason || !selectedWeek || !window.seasonsFilter) return;
let weekStart, weekEnd;
if (selectedWeek === 'all') {
const range = window.seasonsFilter.getSeasonDateRange(selectedSeason);
if (range) { weekStart = range.startDate; weekEnd = range.endDate; }
} else {
const weekNum = selectedWeek === 'final' ? null : parseInt(selectedWeek);
const range = window.seasonsFilter.getWeekDateRange(selectedSeason, weekNum);
if (range) { weekStart = range.startDate; weekEnd = range.endDate; }
}
if (!weekStart || !weekEnd) return;
// Generate NA + EU session for each day in the week
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const current = new Date(Date.UTC(weekStart.getUTCFullYear(), weekStart.getUTCMonth(), weekStart.getUTCDate()));
const end = new Date(Date.UTC(weekEnd.getUTCFullYear(), weekEnd.getUTCMonth(), weekEnd.getUTCDate()));
while (current <= end) {
const dateStr = current.toISOString().slice(0, 10);
const dayName = dayNames[current.getUTCDay()];
const dd = String(current.getUTCDate()).padStart(2, '0');
const mm = String(current.getUTCMonth() + 1).padStart(2, '0');
const label = `${dayName} ${dd}.${mm}`;
const naOpt = document.createElement('option');
naOpt.value = dateStr + '_NA';
naOpt.textContent = `${label} — NA (00:5507:10 UTC)`;
sessionSelect.appendChild(naOpt);
const euOpt = document.createElement('option');
euOpt.value = dateStr + '_EU';
euOpt.textContent = `${label} — EU (13:5522:10 UTC)`;
sessionSelect.appendChild(euOpt);
current.setUTCDate(current.getUTCDate() + 1);
}
}
function onCumulativeWeekChange() {
const cat = document.getElementById('cumulative-filter-category').value;
if (cat === 'session') {
populateSessionOptions();
} else {
applyCumulativeFilters();
}
}
const _playerUID = document.getElementById('playerChartSection').dataset.uid;
const initialVehicles = <%- JSON.stringify((playerData.vehicles || []).filter(v => v.vehicle !== 'DISCONNECTED')) %>;
const noStatsYet = <%- JSON.stringify(Boolean(playerData.no_stats_yet)) %>;
let cumulativeRequestId = 0;
async function fetchAndUpdateVehicles(startDate, endDate) {
const thisRequest = ++cumulativeRequestId;
const tableView = document.getElementById('tableView');
const noVehiclesMsg = document.getElementById('no-vehicles-msg');
const loadingEl = document.getElementById('vehicles-loading');
// Cache hit for all-time data
if (!startDate && !endDate && initialVehicles) {
renderVehicleTable(initialVehicles.map(v => ({...v})));
updateSummaryStats(initialVehicles);
return;
}
if (tableView) tableView.style.display = 'none';
if (noVehiclesMsg) noVehiclesMsg.style.display = 'none';
if (loadingEl) loadingEl.style.display = 'flex';
let endpoint = `/api/player/${_playerUID}`;
if (startDate || endDate) {
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate.toISOString());
if (endDate) params.append('end_date', endDate.toISOString());
endpoint += '?' + params.toString();
}
try {
const data = await window.apiClient.request(endpoint);
if (thisRequest !== cumulativeRequestId) return;
if (loadingEl) loadingEl.style.display = 'none';
renderVehicleTable(data.vehicles || []);
updateSummaryStats(data.vehicles || []);
} catch (err) {
if (thisRequest !== cumulativeRequestId) return;
console.error('[Cumulative Filter] Error fetching vehicle data:', err);
if (loadingEl) loadingEl.style.display = 'none';
if (tableView) tableView.style.display = 'block';
}
}
function renderVehicleTable(vehicles) {
vehicles = vehicles.filter(v => v.vehicle !== 'DISCONNECTED');
const tbody = document.querySelector('.vehicles-table tbody');
const tableView = document.getElementById('tableView');
const noVehiclesMsg = document.getElementById('no-vehicles-msg');
const noVehiclesText = document.getElementById('no-vehicles-text');
document.getElementById('cumulative-filtered-count').textContent = vehicles.length;
if (!vehicles.length) {
if (tableView) tableView.style.display = 'none';
if (noVehiclesMsg) noVehiclesMsg.style.display = 'block';
if (noVehiclesText) {
noVehiclesText.textContent = noStatsYet
? 'No stats yet for this player'
: __t('player.noVehiclesForRange');
}
return;
}
if (noVehiclesMsg) noVehiclesMsg.style.display = 'none';
if (tableView) tableView.style.display = 'block';
if (!tbody) return;
tbody.innerHTML = vehicles.map(vehicle => {
const totalKills = vehicle.stats.ground_kills + vehicle.stats.air_kills;
const kdrNum = vehicle.stats.deaths > 0 ? totalKills / vehicle.stats.deaths : totalKills;
const kdr = kdrNum.toFixed(2);
const kdrColor = kdrNum >= 3 ? '#90EE90' : kdrNum >= 2 ? '#A8E6CF' : kdrNum >= 1.5 ? '#FFD700' : kdrNum >= 1 ? '#FFA500' : '#FF6B6B';
const battles = vehicle.stats.total_battles || 0;
const wins = vehicle.stats.wins || 0;
const winRate = battles > 0 ? ((wins / battles) * 100).toFixed(1) + '%' : '0.0%';
const kps = battles > 0 ? (totalKills / battles).toFixed(2) : '0.00';
return `<tr>
<td class="vehicle-name" data-vehicle-internal="${vehicle.vehicle_internal || ''}">${vehicle.vehicle}</td>
<td class="stat-cell battles">${battles}</td>
<td class="stat-cell wins">${wins}</td>
<td class="stat-cell win-rate">${winRate}</td>
<td class="stat-cell kdr" style="color: ${kdrColor}; text-shadow: 0 0 10px ${kdrColor}40;">${kdr}</td>
<td class="stat-cell kps">${kps}</td>
<td class="stat-cell kills">${vehicle.stats.ground_kills}</td>
<td class="stat-cell air-kills">${vehicle.stats.air_kills}</td>
<td class="stat-cell assists">${vehicle.stats.assists}</td>
<td class="stat-cell captures">${vehicle.stats.captures}</td>
<td class="stat-cell deaths">${vehicle.stats.deaths}</td>
</tr>`;
}).join('');
sortTable(1, 'number');
filterCumulativeVehicles();
if (isMobileView) populateMobileCards();
}
function updateSummaryStats(vehicles) {
vehicles = vehicles.filter(v => v.vehicle !== 'DISCONNECTED');
let totalBattles = 0, totalWins = 0, totalGK = 0, totalAK = 0;
let totalAssists = 0, totalCaptures = 0, totalDeaths = 0;
vehicles.forEach(v => {
totalBattles += v.stats.total_battles || 0;
totalWins += v.stats.wins || 0;
totalGK += v.stats.ground_kills || 0;
totalAK += v.stats.air_kills || 0;
totalAssists += v.stats.assists || 0;
totalCaptures+= v.stats.captures || 0;
totalDeaths += v.stats.deaths || 0;
});
const totalKills = totalGK + totalAK;
const kdr = totalDeaths > 0 ? (totalKills / totalDeaths).toFixed(2) : (totalKills > 0 ? totalKills.toFixed(2) : '0.00');
const winRate = totalBattles > 0 ? ((totalWins / totalBattles) * 100).toFixed(1) + '%' : '0.0%';
const kps = totalBattles > 0 ? (totalKills / totalBattles).toFixed(2) : '0.00';
document.getElementById('stat-total-battles').textContent = totalBattles;
document.getElementById('stat-total-wins').textContent = totalWins;
document.getElementById('stat-win-rate').textContent = winRate;
document.getElementById('stat-total-kills').textContent = totalKills;
document.getElementById('stat-kdr').textContent = kdr;
document.getElementById('stat-kps').textContent = kps;
document.getElementById('stat-air-kills').textContent = totalAK;
document.getElementById('stat-ground-kills').textContent = totalGK;
document.getElementById('stat-assists').textContent = totalAssists;
document.getElementById('stat-deaths').textContent = totalDeaths;
document.getElementById('stat-captures').textContent = totalCaptures;
}
function applyCumulativeFilters() {
const category = document.getElementById('cumulative-filter-category').value;
let startDate = null;
let endDate = null;
const now = new Date();
if (category === 'all') {
updateChartFilter(null, null);
fetchAndUpdateVehicles(null, null);
return;
} else if (category === 'date') {
const filterType = document.getElementById('cumulative-date-filter-type').value;
if (!filterType) {
alert(__t('player.pleaseSelect'));
return;
}
switch (filterType) {
case 'last7':
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
endDate = now;
break;
case 'last30':
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
endDate = now;
break;
case 'last90':
startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
endDate = now;
break;
case 'custom':
const startInput = document.getElementById('cumulative-start-date').value;
const endInput = document.getElementById('cumulative-end-date').value;
if (!startInput && !endInput) return;
startDate = new Date(startInput || endInput);
endDate = new Date(endInput || startInput);
endDate.setHours(23, 59, 59, 999);
break;
}
} else if (category === 'season') {
const seasonSelect = document.getElementById('cumulative-season');
const seasonName = seasonSelect.value;
if (!seasonName) {
alert(__t('player.pleaseSelect'));
return;
}
if (window.seasonsFilter) {
const range = window.seasonsFilter.getSeasonDateRange(seasonName);
if (range) {
startDate = range.startDate;
endDate = range.endDate;
}
}
} else if (category === 'week') {
const weekSeasonSelect = document.getElementById('cumulative-week-season-select');
const weekSelect = document.getElementById('cumulative-week');
const selectedSeason = weekSeasonSelect.value;
const selectedWeek = weekSelect.value;
if (!selectedSeason || !selectedWeek) {
alert(__t('player.pleaseSelect'));
return;
}
if (window.seasonsFilter) {
if (selectedWeek === 'all') {
const range = window.seasonsFilter.getSeasonDateRange(selectedSeason);
if (range) {
startDate = range.startDate;
endDate = range.endDate;
}
} else {
const weekNum = selectedWeek === 'final' ? null : parseInt(selectedWeek);
const range = window.seasonsFilter.getWeekDateRange(selectedSeason, weekNum);
if (range) {
startDate = range.startDate;
endDate = range.endDate;
}
}
}
} else if (category === 'session') {
const weekSeasonSelect = document.getElementById('cumulative-week-season-select');
const weekSelect = document.getElementById('cumulative-week');
const sessionSelect = document.getElementById('cumulative-session');
const selectedSeason = weekSeasonSelect.value;
const selectedWeek = weekSelect.value;
const selectedSession = sessionSelect.value;
if (!selectedSeason || !selectedWeek || !selectedSession) {
return;
}
// Parse session value: "YYYY-MM-DD_NA" or "YYYY-MM-DD_EU"
const parts = selectedSession.split('_');
const dayStr = parts[0];
const region = parts[1]; // "NA" or "EU"
const day = new Date(dayStr);
// NA: 00:55 - 07:10 UTC, EU: 13:55 - 22:10 UTC
if (region === 'NA') {
startDate = new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate(), 0, 55, 0));
endDate = new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate(), 7, 10, 0));
} else {
startDate = new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate(), 13, 55, 0));
endDate = new Date(Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate(), 22, 10, 0));
}
}
// Update chart for season-level or coarser filters only
if (category === 'date' || category === 'season') {
updateChartFilter(startDate, endDate);
}
if (startDate || endDate) {
fetchAndUpdateVehicles(startDate, endDate);
}
}
function clearCumulativeFilters() {
updateChartFilter(null, null);
fetchAndUpdateVehicles(null, null);
}
function resetCumulativeFilters() {
document.getElementById('cumulative-filter-category').value = 'all';
document.getElementById('cumulative-date-type-filter').style.display = 'none';
document.getElementById('cumulative-custom-range-filters').style.display = 'none';
document.getElementById('cumulative-season-select-filter').style.display = 'none';
document.getElementById('cumulative-week-season-filter').style.display = 'none';
document.getElementById('cumulative-week-select-filter').style.display = 'none';
document.getElementById('cumulative-session-filter').style.display = 'none';
document.getElementById('cumulative-vehicle-search').value = '';
updateCumulativeResetBtn();
updateChartFilter(null, null);
fetchAndUpdateVehicles(null, null);
}
function resetAllFilters() {
document.getElementById('date-filter-category').value = 'all';
document.getElementById('vehicle-search').value = '';
document.getElementById('start-date').value = '';
document.getElementById('end-date').value = '';
document.getElementById('specific-date').value = '';
updateFilterCategory();
}
function initializeFilters() {
if (!gamesData || !gamesData.games) return;
document.getElementById('filtered-count').textContent = gamesData.games.length;
const today = new Date().toISOString().split('T')[0];
document.getElementById('start-date').max = today;
document.getElementById('end-date').max = today;
document.getElementById('specific-date').max = today;
if (gamesData.games.length > 0) {
const oldestGame = gamesData.games[gamesData.games.length - 1];
const oldestDate = new Date(oldestGame.timestamp * 1000).toISOString().split('T')[0];
document.getElementById('start-date').min = oldestDate;
document.getElementById('end-date').min = oldestDate;
document.getElementById('specific-date').min = oldestDate;
}
}
function filterCumulativeVehicles() {
const vehicleSearchTerm = document.getElementById('cumulative-vehicle-search').value.toLowerCase().trim();
const table = document.querySelector('.vehicles-table');
if (!table) return;
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
let visibleCount = 0;
rows.forEach(row => {
const vehicleName = row.cells[0].textContent.toLowerCase();
let isVisible = true;
if (vehicleSearchTerm && !vehicleName.includes(vehicleSearchTerm)) {
isVisible = false;
}
if (isVisible) {
row.classList.remove('filtered-out');
row.style.display = '';
visibleCount++;
} else {
row.classList.add('filtered-out');
row.style.display = 'none';
}
});
document.getElementById('cumulative-filtered-count').textContent = visibleCount;
updateCumulativeResetBtn();
if (isMobileView) {
populateMobileCards();
}
}
// Restore filter state from URL parameters on page load
function restoreCumulativeFilterState() {
console.log('[Filter Restore] Starting filter state restoration...');
const urlParams = new URLSearchParams(window.location.search);
const startDate = urlParams.get('start_date');
const endDate = urlParams.get('end_date');
console.log('[Filter Restore] URL params:', { startDate, endDate });
if (!startDate && !endDate) {
// No filters applied, keep default "All Time"
console.log('[Filter Restore] No filters in URL, keeping defaults');
return;
}
// We have date parameters, need to figure out what filter was used
const start = startDate ? new Date(startDate) : null;
const end = endDate ? new Date(endDate) : null;
if (!start || !end) return;
const now = new Date();
const daysDiff = Math.floor((now - start) / (1000 * 60 * 60 * 24));
// Try to detect the filter type based on date range
console.log('[Filter Restore] Days difference:', daysDiff);
if (daysDiff >= 6 && daysDiff <= 8) {
// Last 7 days
console.log('[Filter Restore] Detected Last 7 Days filter');
document.getElementById('cumulative-filter-category').value = 'date';
document.getElementById('cumulative-date-type-filter').style.display = 'flex';
document.getElementById('cumulative-date-filter-type').value = 'last7';
updateCumulativeFilterCategory();
} else if (daysDiff >= 29 && daysDiff <= 31) {
// Last 30 days
console.log('[Filter Restore] Detected Last 30 Days filter');
document.getElementById('cumulative-filter-category').value = 'date';
document.getElementById('cumulative-date-type-filter').style.display = 'flex';
document.getElementById('cumulative-date-filter-type').value = 'last30';
updateCumulativeFilterCategory();
} else if (daysDiff >= 89 && daysDiff <= 91) {
// Last 90 days
console.log('[Filter Restore] Detected Last 90 Days filter');
document.getElementById('cumulative-filter-category').value = 'date';
document.getElementById('cumulative-date-type-filter').style.display = 'flex';
document.getElementById('cumulative-date-filter-type').value = 'last90';
updateCumulativeFilterCategory();
} else {
// Custom date range or season/week/BR filter
// Check if it matches a season or week
if (window.seasonsFilter && window.seasonsFilter.loaded) {
let matchedFilter = false;
// Check seasons
const seasons = window.seasonsFilter.getSeasons();
for (const season of seasons) {
const range = window.seasonsFilter.getSeasonDateRange(season.name);
if (range && areDatesClose(start, range.startDate) && areDatesClose(end, range.endDate)) {
document.getElementById('cumulative-filter-category').value = 'season';
document.getElementById('cumulative-season-select-filter').style.display = 'flex';
document.getElementById('cumulative-season').value = season.name;
matchedFilter = true;
break;
}
// Check weeks within this season
for (const week of season.weeks) {
const weekRange = window.seasonsFilter.getWeekDateRange(season.name, week.weekNumber);
if (weekRange && areDatesClose(start, weekRange.startDate) && areDatesClose(end, weekRange.endDate)) {
document.getElementById('cumulative-filter-category').value = 'week';
document.getElementById('cumulative-week-season-filter').style.display = 'flex';
document.getElementById('cumulative-week-select-filter').style.display = 'flex';
document.getElementById('cumulative-week-season-select').value = season.name;
updateCumulativeWeekOptions();
setTimeout(() => {
document.getElementById('cumulative-week').value = week.weekNumber !== null ? week.weekNumber.toString() : 'final';
}, 100);
matchedFilter = true;
break;
}
}
if (matchedFilter) break;
}
if (matchedFilter) {
updateCumulativeFilterCategory();
return;
}
}
// Default to custom date range
console.log('[Filter Restore] Using custom date range as fallback');
document.getElementById('cumulative-filter-category').value = 'date';
document.getElementById('cumulative-date-type-filter').style.display = 'flex';
document.getElementById('cumulative-date-filter-type').value = 'custom';
document.getElementById('cumulative-custom-range-filters').style.display = 'flex';
document.getElementById('cumulative-start-date').value = startDate;
document.getElementById('cumulative-end-date').value = endDate;
updateCumulativeFilterCategory();
}
console.log('[Filter Restore] Filter state restoration complete');
}
// Helper function to check if two dates are close (within 1 day)
function areDatesClose(date1, date2) {
const diff = Math.abs(date1.getTime() - date2.getTime());
return diff < 86400000; // 1 day in milliseconds
}
// --- Timeline / History Chart ---
let playerHistoryData = null;
let playerHistoryLoaded = false;
let playerChart = null;
let playerMetric = 'kdr';
let chartFilterStart = null;
let chartFilterEnd = null;
let chartScaleMode = 'alltime';
const playerMetricConfig = {
win_rate: { label: __t('common.winRate'), color: '#90EE90', suffix: '%' },
kdr: { label: __t('common.kdr'), color: '#64b5f6', suffix: '' },
battles: { label: __t('common.battles'), color: '#ffb74d', suffix: '' }
};
function formatPeriodLabel(dateStr) {
const d = new Date(dateStr + 'T00:00:00Z');
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
}
function updatePlayerSlider(btn) {
const slider = document.getElementById('playerChartSlider');
slider.style.left = btn.offsetLeft + 'px';
slider.style.width = btn.offsetWidth + 'px';
}
function periodToTimestamp(dateStr) {
return new Date(dateStr + 'T00:00:00Z').getTime();
}
function renderPlayerChart(metric) {
if (!playerHistoryData || !playerHistoryData.history.length) return;
const config = playerMetricConfig[metric];
const dataPoints = playerHistoryData.history
.filter(d => {
if (d[metric] == null) return false;
if (chartFilterStart || chartFilterEnd) {
const ts = periodToTimestamp(d.period);
if (chartFilterStart && ts < chartFilterStart.getTime()) return false;
if (chartFilterEnd && ts > chartFilterEnd.getTime()) return false;
}
return true;
})
.map(d => ({ x: periodToTimestamp(d.period), y: d[metric], label: formatPeriodLabel(d.period) }));
if (!dataPoints.length) return;
// X-axis: "alltime" starts at Jan 1, "relative" starts at first data point
const earliestYear = new Date(playerHistoryData.history[0].period + 'T00:00:00Z').getUTCFullYear();
const xMin = chartScaleMode === 'alltime' ? Date.UTC(earliestYear, 0, 1) : dataPoints[0].x;
const canvas = document.getElementById('playerChart');
if (playerChart) {
playerChart.data.datasets[0].data = dataPoints;
playerChart.data.datasets[0].label = config.label;
playerChart.data.datasets[0].borderColor = config.color;
playerChart.data.datasets[0].backgroundColor = config.color + '18';
playerChart.data.datasets[0].pointBackgroundColor = config.color;
playerChart.options.plugins.tooltip.borderColor = config.color;
playerChart.options.plugins.tooltip.callbacks.label = ctx => `${config.label}: ${ctx.parsed.y}${config.suffix}`;
playerChart.options.plugins.tooltip.callbacks.title = ctx => ctx[0].raw.label;
playerChart.options.scales.x.min = xMin;
playerChart.options.scales.y.ticks.callback = v => parseFloat(v.toFixed(2)) + config.suffix;
playerChart.update();
return;
}
playerChart = new Chart(canvas.getContext('2d'), {
type: 'line',
data: {
datasets: [{
label: config.label,
data: dataPoints,
borderColor: config.color,
backgroundColor: config.color + '18',
borderWidth: 2,
pointBackgroundColor: config.color,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.15,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 300 },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(15,15,15,0.92)',
borderColor: config.color,
borderWidth: 1,
titleColor: 'rgba(255,255,255,0.75)',
bodyColor: config.color,
padding: 10,
callbacks: {
title: ctx => ctx[0].raw.label,
label: ctx => `${config.label}: ${ctx.parsed.y}${config.suffix}`
}
}
},
scales: {
x: {
type: 'linear',
min: xMin,
grid: { color: 'rgba(255,255,255,0.04)' },
ticks: {
color: 'rgba(255,255,255,0.45)',
font: { size: 11 },
maxRotation: 45,
maxTicksLimit: 15,
autoSkip: true,
callback: function(val) {
const d = new Date(val);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
}
}
},
y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: 'rgba(255,255,255,0.45)', font: { size: 11 }, callback: v => parseFloat(v.toFixed(2)) + config.suffix } }
}
}
});
}
async function loadTimelineData() {
if (playerHistoryLoaded) {
if (playerHistoryData && playerHistoryData.history.length > 0) renderPlayerChart(playerMetric);
return;
}
const uid = document.getElementById('playerChartSection').dataset.uid;
try {
playerHistoryData = await window.apiClient.request('/api/player/' + uid + '/history');
playerHistoryLoaded = true;
document.getElementById('timeline-loading').style.display = 'none';
if (!playerHistoryData.history || playerHistoryData.history.length === 0) {
document.getElementById('timeline-empty').style.display = 'block';
return;
}
document.getElementById('playerChart').style.display = 'block';
renderPlayerChart(playerMetric);
requestAnimationFrame(() => {
const activeBtn = document.querySelector('#playerChartToggles .chart-toggle.active');
if (activeBtn) updatePlayerSlider(activeBtn);
});
} catch (e) {
document.getElementById('timeline-loading').style.display = 'none';
document.getElementById('timeline-error').style.display = 'block';
}
}
function updateChartFilter(startDate, endDate) {
chartFilterStart = startDate;
chartFilterEnd = endDate;
if (playerHistoryLoaded && playerHistoryData) renderPlayerChart(playerMetric);
}
document.getElementById('playerChartToggles').addEventListener('click', (e) => {
const btn = e.target.closest('.chart-toggle');
if (!btn) return;
document.querySelectorAll('#playerChartToggles .chart-toggle').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
updatePlayerSlider(btn);
playerMetric = btn.dataset.metric;
if (playerHistoryData) renderPlayerChart(playerMetric);
});
function updateScaleSlider(btn) {
const slider = document.getElementById('chartScaleSlider');
slider.style.left = btn.offsetLeft + 'px';
slider.style.width = btn.offsetWidth + 'px';
}
document.getElementById('chartScaleToggles').addEventListener('click', (e) => {
const btn = e.target.closest('.chart-toggle');
if (!btn || !btn.dataset.scale) return;
document.querySelectorAll('#chartScaleToggles .chart-toggle').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
updateScaleSlider(btn);
chartScaleMode = btn.dataset.scale;
if (playerHistoryData) renderPlayerChart(playerMetric);
});
// Initialize scale slider position after chart loads
requestAnimationFrame(() => {
const activeScaleBtn = document.querySelector('#chartScaleToggles .chart-toggle.active');
if (activeScaleBtn) updateScaleSlider(activeScaleBtn);
});
loadTimelineData();
</script>
<div id="season-recap-modal" class="season-recap-modal hidden" role="dialog" aria-hidden="true">
<div class="season-recap-backdrop" data-close></div>
<div class="season-recap-content">
<header class="season-recap-header">
<h2><%= t('seasonCard.modalTitlePlayer') %></h2>
<button type="button" class="season-recap-close" data-close aria-label="Close">×</button>
</header>
<div class="season-recap-body">
<div class="season-recap-controls">
<div>
<label for="season-recap-select"><%= t('seasonCard.seasonLabel') %></label>
<select id="season-recap-select"></select>
</div>
<div>
<label><%= t('seasonCard.themeLabel') %></label>
<span class="season-recap-theme-group">
<label><input type="radio" name="season-recap-theme" value="dark" checked> <%= t('seasonCard.themeDark') %></label>
<label><input type="radio" name="season-recap-theme" value="light"> <%= t('seasonCard.themeLight') %></label>
</span>
</div>
<div>
<label>&nbsp;</label>
<button type="button" id="season-recap-generate" class="btn btn-primary"><%= t('seasonCard.generate') %></button>
</div>
</div>
<div id="season-recap-status"></div>
<div id="season-recap-image-wrap">
<img id="season-recap-image" alt="Player season recap card" />
</div>
</div>
</div>
</div>
<script src="/js/recap-modal-player.js" data-uid="<%= playerData.uid || '' %>"></script>
</body>
</html>