Eliminate Python s-expression string building across account, orders, federation, and cart services. Visual rendering logic now lives entirely in .sx defcomp components; Python files contain only data serialization, header/layout wiring, and thin wrappers that call defcomps. Phase 0: Shared DRY extraction — auth/orders header defcomps, format-decimal/ pluralize/escape/route-prefix primitives. Phase 1: Account — dashboard, newsletters, login/device/check-email content. Phase 2: Orders — order list, detail, filter, checkout return assembled defcomps. Phase 3: Federation — social nav, post cards, timeline, search, actors, notifications, compose, profile assembled defcomps. Phase 4: Cart — overview, page cart items/calendar/tickets/summary, admin, payments assembled defcomps; orders rendering reuses Phase 2 shared defcomps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
4.6 KiB
Python
130 lines
4.6 KiB
Python
"""
|
|
Account service s-expression page components.
|
|
|
|
Renders login, device auth, and check-email pages. Dashboard and newsletters
|
|
are now fully handled by .sx defcomps called from defpage expressions.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
from shared.sx.jinja_bridge import load_service_components
|
|
from shared.sx.helpers import (
|
|
sx_call, SxExpr,
|
|
root_header_sx, full_page_sx,
|
|
)
|
|
|
|
# Load account-specific .sx components + handlers at import time
|
|
load_service_components(os.path.dirname(os.path.dirname(__file__)),
|
|
service_name="account")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API: Auth pages (login, device, check_email)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_login_page(ctx: dict) -> str:
|
|
"""Full page: login form."""
|
|
error = ctx.get("error", "")
|
|
email = ctx.get("email", "")
|
|
hdr = root_header_sx(ctx)
|
|
content = sx_call("account-login-content", error=error or None, email=email)
|
|
return full_page_sx(ctx, header_rows=hdr,
|
|
content=content,
|
|
meta_html='<title>Login \u2014 Rose Ash</title>')
|
|
|
|
|
|
async def render_device_page(ctx: dict) -> str:
|
|
"""Full page: device authorization form."""
|
|
error = ctx.get("error", "")
|
|
code = ctx.get("code", "")
|
|
hdr = root_header_sx(ctx)
|
|
content = sx_call("account-device-content", error=error or None, code=code)
|
|
return full_page_sx(ctx, header_rows=hdr,
|
|
content=content,
|
|
meta_html='<title>Authorize Device \u2014 Rose Ash</title>')
|
|
|
|
|
|
async def render_device_approved_page(ctx: dict) -> str:
|
|
"""Full page: device approved."""
|
|
hdr = root_header_sx(ctx)
|
|
content = sx_call("account-device-approved")
|
|
return full_page_sx(ctx, header_rows=hdr,
|
|
content=content,
|
|
meta_html='<title>Device Authorized \u2014 Rose Ash</title>')
|
|
|
|
|
|
async def render_check_email_page(ctx: dict) -> str:
|
|
"""Full page: check email after magic link sent."""
|
|
email = ctx.get("email", "")
|
|
email_error = ctx.get("email_error")
|
|
hdr = root_header_sx(ctx)
|
|
content = sx_call("account-check-email-content",
|
|
email=email, email_error=email_error)
|
|
return full_page_sx(ctx, header_rows=hdr,
|
|
content=content,
|
|
meta_html='<title>Check your email \u2014 Rose Ash</title>')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API: Fragment renderers for POST handlers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_newsletter_toggle(un) -> str:
|
|
"""Render a newsletter toggle switch for POST response."""
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
|
|
nid = un.newsletter_id
|
|
from quart import g
|
|
account_url_fn = getattr(g, "_account_url", None)
|
|
if account_url_fn is None:
|
|
from shared.infrastructure.urls import account_url
|
|
account_url_fn = account_url
|
|
|
|
toggle_url = account_url_fn(f"/newsletter/{nid}/toggle/")
|
|
csrf = generate_csrf_token()
|
|
|
|
if un.subscribed:
|
|
bg = "bg-emerald-500"
|
|
translate = "translate-x-6"
|
|
checked = "true"
|
|
else:
|
|
bg = "bg-stone-300"
|
|
translate = "translate-x-1"
|
|
checked = "false"
|
|
|
|
return sx_call(
|
|
"account-newsletter-toggle",
|
|
id=f"nl-{nid}", url=toggle_url,
|
|
hdrs=f'{{"X-CSRFToken": "{csrf}"}}',
|
|
target=f"#nl-{nid}",
|
|
cls=f"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}",
|
|
checked=checked,
|
|
knob_cls=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _fragment_content(frag: object) -> str:
|
|
"""Convert a fragment response to sx content string.
|
|
|
|
SxExpr (from text/sx responses) is embedded as-is; plain strings
|
|
(from text/html) are wrapped in ``~rich-text``.
|
|
"""
|
|
from shared.sx.parser import SxExpr
|
|
if isinstance(frag, SxExpr):
|
|
return frag.source
|
|
s = str(frag) if frag else ""
|
|
if not s:
|
|
return ""
|
|
return f'(~rich-text :html "{_sx_escape(s)}")'
|
|
|
|
|
|
def _sx_escape(s: str) -> str:
|
|
"""Escape a string for embedding in sx string literals."""
|
|
return s.replace("\\", "\\\\").replace('"', '\\"')
|