From 2f9048e8746774f552a37c732f0cb418eb8e30c5 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Sun, 9 Nov 2025 20:36:31 -0500 Subject: PodcastItLater: Add dark mode support - Added theme switcher script with localStorage persistence - Respects system dark mode preference (prefers-color-scheme) - Added theme toggle button to HomePage and BillingPage - Uses Bootstrap 5.3 data-bs-theme attribute for dark mode - Theme persists across page loads - Icon changes between moon (light) and sun (dark) Task: t-64tkB5 --- .tasks/tasks.jsonl | 1 + Biz/PodcastItLater/Web.py | 148 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 127 insertions(+), 22 deletions(-) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 8fc1a5a..335b615 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -35,3 +35,4 @@ {"taskCreatedAt":"2025-11-09T22:19:27.303689497Z","taskDependencies":[],"taskId":"t-1pIV0ZF","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-143KQl2","taskStatus":"Done","taskTitle":"Implement billing page UI component with pricing and upgrade options","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T23:04:20.974801117Z"} {"taskCreatedAt":"2025-11-09T22:38:46.235799803Z","taskDependencies":[],"taskId":"t-1qZlMb4","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Add a 'task show ' command that prints out a long, easy to read (for humans) version of the task. Include dependencies and all information fields in the output","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T22:38:46.235799803Z"} {"taskCreatedAt":"2025-11-09T22:56:18.897655607Z","taskDependencies":[],"taskId":"t-1s8ADC0","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskStatus":"Done","taskTitle":"Make PodcastItLater UI mobile-friendly and responsive","taskType":"WorkTask","taskUpdatedAt":"2025-11-09T23:09:16.712244322Z"} +{"taskCreatedAt":"2025-11-10T01:32:42.893029428Z","taskDependencies":[],"taskId":"t-64tkB5","taskNamespace":"Biz/PodcastItLater.hs","taskParent":null,"taskStatus":"InProgress","taskTitle":"Add dark mode support to PodcastItLater UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-10T01:32:46.388157028Z"} diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index 2032746..39e2240 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -52,6 +52,60 @@ from typing import override logger = Log.setup() + +def create_theme_switcher_script() -> html.script: + """Create JavaScript for theme switching with localStorage persistence.""" + return html.script( + """ + // Theme switcher with localStorage persistence + (function() { + const getStoredTheme = () => localStorage.getItem('theme'); + const setStoredTheme = (theme) => { + localStorage.setItem('theme', theme); + }; + const getPreferredTheme = () => { + const storedTheme = getStoredTheme(); + if (storedTheme) { + return storedTheme; + } + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' : 'light'; + }; + + const setTheme = theme => { + document.documentElement.setAttribute('data-bs-theme', theme); + const icon = document.getElementById('theme-icon'); + if (icon) { + icon.className = theme === 'dark' + ? 'bi bi-sun-fill' + : 'bi bi-moon-fill'; + } + }; + + // Set theme on page load + setTheme(getPreferredTheme()); + + // Theme toggle button handler + window.toggleTheme = () => { + const currentTheme = document.documentElement + .getAttribute('data-bs-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + setTheme(newTheme); + setStoredTheme(newTheme); + }; + + // Listen for system theme changes + window.matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', e => { + if (!getStoredTheme()) { + setTheme(e.matches ? 'dark' : 'light'); + } + }); + })(); + """, + ) + + # Configuration area = App.from_env() BASE_URL = os.getenv("BASE_URL", "http://localhost:8000") @@ -649,24 +703,49 @@ class BillingPage(Component[AnyChildren, BillingPageAttrs]): ), html.div( html.div( - html.h1( - html.i(classes=["bi", "bi-credit-card", "me-2"]), - "Billing & Usage", - classes=["mb-4"], - ), html.div( - html.a( - html.i(classes=["bi", "bi-arrow-left", "me-1"]), - "Back to Dashboard", - href="/", - classes=[ - "btn", - "btn-outline-secondary", - "mb-4", - ], + html.h1( + html.i( + classes=["bi", "bi-credit-card", "me-2"], + ), + "Billing & Usage", + classes=["mb-4"], + ), + html.div( + html.a( + html.i( + classes=["bi", "bi-arrow-left", "me-1"], + ), + "Back to Dashboard", + href="/", + classes=[ + "btn", + "btn-outline-secondary", + "mb-4", + ], + ), + ), + classes=["flex-grow-1"], + ), + html.button( + html.i( + id="theme-icon", + classes=["bi", "bi-moon-fill"], ), + on_click="toggleTheme()", + classes=[ + "btn", + "btn-outline-secondary", + "position-absolute", + "top-0", + "end-0", + "mt-3", + "me-3", + ], + title="Toggle dark mode", + type="button", ), - classes=["mb-4"], + classes=["position-relative", "mb-4"], ), # Success/Error alerts html.div( @@ -803,6 +882,8 @@ class BillingPage(Component[AnyChildren, BillingPageAttrs]): integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL", crossorigin="anonymous", ), + # Theme switcher script + create_theme_switcher_script(), ), ) @@ -936,15 +1017,36 @@ class HomePage(Component[AnyChildren, HomePageAttrs]): html.div( # Header html.div( - html.h1( - "PodcastItLater", - classes=["display-4", "mb-2"], + html.div( + html.h1( + "PodcastItLater", + classes=["display-4", "mb-2"], + ), + html.p( + "Convert web articles to podcast episodes", + classes=["lead", "text-muted"], + ), + classes=["text-center", "flex-grow-1"], ), - html.p( - "Convert web articles to podcast episodes", - classes=["lead", "text-muted"], + html.button( + html.i( + id="theme-icon", + classes=["bi", "bi-moon-fill"], + ), + on_click="toggleTheme()", + classes=[ + "btn", + "btn-outline-secondary", + "position-absolute", + "top-0", + "end-0", + "mt-3", + "me-3", + ], + title="Toggle dark mode", + type="button", ), - classes=["text-center", "mb-4", "pt-4"], + classes=["position-relative", "mb-4", "pt-4"], ), # Error alert html.div( @@ -1078,6 +1180,8 @@ class HomePage(Component[AnyChildren, HomePageAttrs]): integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL", crossorigin="anonymous", ), + # Theme switcher script + create_theme_switcher_script(), ), ) -- cgit v1.2.3