summaryrefslogtreecommitdiff
path: root/Biz/PodcastItLater/Web.py
diff options
context:
space:
mode:
Diffstat (limited to 'Biz/PodcastItLater/Web.py')
-rw-r--r--Biz/PodcastItLater/Web.py504
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,
)