diff --git a/.gitignore b/.gitignore index b78d298..7fddb09 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,7 @@ __pycache__/ *.pyc *.pyo *.pyd -.Python \ No newline at end of file +.Python + +# Temporary scripts +temp/ \ No newline at end of file diff --git a/README.md b/README.md index 73a3602..e982623 100644 --- a/README.md +++ b/README.md @@ -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. -![Oscars Pool 2026](img/preview.png) + + + Oscars Pool 2026 + ## 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 diff --git a/img/preview.png b/img/preview.png index 64713ce..5380dbf 100644 Binary files a/img/preview.png and b/img/preview.png differ diff --git a/img/preview_dark.png b/img/preview_dark.png new file mode 100644 index 0000000..ca79347 Binary files /dev/null and b/img/preview_dark.png differ diff --git a/models/User.js b/models/User.js index 14751e3..971a358 100644 --- a/models/User.js +++ b/models/User.js @@ -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 diff --git a/public/change-password.html b/public/change-password.html index f497085..7a2b011 100644 --- a/public/change-password.html +++ b/public/change-password.html @@ -4,15 +4,37 @@ Pool Oscars - Changer le mot de passe + +
- + Pool Oscars
-
+

Checklist

@@ -149,8 +172,15 @@
+ + + + \ No newline at end of file diff --git a/public/control.html b/public/control.html index 705df9b..568ee48 100644 --- a/public/control.html +++ b/public/control.html @@ -4,6 +4,7 @@ Pool Oscars - Admin + @@ -32,9 +33,10 @@
-
+

Admin

Paramètres, utilisateurs et gestion des films.
@@ -1465,8 +1488,15 @@
+ + + + diff --git a/public/css/app.css b/public/css/app.css index 2bb718a..8be22c0 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -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 */ @@ -457,4 +500,733 @@ footer { .admin-settings-narrow { 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; + } } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 728e0e7..a7021bc 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,7 @@ Pool Oscars + +
- + Pool Oscars
@@ -43,18 +65,10 @@

Pas de compte? Enregistrez-vous

- - -
- diff --git a/public/js/app_header_boot.js b/public/js/app_header_boot.js index 47c49f2..b3860fb 100644 --- a/public/js/app_header_boot.js +++ b/public/js/app_header_boot.js @@ -41,9 +41,17 @@ return; } - const nameEl = document.getElementById('user-name'); - if (nameEl && !nameEl.textContent) { - nameEl.textContent = decoded.name || ''; + 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 = name; + } } const adminLink = document.getElementById('admin-control-link'); diff --git a/public/js/app_movies.js b/public/js/app_movies.js index 05e3acb..16b15d9 100644 --- a/public/js/app_movies.js +++ b/public/js/app_movies.js @@ -579,7 +579,7 @@ window.addEventListener('DOMContentLoaded', async function () {
${movie.rating ? `⭐ ${movie.rating}` : ''} - IMDb +
diff --git a/public/js/app_theme.js b/public/js/app_theme.js new file mode 100644 index 0000000..82a5df3 --- /dev/null +++ b/public/js/app_theme.js @@ -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(); + } +})(); diff --git a/public/js/app_user-stat.js b/public/js/app_user-stat.js index 0171550..bb23626 100644 --- a/public/js/app_user-stat.js +++ b/public/js/app_user-stat.js @@ -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); } diff --git a/public/movies.html b/public/movies.html index 734be31..c6f4a56 100644 --- a/public/movies.html +++ b/public/movies.html @@ -4,6 +4,7 @@ Pool Oscars - Films + @@ -48,9 +49,10 @@
-
+

Films

@@ -104,8 +127,15 @@
+ + + + diff --git a/public/picks.html b/public/picks.html index 1ee4fd7..f5bfaa6 100644 --- a/public/picks.html +++ b/public/picks.html @@ -4,6 +4,7 @@ Pool Oscars - Mes picks + @@ -121,9 +122,10 @@
-
+

Mes picks Oscars

@@ -241,8 +264,15 @@
+ + + + diff --git a/public/player.html b/public/player.html index 82ecf2b..142c2b2 100644 --- a/public/player.html +++ b/public/player.html @@ -6,6 +6,7 @@ Pool Oscars + @@ -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 @@
-
+
@@ -288,10 +319,17 @@
+ + + + diff --git a/public/register.html b/public/register.html index b694e71..1504693 100644 --- a/public/register.html +++ b/public/register.html @@ -4,6 +4,7 @@ Pool Oscars - Enregistrement + @@ -16,11 +17,32 @@ } +
- + Pool Oscars
diff --git a/public/user-stat.html b/public/user-stat.html index a4383b6..359189b 100644 --- a/public/user-stat.html +++ b/public/user-stat.html @@ -4,6 +4,7 @@ Pool Oscars - Statistiques des utilisateurs + @@ -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 @@
-
+

Statistiques des utilisateurs

@@ -185,8 +208,15 @@
+ + + + \ No newline at end of file diff --git a/public/view-picks.html b/public/view-picks.html index 7915ac0..0abf0a1 100644 --- a/public/view-picks.html +++ b/public/view-picks.html @@ -4,6 +4,7 @@ Pool Oscars - Voir Tous les Choix + @@ -48,9 +49,10 @@
-
+

Voir Tous les Choix

@@ -100,8 +123,15 @@
+ + + + diff --git a/routes/users.js b/routes/users.js index b2d733b..051cba9 100644 --- a/routes/users.js +++ b/routes/users.js @@ -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)) {