summaryrefslogtreecommitdiff
path: root/Biz/PodcastItLater/UI.py
diff options
context:
space:
mode:
Diffstat (limited to 'Biz/PodcastItLater/UI.py')
-rw-r--r--Biz/PodcastItLater/UI.py230
1 files changed, 230 insertions, 0 deletions
diff --git a/Biz/PodcastItLater/UI.py b/Biz/PodcastItLater/UI.py
index 99ac29b..4844d67 100644
--- a/Biz/PodcastItLater/UI.py
+++ b/Biz/PodcastItLater/UI.py
@@ -7,6 +7,11 @@ Common UI components and utilities shared across web pages.
# : out podcastitlater-ui
# : dep ludic
import ludic.html as html
+import typing
+from ludic.attrs import Attrs
+from ludic.components import Component
+from ludic.types import AnyChildren
+from typing import override
def format_duration(seconds: int | None) -> str:
@@ -91,6 +96,14 @@ def create_auto_dark_mode_style() -> html.style:
.navbar .navbar-text {
color: #dee2e6 !important;
}
+
+ /* Table header dark mode */
+ .table-light {
+ --bs-table-bg: #343a40;
+ --bs-table-color: #dee2e6;
+ background-color: #343a40 !important;
+ color: #dee2e6 !important;
+ }
}
""",
)
@@ -112,3 +125,220 @@ def create_bootstrap_js() -> html.script:
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL",
crossorigin="anonymous",
)
+
+
+class PageLayoutAttrs(Attrs):
+ """Attributes for PageLayout component."""
+
+ user: dict[str, typing.Any] | None
+ current_page: str
+ error: str | None
+
+
+class PageLayout(Component[AnyChildren, PageLayoutAttrs]):
+ """Reusable page layout with header and navbar."""
+
+ @staticmethod
+ def _render_navbar(
+ user: dict[str, typing.Any] | None,
+ current_page: str,
+ ) -> html.nav:
+ """Render navigation bar."""
+ # Import here to avoid circular dependency
+ import Biz.PodcastItLater.Core as Core # noqa: PLC0415 # type: ignore[import-untyped]
+
+ def is_active(page: str) -> bool:
+ return current_page == page
+
+ return html.nav(
+ html.div(
+ 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",
+ ),
+ html.div(
+ html.ul(
+ html.li(
+ html.a(
+ html.i(
+ classes=[
+ "bi",
+ "bi-house-fill",
+ "me-1",
+ ],
+ ),
+ "Home",
+ href="/",
+ classes=[
+ "nav-link",
+ "active" if is_active("home") else "",
+ ],
+ ),
+ classes=["nav-item"],
+ ),
+ html.li(
+ html.a(
+ html.i(
+ classes=[
+ "bi",
+ "bi-person-circle",
+ "me-1",
+ ],
+ ),
+ "Manage Account",
+ href="/account",
+ classes=[
+ "nav-link",
+ "active" if is_active("account") else "",
+ ],
+ ),
+ classes=["nav-item"],
+ ),
+ html.li(
+ html.a( # type: ignore[call-arg]
+ html.i(
+ classes=[
+ "bi",
+ "bi-gear-fill",
+ "me-1",
+ ],
+ ),
+ "Admin",
+ href="#",
+ id="adminDropdown",
+ role="button",
+ data_bs_toggle="dropdown",
+ aria_expanded="false",
+ classes=[
+ "nav-link",
+ "dropdown-toggle",
+ "active"
+ if is_active("admin")
+ or is_active("admin-users")
+ else "",
+ ],
+ ),
+ html.ul( # type: ignore[call-arg]
+ html.li(
+ html.a(
+ html.i(
+ classes=[
+ "bi",
+ "bi-list-task",
+ "me-2",
+ ],
+ ),
+ "Queue Status",
+ href="/admin",
+ classes=["dropdown-item"],
+ ),
+ ),
+ html.li(
+ html.a(
+ html.i(
+ classes=[
+ "bi",
+ "bi-people-fill",
+ "me-2",
+ ],
+ ),
+ "Manage Users",
+ href="/admin/users",
+ classes=["dropdown-item"],
+ ),
+ ),
+ classes=["dropdown-menu"],
+ aria_labelledby="adminDropdown",
+ ),
+ classes=["nav-item", "dropdown"],
+ )
+ if user and 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",
+ ],
+ )
+
+ @override
+ def render(self) -> html.html:
+ user = self.attrs.get("user")
+ current_page = self.attrs.get("current_page", "")
+ 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"),
+ create_htmx_script(),
+ ),
+ html.body(
+ create_bootstrap_styles(),
+ create_auto_dark_mode_style(),
+ html.div(
+ 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"],
+ ),
+ html.div(
+ html.div(
+ html.i(
+ classes=[
+ "bi",
+ "bi-exclamation-triangle-fill",
+ "me-2",
+ ],
+ ),
+ error or "",
+ classes=[
+ "alert",
+ "alert-danger",
+ "d-flex",
+ "align-items-center",
+ ],
+ role="alert", # type: ignore[call-arg]
+ ),
+ )
+ if error
+ else html.div(),
+ self._render_navbar(user, current_page)
+ if user
+ else html.div(),
+ *self.children,
+ classes=["container", "px-3", "px-md-4"],
+ style={"max-width": "900px"},
+ ),
+ create_bootstrap_js(),
+ ),
+ )