summaryrefslogtreecommitdiff
path: root/Biz
diff options
context:
space:
mode:
authorBen Sima <ben@bensima.com>2025-11-22 08:50:09 -0500
committerBen Sima <ben@bensima.com>2025-11-22 08:50:09 -0500
commitdbd846a335cc1c6e88d74f9444aa3374f2bb3c59 (patch)
tree9eae1fb8cbe31d82a2825076e61f1eeb8c62e8a2 /Biz
parenteccc7e36113d5a2c21c711ab03acc86ec157755c (diff)
parentce00335c34eac85af8161b6a0da972af6eaae067 (diff)
Merge task t-1f9Td4U: Navbar Styling Cleanup
Diffstat (limited to 'Biz')
-rw-r--r--[-rwxr-xr-x]Biz/PodcastItLater/Test.py49
-rw-r--r--Biz/PodcastItLater/UI.py323
2 files changed, 207 insertions, 165 deletions
diff --git a/Biz/PodcastItLater/Test.py b/Biz/PodcastItLater/Test.py
index b2a1d24..ee638f1 100755..100644
--- a/Biz/PodcastItLater/Test.py
+++ b/Biz/PodcastItLater/Test.py
@@ -19,6 +19,7 @@
# : out podcastitlater-e2e-test
# : run ffmpeg
import Biz.PodcastItLater.Core as Core
+import Biz.PodcastItLater.UI as UI
import Biz.PodcastItLater.Web as Web
import Biz.PodcastItLater.Worker as Worker
import Omni.App as App
@@ -208,12 +209,60 @@ class TestEndToEnd(BaseWebTest):
self.assertIn("Other User's Article", response.text)
+class TestUI(Test.TestCase):
+ """Test UI components."""
+
+ def test_render_navbar(self) -> None:
+ """Test navbar rendering."""
+ user = {"email": "test@example.com", "id": 1}
+ layout = UI.PageLayout(
+ user=user,
+ current_page="home",
+ error=None,
+ page_title="Test",
+ meta_tags=[],
+ )
+ navbar = layout._render_navbar(user, "home") # noqa: SLF001
+ html_output = navbar.to_html()
+
+ # Check basic structure
+ self.assertIn("navbar", html_output)
+ self.assertIn("Home", html_output)
+ self.assertIn("Public Feed", html_output)
+ self.assertIn("Pricing", html_output)
+ self.assertIn("Manage Account", html_output)
+
+ # Check active state
+ self.assertIn("active", html_output)
+
+ # Check non-admin user doesn't see admin menu
+ self.assertNotIn("Admin", html_output)
+
+ def test_render_navbar_admin(self) -> None:
+ """Test navbar rendering for admin."""
+ user = {"email": "ben@bensima.com", "id": 1} # Admin email
+ layout = UI.PageLayout(
+ user=user,
+ current_page="admin",
+ error=None,
+ page_title="Test",
+ meta_tags=[],
+ )
+ navbar = layout._render_navbar(user, "admin") # noqa: SLF001
+ html_output = navbar.to_html()
+
+ # Check admin menu present
+ self.assertIn("Admin", html_output)
+ self.assertIn("Queue Status", html_output)
+
+
def test() -> None:
"""Run all end-to-end tests."""
Test.run(
App.Area.Test,
[
TestEndToEnd,
+ TestUI,
],
)
diff --git a/Biz/PodcastItLater/UI.py b/Biz/PodcastItLater/UI.py
index bdf7a5b..cbab2a2 100644
--- a/Biz/PodcastItLater/UI.py
+++ b/Biz/PodcastItLater/UI.py
@@ -91,7 +91,7 @@ def create_auto_dark_mode_style() -> html.style:
/* Navbar dark mode */
.navbar.bg-body-tertiary {
- background-color: #2b3035 !important;
+ background-color: #2b3035 !important;
}
.navbar .navbar-text {
@@ -128,7 +128,6 @@ def create_bootstrap_js() -> html.script:
)
-
class PageLayoutAttrs(Attrs):
"""Attributes for PageLayout component."""
@@ -143,6 +142,78 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]):
"""Reusable page layout with header and navbar."""
@staticmethod
+ def _render_nav_item(
+ label: str,
+ href: str,
+ icon: str,
+ *,
+ is_active: bool,
+ ) -> html.li:
+ return html.li(
+ html.a(
+ html.i(classes=["bi", f"bi-{icon}", "me-1"]),
+ label,
+ href=href,
+ classes=[
+ "nav-link",
+ "active" if is_active else "",
+ ],
+ ),
+ classes=["nav-item"],
+ )
+
+ @staticmethod
+ def _render_admin_dropdown(
+ is_active_func: typing.Callable[[str], bool],
+ ) -> html.li:
+ is_active = is_active_func("admin") or is_active_func("admin-users")
+ return 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 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"],
+ ),
+ ),
+ html.li(
+ html.a(
+ html.i(classes=["bi", "bi-graph-up", "me-2"]),
+ "Metrics",
+ href="/admin/metrics",
+ classes=["dropdown-item"],
+ ),
+ ),
+ classes=["dropdown-menu"],
+ aria_labelledby="adminDropdown",
+ ),
+ classes=["nav-item", "dropdown"],
+ )
+
+ @staticmethod
def _render_navbar(
user: dict[str, typing.Any] | None,
current_page: str,
@@ -166,150 +237,31 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]):
),
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"],
+ PageLayout._render_nav_item(
+ "Home",
+ "/",
+ "house-fill",
+ is_active=is_active("home"),
),
- html.li(
- html.a(
- html.i(
- classes=[
- "bi",
- "bi-globe",
- "me-1",
- ],
- ),
- "Public Feed",
- href="/public",
- classes=[
- "nav-link",
- "active" if is_active("public") else "",
- ],
- ),
- classes=["nav-item"],
+ PageLayout._render_nav_item(
+ "Public Feed",
+ "/public",
+ "globe",
+ is_active=is_active("public"),
),
- html.li(
- html.a(
- html.i(
- classes=[
- "bi",
- "bi-stars",
- "me-1",
- ],
- ),
- "Pricing",
- href="/pricing",
- classes=[
- "nav-link",
- "active" if is_active("pricing") else "",
- ],
- ),
- classes=["nav-item"],
+ PageLayout._render_nav_item(
+ "Pricing",
+ "/pricing",
+ "stars",
+ is_active=is_active("pricing"),
),
- 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"],
+ PageLayout._render_nav_item(
+ "Manage Account",
+ "/account",
+ "person-circle",
+ is_active=is_active("account"),
),
- 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"],
- ),
- ),
- html.li(
- html.a(
- html.i(
- classes=[
- "bi",
- "bi-graph-up",
- "me-2",
- ],
- ),
- "Metrics",
- href="/admin/metrics",
- classes=["dropdown-item"],
- ),
- ),
- classes=["dropdown-menu"],
- aria_labelledby="adminDropdown",
- ),
- classes=["nav-item", "dropdown"],
- )
+ PageLayout._render_admin_dropdown(is_active)
if user and Core.is_admin(user)
else html.span(),
classes=["navbar-nav"],
@@ -420,12 +372,22 @@ class AccountPage(Component[AnyChildren, AccountPageAttrs]):
plan_tier = user.get("plan_tier", "free")
is_paid = plan_tier == "paid"
-
+
article_limit = limits.get("articles_per_period")
article_usage = usage.get("articles", 0)
-
- limit_text = "Unlimited" if article_limit is None else str(article_limit)
-
+
+ limit_text = (
+ "Unlimited" if article_limit is None else str(article_limit)
+ )
+
+ usage_percent = 0
+ if article_limit:
+ usage_percent = min(100, int((article_usage / article_limit) * 100))
+
+ progress_style = (
+ {"width": f"{usage_percent}%"} if article_limit else {"width": "0%"}
+ )
+
return PageLayout(
html.div(
html.div(
@@ -437,8 +399,8 @@ class AccountPage(Component[AnyChildren, AccountPageAttrs]):
classes=[
"bi",
"bi-person-circle",
- "me-2"
- ]
+ "me-2",
+ ],
),
"My Account",
classes=["card-title", "mb-4"],
@@ -453,7 +415,9 @@ class AccountPage(Component[AnyChildren, AccountPageAttrs]):
),
html.p(
html.strong("Member since: "),
- user.get("created_at", "").split("T")[0],
+ user.get("created_at", "").split("T")[
+ 0
+ ],
classes=["mb-4"],
),
classes=["mb-5"],
@@ -488,20 +452,26 @@ class AccountPage(Component[AnyChildren, AccountPageAttrs]):
),
html.div(
html.div(
- f"{article_usage} / {limit_text}",
+ f"{article_usage} / "
+ f"{limit_text}",
classes=["mb-1"],
),
html.div(
html.div(
- classes=["progress-bar"],
+ classes=[
+ "progress-bar",
+ ],
role="progressbar",
- style={
- "width": f"{min(100, (article_usage / article_limit * 100))}%"
- } if article_limit else {"width": "0%"},
+ style=progress_style,
),
- classes=["progress", "mb-3"],
+ classes=[
+ "progress",
+ "mb-3",
+ ],
style={"height": "10px"},
- ) if article_limit else html.div(),
+ )
+ if article_limit
+ else html.div(),
classes=["mb-3"],
),
),
@@ -509,23 +479,43 @@ class AccountPage(Component[AnyChildren, AccountPageAttrs]):
html.div(
html.form(
html.button(
- html.i(classes=["bi", "bi-credit-card", "me-2"]),
+ html.i(
+ classes=[
+ "bi",
+ "bi-credit-card",
+ "me-2",
+ ],
+ ),
"Manage Subscription",
type="submit",
- classes=["btn", "btn-outline-primary"],
+ classes=[
+ "btn",
+ "btn-outline-primary",
+ ],
),
method="post",
action=portal_url,
- ) if is_paid and portal_url else
- html.a(
- html.i(classes=["bi", "bi-star-fill", "me-2"]),
+ )
+ if is_paid and portal_url
+ else html.a(
+ html.i(
+ classes=[
+ "bi",
+ "bi-star-fill",
+ "me-2",
+ ],
+ ),
"Upgrade to Pro",
href="/pricing",
classes=["btn", "btn-primary"],
),
classes=["d-flex", "gap-2"],
),
- classes=["card", "card-body", "bg-light"],
+ classes=[
+ "card",
+ "card-body",
+ "bg-light",
+ ],
),
classes=["mb-5"],
),
@@ -537,12 +527,15 @@ class AccountPage(Component[AnyChildren, AccountPageAttrs]):
classes=[
"bi",
"bi-box-arrow-right",
- "me-2"
- ]
+ "me-2",
+ ],
),
"Log Out",
type="submit",
- classes=["btn", "btn-outline-danger"],
+ classes=[
+ "btn",
+ "btn-outline-danger",
+ ],
),
action="/logout",
method="post",