summaryrefslogtreecommitdiff
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
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
-rw-r--r--.tasks/tasks.jsonl1
-rw-r--r--Biz/PodcastItLater/Web.py148
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 <id>' 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(),
),
)