diff options
| author | Ben Sima <ben@bensima.com> | 2025-11-22 08:50:09 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bensima.com> | 2025-11-22 08:50:09 -0500 |
| commit | dbd846a335cc1c6e88d74f9444aa3374f2bb3c59 (patch) | |
| tree | 9eae1fb8cbe31d82a2825076e61f1eeb8c62e8a2 | |
| parent | eccc7e36113d5a2c21c711ab03acc86ec157755c (diff) | |
| parent | ce00335c34eac85af8161b6a0da972af6eaae067 (diff) | |
Merge task t-1f9Td4U: Navbar Styling Cleanup
| -rw-r--r--[-rwxr-xr-x] | Biz/PodcastItLater/Test.py | 49 | ||||
| -rw-r--r-- | Biz/PodcastItLater/UI.py | 323 |
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", |
