Introduce client-side localization helper and en/fr locale bundles. Route admin control, picks, movies, stats, winners and settings UIs through i18n keys instead of hardcoded text. Remove French text from the English UI and standardize shared messages (saving, network errors, confirmations). Update .gitignore to ignore editor metadata and localization helper artifacts.
291 lines
12 KiB
HTML
291 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title data-i18n="stats.title">Oscar Pool - Statistics</title>
|
|
<script src="./js/app_theme.js"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="./css/app.css" rel="stylesheet">
|
|
<link rel="preload" as="image" href="img/logo_w.png">
|
|
<style>
|
|
/* Hide translatable content until translations are ready - prevents flash during page navigation */
|
|
body:not(.i18n-ready) [data-i18n] {
|
|
opacity: 0;
|
|
transition: opacity 0.1s ease-in;
|
|
}
|
|
body.i18n-ready [data-i18n] {
|
|
opacity: 1;
|
|
}
|
|
/* Fallback: show content after 2 seconds even if JS fails */
|
|
body:not(.i18n-ready) [data-i18n] {
|
|
animation: show-after-delay 0.1s 2s forwards;
|
|
}
|
|
@keyframes show-after-delay {
|
|
to { opacity: 1; }
|
|
}
|
|
</style>
|
|
<script src="./js/localization.js" defer></script>
|
|
<script src="./js/app_header_boot.js" defer></script>
|
|
<style>
|
|
.navbar-custom {
|
|
background-color: black;
|
|
color: white;
|
|
}
|
|
.navbar-custom .navbar-brand,
|
|
.navbar-custom .nav-link {
|
|
color: white;
|
|
}
|
|
.navbar-custom .nav-link:hover {
|
|
color: #ddd;
|
|
}
|
|
.user-table {
|
|
margin-top: 20px;
|
|
}
|
|
.navbar-toggler {
|
|
border-color: white; /* Make the border white */
|
|
}
|
|
.navbar-toggler-icon {
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='white' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
/* Stats tabs */
|
|
.stats-tabs .nav-link {
|
|
font-weight: 400;
|
|
}
|
|
.stats-tabs {
|
|
border-bottom: 0;
|
|
gap: 0.35rem;
|
|
}
|
|
.stats-tab-panel {
|
|
padding-top: 0;
|
|
background-color: var(--app-surface);
|
|
padding: 0.75rem;
|
|
margin-top: -1px;
|
|
border: 1px solid var(--app-border);
|
|
border-top: 0;
|
|
border-radius: 0 0 12px 12px;
|
|
}
|
|
.stats-tab-panel {
|
|
border-top: 1px solid var(--app-border);
|
|
}
|
|
#statsTabs .nav-link.active {
|
|
position: relative;
|
|
z-index: 2;
|
|
top: 1px;
|
|
border-bottom-color: transparent;
|
|
}
|
|
#statsTabsContent > .tab-pane.active {
|
|
position: relative;
|
|
}
|
|
#statsTabsContent > .tab-pane.active::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 2px;
|
|
width: calc(100% + 2px);
|
|
margin-left: -1px;
|
|
background: var(--app-surface);
|
|
pointer-events: none;
|
|
}
|
|
#statsTabsContent > .tab-pane.fade::before {
|
|
background: transparent;
|
|
}
|
|
.stats-tab-panel > .tab-pane:not(#pane-ranking) {
|
|
padding-top: 1rem;
|
|
}
|
|
#pane-ranking .user-table {
|
|
margin-top: 0.25rem;
|
|
}
|
|
.stats-tab-panel .table,
|
|
.stats-tab-panel .table th,
|
|
.stats-tab-panel .table td {
|
|
font-size: 0.9rem;
|
|
}
|
|
html[data-theme="dark"] .stats-tabs .nav-link.active {
|
|
border-bottom-color: transparent;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-light page-loading">
|
|
|
|
<!-- Preloaded loader overlay to avoid initial "clear" flash -->
|
|
<div id="page-loader-overlay" class="page-loader-overlay" data-preloaded="true" role="status" aria-live="polite" aria-busy="true">
|
|
<div class="page-loader-card page-loader-card--baronly">
|
|
<span class="visually-hidden" data-i18n="common.loading">Loading…</span>
|
|
<div class="page-loader-progress" aria-hidden="true">
|
|
<div class="page-loader-bar" id="page-loader-bar" style="width: 8%;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Black banner with buttons and user info -->
|
|
<nav class="navbar navbar-expand-lg navbar-custom">
|
|
<div class="container">
|
|
<a class="navbar-brand" href="movies.html">
|
|
<img src="img/logo_w.png" width="220" height="70" loading="eager" fetchpriority="high" decoding="async" style="max-width: 220px; height: 70px; object-fit: contain;">
|
|
</a>
|
|
<span class="navbar-text ms-auto me-2 d-lg-none navbar-user-name-mobile" data-user-name></span>
|
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="collapse navbar-collapse" id="navbarNav">
|
|
<div class="navbar-nav">
|
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2" data-i18n="nav.films">Movies</a>
|
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2" data-i18n="nav.checklist">Checklist</a>
|
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2" data-i18n="nav.stats">Stats</a>
|
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;" data-i18n="nav.myPicks">My picks</a>
|
|
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none" data-i18n="nav.admin">Admin</a>
|
|
</div>
|
|
<!-- Move user info inside collapsible navbar and align it properly -->
|
|
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center gap-2 navbar-user">
|
|
<span id="user-name" class="navbar-text" data-user-name></span>
|
|
<button class="theme-toggle" type="button" data-theme-toggle role="switch" aria-checked="false" data-i18n-aria-label="common.toggleDarkMode" aria-label="Toggle dark mode">
|
|
<span class="theme-toggle-icon theme-toggle-icon-sun" aria-hidden="true">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="4"></circle>
|
|
<path d="M12 2v2"></path>
|
|
<path d="M12 20v2"></path>
|
|
<path d="M4.93 4.93l1.41 1.41"></path>
|
|
<path d="M17.66 17.66l1.41 1.41"></path>
|
|
<path d="M2 12h2"></path>
|
|
<path d="M20 12h2"></path>
|
|
<path d="M4.93 19.07l1.41-1.41"></path>
|
|
<path d="M17.66 6.34l1.41-1.41"></path>
|
|
</svg>
|
|
</span>
|
|
<span class="theme-toggle-thumb" aria-hidden="true"></span>
|
|
<span class="theme-toggle-icon theme-toggle-icon-moon" aria-hidden="true">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
<button id="log-off" class="btn btn-danger" data-i18n="nav.logout">Log out</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container mt-4">
|
|
<!-- Tabs -->
|
|
<ul class="nav nav-tabs stats-tabs" id="statsTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="tab-ranking" data-bs-toggle="tab" data-bs-target="#pane-ranking" type="button" role="tab" aria-controls="pane-ranking" aria-selected="true">
|
|
<span data-i18n="stats.ranking">Ranking</span>
|
|
<span id="stats-active-year-badge" class="badge bg-dark ms-2 d-none" aria-label="Active year"></span>
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="tab-winners" data-bs-toggle="tab" data-bs-target="#pane-winners" type="button" role="tab" aria-controls="pane-winners" aria-selected="false" data-i18n="stats.winners">
|
|
Winners 🏆
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="tab-completion" data-bs-toggle="tab" data-bs-target="#pane-completion" type="button" role="tab" aria-controls="pane-completion" aria-selected="false" data-i18n="stats.completers">
|
|
100%
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content stats-tab-panel" id="statsTabsContent">
|
|
<!-- Ranking tab -->
|
|
<div class="tab-pane fade show active" id="pane-ranking" role="tabpanel" aria-labelledby="tab-ranking" tabindex="0">
|
|
<div id="stats-error" class="alert alert-danger d-none" role="alert"></div>
|
|
|
|
<!-- Table for users and movies watched -->
|
|
<table class="table table-striped table-hover align-middle user-table d-none">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" data-i18n="stats.rank">#</th>
|
|
<th scope="col" data-i18n="stats.name">Name</th>
|
|
<th scope="col" id="films-header" data-i18n="stats.films">Movies</th>
|
|
<th scope="col" id="bon-picks-header" data-i18n="stats.correctPicks">Correct picks</th>
|
|
<th scope="col" class="text-center" data-i18n="stats.points">Points</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="user-table-body">
|
|
<!-- User rows will be inserted here -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Winners tab -->
|
|
<div class="tab-pane fade" id="pane-winners" role="tabpanel" aria-labelledby="tab-winners" tabindex="0">
|
|
<div class="table-responsive mb-4">
|
|
<table class="table table-sm table-striped align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" style="width: 110px;" data-i18n="stats.year">Year</th>
|
|
<th scope="col" data-i18n="stats.winnersColumn">Winner(s)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="winners-table-body">
|
|
<tr>
|
|
<td colspan="2" class="text-center py-4 text-muted" aria-live="polite" aria-busy="true" data-i18n="common.loading">Loading…</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 100% completers tab -->
|
|
<div class="tab-pane fade" id="pane-completion" role="tabpanel" aria-labelledby="tab-completion" tabindex="0">
|
|
<div class="small text-muted mb-2 d-flex align-items-center gap-2">
|
|
<span class="badge bg-light text-dark border" aria-hidden="true">i</span>
|
|
<span data-i18n="stats.completersHelp">Users who have watched 100% (by year)</span>
|
|
</div>
|
|
<div class="table-responsive mb-4">
|
|
<table class="table table-sm table-striped align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col" style="width: 110px;" data-i18n="stats.year">Year</th>
|
|
<th scope="col" style="width: 140px;" data-i18n="stats.totalFilms">Total films</th>
|
|
<th scope="col" data-i18n="stats.names">Names</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="completers-table-body">
|
|
<tr>
|
|
<td colspan="3" class="text-center py-4 text-muted" aria-live="polite" aria-busy="true" data-i18n="common.loading">Loading…</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal for displaying movies watched -->
|
|
<div class="modal fade" id="moviesModal" tabindex="-1" aria-labelledby="moviesModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="moviesModalLabel" data-i18n="stats.moviesWatched">Movies watched</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="d-flex flex-column gap-3">
|
|
<div id="movies-summary" class="text-muted small"></div>
|
|
|
|
<div id="movies-empty" class="text-muted d-none" data-i18n="stats.noMoviesToShow">No movies to show.</div>
|
|
<div id="movie-grid" class="stats-movies-grid" aria-live="polite"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer Section -->
|
|
<footer>
|
|
<p id="footer-copy"><span data-i18n="footer.oscarPool">Oscar Pool</span> <span aria-hidden="true">·</span> <a id="copy" href="https://github.com/quoije/oscars-pool">quoije</a></p>
|
|
<p id="footer-text"></p>
|
|
</footer>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="./js/app_user-stat.js"></script>
|
|
<script src="./js/app_version.js"></script>
|
|
|
|
</body>
|
|
</html>
|