theme rework #79
@@ -171,3 +171,6 @@ __pycache__/
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
|
||||
# Temporary scripts
|
||||
temp/
|
||||
@@ -5,12 +5,17 @@ Little nodejs + mongodb app that I hack together with ChatGPT and Cursor to keep
|
||||
|
||||
**Disclaimer:** This was a fun, experimental project built with “AI slop” (Cursor), so it may have security issues. For example, `python_video_server` uses the default token‑in‑URL approach to serve video files, which can leave user tokens in logs or browser history. There are ways to improve this, but at this point I’m blinder than Ray Charles.
|
||||
|
||||

|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="img/preview_dark.png">
|
||||
<img src="img/preview.png" alt="Oscars Pool 2026">
|
||||
</picture>
|
||||
|
||||
## Features
|
||||
|
||||
### User Features
|
||||
- **User accounts**: register, login, JWT authentication
|
||||
- **Dark mode**: follows system preference by default, with a user toggle (persisted per user)
|
||||
- **Mobile-friendly UI**: improved responsive navbar + controls for small screens
|
||||
- **Optional registration verification**: via `DOG_NAMES` environment variable (if unset, verification is skipped)
|
||||
- **Multi-year support**: browse movies and stats by Oscar year (admin controls the active year)
|
||||
- **Checklist + progress**: mark movies as watched (with date), progress bar, countdown to Oscars date
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 534 KiB After Width: | Height: | Size: 477 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 476 KiB |
@@ -9,6 +9,8 @@ const userSchema = new mongoose.Schema({
|
||||
mustChangePassword: { type: Boolean, default: false },
|
||||
tempPasswordIssuedAt: { type: Date, default: null },
|
||||
tempPasswordExpiresAt: { type: Date, default: null },
|
||||
// Optional. When unset, UI follows system preference.
|
||||
themePreference: { type: String, enum: ["light", "dark"], default: undefined },
|
||||
watchedMovies: [{
|
||||
imdb_id: { type: String, required: false },
|
||||
watchedDate: { type: Date, default: Date.now } // Store the watched date
|
||||
|
||||
@@ -4,15 +4,37 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Changer le mot de passe</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">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<button class="theme-toggle theme-toggle--floating" type="button" data-theme-toggle role="switch" aria-checked="false" aria-label="Basculer le mode sombre">
|
||||
<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>
|
||||
|
||||
<div class="container d-flex justify-content-center align-items-center vh-100">
|
||||
<div class="card p-4" style="max-width: 420px; width: 100%;">
|
||||
<div class="text-center mb-3">
|
||||
<img src="img/logo.png" style="width: 80%;">
|
||||
<img src="img/logo.png" class="logo-invert" style="width: 80%;" alt="Pool Oscars">
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
|
||||
+34
-4
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Checklist</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">
|
||||
@@ -61,9 +62,10 @@
|
||||
<!-- Black banner with buttons and user info -->
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" style="margin-left: -7px;" href="movies.html">
|
||||
<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>
|
||||
@@ -76,15 +78,36 @@
|
||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">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">
|
||||
<span id="user-name" class="navbar-text me-3"></span>
|
||||
<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" aria-label="Basculer le mode sombre">
|
||||
<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">Déconnexion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="container mt-4">
|
||||
<h2>Checklist</h2>
|
||||
|
||||
<!-- Summary card (match Stats page spacing/structure) -->
|
||||
@@ -149,8 +172,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer>
|
||||
<p id="footer-copy">Oscar Pool <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_checklist.js"></script>
|
||||
<script src="./js/app_version.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+34
-4
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Admin</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">
|
||||
@@ -32,9 +33,10 @@
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" style="margin-left: -7px;" href="movies.html">
|
||||
<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>
|
||||
@@ -46,15 +48,36 @@
|
||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||
<a href="control.html" class="nav-link btn btn-warning me-2">Admin</a>
|
||||
</div>
|
||||
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center">
|
||||
<span id="user-name" class="navbar-text me-3"></span>
|
||||
<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" aria-label="Basculer le mode sombre">
|
||||
<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">Déconnexion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="container mt-4">
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-0">Admin</h2>
|
||||
<div class="text-muted">Paramètres, utilisateurs et gestion des films.</div>
|
||||
@@ -1465,8 +1488,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer>
|
||||
<p id="footer-copy">Oscar Pool <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_control.js"></script>
|
||||
<script src="./js/app_version.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+783
-11
@@ -1,27 +1,36 @@
|
||||
footer {
|
||||
margin-top: auto;
|
||||
padding: 25px;
|
||||
padding: 20px 25px;
|
||||
/* Light mode: keep the original light gray footer */
|
||||
background-color: #f8f9fa;
|
||||
color: var(--app-text);
|
||||
text-align: center;
|
||||
width: min(100%, calc(1320px - 24px));
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
}
|
||||
#footer-text {
|
||||
margin: -5px;
|
||||
margin: 6px 0 0;
|
||||
font-family:monospace;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
#footer-copy {
|
||||
margin: -5px;
|
||||
margin: 0;
|
||||
font-family:monospace;
|
||||
font-size: smaller;
|
||||
padding-top: 10px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#copy {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#copy::after {
|
||||
content: " 🐴";
|
||||
}
|
||||
|
||||
/* Simple, clean loading indicator (no spinner spam) */
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
@@ -219,6 +228,8 @@ footer {
|
||||
color: rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
/* (reserved) */
|
||||
|
||||
.movie-imdb-block {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -245,16 +256,21 @@ footer {
|
||||
}
|
||||
|
||||
.movie-rating-block .imdb-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.movie-imdb-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.movie-imdb-wrap .movie-rating-stars {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.movie-rating-stars {
|
||||
@@ -267,7 +283,7 @@ footer {
|
||||
.movie-star {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(0,0,0,0.25);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
padding: 0 1px;
|
||||
@@ -331,11 +347,23 @@ footer {
|
||||
}
|
||||
|
||||
.imdb-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
height: 18px;
|
||||
padding: 0 6px;
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5c518;
|
||||
color: #111;
|
||||
border-radius: 4px;
|
||||
font-family: "Arial Black", Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.4px;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.watched-badge {
|
||||
@@ -442,10 +470,25 @@ footer {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.stats-movie-imdb.btn {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.stats-movie-imdb.btn:hover,
|
||||
.stats-movie-imdb.btn:focus {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.stats-movie-imdb .imdb-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
margin-left: 0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Admin → Settings: keep controls compact */
|
||||
@@ -458,3 +501,732 @@ footer {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--app-bg: #f5f6fb;
|
||||
--app-surface: #ffffff;
|
||||
--app-surface-alt: #f1f3f9;
|
||||
--app-text: #111827;
|
||||
--app-text-muted: rgba(17, 24, 39, 0.6);
|
||||
--app-border: rgba(15, 23, 42, 0.12);
|
||||
--app-ring: rgba(37, 99, 235, 0.35);
|
||||
--app-shadow: 0 12px 30px rgba(15, 23, 42, 0.12);
|
||||
--app-overlay: rgba(246, 247, 251, 0.7);
|
||||
--app-loader-card: rgba(255, 255, 255, 0.9);
|
||||
--app-navbar: #0a0b0d;
|
||||
--app-navbar-border: rgba(255, 255, 255, 0.08);
|
||||
--app-link: #1d4ed8;
|
||||
--app-link-hover: #1e40af;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
/* Darker overall background + slightly deeper surfaces */
|
||||
--app-bg: #05070b;
|
||||
--app-surface: #0b1017;
|
||||
--app-surface-alt: #101825;
|
||||
--app-text: #e6e9f0;
|
||||
--app-text-muted: rgba(230, 233, 240, 0.7);
|
||||
--app-border: rgba(255, 255, 255, 0.11);
|
||||
--app-ring: rgba(96, 165, 250, 0.45);
|
||||
--app-shadow: 0 18px 38px rgba(0, 0, 0, 0.45);
|
||||
--app-overlay: rgba(4, 6, 10, 0.78);
|
||||
--app-loader-card: rgba(18, 22, 33, 0.9);
|
||||
--app-navbar: #0b0d12;
|
||||
--app-navbar-border: rgba(255, 255, 255, 0.1);
|
||||
--app-link: #8ab4ff;
|
||||
--app-link-hover: #b1ccff;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--app-bg);
|
||||
color: var(--app-text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body > .container {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
body > footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--app-link);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--app-link-hover);
|
||||
}
|
||||
|
||||
/* Tabs (light mode): keep it neutral (no bright blue) */
|
||||
.nav-tabs .nav-link {
|
||||
color: rgba(17, 24, 39, 0.72);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1px;
|
||||
padding: 0.45rem 0.8rem;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: rgba(17, 24, 39, 0.9);
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
|
||||
body.bg-light {
|
||||
background-color: var(--app-bg) !important;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
background-color: var(--app-surface-alt) !important;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: var(--app-text) !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--app-text-muted) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .logo-invert {
|
||||
filter: invert(1) brightness(1.05);
|
||||
}
|
||||
|
||||
/* Dark mode: footer matches tiles/cards */
|
||||
html[data-theme="dark"] footer {
|
||||
background-color: var(--app-surface);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
footer.footer--plain {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
footer.footer--player {
|
||||
width: min(100%, calc(1100px - 24px));
|
||||
}
|
||||
|
||||
footer.footer--auth {
|
||||
width: min(100%, 400px);
|
||||
}
|
||||
|
||||
.card,
|
||||
.modal-content,
|
||||
.list-group-item,
|
||||
.dropdown-menu {
|
||||
background-color: var(--app-surface);
|
||||
color: var(--app-text);
|
||||
border-color: var(--app-border);
|
||||
box-shadow: var(--app-shadow);
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-footer {
|
||||
border-color: var(--app-border);
|
||||
}
|
||||
|
||||
/* Bootstrap modal close (X) visibility in dark mode */
|
||||
html[data-theme="dark"] .modal-header .btn-close {
|
||||
filter: invert(1) grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .modal-header .btn-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .modal-header .btn-close:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(96, 165, 250, 0.25);
|
||||
}
|
||||
|
||||
.table {
|
||||
--bs-table-color: var(--app-text);
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-striped-bg: rgba(15, 23, 42, 0.05);
|
||||
--bs-table-hover-bg: rgba(15, 23, 42, 0.08);
|
||||
--bs-table-border-color: var(--app-border);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .table {
|
||||
--bs-table-striped-bg: rgba(255, 255, 255, 0.04);
|
||||
--bs-table-hover-bg: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .progress {
|
||||
background-color: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .progress-bar {
|
||||
background-color: #60a5fa;
|
||||
}
|
||||
|
||||
.page-loader-overlay {
|
||||
background: var(--app-overlay);
|
||||
}
|
||||
|
||||
.page-loader-card {
|
||||
background: var(--app-loader-card);
|
||||
border-color: var(--app-border);
|
||||
box-shadow: var(--app-shadow);
|
||||
}
|
||||
|
||||
.page-loader-title,
|
||||
.page-loader-subtitle {
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
.page-loader-progress {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .page-loader-progress {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.page-loader-bar {
|
||||
background: var(--app-text);
|
||||
}
|
||||
|
||||
.navbar-custom {
|
||||
background-color: var(--app-navbar);
|
||||
border-bottom: 1px solid var(--app-navbar-border);
|
||||
}
|
||||
|
||||
/* The logo image has a bit of built-in left whitespace; nudge it back to align with page titles. */
|
||||
.navbar-custom .navbar-brand img {
|
||||
margin-left: -9px;
|
||||
}
|
||||
|
||||
/* Mobile navbar: add vertical gaps + clean user row */
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar-custom .navbar-nav {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar-custom .navbar-nav .nav-link.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-right: 0 !important; /* overrides me-2 on mobile */
|
||||
}
|
||||
|
||||
.navbar-custom .navbar-user {
|
||||
width: 100%;
|
||||
margin-top: 0.85rem !important;
|
||||
padding-top: 0.85rem;
|
||||
/* Mobile divider between nav buttons and user row */
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.18);
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbar-custom .navbar-user-name-mobile {
|
||||
max-width: 42vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 0.75rem;
|
||||
margin-right: 0.75rem !important; /* extra gap before hamburger */
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
/* Avoid showing the username twice when the menu is open */
|
||||
.navbar-custom .navbar-user #user-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-custom .navbar-user .theme-toggle {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .navbar-custom .btn-outline-light {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .navbar-custom .btn-outline-light:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .btn-outline-dark {
|
||||
color: var(--app-text);
|
||||
border-color: var(--app-border);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .btn-outline-dark:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .badge.bg-warning.text-dark {
|
||||
color: #1f2937 !important;
|
||||
}
|
||||
|
||||
/* Use a consistent "admin" blue instead of Bootstrap warning yellow. */
|
||||
.btn-warning {
|
||||
background-color: #1d4ed8;
|
||||
border-color: #1d4ed8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #1e40af;
|
||||
border-color: #1e40af;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-warning:focus,
|
||||
.btn-warning:focus-visible {
|
||||
box-shadow: 0 0 0 0.2rem rgba(29, 78, 216, 0.35);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .badge.bg-light {
|
||||
background-color: var(--app-surface-alt) !important;
|
||||
color: var(--app-text) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .badge.bg-dark {
|
||||
background-color: #0f172a !important;
|
||||
}
|
||||
|
||||
.movie-info-box {
|
||||
background: var(--app-surface-alt);
|
||||
border-color: var(--app-border);
|
||||
}
|
||||
|
||||
.movie-rating-block,
|
||||
.movie-imdb-block,
|
||||
.movie-rating-sub,
|
||||
.movie-info-summary,
|
||||
.movie-info-category,
|
||||
.movie-info-desc,
|
||||
.stats-movie-details summary {
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-star {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-star.is-active {
|
||||
color: #f4b400;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .stats-movie-item {
|
||||
background: var(--app-surface);
|
||||
border-color: var(--app-border);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .stats-movie-item--clickable:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .admin-subtabs.nav-pills .nav-link {
|
||||
background: var(--app-surface);
|
||||
border-color: var(--app-border);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .admin-subtabs.nav-pills .nav-link:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .admin-subtabs.nav-pills .nav-link.active {
|
||||
background: #4f7cff;
|
||||
border-color: #4f7cff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .nominee-option:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .nominee-option.selected {
|
||||
box-shadow: 0 2px 8px rgba(79, 124, 255, 0.35);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .nominee-checkmark {
|
||||
background-color: rgba(255, 255, 255, 0.18);
|
||||
color: #cfe1ff;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .nominee-option.selected .nominee-checkmark {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .picks-table tbody tr:hover {
|
||||
background-color: rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .picks-table tbody tr.has-selection {
|
||||
background-color: rgba(99, 102, 241, 0.18);
|
||||
border-left-color: #6ea8fe;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .pick-item {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .correct-pick {
|
||||
background-color: rgba(16, 185, 129, 0.18);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .incorrect-pick {
|
||||
background-color: rgba(248, 113, 113, 0.18);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .meta-muted {
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .player-surface {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .quality-toggle {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .quality-label {
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .quality-pill {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .quality-btn {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .quality-btn.is-active {
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-table thead th,
|
||||
html[data-theme="dark"] .user-table thead th {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .user-table tbody td:first-child,
|
||||
html[data-theme="dark"] .user-table tbody td:nth-child(3) {
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .stats-tabs .nav-link {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .stats-tabs .nav-link:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .stats-tabs .nav-link.active {
|
||||
color: #fff;
|
||||
background: rgba(79, 124, 255, 0.18);
|
||||
border-color: rgba(79, 124, 255, 0.4);
|
||||
}
|
||||
|
||||
.user-table,
|
||||
#pane-winners .table,
|
||||
#pane-completion .table {
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #pane-winners .table,
|
||||
html[data-theme="dark"] #pane-completion .table {
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #pane-winners .table thead th,
|
||||
html[data-theme="dark"] #pane-winners .table tbody td,
|
||||
html[data-theme="dark"] #pane-completion .table thead th,
|
||||
html[data-theme="dark"] #pane-completion .table tbody td {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .user-table a {
|
||||
color: #b9e3ff;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .user-table a:hover {
|
||||
color: #d7efff;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabs .nav-link {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabs .nav-link:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabs .nav-link.active {
|
||||
color: #fff;
|
||||
background: rgba(79, 124, 255, 0.18);
|
||||
border-color: rgba(79, 124, 255, 0.4);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabsContent .table {
|
||||
--bs-table-color: var(--app-text);
|
||||
--bs-table-striped-color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabsContent .table thead th {
|
||||
background-color: var(--app-surface-alt);
|
||||
color: var(--app-text);
|
||||
border-color: var(--app-border);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabsContent .table tbody td {
|
||||
color: rgba(230, 235, 245, 0.92);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabsContent .table .table-success {
|
||||
--bs-table-bg: rgba(96, 165, 250, 0.14);
|
||||
--bs-table-striped-bg: rgba(96, 165, 250, 0.1);
|
||||
--bs-table-hover-bg: rgba(96, 165, 250, 0.18);
|
||||
color: rgba(230, 235, 245, 0.92);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .bg-white {
|
||||
background-color: var(--app-surface) !important;
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .border {
|
||||
border-color: var(--app-border) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabs,
|
||||
html[data-theme="dark"] #editMovieTabs {
|
||||
border-bottom-color: var(--app-border);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabs .nav-link,
|
||||
html[data-theme="dark"] #editMovieTabs .nav-link {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabs .nav-link:hover,
|
||||
html[data-theme="dark"] #editMovieTabs .nav-link:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabs .nav-link.active,
|
||||
html[data-theme="dark"] #editMovieTabs .nav-link.active {
|
||||
color: #fff;
|
||||
background: rgba(79, 124, 255, 0.18);
|
||||
border-color: rgba(79, 124, 255, 0.4);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabsContent .table,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .table {
|
||||
--bs-table-color: var(--app-text);
|
||||
--bs-table-striped-color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabsContent .table thead th,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .table thead th {
|
||||
background-color: var(--app-surface-alt);
|
||||
color: var(--app-text);
|
||||
border-color: var(--app-border);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabsContent .table tbody td,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .table tbody td {
|
||||
color: rgba(230, 235, 245, 0.92);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabsContent .table .text-muted,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .table .text-muted {
|
||||
color: rgba(230, 235, 245, 0.65) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabsContent .table a,
|
||||
html[data-theme="dark"] #adminTabsContent .table .btn-link,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .table a,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .table .btn-link {
|
||||
color: rgba(230, 235, 245, 0.92);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #adminTabsContent .admin-movies-table td,
|
||||
html[data-theme="dark"] #adminTabsContent .admin-movies-table th,
|
||||
html[data-theme="dark"] #adminTabsContent .admin-movies-table .text-muted,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .admin-movies-table td,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .admin-movies-table th,
|
||||
html[data-theme="dark"] #adminTabs + .tab-content .admin-movies-table .text-muted {
|
||||
color: rgba(230, 235, 245, 0.92) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabsContent .btn-outline-primary {
|
||||
color: rgba(226, 232, 240, 0.95);
|
||||
border-color: rgba(148, 163, 184, 0.6);
|
||||
background: rgba(15, 23, 42, 0.2);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #picksTabsContent .btn-outline-primary:hover {
|
||||
color: #fff;
|
||||
border-color: rgba(148, 163, 184, 0.85);
|
||||
background: rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-table .table-success {
|
||||
--bs-table-bg: rgba(96, 165, 250, 0.14);
|
||||
--bs-table-striped-bg: rgba(96, 165, 250, 0.1);
|
||||
--bs-table-hover-bg: rgba(96, 165, 250, 0.18);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-table .table-success td {
|
||||
border-color: rgba(96, 165, 250, 0.22);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-table .table-danger {
|
||||
--bs-table-bg: rgba(148, 163, 184, 0.12);
|
||||
--bs-table-striped-bg: rgba(148, 163, 184, 0.09);
|
||||
--bs-table-hover-bg: rgba(148, 163, 184, 0.16);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-table .table-danger td {
|
||||
border-color: rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-table {
|
||||
--bs-table-color: var(--app-text);
|
||||
--bs-table-striped-color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .movie-table tbody td {
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .form-control,
|
||||
html[data-theme="dark"] .form-select,
|
||||
html[data-theme="dark"] .form-check-input {
|
||||
background-color: var(--app-surface);
|
||||
border-color: var(--app-border);
|
||||
color: var(--app-text);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .form-control:focus,
|
||||
html[data-theme="dark"] .form-select:focus,
|
||||
html[data-theme="dark"] .form-check-input:focus {
|
||||
border-color: rgba(96, 165, 250, 0.6);
|
||||
box-shadow: 0 0 0 0.2rem rgba(96, 165, 250, 0.25);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 30px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--app-border);
|
||||
background: var(--app-surface-alt);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
|
||||
transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
/* Make spacing between toggle and logout feel equal to username→toggle */
|
||||
nav.navbar .theme-toggle {
|
||||
margin-right: 0.45rem;
|
||||
}
|
||||
|
||||
.theme-toggle:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--app-ring);
|
||||
}
|
||||
|
||||
.theme-toggle-icon {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--app-text-muted);
|
||||
transition: color 160ms ease;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-toggle-icon svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.theme-toggle-icon-sun {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.theme-toggle-icon-moon {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.theme-toggle-thumb {
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--app-text);
|
||||
transform: translateX(-12px);
|
||||
transition: transform 180ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .theme-toggle {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .theme-toggle-icon {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .theme-toggle-thumb {
|
||||
transform: translateX(12px);
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.theme-toggle--floating {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 1060;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.theme-toggle--floating {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
}
|
||||
+23
-9
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars</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">
|
||||
<style>
|
||||
@@ -23,11 +24,32 @@
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<button class="theme-toggle theme-toggle--floating" type="button" data-theme-toggle role="switch" aria-checked="false" aria-label="Basculer le mode sombre">
|
||||
<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>
|
||||
|
||||
<div class="container">
|
||||
<div class="card p-4" style="max-width: 400px; margin: auto;">
|
||||
<div class="text-center mb-4">
|
||||
<img src="img/logo.png" style="width: 80%;">
|
||||
<img src="img/logo.png" class="logo-invert" style="width: 80%;" alt="Pool Oscars">
|
||||
</div>
|
||||
<form id="login-form">
|
||||
<div class="mb-3">
|
||||
@@ -43,18 +65,10 @@
|
||||
<p class="mt-3 text-center">Pas de compte? <a href="/register.html">Enregistrez-vous</a></p>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer>
|
||||
|
||||
<p id="footer-copy"><a id="copy" href="https://github.com/quoije/oscars-pool">quoije</a></p>
|
||||
<p id="footer-text"></p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="./js/app_index.js"></script>
|
||||
<script src="./js/app_version.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -41,9 +41,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const name = decoded.name || '';
|
||||
const nameTargets = document.querySelectorAll('[data-user-name]');
|
||||
if (nameTargets.length) {
|
||||
nameTargets.forEach((el) => {
|
||||
if (el && !el.textContent) el.textContent = name;
|
||||
});
|
||||
} else {
|
||||
const nameEl = document.getElementById('user-name');
|
||||
if (nameEl && !nameEl.textContent) {
|
||||
nameEl.textContent = decoded.name || '';
|
||||
nameEl.textContent = name;
|
||||
}
|
||||
}
|
||||
|
||||
const adminLink = document.getElementById('admin-control-link');
|
||||
|
||||
@@ -579,7 +579,7 @@ window.addEventListener('DOMContentLoaded', async function () {
|
||||
<div class="movie-imdb-block">
|
||||
${movie.rating ? `<span>⭐ ${movie.rating}</span>` : ''}
|
||||
<a href="https://www.imdb.com/title/${movie.imdb_id}/" target="_blank" rel="noopener noreferrer" aria-label="Voir sur IMDb">
|
||||
<img src="/img/imdb.png" class="imdb-icon" alt="IMDb">
|
||||
<span class="imdb-icon" aria-hidden="true">IMDb</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="movie-rating-stars" role="group" aria-label="Noter ce film" data-movie-id="${movieId}" data-current-rating="${Number.isFinite(userRating) ? userRating : ''}">
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Theme toggle with system preference fallback.
|
||||
(function () {
|
||||
const STORAGE_KEY = 'theme_preference';
|
||||
const THEME_ATTR = 'data-theme';
|
||||
const SOURCE_ATTR = 'data-theme-source';
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const API_ENDPOINT = '/api/users/theme';
|
||||
|
||||
function normalizeTheme(value) {
|
||||
return value === 'light' || value === 'dark' ? value : null;
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
return mediaQuery.matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function setTheme(theme, source) {
|
||||
document.documentElement.setAttribute(THEME_ATTR, theme);
|
||||
document.documentElement.setAttribute(SOURCE_ATTR, source);
|
||||
updateToggleUi(theme, source);
|
||||
}
|
||||
|
||||
function getAuthToken() {
|
||||
try {
|
||||
return localStorage.getItem('auth_token');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePreferenceToServer(themePreference) {
|
||||
const token = getAuthToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
await fetch(API_ENDPOINT, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ themePreference: themePreference ?? null })
|
||||
});
|
||||
} catch (_) {
|
||||
// Ignore network errors to avoid blocking UI.
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPreferenceFromServer() {
|
||||
const token = getAuthToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch(API_ENDPOINT, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
cache: 'no-store'
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const pref = normalizeTheme(data?.themePreference);
|
||||
if (pref) {
|
||||
localStorage.setItem(STORAGE_KEY, pref);
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
applyStoredOrSystem();
|
||||
} catch (_) {
|
||||
// Ignore fetch errors.
|
||||
}
|
||||
}
|
||||
|
||||
function applyStoredOrSystem() {
|
||||
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
|
||||
const theme = stored || getSystemTheme();
|
||||
const source = stored ? 'user' : 'system';
|
||||
setTheme(theme, source);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
|
||||
const current = stored || getSystemTheme();
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem(STORAGE_KEY, next);
|
||||
setTheme(next, 'user');
|
||||
savePreferenceToServer(next);
|
||||
}
|
||||
|
||||
function resetToSystem() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
applyStoredOrSystem();
|
||||
savePreferenceToServer(null);
|
||||
}
|
||||
|
||||
function updateToggleUi(theme, source) {
|
||||
const buttons = document.querySelectorAll('[data-theme-toggle]');
|
||||
const isDark = theme === 'dark';
|
||||
buttons.forEach((button) => {
|
||||
button.setAttribute('aria-checked', String(isDark));
|
||||
button.dataset.theme = theme;
|
||||
const label = isDark ? 'Mode sombre' : 'Mode clair';
|
||||
const sourceLabel = source === 'system' ? ' (système)' : '';
|
||||
button.setAttribute('title', `${label}${sourceLabel} — Shift+clic pour système`);
|
||||
});
|
||||
}
|
||||
|
||||
applyStoredOrSystem();
|
||||
syncPreferenceFromServer();
|
||||
|
||||
mediaQuery.addEventListener('change', () => {
|
||||
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
|
||||
if (!stored) {
|
||||
applyStoredOrSystem();
|
||||
}
|
||||
});
|
||||
|
||||
function bindToggleButtons() {
|
||||
document.querySelectorAll('[data-theme-toggle]').forEach((button) => {
|
||||
if (button.dataset.themeBound === 'true') return;
|
||||
button.dataset.themeBound = 'true';
|
||||
button.addEventListener('click', (event) => {
|
||||
if (event.shiftKey) {
|
||||
resetToSystem();
|
||||
return;
|
||||
}
|
||||
toggleTheme();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bindToggleButtons);
|
||||
} else {
|
||||
bindToggleButtons();
|
||||
}
|
||||
})();
|
||||
@@ -515,16 +515,11 @@ window.addEventListener('DOMContentLoaded', async function () {
|
||||
imdbLink.rel = 'noopener noreferrer';
|
||||
imdbLink.setAttribute('aria-label', 'Voir sur IMDb');
|
||||
|
||||
const imdbText = document.createElement('span');
|
||||
imdbText.className = 'small';
|
||||
imdbText.textContent = 'IMDb';
|
||||
|
||||
const imdbIcon = document.createElement('img');
|
||||
imdbIcon.src = '/img/imdb.png';
|
||||
const imdbIcon = document.createElement('span');
|
||||
imdbIcon.className = 'imdb-icon';
|
||||
imdbIcon.alt = 'IMDb';
|
||||
imdbIcon.setAttribute('aria-hidden', 'true');
|
||||
imdbIcon.textContent = 'IMDb';
|
||||
|
||||
imdbLink.appendChild(imdbText);
|
||||
imdbLink.appendChild(imdbIcon);
|
||||
right.appendChild(imdbLink);
|
||||
}
|
||||
|
||||
+34
-4
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Films</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">
|
||||
@@ -48,9 +49,10 @@
|
||||
<!-- Black banner with buttons and user info -->
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" style="margin-left: -7px;" href="movies.html">
|
||||
<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>
|
||||
@@ -63,15 +65,36 @@
|
||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">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">
|
||||
<span id="user-name" class="navbar-text me-3"></span>
|
||||
<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" aria-label="Basculer le mode sombre">
|
||||
<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">Déconnexion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5 pb-5">
|
||||
<div class="container mt-4 pb-5">
|
||||
<h2>Films</h2>
|
||||
|
||||
<!-- Summary card (match Statistiques/Checklist structure) -->
|
||||
@@ -104,8 +127,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer>
|
||||
<p id="footer-copy">Oscar Pool <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_movies.js"></script>
|
||||
<script src="./js/app_version.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+34
-4
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Mes picks</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">
|
||||
@@ -121,9 +122,10 @@
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" style="margin-left: -7px;" href="movies.html">
|
||||
<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>
|
||||
@@ -135,15 +137,36 @@
|
||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
||||
</div>
|
||||
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center">
|
||||
<span id="user-name" class="navbar-text me-3"></span>
|
||||
<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" aria-label="Basculer le mode sombre">
|
||||
<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">Déconnexion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5 pb-5">
|
||||
<div class="container mt-4 pb-5">
|
||||
<h2>Mes picks Oscars</h2>
|
||||
|
||||
<div id="alert-container"></div>
|
||||
@@ -241,8 +264,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer>
|
||||
<p id="footer-copy">Oscar Pool <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.js"></script>
|
||||
<script src="./js/app_picks.js"></script>
|
||||
<script src="./js/app_version.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+43
-5
@@ -6,6 +6,7 @@
|
||||
<!-- IMPORTANT: the player may use ?token=... for cross-origin video streaming -->
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<title>Pool Oscars</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">
|
||||
@@ -123,7 +124,7 @@
|
||||
.star-btn {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: rgba(0,0,0,0.25);
|
||||
color: rgba(0,0,0,0.5);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
padding: .15rem .2rem;
|
||||
@@ -131,6 +132,14 @@
|
||||
.star-btn.is-active {
|
||||
color: #f4b400;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .star-btn {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .star-btn.is-active {
|
||||
color: #f4b400;
|
||||
}
|
||||
.star-btn:focus-visible {
|
||||
outline: 2px solid rgba(13,110,253,.35);
|
||||
outline-offset: 2px;
|
||||
@@ -194,9 +203,10 @@
|
||||
<!-- Black banner with buttons and user info -->
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" style="margin-left: -7px;" href="movies.html">
|
||||
<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>
|
||||
@@ -208,15 +218,36 @@
|
||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
||||
</div>
|
||||
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center gap-2">
|
||||
<span id="user-name" class="navbar-text me-1"></span>
|
||||
<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" aria-label="Basculer le mode sombre">
|
||||
<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">Déconnexion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5 pb-4 player-wrap">
|
||||
<div class="container mt-4 pb-4 player-wrap">
|
||||
<div id="alert" class="alert alert-danger d-none" role="alert"></div>
|
||||
|
||||
<!-- Header / metadata -->
|
||||
@@ -288,10 +319,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer class="footer--player">
|
||||
<p id="footer-copy">Oscar Pool <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>
|
||||
<!-- HLS support (m3u8) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js"></script>
|
||||
<script src="./js/app_player.js"></script>
|
||||
<script src="./js/app_version.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
+23
-1
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Enregistrement</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">
|
||||
</head>
|
||||
@@ -16,11 +17,32 @@
|
||||
}
|
||||
</style>
|
||||
<body class="bg-light">
|
||||
<button class="theme-toggle theme-toggle--floating" type="button" data-theme-toggle role="switch" aria-checked="false" aria-label="Basculer le mode sombre">
|
||||
<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>
|
||||
|
||||
<div class="container d-flex justify-content-center align-items-center vh-100">
|
||||
<div class="card p-4" style="max-width: 400px;">
|
||||
<div class="text-center mb-4">
|
||||
<img src="img/logo.png" style="width: 80%";>
|
||||
<img src="img/logo.png" class="logo-invert" style="width: 80%;" alt="Pool Oscars">
|
||||
</div>
|
||||
<form id="register-form">
|
||||
<div class="mb-3">
|
||||
|
||||
+35
-5
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Statistiques des utilisateurs</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">
|
||||
@@ -32,7 +33,7 @@
|
||||
|
||||
/* Stats tabs */
|
||||
.stats-tabs .nav-link {
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
}
|
||||
.stats-tab-panel {
|
||||
padding-top: 1rem;
|
||||
@@ -54,9 +55,10 @@
|
||||
<!-- Black banner with buttons and user info -->
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" style="margin-left: -7px;" href="movies.html">
|
||||
<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>
|
||||
@@ -69,15 +71,36 @@
|
||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">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">
|
||||
<span id="user-name" class="navbar-text me-3"></span>
|
||||
<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" aria-label="Basculer le mode sombre">
|
||||
<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">Déconnexion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="container mt-4">
|
||||
<h2>Statistiques des utilisateurs</h2>
|
||||
|
||||
<!-- Tabs -->
|
||||
@@ -185,8 +208,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer>
|
||||
<p id="footer-copy">Oscar Pool <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>
|
||||
+34
-4
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pool Oscars - Voir Tous les Choix</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">
|
||||
@@ -48,9 +49,10 @@
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-custom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" style="margin-left: -7px;" href="movies.html">
|
||||
<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>
|
||||
@@ -63,15 +65,36 @@
|
||||
<a href="control.html" class="nav-link btn btn-warning me-2">Admin</a>
|
||||
<a href="view-picks.html" class="nav-link btn btn-info me-2 active">Voir Tous les Choix</a>
|
||||
</div>
|
||||
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center">
|
||||
<span id="user-name" class="navbar-text me-3"></span>
|
||||
<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" aria-label="Basculer le mode sombre">
|
||||
<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">Déconnexion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5 pb-5">
|
||||
<div class="container mt-4 pb-5">
|
||||
<h2>Voir Tous les Choix</h2>
|
||||
|
||||
<div id="alert-container"></div>
|
||||
@@ -100,8 +123,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Section -->
|
||||
<footer>
|
||||
<p id="footer-copy">Oscar Pool <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.js"></script>
|
||||
<script src="./js/app_view-picks.js"></script>
|
||||
<script src="./js/app_version.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -93,6 +93,13 @@ function signUserToken(user) {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeThemePreference(value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
const raw = String(value || "").trim().toLowerCase();
|
||||
if (raw === "light" || raw === "dark") return raw;
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateTempPassword() {
|
||||
// 12 chars, URL-safe (no spaces/specials that break copy/paste)
|
||||
return crypto.randomBytes(9).toString("base64url");
|
||||
@@ -419,6 +426,40 @@ router.post("/login", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// User: get theme preference
|
||||
router.get("/theme", verifyToken, async (req, res) => {
|
||||
try {
|
||||
res.set("Cache-Control", "private, no-store, must-revalidate");
|
||||
res.set("Vary", "Authorization");
|
||||
|
||||
const user = await User.findById(req.user.id).select("themePreference").lean();
|
||||
if (!user) return res.status(404).json({ message: "User not found" });
|
||||
return res.status(200).json({ themePreference: user.themePreference || null });
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: err?.message || "Erreur interne" });
|
||||
}
|
||||
});
|
||||
|
||||
// User: update theme preference (light/dark or null for system)
|
||||
router.put("/theme", verifyToken, async (req, res) => {
|
||||
try {
|
||||
const preference = normalizeThemePreference(req.body?.themePreference);
|
||||
const provided = req.body?.themePreference;
|
||||
if (provided !== null && provided !== undefined && preference === null) {
|
||||
return res.status(400).json({ message: "Préférence invalide (light/dark/null)." });
|
||||
}
|
||||
|
||||
const update = preference ? { $set: { themePreference: preference } } : { $unset: { themePreference: 1 } };
|
||||
|
||||
const user = await User.findByIdAndUpdate(req.user.id, update, { new: true, select: "themePreference" }).lean();
|
||||
|
||||
if (!user) return res.status(404).json({ message: "User not found" });
|
||||
return res.status(200).json({ themePreference: user.themePreference || null });
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: err?.message || "Erreur interne" });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin: reset a user's password to a random temp password and force change on next login.
|
||||
router.post("/admin/reset-temp-password", verifyToken, async (req, res) => {
|
||||
if (!isAdminFromDecoded(req.user)) {
|
||||
|
||||
Reference in New Issue
Block a user