2b399fdb81
PR #1223 only staged the deletions of the old paths because the new top-level directories were still untracked when the commit was authored. This commit adds the actual restructured tree: SREBOT/ (existing bot), SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES, update_game_files), and TSSBOT/ (skeleton). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2894 lines
130 KiB
Plaintext
2894 lines
130 KiB
Plaintext
<!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;
|
||
}
|
||
|
||
.stat-card-performance .stat-label {
|
||
margin-top: 0;
|
||
margin-bottom: 0.35rem;
|
||
}
|
||
|
||
.stat-performance-wrap {
|
||
width: 100%;
|
||
margin-top: 0.35rem;
|
||
}
|
||
|
||
.stat-performance-wrap .sq-performance-bar {
|
||
width: 100%;
|
||
}
|
||
|
||
.sq-performance-bar {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.55rem;
|
||
}
|
||
|
||
.sq-performance-bar-track {
|
||
position: relative;
|
||
flex: 1;
|
||
height: 12px;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(144, 238, 144, 0.16);
|
||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.sq-performance-bar-fill {
|
||
height: 100%;
|
||
border-radius: inherit;
|
||
background: linear-gradient(90deg, rgba(245, 245, 220, 0.96), rgba(144, 238, 144, 0.96));
|
||
box-shadow: 0 0 14px rgba(144, 238, 144, 0.25);
|
||
transition: width 0.35s ease;
|
||
}
|
||
|
||
.sq-performance-bar-value {
|
||
min-width: 2.75rem;
|
||
text-align: right;
|
||
font-variant-numeric: tabular-nums;
|
||
font-size: 0.9rem;
|
||
font-weight: 700;
|
||
color: #F5F5DC;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
|
||
.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>
|
||
<% } %>
|
||
|
||
<%
|
||
const performanceLabel = 'ELO';
|
||
const buildPerformanceBarHtml = (score) => `
|
||
<div class="sq-performance-bar" role="progressbar" aria-valuemin="0" aria-valuemax="5" aria-valuenow="${score}" aria-label="${performanceLabel} ${score}">
|
||
<div class="sq-performance-bar-track">
|
||
<div class="sq-performance-bar-fill" style="width:${Math.max(0, Math.min(100, (Number(score) || 0) * 20))}%;"></div>
|
||
</div>
|
||
<span class="sq-performance-bar-value">${Number(score || 0).toFixed(2)}</span>
|
||
</div>
|
||
`;
|
||
const initialPerformance = Math.max(0, Math.min(5, Number(playerData.performance || 0)));
|
||
%>
|
||
<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 stat-card-performance">
|
||
<div class="stat-label">ELO</div>
|
||
<div id="stat-performance" class="stat-performance-wrap"><%- buildPerformanceBarHtml(initialPerformance) %></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:55–07:10 UTC)`;
|
||
sessionSelect.appendChild(naOpt);
|
||
|
||
const euOpt = document.createElement('option');
|
||
euOpt.value = dateStr + '_EU';
|
||
euOpt.textContent = `${label} — EU (13:55–22: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 initialPerformanceRating = <%- JSON.stringify(initialPerformance) %>;
|
||
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, initialPerformanceRating);
|
||
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 || [], data.performance);
|
||
} 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 buildPerformanceBarHtml(score) {
|
||
const rating = Math.max(0, Math.min(5, Number(score) || 0));
|
||
return `
|
||
<div class="sq-performance-bar" role="progressbar" aria-valuemin="0" aria-valuemax="5" aria-valuenow="${rating}" aria-label="ELO ${rating.toFixed(2)}">
|
||
<div class="sq-performance-bar-track">
|
||
<div class="sq-performance-bar-fill" style="width:${rating * 20}%;"></div>
|
||
</div>
|
||
<span class="sq-performance-bar-value">${rating.toFixed(2)}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function updateSummaryStats(vehicles, performanceScore) {
|
||
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;
|
||
const performanceWrap = document.getElementById('stat-performance');
|
||
if (performanceWrap) performanceWrap.innerHTML = buildPerformanceBarHtml(performanceScore ?? initialPerformanceRating);
|
||
}
|
||
|
||
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> </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>
|