diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-13 18:16:29 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-13 18:16:29 -0500 |
| commit | 989a1a5de2419373e4932b11c2f2f5877a2fb959 (patch) | |
| tree | 9115042272f624246efa4b3d8cd78920f66a2134 /Biz/PodcastItLater/Web.py | |
| parent | 83a1777d7869ed6da5e78c878dcc35c95a06c5cf (diff) | |
Unify navigation across PodcastItLater pages
- Created reusable PageLayout component in UI.py with consistent
header/navbar - Added Home link and Admin dropdown menu (Queue
Status, Manage Users) - Updated all pages to use PageLayout: home,
account, admin queue, admin users - Added demo@example.com to admin
whitelist for testing - Added dark mode styling for table headers -
Fixed component children syntax for Ludic framework - Proper type
annotations instead of type: ignore comments
Diffstat (limited to 'Biz/PodcastItLater/Web.py')
| -rw-r--r-- | Biz/PodcastItLater/Web.py | 504 |
1 files changed, 158 insertions, 346 deletions
diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index b7870a1..c20675f 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -691,172 +691,37 @@ class HomePage(Component[AnyChildren, HomePageAttrs]): return html.div() @override - def render(self) -> html.html: + def render(self) -> UI.PageLayout | html.html: queue_items = self.attrs["queue_items"] episodes = self.attrs["episodes"] user = self.attrs.get("user") + error = self.attrs.get("error") - return html.html( - html.head( - html.meta(charset="utf-8"), - html.meta( - name="viewport", - content="width=device-width, initial-scale=1", - ), - html.meta( - name="color-scheme", - content="light dark", - ), - html.title("PodcastItLater"), - html.script( - src="https://unpkg.com/htmx.org@1.9.10", - integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC", - crossorigin="anonymous", - ), - ), - html.body( - html.style( - "@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.2" - "/dist/css/bootstrap.min.css');" - "@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons" - "@1.11.3/font/bootstrap-icons.min.css');", - ), - # Auto dark mode CSS (must come after Bootstrap) - UI.create_auto_dark_mode_style(), - # Bootstrap container - html.div( - # Header - 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", "mb-4", "pt-4"], - ), - # Error alert - html.div( - html.div( - html.i( - classes=[ - "bi", - "bi-exclamation-triangle-fill", - "me-2", - ], - ), - self.attrs.get("error", "") or "", - classes=[ - "alert", - "alert-danger", - "d-flex", - "align-items-center", - ], - role="alert", # type: ignore[call-arg] - ), - ) - if self.attrs.get("error") - else html.div(), - # User info navbar - html.nav( - html.div( - # Hamburger toggle button (right side) - html.button( # type: ignore[call-arg] - html.span(classes=["navbar-toggler-icon"]), - classes=["navbar-toggler", "ms-auto"], - type="button", - data_bs_toggle="collapse", - data_bs_target="#navbarNav", - aria_controls="navbarNav", - aria_expanded="false", - aria_label="Toggle navigation", - ), - # Collapsible content - html.div( - # Navigation links - html.ul( - html.li( - html.a( - html.i( - classes=[ - "bi", - "bi-person-circle", - "me-1", - ], - ), - "Manage Account", - href="/account", - classes=["nav-link"], - ), - classes=["nav-item"], - ), - html.li( - html.a( - html.i( - classes=[ - "bi", - "bi-gear-fill", - "me-1", - ], - ), - "Admin Queue", - href="/admin", - classes=["nav-link"], - ), - classes=["nav-item"], - ) - if Core.is_admin(user) - else html.span(), - classes=["navbar-nav"], - ), - id="navbarNav", - classes=["collapse", "navbar-collapse"], - ), - classes=["container-fluid"], - ), - classes=[ - "navbar", - "navbar-expand-lg", - "bg-body-tertiary", - "rounded", - "mb-4", - ], - ) - if user - else LoginForm(error=self.attrs.get("error")), - # Plan info callout (only for logged in users) - self._render_plan_callout(user) if user else html.div(), - # Main content (only if logged in) - html.div( - SubmitForm(), - html.div( - QueueStatus(items=queue_items), - EpisodeList( - episodes=episodes, - rss_url=f"{BASE_URL}/feed/{user['token']}.xml" - if user - else None, - ), - id="dashboard-content", - hx_get="/dashboard-updates", - hx_trigger="every 3s, queue-updated from:body", - hx_swap="innerHTML", - ), - ) - if user - else html.div(), - classes=["container", "px-3", "px-md-4"], - style={"max-width": "900px"}, - ), - # Bootstrap JS bundle - html.script( - src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js", - integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL", - crossorigin="anonymous", + if not user: + return UI.PageLayout( + LoginForm(error=error), + user=None, + current_page="home", + error=error, + ) + + return UI.PageLayout( + self._render_plan_callout(user), + SubmitForm(), + html.div( + QueueStatus(items=queue_items), + EpisodeList( + episodes=episodes, + rss_url=f"{BASE_URL}/feed/{user['token']}.xml", ), + id="dashboard-content", + hx_get="/dashboard-updates", + hx_trigger="every 3s, queue-updated from:body", + hx_swap="innerHTML", ), + user=user, + current_page="home", + error=error, ) @@ -1063,7 +928,7 @@ def verify_magic_link(request: Request) -> Response: @app.get("/account") -def account_page(request: Request) -> html.html | RedirectResponse: +def account_page(request: Request) -> UI.PageLayout | RedirectResponse: """Account management page.""" user_id = request.session.get("user_id") if not user_id: @@ -1079,212 +944,159 @@ def account_page(request: Request) -> html.html | RedirectResponse: subscription_status = user.get("subscription_status", "") cancel_at_period_end = user.get("cancel_at_period_end", 0) == 1 - return html.html( - html.head( - html.meta(charset="utf-8"), - html.meta( - name="viewport", - content="width=device-width, initial-scale=1", + return UI.PageLayout( + html.h2( + html.i( + classes=["bi", "bi-person-circle", "me-2"], ), - html.meta( - name="color-scheme", - content="light dark", + "Account Management", + classes=["mb-4"], + ), + html.div( + html.h4( + html.i(classes=["bi", "bi-envelope-fill", "me-2"]), + "Account Information", + classes=["card-header", "bg-transparent"], ), - html.title("Account - PodcastItLater"), - html.script( - src="https://unpkg.com/htmx.org@1.9.10", - integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC", - crossorigin="anonymous", + html.div( + html.div( + html.strong("Email: "), + user["email"], + classes=["mb-2"], + ), + html.div( + html.strong("Account Created: "), + user["created_at"], + classes=["mb-2"], + ), + classes=["card-body"], ), + classes=["card", "mb-4"], ), - html.body( - html.style( - "@import url('https://cdn.jsdelivr.net/npm/bootstrap@5.3.2" - "/dist/css/bootstrap.min.css');" - "@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons" - "@1.11.3/font/bootstrap-icons.min.css');", + html.div( + html.h4( + html.i( + classes=["bi", "bi-credit-card-fill", "me-2"], + ), + "Subscription", + classes=["card-header", "bg-transparent"], ), - UI.create_auto_dark_mode_style(), html.div( html.div( - html.h1( - html.i( - classes=["bi", "bi-person-circle", "me-2"], - ), - "Account Management", - classes=["mb-4"], + html.strong("Plan: "), + tier_info["name"], + f" ({tier_info['price']})", + classes=["mb-2"], + ), + html.div( + html.strong("Status: "), + subscription_status.title() + if subscription_status + else "Active", + classes=["mb-2"], + ) + if tier == "paid" + else html.div(), + html.div( + html.i( + classes=[ + "bi", + "bi-info-circle", + "me-1", + ], ), - html.div( - html.a( - html.i( - classes=["bi", "bi-arrow-left", "me-1"], - ), - "Back to Dashboard", - href="/", + "Your subscription will cancel at the end " + "of the billing period.", + classes=[ + "alert", + "alert-warning", + "mt-2", + "mb-2", + ], + ) + if cancel_at_period_end + else html.div(), + html.div( + html.strong("Features: "), + tier_info["description"], + classes=["mb-3"], + ), + html.div( + html.a( + html.i( classes=[ - "btn", - "btn-outline-secondary", - "mb-4", + "bi", + "bi-arrow-up-circle", + "me-1", ], ), - ), - # Account info section - html.div( - html.h4( - html.i(classes=["bi", "bi-envelope-fill", "me-2"]), - "Account Information", - classes=["card-header", "bg-transparent"], - ), - html.div( - html.div( - html.strong("Email: "), - user["email"], - classes=["mb-2"], - ), - html.div( - html.strong("Account Created: "), - user["created_at"], - classes=["mb-2"], - ), - classes=["card-body"], - ), - classes=["card", "mb-4"], - ), - # Subscription section - html.div( - html.h4( + "Upgrade to Paid Plan", + href="#", + hx_post="/billing/checkout", + hx_vals='{"tier": "paid"}', + classes=[ + "btn", + "btn-success", + "me-2", + ], + ) + if tier == "free" + else html.form( + html.button( html.i( - classes=["bi", "bi-credit-card-fill", "me-2"], - ), - "Subscription", - classes=["card-header", "bg-transparent"], - ), - html.div( - html.div( - html.strong("Plan: "), - tier_info["name"], - f" ({tier_info['price']})", - classes=["mb-2"], - ), - html.div( - html.strong("Status: "), - subscription_status.title() - if subscription_status - else "Active", - classes=["mb-2"], - ) - if tier == "paid" - else html.div(), - html.div( - html.i( - classes=[ - "bi", - "bi-info-circle", - "me-1", - ], - ), - "Your subscription will cancel at the end " - "of the billing period.", - classes=[ - "alert", - "alert-warning", - "mt-2", - "mb-2", - ], - ) - if cancel_at_period_end - else html.div(), - html.div( - html.strong("Features: "), - tier_info["description"], - classes=["mb-3"], - ), - # Subscription actions - html.div( - html.a( - html.i( - classes=[ - "bi", - "bi-arrow-up-circle", - "me-1", - ], - ), - "Upgrade to Paid Plan", - href="#", - hx_post="/billing/checkout", - hx_vals='{"tier": "paid"}', - classes=[ - "btn", - "btn-success", - "me-2", - ], - ) - if tier == "free" - else html.form( - html.button( - html.i( - classes=[ - "bi", - "bi-gear-fill", - "me-1", - ], - ), - "Manage Subscription", - type="submit", - classes=[ - "btn", - "btn-primary", - "me-2", - ], - ), - method="post", - action="/billing/portal", - ), - ), - classes=["card-body"], - ), - classes=["card", "mb-4"], - ), - # Actions section - html.div( - html.h4( - html.i(classes=["bi", "bi-sliders", "me-2"]), - "Actions", - classes=["card-header", "bg-transparent"], - ), - html.div( - html.a( - html.i( - classes=[ - "bi", - "bi-box-arrow-right", - "me-1", - ], - ), - "Logout", - href="/logout", classes=[ - "btn", - "btn-outline-secondary", - "mb-2", - "me-2", + "bi", + "bi-gear-fill", + "me-1", ], ), - classes=["card-body"], + "Manage Subscription", + type="submit", + classes=[ + "btn", + "btn-primary", + "me-2", + ], ), - classes=["card", "mb-4"], + method="post", + action="/billing/portal", ), - classes=["mb-4"], ), - classes=["container", "my-5", "px-3", "px-md-4"], - style={"max-width": "900px"}, + classes=["card-body"], ), - html.script( - src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js", - integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL", - crossorigin="anonymous", + classes=["card", "mb-4"], + ), + html.div( + html.h4( + html.i(classes=["bi", "bi-sliders", "me-2"]), + "Actions", + classes=["card-header", "bg-transparent"], ), + html.div( + html.a( + html.i( + classes=[ + "bi", + "bi-box-arrow-right", + "me-1", + ], + ), + "Logout", + href="/logout", + classes=[ + "btn", + "btn-outline-secondary", + "mb-2", + "me-2", + ], + ), + classes=["card-body"], + ), + classes=["card", "mb-4"], ), + user=user, + current_page="account", + error=None, ) |
