Replace Python sx_call loops with data-driven SX defcomps using map
Move rendering logic from Python for-loops building sx_call strings into SX defcomp components that use map/lambda over data dicts. Python now serializes display data into plain dicts and passes them via a single sx_call; the SX layer handles iteration and conditional rendering. Covers orders (rows, items, calendar, tickets), federation (timeline, search, actors, profile activities), and blog (cards, pages, filters, snippets, menu items, tag groups, page search, nav OOB). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,80 @@
|
||||
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
|
||||
edit-form delete-form))
|
||||
|
||||
;; Data-driven snippets list (replaces Python _snippets_sx loop)
|
||||
(defcomp ~blog-snippets-from-data (&key snippets user-id is-admin csrf badge-colours)
|
||||
(~blog-snippets-list
|
||||
:rows (<> (map (lambda (s)
|
||||
(let* ((s-id (get s "id"))
|
||||
(s-name (get s "name"))
|
||||
(s-uid (get s "user_id"))
|
||||
(s-vis (get s "visibility"))
|
||||
(owner (if (= s-uid user-id) "You" (str "User #" s-uid)))
|
||||
(badge-cls (or (get badge-colours s-vis) "bg-stone-200 text-stone-700"))
|
||||
(extra (<>
|
||||
(when is-admin
|
||||
(~blog-snippet-visibility-select
|
||||
:patch-url (get s "patch_url")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:options (<>
|
||||
(~blog-snippet-option :value "private" :selected (= s-vis "private") :label "private")
|
||||
(~blog-snippet-option :value "shared" :selected (= s-vis "shared") :label "shared")
|
||||
(~blog-snippet-option :value "admin" :selected (= s-vis "admin") :label "admin"))
|
||||
:cls "text-sm border border-stone-300 rounded px-2 py-1"))
|
||||
(when (or (= s-uid user-id) is-admin)
|
||||
(~delete-btn :url (get s "delete_url") :trigger-target "#snippets-list"
|
||||
:title "Delete snippet?"
|
||||
:text (str "Delete \u201c" s-name "\u201d?")
|
||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0")))))
|
||||
(~blog-snippet-row :name s-name :owner owner :badge-cls badge-cls
|
||||
:visibility s-vis :extra extra)))
|
||||
(or snippets (list))))))
|
||||
|
||||
;; Data-driven menu items list (replaces Python _menu_items_list_sx loop)
|
||||
(defcomp ~blog-menu-items-from-data (&key items csrf)
|
||||
(~blog-menu-items-list
|
||||
:rows (<> (map (lambda (item)
|
||||
(let* ((img (~img-or-placeholder :src (get item "feature_image") :alt (get item "label")
|
||||
:size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")))
|
||||
(~blog-menu-item-row
|
||||
:img img :label (get item "label") :slug (get item "slug")
|
||||
:sort-order (get item "sort_order") :edit-url (get item "edit_url")
|
||||
:delete-url (get item "delete_url")
|
||||
:confirm-text (str "Remove " (get item "label") " from the menu?")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}"))))
|
||||
(or items (list))))))
|
||||
|
||||
;; Data-driven tag groups main (replaces Python _tag_groups_main_panel_sx loops)
|
||||
(defcomp ~blog-tag-groups-from-data (&key groups unassigned-tags csrf create-url)
|
||||
(~blog-tag-groups-main
|
||||
:form (~blog-tag-groups-create-form :create-url create-url :csrf csrf)
|
||||
:groups (if (empty? (or groups (list)))
|
||||
(~empty-state :message "No tag groups yet." :cls "text-stone-500 text-sm")
|
||||
(~blog-tag-groups-list
|
||||
:items (<> (map (lambda (g)
|
||||
(let* ((icon (if (get g "feature_image")
|
||||
(~blog-tag-group-icon-image :src (get g "feature_image") :name (get g "name"))
|
||||
(~blog-tag-group-icon-color :style (get g "style") :initial (get g "initial")))))
|
||||
(~blog-tag-group-li :icon icon :edit-href (get g "edit_href")
|
||||
:name (get g "name") :slug (get g "slug") :sort-order (get g "sort_order"))))
|
||||
groups))))
|
||||
:unassigned (when (not (empty? (or unassigned-tags (list))))
|
||||
(~blog-unassigned-tags
|
||||
:heading (str "Unassigned Tags (" (len unassigned-tags) ")")
|
||||
:spans (<> (map (lambda (t)
|
||||
(~blog-unassigned-tag :name (get t "name")))
|
||||
unassigned-tags))))))
|
||||
|
||||
;; Data-driven tag group edit (replaces Python _tag_groups_edit_main_panel_sx loop)
|
||||
(defcomp ~blog-tag-checkboxes-from-data (&key tags)
|
||||
(<> (map (lambda (t)
|
||||
(~blog-tag-checkbox
|
||||
:tag-id (get t "tag_id") :checked (get t "checked")
|
||||
:img (when (get t "feature_image") (~blog-tag-checkbox-image :src (get t "feature_image")))
|
||||
:name (get t "name")))
|
||||
(or tags (list)))))
|
||||
|
||||
;; Preview panel components
|
||||
|
||||
(defcomp ~blog-preview-panel (&key sections)
|
||||
|
||||
@@ -107,6 +107,43 @@
|
||||
(ul :class "flex flex-wrap gap-2 text-sm"
|
||||
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors))))))))
|
||||
|
||||
;; Data-driven blog cards list (replaces Python _blog_cards_sx loop)
|
||||
(defcomp ~blog-cards-from-data (&key posts view sentinel)
|
||||
(<>
|
||||
(map (lambda (p)
|
||||
(if (= view "tile")
|
||||
(~blog-card-tile
|
||||
:href (get p "href") :hx-select (get p "hx_select")
|
||||
:feature-image (get p "feature_image") :title (get p "title")
|
||||
:is-draft (get p "is_draft") :publish-requested (get p "publish_requested")
|
||||
:status-timestamp (get p "status_timestamp")
|
||||
:excerpt (get p "excerpt") :tags (get p "tags") :authors (get p "authors"))
|
||||
(~blog-card
|
||||
:slug (get p "slug") :href (get p "href") :hx-select (get p "hx_select")
|
||||
:title (get p "title") :feature-image (get p "feature_image")
|
||||
:excerpt (get p "excerpt") :is-draft (get p "is_draft")
|
||||
:publish-requested (get p "publish_requested")
|
||||
:status-timestamp (get p "status_timestamp")
|
||||
:has-like (get p "has_like") :liked (get p "liked")
|
||||
:like-url (get p "like_url") :csrf-token (get p "csrf_token")
|
||||
:tags (get p "tags") :authors (get p "authors")
|
||||
:widget (when (get p "widget") (~rich-text :html (get p "widget"))))))
|
||||
(or posts (list)))
|
||||
sentinel))
|
||||
|
||||
;; Data-driven page cards list (replaces Python _page_cards_sx loop)
|
||||
(defcomp ~page-cards-from-data (&key pages sentinel)
|
||||
(<>
|
||||
(map (lambda (pg)
|
||||
(~blog-page-card
|
||||
:href (get pg "href") :hx-select (get pg "hx_select")
|
||||
:title (get pg "title")
|
||||
:has-calendar (get pg "has_calendar") :has-market (get pg "has_market")
|
||||
:pub-timestamp (get pg "pub_timestamp")
|
||||
:feature-image (get pg "feature_image") :excerpt (get pg "excerpt")))
|
||||
(or pages (list)))
|
||||
sentinel))
|
||||
|
||||
(defcomp ~blog-page-badges (&key has-calendar has-market)
|
||||
(div :class "flex justify-center gap-2 mt-2"
|
||||
(when has-calendar (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"
|
||||
|
||||
@@ -63,3 +63,39 @@
|
||||
|
||||
(defcomp ~blog-filter-summary (&key text)
|
||||
(span :class "text-sm text-stone-600" text))
|
||||
|
||||
;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop)
|
||||
(defcomp ~blog-tag-groups-filter-from-data (&key groups selected-groups hx-select)
|
||||
(let* ((is-any (empty? (or selected-groups (list))))
|
||||
(any-cls (if is-any "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")))
|
||||
(~blog-filter-nav
|
||||
:items (<>
|
||||
(~blog-filter-any-topic :cls any-cls :hx-select hx-select)
|
||||
(map (lambda (g)
|
||||
(let* ((slug (get g "slug"))
|
||||
(name (get g "name"))
|
||||
(is-on (contains? selected-groups slug))
|
||||
(cls (if is-on "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))
|
||||
(icon (if (get g "feature_image")
|
||||
(~blog-filter-group-icon-image :src (get g "feature_image") :name name)
|
||||
(~blog-filter-group-icon-color :style (get g "style") :initial (get g "initial")))))
|
||||
(~blog-filter-group-li :cls cls :hx-get (str "?group=" slug "&page=1") :hx-select hx-select
|
||||
:icon icon :name name :count (get g "count"))))
|
||||
(or groups (list)))))))
|
||||
|
||||
;; Data-driven authors filter (replaces Python _authors_filter_sx loop)
|
||||
(defcomp ~blog-authors-filter-from-data (&key authors selected-authors hx-select)
|
||||
(let* ((is-any (empty? (or selected-authors (list))))
|
||||
(any-cls (if is-any "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")))
|
||||
(~blog-filter-nav
|
||||
:items (<>
|
||||
(~blog-filter-any-author :cls any-cls :hx-select hx-select)
|
||||
(map (lambda (a)
|
||||
(let* ((slug (get a "slug"))
|
||||
(is-on (contains? selected-authors slug))
|
||||
(cls (if is-on "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))
|
||||
(icon (when (get a "profile_image")
|
||||
(~blog-filter-author-icon :src (get a "profile_image") :name (get a "name")))))
|
||||
(~blog-filter-author-li :cls cls :hx-get (str "?author=" slug "&page=1") :hx-select hx-select
|
||||
:icon icon :name (get a "name") :count (get a "count"))))
|
||||
(or authors (list)))))))
|
||||
|
||||
@@ -24,3 +24,37 @@
|
||||
(defcomp ~page-search-empty (&key query)
|
||||
(div :class "p-3 text-center text-stone-400 border border-stone-200 rounded-md"
|
||||
(str "No pages found matching \"" query "\"")))
|
||||
|
||||
;; Data-driven page search results (replaces Python render_page_search_results loop)
|
||||
(defcomp ~page-search-results-from-data (&key pages query has-more search-url next-page)
|
||||
(if (and (not pages) query)
|
||||
(~page-search-empty :query query)
|
||||
(when pages
|
||||
(~page-search-results
|
||||
:items (<> (map (lambda (p)
|
||||
(~page-search-item
|
||||
:id (get p "id") :title (get p "title")
|
||||
:slug (get p "slug") :feature-image (get p "feature_image")))
|
||||
pages))
|
||||
:sentinel (when has-more
|
||||
(~page-search-sentinel :url search-url :query query :next-page next-page))))))
|
||||
|
||||
;; Data-driven menu nav items (replaces Python render_menu_items_nav_oob loop)
|
||||
(defcomp ~blog-menu-nav-from-data (&key items nav-cls container-id arrow-cls scroll-hs)
|
||||
(if (not items)
|
||||
(~blog-nav-empty :wrapper-id "menu-items-nav-wrapper")
|
||||
(~scroll-nav-wrapper :wrapper-id "menu-items-nav-wrapper" :container-id container-id
|
||||
:arrow-cls arrow-cls
|
||||
:left-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft - 200")
|
||||
:scroll-hs scroll-hs
|
||||
:right-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft + 200")
|
||||
:items (<> (map (lambda (item)
|
||||
(let* ((img (~img-or-placeholder :src (get item "feature_image") :alt (get item "label")
|
||||
:size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0")))
|
||||
(if (= (get item "slug") "cart")
|
||||
(~blog-nav-item-plain :href (get item "href") :selected (get item "selected")
|
||||
:nav-cls nav-cls :img img :label (get item "label"))
|
||||
(~blog-nav-item-link :href (get item "href") :hx-get (get item "hx_get")
|
||||
:selected (get item "selected") :nav-cls nav-cls :img img :label (get item "label")))))
|
||||
items))
|
||||
:oob true)))
|
||||
|
||||
@@ -212,14 +212,9 @@ def _blog_cards_sx(ctx: dict) -> str:
|
||||
"""S-expression wire format for blog cards (client renders)."""
|
||||
posts = ctx.get("posts") or []
|
||||
view = ctx.get("view")
|
||||
parts = []
|
||||
for p in posts:
|
||||
if view == "tile":
|
||||
parts.append(_blog_card_tile_sx(p, ctx))
|
||||
else:
|
||||
parts.append(_blog_card_sx(p, ctx))
|
||||
parts.append(_blog_sentinel_sx(ctx))
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
post_dicts = [_blog_card_data(p, ctx) for p in posts]
|
||||
sentinel = SxExpr(_blog_sentinel_sx(ctx))
|
||||
return sx_call("blog-cards-from-data", posts=post_dicts, view=view, sentinel=sentinel)
|
||||
|
||||
|
||||
def _format_ts(dt) -> str:
|
||||
@@ -249,8 +244,8 @@ def _author_data(authors: list) -> list[dict]:
|
||||
return result
|
||||
|
||||
|
||||
def _blog_card_sx(post: dict, ctx: dict) -> str:
|
||||
"""Single blog card as sx call (wire format) — pure data, no HTML."""
|
||||
def _blog_card_data(post: dict, ctx: dict) -> dict:
|
||||
"""Serialize a blog post to a display-data dict for sx rendering."""
|
||||
from quart import g
|
||||
|
||||
slug = post.get("slug", "")
|
||||
@@ -274,7 +269,7 @@ def _blog_card_sx(post: dict, ctx: dict) -> str:
|
||||
tags = _tag_data(post.get("tags") or [])
|
||||
authors = _author_data(post.get("authors") or [])
|
||||
|
||||
kwargs = dict(
|
||||
d: dict[str, Any] = dict(
|
||||
slug=slug, href=href, hx_select=hx_select,
|
||||
title=post.get("title", ""),
|
||||
feature_image=fi, excerpt=excerpt,
|
||||
@@ -285,54 +280,18 @@ def _blog_card_sx(post: dict, ctx: dict) -> str:
|
||||
)
|
||||
|
||||
if user:
|
||||
kwargs["liked"] = post.get("is_liked", False)
|
||||
kwargs["like_url"] = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
||||
kwargs["csrf_token"] = _ctx_csrf(ctx)
|
||||
d["liked"] = post.get("is_liked", False)
|
||||
d["like_url"] = call_url(ctx, "blog_url", f"/{slug}/like/toggle/")
|
||||
d["csrf_token"] = _ctx_csrf(ctx)
|
||||
|
||||
if tags:
|
||||
kwargs["tags"] = tags
|
||||
d["tags"] = tags
|
||||
if authors:
|
||||
kwargs["authors"] = authors
|
||||
d["authors"] = authors
|
||||
if widget:
|
||||
kwargs["widget"] = SxExpr(widget) if widget else None
|
||||
d["widget"] = widget
|
||||
|
||||
return sx_call("blog-card", **kwargs)
|
||||
|
||||
|
||||
def _blog_card_tile_sx(post: dict, ctx: dict) -> str:
|
||||
"""Single blog card tile as sx call (wire format) — pure data."""
|
||||
slug = post.get("slug", "")
|
||||
href = call_url(ctx, "blog_url", f"/{slug}/")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
fi = post.get("feature_image")
|
||||
status = post.get("status", "published")
|
||||
is_draft = status == "draft"
|
||||
|
||||
if is_draft:
|
||||
status_timestamp = _format_ts(post.get("updated_at"))
|
||||
else:
|
||||
status_timestamp = _format_ts(post.get("published_at"))
|
||||
|
||||
excerpt = post.get("custom_excerpt") or post.get("excerpt", "")
|
||||
tags = _tag_data(post.get("tags") or [])
|
||||
authors = _author_data(post.get("authors") or [])
|
||||
|
||||
kwargs = dict(
|
||||
href=href, hx_select=hx_select, feature_image=fi,
|
||||
title=post.get("title", ""),
|
||||
is_draft=is_draft,
|
||||
publish_requested=post.get("publish_requested", False) if is_draft else False,
|
||||
status_timestamp=status_timestamp,
|
||||
excerpt=excerpt,
|
||||
)
|
||||
|
||||
if tags:
|
||||
kwargs["tags"] = tags
|
||||
if authors:
|
||||
kwargs["authors"] = authors
|
||||
|
||||
return sx_call("blog-card-tile", **kwargs)
|
||||
return d
|
||||
|
||||
|
||||
def _at_bar_sx(post: dict, ctx: dict) -> str:
|
||||
@@ -359,6 +318,22 @@ def _at_bar_sx(post: dict, ctx: dict) -> str:
|
||||
|
||||
|
||||
|
||||
def _page_card_data(page: dict, ctx: dict) -> dict:
|
||||
"""Serialize a page to a display-data dict for sx rendering."""
|
||||
slug = page.get("slug", "")
|
||||
href = call_url(ctx, "blog_url", f"/{slug}/")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
features = page.get("features") or {}
|
||||
return dict(
|
||||
href=href, hx_select=hx_select, title=page.get("title", ""),
|
||||
has_calendar=features.get("calendar", False),
|
||||
has_market=features.get("market", False),
|
||||
pub_timestamp=_format_ts(page.get("published_at")),
|
||||
feature_image=page.get("feature_image"),
|
||||
excerpt=page.get("custom_excerpt") or page.get("excerpt", ""),
|
||||
)
|
||||
|
||||
|
||||
def _page_cards_sx(ctx: dict) -> str:
|
||||
"""Render page cards with sentinel (sx)."""
|
||||
pages = ctx.get("pages") or ctx.get("posts") or []
|
||||
@@ -366,45 +341,22 @@ def _page_cards_sx(ctx: dict) -> str:
|
||||
total_pages = ctx.get("total_pages", 1)
|
||||
if isinstance(total_pages, str):
|
||||
total_pages = int(total_pages)
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
parts = []
|
||||
for pg in pages:
|
||||
parts.append(_page_card_sx(pg, ctx))
|
||||
page_dicts = [_page_card_data(pg, ctx) for pg in pages]
|
||||
|
||||
if page_num < total_pages:
|
||||
current_local_href = ctx.get("current_local_href", "/index?type=pages")
|
||||
next_url = f"{current_local_href}&page={page_num + 1}" if "?" in current_local_href else f"{current_local_href}?page={page_num + 1}"
|
||||
parts.append(sx_call("sentinel-simple",
|
||||
id=f"sentinel-{page_num}-d", next_url=next_url,
|
||||
))
|
||||
sentinel = SxExpr(sx_call("sentinel-simple",
|
||||
id=f"sentinel-{page_num}-d", next_url=next_url))
|
||||
elif pages:
|
||||
parts.append(sx_call("end-of-results"))
|
||||
sentinel = SxExpr(sx_call("end-of-results"))
|
||||
elif not pages:
|
||||
sentinel = SxExpr(sx_call("blog-no-pages"))
|
||||
else:
|
||||
parts.append(sx_call("blog-no-pages"))
|
||||
sentinel = None
|
||||
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
|
||||
|
||||
def _page_card_sx(page: dict, ctx: dict) -> str:
|
||||
"""Single page card as sx."""
|
||||
slug = page.get("slug", "")
|
||||
href = call_url(ctx, "blog_url", f"/{slug}/")
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
features = page.get("features") or {}
|
||||
pub_timestamp = _format_ts(page.get("published_at"))
|
||||
|
||||
fi = page.get("feature_image")
|
||||
excerpt = page.get("custom_excerpt") or page.get("excerpt", "")
|
||||
|
||||
return sx_call("blog-page-card",
|
||||
href=href, hx_select=hx_select, title=page.get("title", ""),
|
||||
has_calendar=features.get("calendar", False),
|
||||
has_market=features.get("market", False),
|
||||
pub_timestamp=pub_timestamp, feature_image=fi,
|
||||
excerpt=excerpt,
|
||||
)
|
||||
return sx_call("page-cards-from-data", pages=page_dicts, sentinel=sentinel)
|
||||
|
||||
|
||||
def _view_toggle_sx(ctx: dict) -> str:
|
||||
@@ -566,15 +518,10 @@ def _action_buttons_sx(ctx: dict) -> str:
|
||||
def _tag_groups_filter_sx(ctx: dict) -> str:
|
||||
"""Tag group filter bar as sx."""
|
||||
tag_groups = ctx.get("tag_groups") or []
|
||||
selected_groups = ctx.get("selected_groups") or ()
|
||||
selected_tags = ctx.get("selected_tags") or ()
|
||||
selected_groups = list(ctx.get("selected_groups") or ())
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
is_any = len(selected_groups) == 0 and len(selected_tags) == 0
|
||||
any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
li_parts = [sx_call("blog-filter-any-topic", cls=any_cls, hx_select=hx_select)]
|
||||
|
||||
group_dicts = []
|
||||
for group in tag_groups:
|
||||
g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "")
|
||||
g_name = getattr(group, "name", "") if hasattr(group, "name") else group.get("name", "")
|
||||
@@ -585,55 +532,35 @@ def _tag_groups_filter_sx(ctx: dict) -> str:
|
||||
if g_count <= 0 and g_slug not in selected_groups:
|
||||
continue
|
||||
|
||||
is_on = g_slug in selected_groups
|
||||
cls = "bg-stone-900 text-white border-stone-900" if is_on else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
if g_fi:
|
||||
icon = sx_call("blog-filter-group-icon-image", src=g_fi, name=g_name)
|
||||
else:
|
||||
style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;"
|
||||
icon = sx_call("blog-filter-group-icon-color", style=style, initial=g_name[:1])
|
||||
|
||||
li_parts.append(sx_call("blog-filter-group-li",
|
||||
cls=cls, hx_get=f"?group={g_slug}&page=1", hx_select=hx_select,
|
||||
icon=SxExpr(icon), name=g_name, count=str(g_count),
|
||||
style = (f"background-color: {g_colour}; color: white;" if g_colour
|
||||
else "background-color: #e7e5e4; color: #57534e;") if not g_fi else None
|
||||
group_dicts.append(dict(
|
||||
slug=g_slug, name=g_name, feature_image=g_fi,
|
||||
style=style, initial=g_name[:1], count=str(g_count),
|
||||
))
|
||||
|
||||
items = "(<> " + " ".join(li_parts) + ")"
|
||||
return sx_call("blog-filter-nav", items=SxExpr(items))
|
||||
return sx_call("blog-tag-groups-filter-from-data",
|
||||
groups=group_dicts, selected_groups=selected_groups, hx_select=hx_select)
|
||||
|
||||
|
||||
def _authors_filter_sx(ctx: dict) -> str:
|
||||
"""Author filter bar as sx."""
|
||||
authors = ctx.get("authors") or []
|
||||
selected_authors = ctx.get("selected_authors") or ()
|
||||
selected_authors = list(ctx.get("selected_authors") or ())
|
||||
hx_select = ctx.get("hx_select_search", "#main-panel")
|
||||
|
||||
is_any = len(selected_authors) == 0
|
||||
any_cls = "bg-stone-900 text-white border-stone-900" if is_any else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
li_parts = [sx_call("blog-filter-any-author", cls=any_cls, hx_select=hx_select)]
|
||||
|
||||
author_dicts = []
|
||||
for author in authors:
|
||||
a_slug = getattr(author, "slug", "") if hasattr(author, "slug") else author.get("slug", "")
|
||||
a_name = getattr(author, "name", "") if hasattr(author, "name") else author.get("name", "")
|
||||
a_img = getattr(author, "profile_image", None) if hasattr(author, "profile_image") else author.get("profile_image")
|
||||
a_count = getattr(author, "published_post_count", 0) if hasattr(author, "published_post_count") else author.get("published_post_count", 0)
|
||||
|
||||
is_on = a_slug in selected_authors
|
||||
cls = "bg-stone-900 text-white border-stone-900" if is_on else "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"
|
||||
|
||||
icon_sx = None
|
||||
if a_img:
|
||||
icon_sx = sx_call("blog-filter-author-icon", src=a_img, name=a_name)
|
||||
|
||||
li_parts.append(sx_call("blog-filter-author-li",
|
||||
cls=cls, hx_get=f"?author={a_slug}&page=1", hx_select=hx_select,
|
||||
icon=SxExpr(icon_sx) if icon_sx else None, name=a_name, count=str(a_count),
|
||||
author_dicts.append(dict(
|
||||
slug=a_slug, name=a_name, profile_image=a_img, count=str(a_count),
|
||||
))
|
||||
|
||||
items = "(<> " + " ".join(li_parts) + ")"
|
||||
return sx_call("blog-filter-nav", items=SxExpr(items))
|
||||
return sx_call("blog-authors-filter-from-data",
|
||||
authors=author_dicts, selected_authors=selected_authors, hx_select=hx_select)
|
||||
|
||||
|
||||
def _tag_groups_filter_summary_sx(ctx: dict) -> str:
|
||||
@@ -846,48 +773,21 @@ def _snippets_list_sx(ctx: dict) -> str:
|
||||
"admin": "bg-amber-100 text-amber-700",
|
||||
}
|
||||
|
||||
row_parts = []
|
||||
snippet_dicts = []
|
||||
for s in snippets:
|
||||
s_id = getattr(s, "id", None) or s.get("id")
|
||||
s_name = getattr(s, "name", "") if hasattr(s, "name") else s.get("name", "")
|
||||
s_uid = getattr(s, "user_id", None) if hasattr(s, "user_id") else s.get("user_id")
|
||||
s_vis = getattr(s, "visibility", "private") if hasattr(s, "visibility") else s.get("visibility", "private")
|
||||
|
||||
owner = "You" if s_uid == user_id else f"User #{s_uid}"
|
||||
badge_cls = badge_colours.get(s_vis, "bg-stone-200 text-stone-700")
|
||||
|
||||
extra = ""
|
||||
if is_admin:
|
||||
patch_url = qurl("snippets.patch_visibility", snippet_id=s_id)
|
||||
opts = ""
|
||||
for v in ["private", "shared", "admin"]:
|
||||
opts += sx_call("blog-snippet-option",
|
||||
value=v, selected=(s_vis == v), label=v,
|
||||
)
|
||||
extra += sx_call("blog-snippet-visibility-select",
|
||||
patch_url=patch_url,
|
||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
options=SxExpr("(<> " + opts + ")") if opts else None,
|
||||
cls="text-sm border border-stone-300 rounded px-2 py-1",
|
||||
)
|
||||
|
||||
if s_uid == user_id or is_admin:
|
||||
del_url = qurl("snippets.delete_snippet", snippet_id=s_id)
|
||||
extra += sx_call("delete-btn",
|
||||
url=del_url, trigger_target="#snippets-list",
|
||||
title="Delete snippet?",
|
||||
text=f'Delete \u201c{s_name}\u201d?',
|
||||
sx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
cls="px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0",
|
||||
)
|
||||
|
||||
row_parts.append(sx_call("blog-snippet-row",
|
||||
name=s_name, owner=owner, badge_cls=badge_cls,
|
||||
visibility=s_vis, extra=SxExpr("(<> " + extra + ")") if extra else None,
|
||||
snippet_dicts.append(dict(
|
||||
id=s_id, name=s_name, user_id=s_uid, visibility=s_vis,
|
||||
patch_url=qurl("snippets.patch_visibility", snippet_id=s_id),
|
||||
delete_url=qurl("snippets.delete_snippet", snippet_id=s_id),
|
||||
))
|
||||
|
||||
rows = "(<> " + " ".join(row_parts) + ")"
|
||||
return sx_call("blog-snippets-list", rows=SxExpr(rows))
|
||||
return sx_call("blog-snippets-from-data",
|
||||
snippets=snippet_dicts, user_id=user_id, is_admin=is_admin,
|
||||
csrf=csrf, badge_colours=badge_colours)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -911,29 +811,21 @@ def _menu_items_list_sx(ctx: dict) -> str:
|
||||
if not menu_items:
|
||||
return sx_call("empty-state", icon="fa fa-inbox", message="No menu items yet. Add one to get started!")
|
||||
|
||||
row_parts = []
|
||||
item_dicts = []
|
||||
for item in menu_items:
|
||||
i_id = getattr(item, "id", None) or item.get("id")
|
||||
label = getattr(item, "label", "") if hasattr(item, "label") else item.get("label", "")
|
||||
slug = getattr(item, "slug", "") if hasattr(item, "slug") else item.get("slug", "")
|
||||
fi = getattr(item, "feature_image", None) if hasattr(item, "feature_image") else item.get("feature_image")
|
||||
sort = getattr(item, "sort_order", 0) if hasattr(item, "sort_order") else item.get("sort_order", 0)
|
||||
|
||||
edit_url = qurl("menu_items.edit_menu_item", item_id=i_id)
|
||||
del_url = qurl("menu_items.delete_menu_item_route", item_id=i_id)
|
||||
|
||||
img_sx = sx_call("img-or-placeholder", src=fi, alt=label,
|
||||
size_cls="w-12 h-12 rounded-full object-cover flex-shrink-0")
|
||||
|
||||
row_parts.append(sx_call("blog-menu-item-row",
|
||||
img=SxExpr(img_sx), label=label, slug=slug,
|
||||
sort_order=str(sort), edit_url=edit_url, delete_url=del_url,
|
||||
confirm_text=f"Remove {label} from the menu?",
|
||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
item_dicts.append(dict(
|
||||
label=label, slug=slug, feature_image=fi,
|
||||
sort_order=str(sort),
|
||||
edit_url=qurl("menu_items.edit_menu_item", item_id=i_id),
|
||||
delete_url=qurl("menu_items.delete_menu_item_route", item_id=i_id),
|
||||
))
|
||||
|
||||
rows = "(<> " + " ".join(row_parts) + ")"
|
||||
return sx_call("blog-menu-items-list", rows=SxExpr(rows))
|
||||
return sx_call("blog-menu-items-from-data", items=item_dicts, csrf=csrf)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -946,57 +838,32 @@ def _tag_groups_main_panel_sx(ctx: dict) -> str:
|
||||
groups = ctx.get("groups") or []
|
||||
unassigned_tags = ctx.get("unassigned_tags") or []
|
||||
csrf = _ctx_csrf(ctx)
|
||||
|
||||
create_url = qurl("blog.tag_groups_admin.create")
|
||||
form_sx = sx_call("blog-tag-groups-create-form",
|
||||
create_url=create_url, csrf=csrf,
|
||||
)
|
||||
|
||||
# Groups list
|
||||
groups_html = ""
|
||||
if groups:
|
||||
li_parts = []
|
||||
for group in groups:
|
||||
g_id = getattr(group, "id", None) or group.get("id")
|
||||
g_name = getattr(group, "name", "") if hasattr(group, "name") else group.get("name", "")
|
||||
g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "")
|
||||
g_fi = getattr(group, "feature_image", None) if hasattr(group, "feature_image") else group.get("feature_image")
|
||||
g_colour = getattr(group, "colour", None) if hasattr(group, "colour") else group.get("colour")
|
||||
g_sort = getattr(group, "sort_order", 0) if hasattr(group, "sort_order") else group.get("sort_order", 0)
|
||||
group_dicts = []
|
||||
for group in groups:
|
||||
g_id = getattr(group, "id", None) or group.get("id")
|
||||
g_name = getattr(group, "name", "") if hasattr(group, "name") else group.get("name", "")
|
||||
g_slug = getattr(group, "slug", "") if hasattr(group, "slug") else group.get("slug", "")
|
||||
g_fi = getattr(group, "feature_image", None) if hasattr(group, "feature_image") else group.get("feature_image")
|
||||
g_colour = getattr(group, "colour", None) if hasattr(group, "colour") else group.get("colour")
|
||||
g_sort = getattr(group, "sort_order", 0) if hasattr(group, "sort_order") else group.get("sort_order", 0)
|
||||
style = (f"background-color: {g_colour}; color: white;" if g_colour
|
||||
else "background-color: #e7e5e4; color: #57534e;") if not g_fi else None
|
||||
group_dicts.append(dict(
|
||||
name=g_name, slug=g_slug, feature_image=g_fi,
|
||||
style=style, initial=g_name[:1], sort_order=str(g_sort),
|
||||
edit_href=qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id),
|
||||
))
|
||||
|
||||
edit_href = qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id)
|
||||
unassigned_dicts = []
|
||||
for tag in unassigned_tags:
|
||||
t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "")
|
||||
unassigned_dicts.append(dict(name=t_name))
|
||||
|
||||
if g_fi:
|
||||
icon = sx_call("blog-tag-group-icon-image", src=g_fi, name=g_name)
|
||||
else:
|
||||
style = f"background-color: {g_colour}; color: white;" if g_colour else "background-color: #e7e5e4; color: #57534e;"
|
||||
icon = sx_call("blog-tag-group-icon-color", style=style, initial=g_name[:1])
|
||||
|
||||
li_parts.append(sx_call("blog-tag-group-li",
|
||||
icon=SxExpr(icon), edit_href=edit_href, name=g_name,
|
||||
slug=g_slug, sort_order=str(g_sort),
|
||||
))
|
||||
groups_sx = sx_call("blog-tag-groups-list", items=SxExpr("(<> " + " ".join(li_parts) + ")"))
|
||||
else:
|
||||
groups_sx = sx_call("empty-state", message="No tag groups yet.", cls="text-stone-500 text-sm")
|
||||
|
||||
# Unassigned tags
|
||||
unassigned_sx = ""
|
||||
if unassigned_tags:
|
||||
tag_spans = []
|
||||
for tag in unassigned_tags:
|
||||
t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "")
|
||||
tag_spans.append(sx_call("blog-unassigned-tag", name=t_name))
|
||||
unassigned_sx = sx_call("blog-unassigned-tags",
|
||||
heading=f"Unassigned Tags ({len(unassigned_tags)})",
|
||||
spans=SxExpr("(<> " + " ".join(tag_spans) + ")"),
|
||||
)
|
||||
|
||||
return sx_call("blog-tag-groups-main",
|
||||
form=SxExpr(form_sx),
|
||||
groups=SxExpr(groups_sx),
|
||||
unassigned=SxExpr(unassigned_sx) if unassigned_sx else None,
|
||||
)
|
||||
return sx_call("blog-tag-groups-from-data",
|
||||
groups=group_dicts, unassigned_tags=unassigned_dicts,
|
||||
csrf=csrf, create_url=create_url)
|
||||
|
||||
|
||||
def _tag_groups_edit_main_panel_sx(ctx: dict) -> str:
|
||||
@@ -1016,24 +883,24 @@ def _tag_groups_edit_main_panel_sx(ctx: dict) -> str:
|
||||
save_url = qurl("blog.tag_groups_admin.save", id=g_id)
|
||||
del_url = qurl("blog.tag_groups_admin.delete_group", id=g_id)
|
||||
|
||||
# Tag checkboxes
|
||||
tag_items = []
|
||||
# Tag checkboxes — pass data dicts to defcomp
|
||||
tag_dicts = []
|
||||
for tag in all_tags:
|
||||
t_id = getattr(tag, "id", None) or tag.get("id")
|
||||
t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "")
|
||||
t_fi = getattr(tag, "feature_image", None) if hasattr(tag, "feature_image") else tag.get("feature_image")
|
||||
checked = t_id in assigned_tag_ids
|
||||
img = sx_call("blog-tag-checkbox-image", src=t_fi) if t_fi else ""
|
||||
tag_items.append(sx_call("blog-tag-checkbox",
|
||||
tag_id=str(t_id), checked=checked,
|
||||
img=SxExpr(img) if img else None, name=t_name,
|
||||
tag_dicts.append(dict(
|
||||
tag_id=str(t_id), name=t_name, feature_image=t_fi,
|
||||
checked=t_id in assigned_tag_ids,
|
||||
))
|
||||
|
||||
tags_sx = sx_call("blog-tag-checkboxes-from-data", tags=tag_dicts)
|
||||
|
||||
edit_form = sx_call("blog-tag-group-edit-form",
|
||||
save_url=save_url, csrf=csrf,
|
||||
name=g_name, colour=g_colour or "", sort_order=str(g_sort),
|
||||
feature_image=g_fi or "",
|
||||
tags=SxExpr("(<> " + " ".join(tag_items) + ")"),
|
||||
tags=SxExpr(tags_sx),
|
||||
)
|
||||
|
||||
del_form = sx_call("blog-tag-group-delete-form",
|
||||
@@ -2171,24 +2038,17 @@ def render_page_search_results(pages, query, page, has_more) -> str:
|
||||
if not pages:
|
||||
return ""
|
||||
|
||||
items = []
|
||||
for post in pages:
|
||||
items.append(sx_call("page-search-item",
|
||||
id=post.id, title=post.title,
|
||||
slug=post.slug,
|
||||
feature_image=post.feature_image or None))
|
||||
page_dicts = [
|
||||
dict(id=post.id, title=post.title, slug=post.slug,
|
||||
feature_image=post.feature_image or None)
|
||||
for post in pages
|
||||
]
|
||||
|
||||
sentinel = ""
|
||||
if has_more:
|
||||
search_url = qurl("menu_items.search_pages_route")
|
||||
sentinel = sx_call("page-search-sentinel",
|
||||
url=search_url, query=query,
|
||||
next_page=page + 1)
|
||||
search_url = qurl("menu_items.search_pages_route") if has_more else None
|
||||
|
||||
items_sx = "(<> " + " ".join(items) + ")"
|
||||
return sx_call("page-search-results",
|
||||
items=SxExpr(items_sx),
|
||||
sentinel=SxExpr(sentinel) if sentinel else None)
|
||||
return sx_call("page-search-results-from-data",
|
||||
pages=page_dicts, query=query, has_more=has_more,
|
||||
search_url=search_url, next_page=page + 1)
|
||||
|
||||
|
||||
def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
@@ -2203,26 +2063,23 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
if not menu_items:
|
||||
return sx_call("blog-nav-empty", wrapper_id="menu-items-nav-wrapper")
|
||||
|
||||
# Resolve URL helpers from context or fall back to template globals
|
||||
if ctx is None:
|
||||
ctx = {}
|
||||
|
||||
first_seg = qrequest.path.strip("/").split("/")[0] if qrequest else ""
|
||||
|
||||
# nav_button style (matches shared/infrastructure/jinja_setup.py)
|
||||
select_colours = (
|
||||
"[.hover-capable_&]:hover:bg-yellow-300"
|
||||
" aria-selected:bg-stone-500 aria-selected:text-white"
|
||||
" [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500"
|
||||
)
|
||||
nav_button_cls = (
|
||||
nav_cls = (
|
||||
f"justify-center cursor-pointer flex flex-row items-center gap-2"
|
||||
f" rounded bg-stone-200 text-black {select_colours} p-3"
|
||||
)
|
||||
|
||||
container_id = "menu-items-container"
|
||||
arrow_cls = f"scrolling-menu-arrow-{container_id}"
|
||||
|
||||
scroll_hs = (
|
||||
f"on load or scroll"
|
||||
f" if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth"
|
||||
@@ -2234,7 +2091,7 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
cart_url_fn = ctx.get("cart_url")
|
||||
app_name = ctx.get("app_name", "")
|
||||
|
||||
item_parts = []
|
||||
item_dicts = []
|
||||
for item in menu_items:
|
||||
item_slug = getattr(item, "slug", "") if hasattr(item, "slug") else item.get("slug", "")
|
||||
label = getattr(item, "label", "") if hasattr(item, "label") else item.get("label", "")
|
||||
@@ -2248,31 +2105,16 @@ def render_menu_items_nav_oob(menu_items, ctx: dict | None = None) -> str:
|
||||
href = f"/{item_slug}/"
|
||||
|
||||
selected = "true" if (item_slug == first_seg or item_slug == app_name) else "false"
|
||||
hx_get = f"/{item_slug}/" if item_slug != "cart" else None
|
||||
|
||||
img_sx = sx_call("img-or-placeholder", src=fi, alt=label,
|
||||
size_cls="w-8 h-8 rounded-full object-cover flex-shrink-0")
|
||||
item_dicts.append(dict(
|
||||
slug=item_slug, label=label, feature_image=fi,
|
||||
href=href, hx_get=hx_get, selected=selected,
|
||||
))
|
||||
|
||||
if item_slug != "cart":
|
||||
item_parts.append(sx_call("blog-nav-item-link",
|
||||
href=href, hx_get=f"/{item_slug}/", selected=selected,
|
||||
nav_cls=nav_button_cls, img=SxExpr(img_sx), label=label,
|
||||
))
|
||||
else:
|
||||
item_parts.append(sx_call("blog-nav-item-plain",
|
||||
href=href, selected=selected, nav_cls=nav_button_cls,
|
||||
img=SxExpr(img_sx), label=label,
|
||||
))
|
||||
|
||||
items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else ""
|
||||
|
||||
return sx_call("scroll-nav-wrapper",
|
||||
wrapper_id="menu-items-nav-wrapper", container_id=container_id,
|
||||
arrow_cls=arrow_cls,
|
||||
left_hs=f"on click set #{container_id}.scrollLeft to #{container_id}.scrollLeft - 200",
|
||||
scroll_hs=scroll_hs,
|
||||
right_hs=f"on click set #{container_id}.scrollLeft to #{container_id}.scrollLeft + 200",
|
||||
items=SxExpr(items_sx) if items_sx else None, oob=True,
|
||||
)
|
||||
return sx_call("blog-menu-nav-from-data",
|
||||
items=item_dicts, nav_cls=nav_cls, container_id=container_id,
|
||||
arrow_cls=arrow_cls, scroll_hs=scroll_hs)
|
||||
|
||||
|
||||
# ---- Features panel ----
|
||||
|
||||
@@ -53,3 +53,16 @@
|
||||
|
||||
(defcomp ~federation-profile-summary-text (&key text)
|
||||
(p :class "mt-2" text))
|
||||
|
||||
;; Data-driven activities list (replaces Python loop in render_profile_page)
|
||||
(defcomp ~federation-activities-from-data (&key activities)
|
||||
(if (empty? (or activities (list)))
|
||||
(~federation-activities-empty)
|
||||
(~federation-activities-list
|
||||
:items (<> (map (lambda (a)
|
||||
(~federation-activity-card
|
||||
:activity-type (get a "activity_type")
|
||||
:published (get a "published")
|
||||
:obj-type (when (get a "object_type")
|
||||
(~federation-activity-obj-type :obj-type (get a "object_type")))))
|
||||
activities)))))
|
||||
|
||||
@@ -40,6 +40,47 @@
|
||||
summary)
|
||||
button))
|
||||
|
||||
;; Data-driven actor card (replaces Python _actor_card_sx loop)
|
||||
(defcomp ~federation-actor-card-from-data (&key d has-actor csrf follow-url unfollow-url list-type)
|
||||
(let* ((icon-url (get d "icon_url"))
|
||||
(display-name (get d "display_name"))
|
||||
(username (get d "username"))
|
||||
(domain (get d "domain"))
|
||||
(actor-url (get d "actor_url"))
|
||||
(safe-id (get d "safe_id"))
|
||||
(initial (or (get d "initial") "?"))
|
||||
(avatar (~avatar
|
||||
:src icon-url
|
||||
:cls (if icon-url "w-12 h-12 rounded-full"
|
||||
"w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold")
|
||||
:initial (when (not icon-url) initial)))
|
||||
(name-sx (if (get d "external_link")
|
||||
(~federation-actor-name-link-external :href (get d "name_href") :name display-name)
|
||||
(~federation-actor-name-link :href (get d "name_href") :name display-name)))
|
||||
(summary-sx (when (get d "summary")
|
||||
(~federation-actor-summary :summary (get d "summary"))))
|
||||
(is-followed (get d "is_followed"))
|
||||
(button (when has-actor
|
||||
(if (or (= list-type "following") is-followed)
|
||||
(~federation-unfollow-button :action unfollow-url :csrf csrf :actor-url actor-url)
|
||||
(~federation-follow-button :action follow-url :csrf csrf :actor-url actor-url
|
||||
:label (if (= list-type "followers") "Follow Back" "Follow"))))))
|
||||
(~federation-actor-card
|
||||
:cls "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4"
|
||||
:id (str "actor-" safe-id)
|
||||
:avatar avatar :name name-sx :username username :domain domain
|
||||
:summary summary-sx :button button)))
|
||||
|
||||
;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops)
|
||||
(defcomp ~federation-actor-list-from-data (&key actors next-url has-actor csrf
|
||||
follow-url unfollow-url list-type)
|
||||
(<>
|
||||
(map (lambda (d)
|
||||
(~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf
|
||||
:follow-url follow-url :unfollow-url unfollow-url :list-type list-type))
|
||||
(or actors (list)))
|
||||
(when next-url (~federation-scroll-sentinel :url next-url))))
|
||||
|
||||
(defcomp ~federation-search-info (&key cls text)
|
||||
(p :class cls text))
|
||||
|
||||
|
||||
@@ -90,6 +90,65 @@
|
||||
compose)
|
||||
(div :id "timeline" timeline))
|
||||
|
||||
;; --- Data-driven post card (replaces Python _post_card_sx loop) ---
|
||||
|
||||
(defcomp ~federation-post-card-from-data (&key d has-actor csrf
|
||||
like-url unlike-url
|
||||
boost-url unboost-url)
|
||||
(let* ((boosted-by (get d "boosted_by"))
|
||||
(actor-icon (get d "actor_icon"))
|
||||
(actor-name (get d "actor_name"))
|
||||
(initial (or (get d "initial") "?"))
|
||||
(avatar (~avatar
|
||||
:src actor-icon
|
||||
:cls (if actor-icon "w-10 h-10 rounded-full"
|
||||
"w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm")
|
||||
:initial (when (not actor-icon) initial)))
|
||||
(boost (when boosted-by (~federation-boost-label :name boosted-by)))
|
||||
(content-sx (if (get d "summary")
|
||||
(~federation-content :content (get d "content") :summary (get d "summary"))
|
||||
(~federation-content :content (get d "content"))))
|
||||
(original (when (get d "original_url")
|
||||
(~federation-original-link :url (get d "original_url"))))
|
||||
(safe-id (get d "safe_id"))
|
||||
(interactions (when has-actor
|
||||
(let* ((oid (get d "object_id"))
|
||||
(ainbox (get d "author_inbox"))
|
||||
(target (str "#interactions-" safe-id))
|
||||
(liked (get d "liked_by_me"))
|
||||
(boosted-me (get d "boosted_by_me"))
|
||||
(l-action (if liked unlike-url like-url))
|
||||
(l-cls (str "flex items-center gap-1 " (if liked "text-red-500 hover:text-red-600" "hover:text-red-500")))
|
||||
(l-icon (if liked "\u2665" "\u2661"))
|
||||
(b-action (if boosted-me unboost-url boost-url))
|
||||
(b-cls (str "flex items-center gap-1 " (if boosted-me "text-green-600 hover:text-green-700" "hover:text-green-600")))
|
||||
(reply-url (get d "reply_url"))
|
||||
(reply (when reply-url (~federation-reply-link :url reply-url)))
|
||||
(like-form (~federation-like-form
|
||||
:action l-action :target target :oid oid :ainbox ainbox
|
||||
:csrf csrf :cls l-cls :icon l-icon :count (get d "like_count")))
|
||||
(boost-form (~federation-boost-form
|
||||
:action b-action :target target :oid oid :ainbox ainbox
|
||||
:csrf csrf :cls b-cls :count (get d "boost_count"))))
|
||||
(div :id (str "interactions-" safe-id)
|
||||
(~federation-interaction-buttons :like like-form :boost boost-form :reply reply))))))
|
||||
(~federation-post-card
|
||||
:boost boost :avatar avatar
|
||||
:actor-name actor-name :actor-username (get d "actor_username")
|
||||
:domain (get d "domain") :time (get d "time")
|
||||
:content content-sx :original original
|
||||
:interactions interactions)))
|
||||
|
||||
;; Data-driven timeline items (replaces Python _timeline_items_sx loop)
|
||||
(defcomp ~federation-timeline-items-from-data (&key items next-url has-actor csrf
|
||||
like-url unlike-url boost-url unboost-url)
|
||||
(<>
|
||||
(map (lambda (d)
|
||||
(~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf
|
||||
:like-url like-url :unlike-url unlike-url :boost-url boost-url :unboost-url unboost-url))
|
||||
(or items (list)))
|
||||
(when next-url (~federation-scroll-sentinel :url next-url))))
|
||||
|
||||
;; --- Compose ---
|
||||
|
||||
(defcomp ~federation-compose-reply (&key reply-to)
|
||||
|
||||
@@ -147,62 +147,49 @@ def _interaction_buttons_sx(item: Any, actor: Any) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _post_card_sx(item: Any, actor: Any) -> str:
|
||||
"""Render a single timeline post card."""
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post card data serializer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _post_card_data(item: Any, actor: Any) -> dict:
|
||||
"""Serialize a timeline item to a display-data dict for sx rendering."""
|
||||
from quart import url_for
|
||||
|
||||
boosted_by = getattr(item, "boosted_by", None)
|
||||
actor_icon = getattr(item, "actor_icon", None)
|
||||
actor_name = getattr(item, "actor_name", "?")
|
||||
actor_username = getattr(item, "actor_username", "")
|
||||
actor_domain = getattr(item, "actor_domain", "")
|
||||
content = getattr(item, "content", "")
|
||||
summary = getattr(item, "summary", None)
|
||||
published = getattr(item, "published", None)
|
||||
oid = getattr(item, "object_id", "") or ""
|
||||
safe_id = oid.replace("/", "_").replace(":", "_")
|
||||
initial = actor_name[0].upper() if (not actor_icon and actor_name) else "?"
|
||||
url = getattr(item, "url", None)
|
||||
post_type = getattr(item, "post_type", "")
|
||||
published = getattr(item, "published", None)
|
||||
summary = getattr(item, "summary", None)
|
||||
|
||||
boost_sx = sx_call(
|
||||
"federation-boost-label", name=str(escape(boosted_by)),
|
||||
) if boosted_by else ""
|
||||
|
||||
initial = actor_name[0].upper() if (not actor_icon and actor_name) else "?"
|
||||
avatar = sx_call(
|
||||
"avatar", src=actor_icon or None,
|
||||
cls="w-10 h-10 rounded-full" if actor_icon else "w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm",
|
||||
initial=None if actor_icon else initial,
|
||||
)
|
||||
|
||||
domain_str = f"@{escape(actor_domain)}" if actor_domain else ""
|
||||
time_str = published.strftime("%b %d, %H:%M") if published else ""
|
||||
|
||||
if summary:
|
||||
content_sx = sx_call(
|
||||
"federation-content",
|
||||
content=content, summary=str(escape(summary)),
|
||||
)
|
||||
else:
|
||||
content_sx = sx_call("federation-content", content=content)
|
||||
|
||||
original_sx = ""
|
||||
if url and post_type == "remote":
|
||||
original_sx = sx_call("federation-original-link", url=url)
|
||||
|
||||
interactions_sx = ""
|
||||
if actor:
|
||||
oid = getattr(item, "object_id", "") or ""
|
||||
safe_id = oid.replace("/", "_").replace(":", "_")
|
||||
interactions_sx = f'(div :id {serialize(f"interactions-{safe_id}")} {_interaction_buttons_sx(item, actor)})'
|
||||
|
||||
return sx_call(
|
||||
"federation-post-card",
|
||||
boost=SxExpr(boost_sx) if boost_sx else None,
|
||||
avatar=SxExpr(avatar),
|
||||
d: dict[str, Any] = dict(
|
||||
boosted_by=str(escape(boosted_by)) if boosted_by else None,
|
||||
actor_icon=actor_icon or None,
|
||||
actor_name=str(escape(actor_name)),
|
||||
actor_username=str(escape(actor_username)),
|
||||
domain=domain_str, time=time_str,
|
||||
content=SxExpr(content_sx),
|
||||
original=SxExpr(original_sx) if original_sx else None,
|
||||
interactions=SxExpr(interactions_sx) if interactions_sx else None,
|
||||
domain=f"@{escape(actor_domain)}" if actor_domain else "",
|
||||
time=published.strftime("%b %d, %H:%M") if published else "",
|
||||
content=getattr(item, "content", ""),
|
||||
summary=str(escape(summary)) if summary else None,
|
||||
original_url=url if url and post_type == "remote" else None,
|
||||
object_id=oid,
|
||||
author_inbox=getattr(item, "author_inbox", "") or "",
|
||||
safe_id=safe_id,
|
||||
initial=initial,
|
||||
like_count=str(getattr(item, "like_count", 0) or 0),
|
||||
boost_count=str(getattr(item, "boost_count", 0) or 0),
|
||||
liked_by_me=bool(getattr(item, "liked_by_me", False)),
|
||||
boosted_by_me=bool(getattr(item, "boosted_by_me", False)),
|
||||
)
|
||||
if actor and oid:
|
||||
d["reply_url"] = url_for("social.defpage_compose_form", reply_to=oid)
|
||||
return d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -213,9 +200,11 @@ def _timeline_items_sx(items: list, timeline_type: str, actor: Any,
|
||||
actor_id: int | None = None) -> str:
|
||||
"""Render timeline items with infinite scroll sentinel."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
parts = [_post_card_sx(item, actor) for item in items]
|
||||
item_dicts = [_post_card_data(item, actor) for item in items]
|
||||
|
||||
next_url = None
|
||||
if items:
|
||||
last = items[-1]
|
||||
before = last.published.isoformat() if last.published else ""
|
||||
@@ -223,22 +212,28 @@ def _timeline_items_sx(items: list, timeline_type: str, actor: Any,
|
||||
next_url = url_for("social.actor_timeline_page", id=actor_id, before=before)
|
||||
else:
|
||||
next_url = url_for(f"social.{timeline_type}_timeline_page", before=before)
|
||||
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
|
||||
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
kwargs: dict[str, Any] = dict(items=item_dicts, next_url=next_url)
|
||||
if actor:
|
||||
kwargs["has_actor"] = True
|
||||
kwargs["csrf"] = generate_csrf_token()
|
||||
kwargs["like_url"] = url_for("social.like")
|
||||
kwargs["unlike_url"] = url_for("social.unlike")
|
||||
kwargs["boost_url"] = url_for("social.boost")
|
||||
kwargs["unboost_url"] = url_for("social.unboost")
|
||||
|
||||
return sx_call("federation-timeline-items-from-data", **kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Search results (pagination fragment)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
|
||||
*, list_type: str = "search") -> str:
|
||||
"""Render a single actor card with follow/unfollow button."""
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
def _actor_card_data(a: Any, followed_urls: set,
|
||||
*, list_type: str = "search") -> dict:
|
||||
"""Serialize an actor to a display-data dict for sx rendering."""
|
||||
from quart import url_for
|
||||
|
||||
csrf = generate_csrf_token()
|
||||
display_name = getattr(a, "display_name", None) or getattr(a, "preferred_username", "")
|
||||
username = getattr(a, "preferred_username", "")
|
||||
domain = getattr(a, "domain", "")
|
||||
@@ -248,55 +243,43 @@ def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
|
||||
aid = getattr(a, "id", None)
|
||||
|
||||
safe_id = actor_url.replace("/", "_").replace(":", "_")
|
||||
|
||||
initial = (display_name or username)[0].upper() if (not icon_url and (display_name or username)) else "?"
|
||||
avatar = sx_call(
|
||||
"avatar", src=icon_url or None,
|
||||
cls="w-12 h-12 rounded-full" if icon_url else "w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold",
|
||||
initial=None if icon_url else initial,
|
||||
is_followed = actor_url in (followed_urls or set())
|
||||
|
||||
d: dict[str, Any] = dict(
|
||||
display_name=str(escape(display_name)),
|
||||
username=str(escape(username)),
|
||||
domain=str(escape(domain)),
|
||||
icon_url=icon_url or None,
|
||||
actor_url=actor_url,
|
||||
summary=summary,
|
||||
safe_id=safe_id,
|
||||
initial=initial,
|
||||
is_followed=is_followed,
|
||||
)
|
||||
|
||||
# Name link
|
||||
if (list_type in ("following", "search")) and aid:
|
||||
name_sx = sx_call(
|
||||
"federation-actor-name-link",
|
||||
href=url_for("social.defpage_actor_timeline", id=aid),
|
||||
name=str(escape(display_name)),
|
||||
)
|
||||
d["name_href"] = url_for("social.defpage_actor_timeline", id=aid)
|
||||
else:
|
||||
name_sx = sx_call(
|
||||
"federation-actor-name-link-external",
|
||||
href=f"https://{domain}/@{username}",
|
||||
name=str(escape(display_name)),
|
||||
)
|
||||
d["name_href"] = f"https://{domain}/@{username}"
|
||||
d["external_link"] = True
|
||||
return d
|
||||
|
||||
summary_sx = sx_call("federation-actor-summary", summary=summary) if summary else ""
|
||||
|
||||
# Follow/unfollow button
|
||||
button_sx = ""
|
||||
if actor:
|
||||
is_followed = actor_url in (followed_urls or set())
|
||||
if list_type == "following" or is_followed:
|
||||
button_sx = sx_call(
|
||||
"federation-unfollow-button",
|
||||
action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url,
|
||||
)
|
||||
else:
|
||||
label = "Follow Back" if list_type == "followers" else "Follow"
|
||||
button_sx = sx_call(
|
||||
"federation-follow-button",
|
||||
action=url_for("social.follow"), csrf=csrf, actor_url=actor_url, label=label,
|
||||
)
|
||||
def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
|
||||
*, list_type: str = "search") -> str:
|
||||
"""Render a single actor card (used by follow/unfollow re-render)."""
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from quart import url_for
|
||||
|
||||
d = _actor_card_data(a, followed_urls, list_type=list_type)
|
||||
return sx_call(
|
||||
"federation-actor-card",
|
||||
cls="bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4",
|
||||
id=f"actor-{safe_id}",
|
||||
avatar=SxExpr(avatar),
|
||||
name=SxExpr(name_sx),
|
||||
username=str(escape(username)), domain=str(escape(domain)),
|
||||
summary=SxExpr(summary_sx) if summary_sx else None,
|
||||
button=SxExpr(button_sx) if button_sx else None,
|
||||
"federation-actor-card-from-data",
|
||||
d=d,
|
||||
has_actor=actor is not None,
|
||||
csrf=generate_csrf_token() if actor else None,
|
||||
follow_url=url_for("social.follow") if actor else None,
|
||||
unfollow_url=url_for("social.unfollow") if actor else None,
|
||||
list_type=list_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -304,24 +287,38 @@ def _search_results_sx(actors: list, query: str, page: int,
|
||||
followed_urls: set, actor: Any) -> str:
|
||||
"""Render search results with pagination sentinel."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
parts = [_actor_card_sx(a, actor, followed_urls, list_type="search") for a in actors]
|
||||
if len(actors) >= 20:
|
||||
next_url = url_for("social.search_page", q=query, page=page + 1)
|
||||
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
actor_dicts = [_actor_card_data(a, followed_urls, list_type="search") for a in actors]
|
||||
next_url = url_for("social.search_page", q=query, page=page + 1) if len(actors) >= 20 else None
|
||||
|
||||
return sx_call(
|
||||
"federation-actor-list-from-data",
|
||||
actors=actor_dicts, next_url=next_url, list_type="search",
|
||||
has_actor=actor is not None,
|
||||
csrf=generate_csrf_token() if actor else None,
|
||||
follow_url=url_for("social.follow") if actor else None,
|
||||
unfollow_url=url_for("social.unfollow") if actor else None,
|
||||
)
|
||||
|
||||
|
||||
def _actor_list_items_sx(actors: list, page: int, list_type: str,
|
||||
followed_urls: set, actor: Any) -> str:
|
||||
"""Render actor list items (following/followers) with pagination sentinel."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
parts = [_actor_card_sx(a, actor, followed_urls, list_type=list_type) for a in actors]
|
||||
if len(actors) >= 20:
|
||||
next_url = url_for(f"social.{list_type}_list_page", page=page + 1)
|
||||
parts.append(sx_call("federation-scroll-sentinel", url=next_url))
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
actor_dicts = [_actor_card_data(a, followed_urls, list_type=list_type) for a in actors]
|
||||
next_url = url_for(f"social.{list_type}_list_page", page=page + 1) if len(actors) >= 20 else None
|
||||
|
||||
return sx_call(
|
||||
"federation-actor-list-from-data",
|
||||
actors=actor_dicts, next_url=next_url, list_type=list_type,
|
||||
has_actor=actor is not None,
|
||||
csrf=generate_csrf_token() if actor else None,
|
||||
follow_url=url_for("social.follow") if actor else None,
|
||||
unfollow_url=url_for("social.unfollow") if actor else None,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -683,23 +680,15 @@ async def render_profile_page(ctx: dict, actor: Any, activities: list,
|
||||
"federation-profile-summary-text", text=str(escape(actor.summary)),
|
||||
) if actor.summary else ""
|
||||
|
||||
activities_sx = ""
|
||||
if activities:
|
||||
parts = []
|
||||
for a in activities:
|
||||
published = a.published.strftime("%Y-%m-%d %H:%M") if a.published else ""
|
||||
obj_type_sx = sx_call(
|
||||
"federation-activity-obj-type", obj_type=a.object_type,
|
||||
) if a.object_type else ""
|
||||
parts.append(sx_call(
|
||||
"federation-activity-card",
|
||||
activity_type=a.activity_type, published=published,
|
||||
obj_type=SxExpr(obj_type_sx) if obj_type_sx else None,
|
||||
))
|
||||
items_sx = "(<> " + " ".join(parts) + ")"
|
||||
activities_sx = sx_call("federation-activities-list", items=SxExpr(items_sx))
|
||||
else:
|
||||
activities_sx = sx_call("federation-activities-empty")
|
||||
activity_dicts = [
|
||||
dict(
|
||||
activity_type=a.activity_type,
|
||||
published=a.published.strftime("%Y-%m-%d %H:%M") if a.published else "",
|
||||
object_type=a.object_type,
|
||||
)
|
||||
for a in activities
|
||||
]
|
||||
activities_sx = sx_call("federation-activities-from-data", activities=activity_dicts)
|
||||
|
||||
content = sx_call(
|
||||
"federation-profile-page",
|
||||
|
||||
@@ -47,6 +47,17 @@
|
||||
(h2 :class "text-base sm:text-lg font-semibold" "Event tickets in this order")
|
||||
(ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
|
||||
|
||||
;; Data-driven ticket items (replaces Python loop)
|
||||
(defcomp ~checkout-return-tickets-from-data (&key tickets)
|
||||
(~checkout-return-tickets
|
||||
:items (<> (map (lambda (tk)
|
||||
(~checkout-return-ticket
|
||||
:name (get tk "name") :pill (get tk "pill")
|
||||
:state (get tk "state") :type-name (get tk "type_name")
|
||||
:date-str (get tk "date_str") :code (get tk "code")
|
||||
:price (get tk "price")))
|
||||
(or tickets (list))))))
|
||||
|
||||
(defcomp ~checkout-return-content (&key summary items calendar tickets status-message)
|
||||
(div :class "max-w-full px-1 py-1"
|
||||
(when summary
|
||||
|
||||
@@ -99,29 +99,14 @@ def _orders_rows_sx(orders: list, page: int, total_pages: int,
|
||||
from shared.utils import route_prefix
|
||||
pfx = route_prefix()
|
||||
|
||||
parts = []
|
||||
for o in orders:
|
||||
d = _order_row_data(o, pfx + url_for_fn("orders.defpage_order_detail", order_id=o.id))
|
||||
parts.append(sx_call("order-row-desktop",
|
||||
oid=d["oid"], created=d["created"],
|
||||
desc=d["desc"], total=d["total"],
|
||||
pill=d["pill_desktop"], status=d["status"],
|
||||
url=d["url"]))
|
||||
parts.append(sx_call("order-row-mobile",
|
||||
oid=d["oid"], created=d["created"],
|
||||
total=d["total"], pill=d["pill_mobile"],
|
||||
status=d["status"], url=d["url"]))
|
||||
|
||||
if page < total_pages:
|
||||
next_url = pfx + url_for_fn("orders.orders_rows") + qs_fn(page=page + 1)
|
||||
parts.append(sx_call("infinite-scroll",
|
||||
url=next_url, page=page,
|
||||
total_pages=total_pages,
|
||||
id_prefix="orders", colspan=5))
|
||||
else:
|
||||
parts.append(sx_call("order-end-row"))
|
||||
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
order_dicts = [
|
||||
_order_row_data(o, pfx + url_for_fn("orders.defpage_order_detail", order_id=o.id))
|
||||
for o in orders
|
||||
]
|
||||
next_url = (pfx + url_for_fn("orders.orders_rows") + qs_fn(page=page + 1)) if page < total_pages else None
|
||||
return sx_call("order-rows-from-data",
|
||||
orders=order_dicts, page=page, total_pages=total_pages,
|
||||
next_url=next_url)
|
||||
|
||||
|
||||
def _orders_main_panel_sx(orders: list, rows_sx: str) -> str:
|
||||
@@ -153,35 +138,25 @@ def _order_items_sx(order: Any) -> str:
|
||||
"""Render order items list as sx."""
|
||||
if not order or not order.items:
|
||||
return ""
|
||||
items = []
|
||||
for item in order.items:
|
||||
prod_url = market_product_url(item.product_slug)
|
||||
if item.product_image:
|
||||
img = sx_call(
|
||||
"order-item-image",
|
||||
src=item.product_image, alt=item.product_title or "Product image",
|
||||
)
|
||||
else:
|
||||
img = sx_call("order-item-no-image")
|
||||
|
||||
items.append(sx_call(
|
||||
"order-item-row",
|
||||
href=prod_url, img=SxExpr(img),
|
||||
title=item.product_title or "Unknown product",
|
||||
pid=f"Product ID: {item.product_id}",
|
||||
qty=f"Qty: {item.quantity}",
|
||||
item_dicts = [
|
||||
dict(
|
||||
href=market_product_url(item.product_slug),
|
||||
product_image=item.product_image,
|
||||
product_title=item.product_title or "Unknown product",
|
||||
product_id=str(item.product_id),
|
||||
quantity=str(item.quantity),
|
||||
price=f"{item.currency or order.currency or 'GBP'} {item.unit_price or 0:.2f}",
|
||||
))
|
||||
|
||||
items_sx = "(<> " + " ".join(items) + ")"
|
||||
return sx_call("order-items-panel", items=SxExpr(items_sx))
|
||||
)
|
||||
for item in order.items
|
||||
]
|
||||
return sx_call("order-items-from-data", items=item_dicts)
|
||||
|
||||
|
||||
def _calendar_items_sx(calendar_entries: list | None) -> str:
|
||||
"""Render calendar bookings for an order as sx."""
|
||||
if not calendar_entries:
|
||||
return ""
|
||||
items = []
|
||||
entry_dicts = []
|
||||
for e in calendar_entries:
|
||||
st = e.state or ""
|
||||
pill = (
|
||||
@@ -193,16 +168,13 @@ def _calendar_items_sx(calendar_entries: list | None) -> str:
|
||||
ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else ""
|
||||
if e.end_at:
|
||||
ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}"
|
||||
items.append(sx_call(
|
||||
"order-calendar-entry",
|
||||
entry_dicts.append(dict(
|
||||
name=e.name,
|
||||
pill=f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}",
|
||||
status=st.capitalize(), date_str=ds,
|
||||
cost=f"\u00a3{e.cost or 0:.2f}",
|
||||
))
|
||||
|
||||
items_sx = "(<> " + " ".join(items) + ")"
|
||||
return sx_call("order-calendar-section", items=SxExpr(items_sx))
|
||||
return sx_call("order-calendar-from-data", entries=entry_dicts)
|
||||
|
||||
|
||||
def _order_main_sx(order: Any, calendar_entries: list | None) -> str:
|
||||
@@ -287,7 +259,7 @@ def _ticket_items_sx(order_tickets: list | None) -> str:
|
||||
"""Render ticket items for an order as sx."""
|
||||
if not order_tickets:
|
||||
return ""
|
||||
items = []
|
||||
ticket_dicts = []
|
||||
for tk in order_tickets:
|
||||
st = tk.state or ""
|
||||
pill = (
|
||||
@@ -296,22 +268,19 @@ def _ticket_items_sx(order_tickets: list | None) -> str:
|
||||
else "bg-blue-100 text-blue-800" if st == "checked_in"
|
||||
else "bg-stone-100 text-stone-700"
|
||||
)
|
||||
pill_cls = f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}"
|
||||
ds = tk.entry_start_at.strftime("%-d %b %Y, %H:%M") if tk.entry_start_at else ""
|
||||
if tk.entry_end_at:
|
||||
ds += f" – {tk.entry_end_at.strftime('%-d %b %Y, %H:%M')}"
|
||||
items.append(sx_call(
|
||||
"checkout-return-ticket",
|
||||
ticket_dicts.append(dict(
|
||||
name=tk.entry_name,
|
||||
pill=pill_cls,
|
||||
pill=f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}",
|
||||
state=st.replace("_", " ").capitalize(),
|
||||
type_name=tk.ticket_type_name or None,
|
||||
date_str=ds,
|
||||
code=tk.code,
|
||||
price=f"£{tk.price or 0:.2f}",
|
||||
))
|
||||
items_sx = "(<> " + " ".join(items) + ")"
|
||||
return sx_call("checkout-return-tickets", items=SxExpr(items_sx))
|
||||
return sx_call("checkout-return-tickets-from-data", tickets=ticket_dicts)
|
||||
|
||||
|
||||
async def render_checkout_return_page(ctx: dict, order: Any | None,
|
||||
|
||||
@@ -120,6 +120,57 @@
|
||||
(<> auth (~header-child-sx :id "auth-header-child" :inner
|
||||
(<> orders (~header-child-sx :id "orders-header-child" :inner order))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Data-driven order rows (replaces Python loop)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~order-rows-from-data (&key orders page total-pages next-url)
|
||||
(<>
|
||||
(map (lambda (o)
|
||||
(<>
|
||||
(~order-row-desktop :oid (get o "oid") :created (get o "created")
|
||||
:desc (get o "desc") :total (get o "total")
|
||||
:pill (get o "pill_desktop") :status (get o "status") :url (get o "url"))
|
||||
(~order-row-mobile :oid (get o "oid") :created (get o "created")
|
||||
:total (get o "total") :pill (get o "pill_mobile")
|
||||
:status (get o "status") :url (get o "url"))))
|
||||
(or orders (list)))
|
||||
(if next-url
|
||||
(~infinite-scroll :url next-url :page page :total-pages total-pages
|
||||
:id-prefix "orders" :colspan 5)
|
||||
(~order-end-row))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Data-driven order items (replaces Python loop)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~order-items-from-data (&key items)
|
||||
(~order-items-panel
|
||||
:items (<> (map (lambda (item)
|
||||
(let* ((img (if (get item "product_image")
|
||||
(~order-item-image :src (get item "product_image") :alt (or (get item "product_title") "Product image"))
|
||||
(~order-item-no-image))))
|
||||
(~order-item-row
|
||||
:href (get item "href") :img img
|
||||
:title (or (get item "product_title") "Unknown product")
|
||||
:pid (str "Product ID: " (get item "product_id"))
|
||||
:qty (str "Qty: " (get item "quantity"))
|
||||
:price (get item "price"))))
|
||||
(or items (list))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Data-driven calendar entries (replaces Python loop)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~order-calendar-from-data (&key entries)
|
||||
(~order-calendar-section
|
||||
:items (<> (map (lambda (e)
|
||||
(~order-calendar-entry
|
||||
:name (get e "name") :pill (get e "pill")
|
||||
:status (get e "status") :date-str (get e "date_str")
|
||||
:cost (get e "cost")))
|
||||
(or entries (list))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Checkout error screens
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user