summaryrefslogtreecommitdiff
path: root/Biz/PodcastItLater
diff options
context:
space:
mode:
authorBen Sima <ben@bsima.me>2025-11-09 20:36:31 -0500
committerBen Sima <ben@bsima.me>2025-11-09 20:36:31 -0500
commit2f9048e8746774f552a37c732f0cb418eb8e30c5 (patch)
tree05a89fc25f1f4c9f6821e3293967067647502dac /Biz/PodcastItLater
parent14af38a21dc8e790b9ddc29e241784fd769dc3fc (diff)
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
Diffstat (limited to 'Biz/PodcastItLater')
-rw-r--r--Biz/PodcastItLater/Web.py148
1 files changed, 126 insertions, 22 deletions
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(),
),
)