Rename all 1,169 components to path-based names with namespace support

Component names now reflect filesystem location using / as path separator
and : as namespace separator for shared components:
  ~sx-header → ~layouts/header
  ~layout-app-body → ~shared:layout/app-body
  ~blog-admin-dashboard → ~admin/dashboard

209 files, 4,941 replacements across all services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:00:12 +00:00
parent de80d921e9
commit b0920a1121
209 changed files with 4620 additions and 4620 deletions

View File

@@ -1,12 +1,12 @@
;; Auth page components (device auth — account-specific) ;; Auth page components (device auth — account-specific)
;; Login and check-email components are shared: see shared/sx/templates/auth.sx ;; Login and check-email components are shared: see shared/sx/templates/auth.sx
(defcomp ~account-device-error (&key (error :as string)) (defcomp ~auth/device-error (&key (error :as string))
(when error (when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
error))) error)))
(defcomp ~account-device-form (&key error (action :as string) (csrf-token :as string) (code :as string)) (defcomp ~auth/device-form (&key error (action :as string) (csrf-token :as string) (code :as string))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Authorize device") (h1 :class "text-2xl font-bold mb-6" "Authorize device")
(p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.") (p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.")
@@ -22,30 +22,30 @@
:class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition" :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
"Authorize")))) "Authorize"))))
(defcomp ~account-device-approved () (defcomp ~auth/device-approved ()
(div :class "py-8 max-w-md mx-auto text-center" (div :class "py-8 max-w-md mx-auto text-center"
(h1 :class "text-2xl font-bold mb-4" "Device authorized") (h1 :class "text-2xl font-bold mb-4" "Device authorized")
(p :class "text-stone-600" "You can close this window and return to your terminal."))) (p :class "text-stone-600" "You can close this window and return to your terminal.")))
;; Assembled auth page content — replaces Python _login_page_content etc. ;; Assembled auth page content — replaces Python _login_page_content etc.
(defcomp ~account-login-content (&key (error :as string?) (email :as string?)) (defcomp ~auth/login-content (&key (error :as string?) (email :as string?))
(~auth-login-form (~shared:auth/login-form
:error (when error (~auth-error-banner :error error)) :error (when error (~shared:auth/error-banner :error error))
:action (url-for "auth.start_login") :action (url-for "auth.start_login")
:csrf-token (csrf-token) :csrf-token (csrf-token)
:email (or email ""))) :email (or email "")))
(defcomp ~account-device-content (&key (error :as string?) (code :as string?)) (defcomp ~auth/device-content (&key (error :as string?) (code :as string?))
(~account-device-form (~auth/device-form
:error (when error (~account-device-error :error error)) :error (when error (~auth/device-error :error error))
:action (url-for "auth.device_submit") :action (url-for "auth.device_submit")
:csrf-token (csrf-token) :csrf-token (csrf-token)
:code (or code ""))) :code (or code "")))
(defcomp ~account-check-email-content (&key (email :as string?) (email-error :as string?)) (defcomp ~auth/check-email-content (&key (email :as string?) (email-error :as string?))
(~auth-check-email (~shared:auth/check-email
:email (escape (or email "")) :email (escape (or email ""))
:error (when email-error :error (when email-error
(~auth-check-email-error :error (escape email-error))))) (~shared:auth/check-email-error :error (escape email-error)))))

View File

@@ -1,36 +1,36 @@
;; Account dashboard components ;; Account dashboard components
(defcomp ~account-error-banner (&key (error :as string)) (defcomp ~dashboard/error-banner (&key (error :as string))
(when error (when error
(div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm" (div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm"
error))) error)))
(defcomp ~account-user-email (&key (email :as string)) (defcomp ~dashboard/user-email (&key (email :as string))
(when email (when email
(p :class "text-sm text-stone-500 mt-1" email))) (p :class "text-sm text-stone-500 mt-1" email)))
(defcomp ~account-user-name (&key (name :as string)) (defcomp ~dashboard/user-name (&key (name :as string))
(when name (when name
(p :class "text-sm text-stone-600" name))) (p :class "text-sm text-stone-600" name)))
(defcomp ~account-logout-form (&key (csrf-token :as string)) (defcomp ~dashboard/logout-form (&key (csrf-token :as string))
(form :action "/auth/logout/" :method "post" (form :action "/auth/logout/" :method "post"
(input :type "hidden" :name "csrf_token" :value csrf-token) (input :type "hidden" :name "csrf_token" :value csrf-token)
(button :type "submit" (button :type "submit"
:class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition" :class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition"
(i :class "fa-solid fa-right-from-bracket text-xs") " Sign out"))) (i :class "fa-solid fa-right-from-bracket text-xs") " Sign out")))
(defcomp ~account-label-item (&key (name :as string)) (defcomp ~dashboard/label-item (&key (name :as string))
(span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60" (span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60"
name)) name))
(defcomp ~account-labels-section (&key items) (defcomp ~dashboard/labels-section (&key items)
(when items (when items
(div (div
(h2 :class "text-base font-semibold tracking-tight mb-3" "Labels") (h2 :class "text-base font-semibold tracking-tight mb-3" "Labels")
(div :class "flex flex-wrap gap-2" items)))) (div :class "flex flex-wrap gap-2" items))))
(defcomp ~account-main-panel (&key error email name logout labels) (defcomp ~dashboard/main-panel (&key error email name logout labels)
(div :class "w-full max-w-3xl mx-auto px-4 py-6" (div :class "w-full max-w-3xl mx-auto px-4 py-6"
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-8" (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-8"
error error
@@ -43,18 +43,18 @@
labels))) labels)))
;; Assembled dashboard content — replaces Python _account_main_panel_sx ;; Assembled dashboard content — replaces Python _account_main_panel_sx
(defcomp ~account-dashboard-content (&key (error :as string?)) (defcomp ~dashboard/content (&key (error :as string?))
(let* ((user (current-user)) (let* ((user (current-user))
(csrf (csrf-token))) (csrf (csrf-token)))
(~account-main-panel (~dashboard/main-panel
:error (when error (~account-error-banner :error error)) :error (when error (~dashboard/error-banner :error error))
:email (when (get user "email") :email (when (get user "email")
(~account-user-email :email (get user "email"))) (~dashboard/user-email :email (get user "email")))
:name (when (get user "name") :name (when (get user "name")
(~account-user-name :name (get user "name"))) (~dashboard/user-name :name (get user "name")))
:logout (~account-logout-form :csrf-token csrf) :logout (~dashboard/logout-form :csrf-token csrf)
:labels (when (not (empty? (or (get user "labels") (list)))) :labels (when (not (empty? (or (get user "labels") (list))))
(~account-labels-section (~dashboard/labels-section
:items (map (lambda (label) :items (map (lambda (label)
(~account-label-item :name (get label "name"))) (~dashboard/label-item :name (get label "name")))
(get user "labels"))))))) (get user "labels")))))))

View File

@@ -2,19 +2,19 @@
;; Registered via register_sx_layout("account", ...) in __init__.py. ;; Registered via register_sx_layout("account", ...) in __init__.py.
;; Full page: root header + auth header row in header-child ;; Full page: root header + auth header row in header-child
(defcomp ~account-layout-full () (defcomp ~layouts/full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (~auth-header-row-auto)))) :inner (~auth-header-row-auto))))
;; OOB (HTMX): auth row + root header, both with oob=true ;; OOB (HTMX): auth row + root header, both with oob=true
(defcomp ~account-layout-oob () (defcomp ~layouts/oob ()
(<> (~auth-header-row-auto true) (<> (~auth-header-row-auto true)
(~root-header-auto true))) (~root-header-auto true)))
;; Mobile menu: auth section + root nav ;; Mobile menu: auth section + root nav
(defcomp ~account-layout-mobile () (defcomp ~layouts/mobile ()
(<> (~mobile-menu-section (<> (~shared:layout/mobile-menu-section
:label "account" :href "/" :level 1 :colour "sky" :label "account" :href "/" :level 1 :colour "sky"
:items (~auth-nav-items-auto)) :items (~auth-nav-items-auto))
(~root-mobile-auto))) (~root-mobile-auto)))

View File

@@ -1,30 +1,30 @@
;; Newsletter management components ;; Newsletter management components
(defcomp ~account-newsletter-desc (&key (description :as string)) (defcomp ~newsletters/desc (&key (description :as string))
(when description (when description
(p :class "text-xs text-stone-500 mt-0.5 truncate" description))) (p :class "text-xs text-stone-500 mt-0.5 truncate" description)))
(defcomp ~account-newsletter-toggle (&key (id :as string) (url :as string) (hdrs :as dict) (target :as string) (cls :as string) (checked :as string) (knob-cls :as string)) (defcomp ~newsletters/toggle (&key (id :as string) (url :as string) (hdrs :as dict) (target :as string) (cls :as string) (checked :as string) (knob-cls :as string))
(div :id id :class "flex items-center" (div :id id :class "flex items-center"
(button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML" (button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML"
:class cls :role "switch" :aria-checked checked :class cls :role "switch" :aria-checked checked
(span :class knob-cls)))) (span :class knob-cls))))
(defcomp ~account-newsletter-item (&key (name :as string) desc toggle) (defcomp ~newsletters/item (&key (name :as string) desc toggle)
(div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0" (div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0"
(div :class "min-w-0 flex-1" (div :class "min-w-0 flex-1"
(p :class "text-sm font-medium text-stone-800" name) (p :class "text-sm font-medium text-stone-800" name)
desc) desc)
(div :class "ml-4 flex-shrink-0" toggle))) (div :class "ml-4 flex-shrink-0" toggle)))
(defcomp ~account-newsletter-list (&key items) (defcomp ~newsletters/list (&key items)
(div :class "divide-y divide-stone-100" items)) (div :class "divide-y divide-stone-100" items))
(defcomp ~account-newsletter-empty () (defcomp ~newsletters/empty ()
(p :class "text-sm text-stone-500" "No newsletters available.")) (p :class "text-sm text-stone-500" "No newsletters available."))
(defcomp ~account-newsletters-panel (&key list) (defcomp ~newsletters/panel (&key list)
(div :class "w-full max-w-3xl mx-auto px-4 py-6" (div :class "w-full max-w-3xl mx-auto px-4 py-6"
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6" (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
(h1 :class "text-xl font-semibold tracking-tight" "Newsletters") (h1 :class "text-xl font-semibold tracking-tight" "Newsletters")
@@ -32,12 +32,12 @@
;; Assembled newsletters content — replaces Python _newsletters_panel_sx ;; Assembled newsletters content — replaces Python _newsletters_panel_sx
;; Takes pre-fetched newsletter-list from page helper ;; Takes pre-fetched newsletter-list from page helper
(defcomp ~account-newsletters-content (&key (newsletter-list :as list) (account-url :as string?)) (defcomp ~newsletters/content (&key (newsletter-list :as list) (account-url :as string?))
(let* ((csrf (csrf-token))) (let* ((csrf (csrf-token)))
(if (empty? newsletter-list) (if (empty? newsletter-list)
(~account-newsletter-empty) (~newsletters/empty)
(~account-newsletters-panel (~newsletters/panel
:list (~account-newsletter-list :list (~newsletters/list
:items (map (lambda (item) :items (map (lambda (item)
(let* ((nl (get item "newsletter")) (let* ((nl (get item "newsletter"))
(un (get item "un")) (un (get item "un"))
@@ -47,11 +47,11 @@
(bg (if subscribed "bg-emerald-500" "bg-stone-300")) (bg (if subscribed "bg-emerald-500" "bg-stone-300"))
(translate (if subscribed "translate-x-6" "translate-x-1")) (translate (if subscribed "translate-x-6" "translate-x-1"))
(checked (if subscribed "true" "false"))) (checked (if subscribed "true" "false")))
(~account-newsletter-item (~newsletters/item
:name (get nl "name") :name (get nl "name")
:desc (when (get nl "description") :desc (when (get nl "description")
(~account-newsletter-desc :description (get nl "description"))) (~newsletters/desc :description (get nl "description")))
:toggle (~account-newsletter-toggle :toggle (~newsletters/toggle
:id (str "nl-" nid) :id (str "nl-" nid)
:url toggle-url :url toggle-url
:hdrs {:X-CSRFToken csrf} :hdrs {:X-CSRFToken csrf}

View File

@@ -8,7 +8,7 @@
:path "/" :path "/"
:auth :login :auth :login
:layout :account :layout :account
:content (~account-dashboard-content)) :content (~dashboard/content))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Newsletters ;; Newsletters
@@ -19,7 +19,7 @@
:auth :login :auth :login
:layout :account :layout :account
:data (service "account-page" "newsletters-data") :data (service "account-page" "newsletters-data")
:content (~account-newsletters-content :content (~newsletters/content
:newsletter-list newsletter-list :newsletter-list newsletter-list
:account-url account-url)) :account-url account-url))

View File

@@ -256,7 +256,7 @@ def _image(node: dict) -> str:
parts.append(f':width "{_esc(width)}"') parts.append(f':width "{_esc(width)}"')
if href: if href:
parts.append(f':href "{_esc(href)}"') parts.append(f':href "{_esc(href)}"')
return "(~kg-image " + " ".join(parts) + ")" return "(~kg_cards/kg-image " + " ".join(parts) + ")"
@_converter("gallery") @_converter("gallery")
@@ -282,14 +282,14 @@ def _gallery(node: dict) -> str:
images_sx = "(list " + " ".join(rows) + ")" images_sx = "(list " + " ".join(rows) + ")"
caption = node.get("caption", "") caption = node.get("caption", "")
caption_attr = f" :caption {html_to_sx(caption)}" if caption else "" caption_attr = f" :caption {html_to_sx(caption)}" if caption else ""
return f"(~kg-gallery :images {images_sx}{caption_attr})" return f"(~kg_cards/kg-gallery :images {images_sx}{caption_attr})"
@_converter("html") @_converter("html")
def _html_card(node: dict) -> str: def _html_card(node: dict) -> str:
raw = node.get("html", "") raw = node.get("html", "")
inner = html_to_sx(raw) inner = html_to_sx(raw)
return f"(~kg-html {inner})" return f"(~kg_cards/kg-html {inner})"
@_converter("embed") @_converter("embed")
@@ -299,7 +299,7 @@ def _embed(node: dict) -> str:
parts = [f':html "{_esc(embed_html)}"'] parts = [f':html "{_esc(embed_html)}"']
if caption: if caption:
parts.append(f":caption {html_to_sx(caption)}") parts.append(f":caption {html_to_sx(caption)}")
return "(~kg-embed " + " ".join(parts) + ")" return "(~kg_cards/kg-embed " + " ".join(parts) + ")"
@_converter("bookmark") @_converter("bookmark")
@@ -330,7 +330,7 @@ def _bookmark(node: dict) -> str:
if caption: if caption:
parts.append(f":caption {html_to_sx(caption)}") parts.append(f":caption {html_to_sx(caption)}")
return "(~kg-bookmark " + " ".join(parts) + ")" return "(~kg_cards/kg-bookmark " + " ".join(parts) + ")"
@_converter("callout") @_converter("callout")
@@ -344,7 +344,7 @@ def _callout(node: dict) -> str:
parts.append(f':emoji "{_esc(emoji)}"') parts.append(f':emoji "{_esc(emoji)}"')
if inner: if inner:
parts.append(f':content {inner}') parts.append(f':content {inner}')
return "(~kg-callout " + " ".join(parts) + ")" return "(~kg_cards/kg-callout " + " ".join(parts) + ")"
@_converter("button") @_converter("button")
@@ -352,7 +352,7 @@ def _button(node: dict) -> str:
text = node.get("buttonText", "") text = node.get("buttonText", "")
url = node.get("buttonUrl", "") url = node.get("buttonUrl", "")
alignment = node.get("alignment", "center") alignment = node.get("alignment", "center")
return f'(~kg-button :url "{_esc(url)}" :text "{_esc(text)}" :alignment "{_esc(alignment)}")' return f'(~kg_cards/kg-button :url "{_esc(url)}" :text "{_esc(text)}" :alignment "{_esc(alignment)}")'
@_converter("toggle") @_converter("toggle")
@@ -360,7 +360,7 @@ def _toggle(node: dict) -> str:
heading = node.get("heading", "") heading = node.get("heading", "")
inner = _convert_children(node.get("children", [])) inner = _convert_children(node.get("children", []))
content_attr = f" :content {inner}" if inner else "" content_attr = f" :content {inner}" if inner else ""
return f'(~kg-toggle :heading "{_esc(heading)}"{content_attr})' return f'(~kg_cards/kg-toggle :heading "{_esc(heading)}"{content_attr})'
@_converter("audio") @_converter("audio")
@@ -380,7 +380,7 @@ def _audio(node: dict) -> str:
parts.append(f':duration "{duration_str}"') parts.append(f':duration "{duration_str}"')
if thumbnail: if thumbnail:
parts.append(f':thumbnail "{_esc(thumbnail)}"') parts.append(f':thumbnail "{_esc(thumbnail)}"')
return "(~kg-audio " + " ".join(parts) + ")" return "(~kg_cards/kg-audio " + " ".join(parts) + ")"
@_converter("video") @_converter("video")
@@ -400,7 +400,7 @@ def _video(node: dict) -> str:
parts.append(f':thumbnail "{_esc(thumbnail)}"') parts.append(f':thumbnail "{_esc(thumbnail)}"')
if loop: if loop:
parts.append(":loop true") parts.append(":loop true")
return "(~kg-video " + " ".join(parts) + ")" return "(~kg_cards/kg-video " + " ".join(parts) + ")"
@_converter("file") @_converter("file")
@@ -429,12 +429,12 @@ def _file(node: dict) -> str:
parts.append(f':filesize "{size_str}"') parts.append(f':filesize "{size_str}"')
if caption: if caption:
parts.append(f":caption {html_to_sx(caption)}") parts.append(f":caption {html_to_sx(caption)}")
return "(~kg-file " + " ".join(parts) + ")" return "(~kg_cards/kg-file " + " ".join(parts) + ")"
@_converter("paywall") @_converter("paywall")
def _paywall(_node: dict) -> str: def _paywall(_node: dict) -> str:
return "(~kg-paywall)" return "(~kg_cards/kg-paywall)"
@_converter("markdown") @_converter("markdown")
@@ -442,4 +442,4 @@ def _markdown(node: dict) -> str:
md_text = node.get("markdown", "") md_text = node.get("markdown", "")
rendered = mistune.html(md_text) rendered = mistune.html(md_text)
inner = html_to_sx(rendered) inner = html_to_sx(rendered)
return f"(~kg-md {inner})" return f"(~kg_cards/kg-md {inner})"

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Re-convert sx_content from lexical JSON to eliminate ~kg-html wrappers and Re-convert sx_content from lexical JSON to eliminate ~kg_cards/kg-html wrappers and
raw caption strings. raw caption strings.
The updated lexical_to_sx converter now produces native sx expressions instead The updated lexical_to_sx converter now produces native sx expressions instead
of (1) wrapping HTML/markdown cards in (~kg-html :html "...") and (2) storing of (1) wrapping HTML/markdown cards in (~kg_cards/kg-html :html "...") and (2) storing
captions as escaped HTML strings. This script re-runs the conversion on all captions as escaped HTML strings. This script re-runs the conversion on all
posts that already have sx_content, overwriting the old output. posts that already have sx_content, overwriting the old output.
@@ -50,11 +50,11 @@ async def migrate(dry_run: bool = False) -> int:
continue continue
if dry_run: if dry_run:
old_has_kg = "~kg-html" in (post.sx_content or "") old_has_kg = "~kg_cards/kg-html" in (post.sx_content or "")
old_has_raw = "raw! caption" in (post.sx_content or "") old_has_raw = "raw! caption" in (post.sx_content or "")
markers = [] markers = []
if old_has_kg: if old_has_kg:
markers.append("~kg-html") markers.append("~kg_cards/kg-html")
if old_has_raw: if old_has_raw:
markers.append("raw-caption") markers.append("raw-caption")
tag = f" [{', '.join(markers)}]" if markers else "" tag = f" [{', '.join(markers)}]" if markers else ""
@@ -76,7 +76,7 @@ async def migrate(dry_run: bool = False) -> int:
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Re-convert sx_content to eliminate ~kg-html and raw captions" description="Re-convert sx_content to eliminate ~kg_cards/kg-html and raw captions"
) )
parser.add_argument("--dry-run", action="store_true", parser.add_argument("--dry-run", action="store_true",
help="Preview changes without writing to database") help="Preview changes without writing to database")

View File

@@ -398,7 +398,7 @@ class BlogPageService:
} }
def post_detail_data(self, post, user, rights, csrf, blog_url_base): def post_detail_data(self, post, user, rights, csrf, blog_url_base):
"""Serialize post detail view data for ~blog-post-detail-content defcomp.""" """Serialize post detail view data for ~detail/post-detail-content defcomp."""
slug = post.get("slug", "") slug = post.get("slug", "")
is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
user_id = getattr(user, "id", None) if user else None user_id = getattr(user, "id", None) if user else None

View File

@@ -1,6 +1,6 @@
;; Blog admin panel components ;; Blog admin panel components
(defcomp ~blog-cache-panel (&key (clear-url :as string) (csrf :as string)) (defcomp ~admin/cache-panel (&key (clear-url :as string) (csrf :as string))
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-6" (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
(div :class "flex flex-col md:flex-row gap-3 items-start" (div :class "flex flex-col md:flex-row gap-3 items-start"
(form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML" (form :sx-post clear-url :sx-trigger "submit" :sx-target "#cache-status" :sx-swap "innerHTML"
@@ -8,21 +8,21 @@
(button :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" :type "submit" "Clear cache")) (button :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" :type "submit" "Clear cache"))
(div :id "cache-status" :class "py-2")))) (div :id "cache-status" :class "py-2"))))
(defcomp ~blog-snippets-panel (&key list) (defcomp ~admin/snippets-panel (&key list)
(div :class "max-w-4xl mx-auto p-6" (div :class "max-w-4xl mx-auto p-6"
(div :class "mb-6 flex justify-between items-center" (div :class "mb-6 flex justify-between items-center"
(h1 :class "text-3xl font-bold" "Snippets")) (h1 :class "text-3xl font-bold" "Snippets"))
(div :id "snippets-list" list))) (div :id "snippets-list" list)))
(defcomp ~blog-snippet-visibility-select (&key patch-url hx-headers options cls) (defcomp ~admin/snippet-visibility-select (&key patch-url hx-headers options cls)
(select :name "visibility" :sx-patch patch-url :sx-target "#snippets-list" :sx-swap "innerHTML" (select :name "visibility" :sx-patch patch-url :sx-target "#snippets-list" :sx-swap "innerHTML"
:sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1" :sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1"
options)) options))
(defcomp ~blog-snippet-option (&key (value :as string) (selected :as boolean) (label :as string)) (defcomp ~admin/snippet-option (&key (value :as string) (selected :as boolean) (label :as string))
(option :value value :selected selected label)) (option :value value :selected selected label))
(defcomp ~blog-snippet-row (&key (name :as string) (owner :as string) (badge-cls :as string) (visibility :as string) extra) (defcomp ~admin/snippet-row (&key (name :as string) (owner :as string) (badge-cls :as string) (visibility :as string) extra)
(div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
@@ -30,10 +30,10 @@
(span :class (str "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium " badge-cls) visibility) (span :class (str "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium " badge-cls) visibility)
extra)) extra))
(defcomp ~blog-snippets-list (&key rows) (defcomp ~admin/snippets-list (&key rows)
(div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows))) (div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows)))
(defcomp ~blog-menu-items-panel (&key new-url list) (defcomp ~admin/menu-items-panel (&key new-url list)
(div :class "max-w-4xl mx-auto p-6" (div :class "max-w-4xl mx-auto p-6"
(div :class "mb-6 flex justify-end items-center" (div :class "mb-6 flex justify-end items-center"
(button :type "button" :sx-get new-url :sx-target "#menu-item-form" :sx-swap "innerHTML" (button :type "button" :sx-get new-url :sx-target "#menu-item-form" :sx-swap "innerHTML"
@@ -42,7 +42,7 @@
(div :id "menu-item-form" :class "mb-6") (div :id "menu-item-form" :class "mb-6")
(div :id "menu-items-list" list))) (div :id "menu-items-list" list)))
(defcomp ~blog-menu-item-row (&key img (label :as string) (slug :as string) (sort-order :as string) (edit-url :as string) (delete-url :as string) (confirm-text :as string) hx-headers) (defcomp ~admin/menu-item-row (&key img (label :as string) (slug :as string) (sort-order :as string) (edit-url :as string) (delete-url :as string) (confirm-text :as string) hx-headers)
(div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition" (div :class "flex items-center gap-4 p-4 hover:bg-stone-50 transition"
(div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical")) (div :class "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical"))
img img
@@ -54,16 +54,16 @@
(button :type "button" :sx-get edit-url :sx-target "#menu-item-form" :sx-swap "innerHTML" (button :type "button" :sx-get edit-url :sx-target "#menu-item-form" :sx-swap "innerHTML"
:class "px-3 py-1 text-sm bg-stone-200 hover:bg-stone-300 rounded" :class "px-3 py-1 text-sm bg-stone-200 hover:bg-stone-300 rounded"
(i :class "fa fa-edit") " Edit") (i :class "fa fa-edit") " Edit")
(~delete-btn :url delete-url :trigger-target "#menu-items-list" (~shared:misc/delete-btn :url delete-url :trigger-target "#menu-items-list"
:title "Delete menu item?" :text confirm-text :title "Delete menu item?" :text confirm-text
:sx-headers hx-headers)))) :sx-headers hx-headers))))
(defcomp ~blog-menu-items-list (&key rows) (defcomp ~admin/menu-items-list (&key rows)
(div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows))) (div :class "bg-white rounded-lg shadow" (div :class "divide-y" rows)))
;; Tag groups admin ;; Tag groups admin
(defcomp ~blog-tag-groups-create-form (&key create-url csrf) (defcomp ~admin/tag-groups-create-form (&key create-url csrf)
(form :method "post" :action create-url :class "border rounded p-4 bg-white space-y-3" (form :method "post" :action create-url :class "border rounded p-4 bg-white space-y-3"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(h3 :class "text-sm font-semibold text-stone-700" "New Group") (h3 :class "text-sm font-semibold text-stone-700" "New Group")
@@ -74,14 +74,14 @@
(input :type "text" :name "feature_image" :placeholder "Image URL (optional)" :class "w-full border rounded px-3 py-2 text-sm") (input :type "text" :name "feature_image" :placeholder "Image URL (optional)" :class "w-full border rounded px-3 py-2 text-sm")
(button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Create"))) (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Create")))
(defcomp ~blog-tag-group-icon-image (&key src name) (defcomp ~admin/tag-group-icon-image (&key src name)
(img :src src :alt name :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0")) (img :src src :alt name :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
(defcomp ~blog-tag-group-icon-color (&key style initial) (defcomp ~admin/tag-group-icon-color (&key style initial)
(div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0" (div :class "h-8 w-8 rounded-full text-xs font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0"
:style style initial)) :style style initial))
(defcomp ~blog-tag-group-li (&key icon (edit-href :as string) (name :as string) (slug :as string) (sort-order :as number)) (defcomp ~admin/tag-group-li (&key icon (edit-href :as string) (name :as string) (slug :as string) (sort-order :as number))
(li :class "border rounded p-3 bg-white flex items-center gap-3" (li :class "border rounded p-3 bg-white flex items-center gap-3"
icon icon
(div :class "flex-1" (div :class "flex-1"
@@ -89,32 +89,32 @@
(span :class "text-xs text-stone-500 ml-2" slug)) (span :class "text-xs text-stone-500 ml-2" slug))
(span :class "text-xs text-stone-500" (str "order: " sort-order)))) (span :class "text-xs text-stone-500" (str "order: " sort-order))))
(defcomp ~blog-tag-groups-list (&key items) (defcomp ~admin/tag-groups-list (&key items)
(ul :class "space-y-2" items)) (ul :class "space-y-2" items))
(defcomp ~blog-unassigned-tag (&key name) (defcomp ~admin/unassigned-tag (&key name)
(span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200 rounded" name)) (span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200 rounded" name))
(defcomp ~blog-unassigned-tags (&key heading spans) (defcomp ~admin/unassigned-tags (&key heading spans)
(div :class "border-t pt-4" (div :class "border-t pt-4"
(h3 :class "text-sm font-semibold text-stone-700 mb-2" heading) (h3 :class "text-sm font-semibold text-stone-700 mb-2" heading)
(div :class "flex flex-wrap gap-2" spans))) (div :class "flex flex-wrap gap-2" spans)))
(defcomp ~blog-tag-groups-main (&key form groups unassigned) (defcomp ~admin/tag-groups-main (&key form groups unassigned)
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-8" (div :class "max-w-2xl mx-auto px-4 py-6 space-y-8"
form groups unassigned)) form groups unassigned))
;; Tag group edit ;; Tag group edit
(defcomp ~blog-tag-checkbox (&key (tag-id :as string) (checked :as boolean) img (name :as string)) (defcomp ~admin/tag-checkbox (&key (tag-id :as string) (checked :as boolean) img (name :as string))
(label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer" (label :class "flex items-center gap-2 px-2 py-1 hover:bg-stone-50 rounded text-sm cursor-pointer"
(input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300") (input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300")
img (span name))) img (span name)))
(defcomp ~blog-tag-checkbox-image (&key src) (defcomp ~admin/tag-checkbox-image (&key src)
(img :src src :alt "" :class "h-4 w-4 rounded-full object-cover")) (img :src src :alt "" :class "h-4 w-4 rounded-full object-cover"))
(defcomp ~blog-tag-group-edit-form (&key (save-url :as string) (csrf :as string) (name :as string) (colour :as string?) (sort-order :as number) (feature-image :as string?) tags) (defcomp ~admin/tag-group-edit-form (&key (save-url :as string) (csrf :as string) (name :as string) (colour :as string?) (sort-order :as number) (feature-image :as string?) tags)
(form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4" (form :method "post" :action save-url :class "border rounded p-4 bg-white space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(div :class "space-y-3" (div :class "space-y-3"
@@ -133,19 +133,19 @@
(div :class "flex gap-3" (div :class "flex gap-3"
(button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save")))) (button :type "submit" :class "border rounded px-4 py-2 bg-stone-800 text-white text-sm" "Save"))))
(defcomp ~blog-tag-group-delete-form (&key (delete-url :as string) (csrf :as string)) (defcomp ~admin/tag-group-delete-form (&key (delete-url :as string) (csrf :as string))
(form :method "post" :action delete-url :class "border-t pt-4" (form :method "post" :action delete-url :class "border-t pt-4"
:onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')" :onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "border rounded px-4 py-2 bg-red-600 text-white text-sm" "Delete Group"))) (button :type "submit" :class "border rounded px-4 py-2 bg-red-600 text-white text-sm" "Delete Group")))
(defcomp ~blog-tag-group-edit-main (&key edit-form delete-form) (defcomp ~admin/tag-group-edit-main (&key edit-form delete-form)
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-6" (div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
edit-form delete-form)) edit-form delete-form))
;; Data-driven snippets list (replaces Python _snippets_sx loop) ;; Data-driven snippets list (replaces Python _snippets_sx loop)
(defcomp ~blog-snippets-from-data (&key snippets user-id is-admin csrf badge-colours) (defcomp ~admin/snippets-from-data (&key snippets user-id is-admin csrf badge-colours)
(~blog-snippets-list (~admin/snippets-list
:rows (<> (map (lambda (s) :rows (<> (map (lambda (s)
(let* ((s-id (get s "id")) (let* ((s-id (get s "id"))
(s-name (get s "name")) (s-name (get s "name"))
@@ -155,31 +155,31 @@
(badge-cls (or (get badge-colours s-vis) "bg-stone-200 text-stone-700")) (badge-cls (or (get badge-colours s-vis) "bg-stone-200 text-stone-700"))
(extra (<> (extra (<>
(when is-admin (when is-admin
(~blog-snippet-visibility-select (~admin/snippet-visibility-select
:patch-url (get s "patch_url") :patch-url (get s "patch_url")
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}") :hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
:options (<> :options (<>
(~blog-snippet-option :value "private" :selected (= s-vis "private") :label "private") (~admin/snippet-option :value "private" :selected (= s-vis "private") :label "private")
(~blog-snippet-option :value "shared" :selected (= s-vis "shared") :label "shared") (~admin/snippet-option :value "shared" :selected (= s-vis "shared") :label "shared")
(~blog-snippet-option :value "admin" :selected (= s-vis "admin") :label "admin")) (~admin/snippet-option :value "admin" :selected (= s-vis "admin") :label "admin"))
:cls "text-sm border border-stone-300 rounded px-2 py-1")) :cls "text-sm border border-stone-300 rounded px-2 py-1"))
(when (or (= s-uid user-id) is-admin) (when (or (= s-uid user-id) is-admin)
(~delete-btn :url (get s "delete_url") :trigger-target "#snippets-list" (~shared:misc/delete-btn :url (get s "delete_url") :trigger-target "#snippets-list"
:title "Delete snippet?" :title "Delete snippet?"
:text (str "Delete \u201c" s-name "\u201d?") :text (str "Delete \u201c" s-name "\u201d?")
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}") :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"))))) :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 (~admin/snippet-row :name s-name :owner owner :badge-cls badge-cls
:visibility s-vis :extra extra))) :visibility s-vis :extra extra)))
(or snippets (list)))))) (or snippets (list))))))
;; Data-driven menu items list (replaces Python _menu_items_list_sx loop) ;; Data-driven menu items list (replaces Python _menu_items_list_sx loop)
(defcomp ~blog-menu-items-from-data (&key items csrf) (defcomp ~admin/menu-items-from-data (&key items csrf)
(~blog-menu-items-list (~admin/menu-items-list
:rows (<> (map (lambda (item) :rows (<> (map (lambda (item)
(let* ((img (~img-or-placeholder :src (get item "feature_image") :alt (get item "label") (let* ((img (~shared:misc/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"))) :size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")))
(~blog-menu-item-row (~admin/menu-item-row
:img img :label (get item "label") :slug (get item "slug") :img img :label (get item "label") :slug (get item "slug")
:sort-order (get item "sort_order") :edit-url (get item "edit_url") :sort-order (get item "sort_order") :edit-url (get item "edit_url")
:delete-url (get item "delete_url") :delete-url (get item "delete_url")
@@ -188,38 +188,38 @@
(or items (list)))))) (or items (list))))))
;; Data-driven tag groups main (replaces Python _tag_groups_main_panel_sx loops) ;; 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) (defcomp ~admin/tag-groups-from-data (&key groups unassigned-tags csrf create-url)
(~blog-tag-groups-main (~admin/tag-groups-main
:form (~blog-tag-groups-create-form :create-url create-url :csrf csrf) :form (~admin/tag-groups-create-form :create-url create-url :csrf csrf)
:groups (if (empty? (or groups (list))) :groups (if (empty? (or groups (list)))
(~empty-state :message "No tag groups yet." :cls "text-stone-500 text-sm") (~shared:misc/empty-state :message "No tag groups yet." :cls "text-stone-500 text-sm")
(~blog-tag-groups-list (~admin/tag-groups-list
:items (<> (map (lambda (g) :items (<> (map (lambda (g)
(let* ((icon (if (get g "feature_image") (let* ((icon (if (get g "feature_image")
(~blog-tag-group-icon-image :src (get g "feature_image") :name (get g "name")) (~admin/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"))))) (~admin/tag-group-icon-color :style (get g "style") :initial (get g "initial")))))
(~blog-tag-group-li :icon icon :edit-href (get g "edit_href") (~admin/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")))) :name (get g "name") :slug (get g "slug") :sort-order (get g "sort_order"))))
groups)))) groups))))
:unassigned (when (not (empty? (or unassigned-tags (list)))) :unassigned (when (not (empty? (or unassigned-tags (list))))
(~blog-unassigned-tags (~admin/unassigned-tags
:heading (str "Unassigned Tags (" (len unassigned-tags) ")") :heading (str "Unassigned Tags (" (len unassigned-tags) ")")
:spans (<> (map (lambda (t) :spans (<> (map (lambda (t)
(~blog-unassigned-tag :name (get t "name"))) (~admin/unassigned-tag :name (get t "name")))
unassigned-tags)))))) unassigned-tags))))))
;; Data-driven tag group edit (replaces Python _tag_groups_edit_main_panel_sx loop) ;; Data-driven tag group edit (replaces Python _tag_groups_edit_main_panel_sx loop)
(defcomp ~blog-tag-checkboxes-from-data (&key tags) (defcomp ~admin/tag-checkboxes-from-data (&key tags)
(<> (map (lambda (t) (<> (map (lambda (t)
(~blog-tag-checkbox (~admin/tag-checkbox
:tag-id (get t "tag_id") :checked (get t "checked") :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"))) :img (when (get t "feature_image") (~admin/tag-checkbox-image :src (get t "feature_image")))
:name (get t "name"))) :name (get t "name")))
(or tags (list))))) (or tags (list)))))
;; Preview panel components ;; Preview panel components
(defcomp ~blog-preview-panel (&key sections) (defcomp ~admin/preview-panel (&key sections)
(div :class "max-w-4xl mx-auto px-4 py-6 space-y-4" (div :class "max-w-4xl mx-auto px-4 py-6 space-y-4"
(style " (style "
.sx-pretty, .json-pretty { font-family: monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; } .sx-pretty, .json-pretty { font-family: monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
@@ -239,18 +239,18 @@
") ")
sections)) sections))
(defcomp ~blog-preview-section (&key title content) (defcomp ~admin/preview-section (&key title content)
(details :class "border rounded bg-white" (details :class "border rounded bg-white"
(summary :class "cursor-pointer px-4 py-3 font-medium text-sm bg-stone-100 hover:bg-stone-200 select-none" title) (summary :class "cursor-pointer px-4 py-3 font-medium text-sm bg-stone-100 hover:bg-stone-200 select-none" title)
(div :class "p-4 overflow-x-auto text-xs" content))) (div :class "p-4 overflow-x-auto text-xs" content)))
(defcomp ~blog-preview-rendered (&key html) (defcomp ~admin/preview-rendered (&key html)
(div :class "blog-content prose max-w-none" (raw! html))) (div :class "blog-content prose max-w-none" (raw! html)))
(defcomp ~blog-preview-empty () (defcomp ~admin/preview-empty ()
(div :class "p-8 text-stone-500" "No content to preview.")) (div :class "p-8 text-stone-500" "No content to preview."))
(defcomp ~blog-admin-placeholder () (defcomp ~admin/placeholder ()
(div :class "pb-8")) (div :class "pb-8"))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -258,12 +258,12 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Snippets — receives serialized snippet dicts from service ;; Snippets — receives serialized snippet dicts from service
(defcomp ~blog-snippets-content (&key snippets is-admin csrf) (defcomp ~admin/snippets-content (&key snippets is-admin csrf)
(~blog-snippets-panel (~admin/snippets-panel
:list (if (empty? (or snippets (list))) :list (if (empty? (or snippets (list)))
(~empty-state :icon "fa fa-puzzle-piece" (~shared:misc/empty-state :icon "fa fa-puzzle-piece"
:message "No snippets yet. Create one from the blog editor.") :message "No snippets yet. Create one from the blog editor.")
(~blog-snippets-list (~admin/snippets-list
:rows (map (lambda (s) :rows (map (lambda (s)
(let* ((badge-colours (dict (let* ((badge-colours (dict
"private" "bg-stone-200 text-stone-700" "private" "bg-stone-200 text-stone-700"
@@ -274,19 +274,19 @@
(name (get s "name")) (name (get s "name"))
(owner (get s "owner")) (owner (get s "owner"))
(can-delete (get s "can_delete"))) (can-delete (get s "can_delete")))
(~blog-snippet-row (~admin/snippet-row
:name name :owner owner :badge-cls badge-cls :visibility vis :name name :owner owner :badge-cls badge-cls :visibility vis
:extra (<> :extra (<>
(when is-admin (when is-admin
(~blog-snippet-visibility-select (~admin/snippet-visibility-select
:patch-url (get s "patch_url") :patch-url (get s "patch_url")
:hx-headers {:X-CSRFToken csrf} :hx-headers {:X-CSRFToken csrf}
:options (<> :options (<>
(~blog-snippet-option :value "private" :selected (= vis "private") :label "private") (~admin/snippet-option :value "private" :selected (= vis "private") :label "private")
(~blog-snippet-option :value "shared" :selected (= vis "shared") :label "shared") (~admin/snippet-option :value "shared" :selected (= vis "shared") :label "shared")
(~blog-snippet-option :value "admin" :selected (= vis "admin") :label "admin")))) (~admin/snippet-option :value "admin" :selected (= vis "admin") :label "admin"))))
(when can-delete (when can-delete
(~delete-btn (~shared:misc/delete-btn
:url (get s "delete_url") :url (get s "delete_url")
:trigger-target "#snippets-list" :trigger-target "#snippets-list"
:title "Delete snippet?" :title "Delete snippet?"
@@ -296,16 +296,16 @@
(or snippets (list))))))) (or snippets (list)))))))
;; Menu Items — receives serialized menu item dicts from service ;; Menu Items — receives serialized menu item dicts from service
(defcomp ~blog-menu-items-content (&key menu-items new-url csrf) (defcomp ~admin/menu-items-content (&key menu-items new-url csrf)
(~blog-menu-items-panel (~admin/menu-items-panel
:new-url new-url :new-url new-url
:list (if (empty? (or menu-items (list))) :list (if (empty? (or menu-items (list)))
(~empty-state :icon "fa fa-inbox" (~shared:misc/empty-state :icon "fa fa-inbox"
:message "No menu items yet. Add one to get started!") :message "No menu items yet. Add one to get started!")
(~blog-menu-items-list (~admin/menu-items-list
:rows (map (lambda (mi) :rows (map (lambda (mi)
(~blog-menu-item-row (~admin/menu-item-row
:img (~img-or-placeholder :img (~shared:misc/img-or-placeholder
:src (get mi "feature_image") :alt (get mi "label") :src (get mi "feature_image") :alt (get mi "label")
:size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0") :size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")
:label (get mi "label") :label (get mi "label")
@@ -318,23 +318,23 @@
(or menu-items (list))))))) (or menu-items (list)))))))
;; Tag Groups — receives serialized tag group data from service ;; Tag Groups — receives serialized tag group data from service
(defcomp ~blog-tag-groups-content (&key groups unassigned-tags create-url csrf) (defcomp ~admin/tag-groups-content (&key groups unassigned-tags create-url csrf)
(~blog-tag-groups-main (~admin/tag-groups-main
:form (~blog-tag-groups-create-form :create-url create-url :csrf csrf) :form (~admin/tag-groups-create-form :create-url create-url :csrf csrf)
:groups (if (empty? (or groups (list))) :groups (if (empty? (or groups (list)))
(~empty-state :icon "fa fa-tags" :message "No tag groups yet.") (~shared:misc/empty-state :icon "fa fa-tags" :message "No tag groups yet.")
(~blog-tag-groups-list (~admin/tag-groups-list
:items (map (lambda (g) :items (map (lambda (g)
(let* ((fi (get g "feature_image")) (let* ((fi (get g "feature_image"))
(colour (get g "colour")) (colour (get g "colour"))
(name (get g "name")) (name (get g "name"))
(initial (slice (or name "?") 0 1)) (initial (slice (or name "?") 0 1))
(icon (if fi (icon (if fi
(~blog-tag-group-icon-image :src fi :name name) (~admin/tag-group-icon-image :src fi :name name)
(~blog-tag-group-icon-color (~admin/tag-group-icon-color
:style (if colour (str "background:" colour) "background:#e7e5e4") :style (if colour (str "background:" colour) "background:#e7e5e4")
:initial initial)))) :initial initial))))
(~blog-tag-group-li (~admin/tag-group-li
:icon icon :icon icon
:edit-href (get g "edit_href") :edit-href (get g "edit_href")
:name name :name name
@@ -342,57 +342,57 @@
:sort-order (or (get g "sort_order") 0)))) :sort-order (or (get g "sort_order") 0))))
(or groups (list))))) (or groups (list)))))
:unassigned (when (not (empty? (or unassigned-tags (list)))) :unassigned (when (not (empty? (or unassigned-tags (list))))
(~blog-unassigned-tags (~admin/unassigned-tags
:heading (str (len (or unassigned-tags (list))) " Unassigned Tags") :heading (str (len (or unassigned-tags (list))) " Unassigned Tags")
:spans (map (lambda (t) :spans (map (lambda (t)
(~blog-unassigned-tag :name (get t "name"))) (~admin/unassigned-tag :name (get t "name")))
(or unassigned-tags (list))))))) (or unassigned-tags (list)))))))
;; Tag Group Edit — receives serialized tag group + tags from service ;; Tag Group Edit — receives serialized tag group + tags from service
(defcomp ~blog-tag-group-edit-content (&key group all-tags save-url delete-url csrf) (defcomp ~admin/tag-group-edit-content (&key group all-tags save-url delete-url csrf)
(~blog-tag-group-edit-main (~admin/tag-group-edit-main
:edit-form (~blog-tag-group-edit-form :edit-form (~admin/tag-group-edit-form
:save-url save-url :csrf csrf :save-url save-url :csrf csrf
:name (get group "name") :name (get group "name")
:colour (get group "colour") :colour (get group "colour")
:sort-order (get group "sort_order") :sort-order (get group "sort_order")
:feature-image (get group "feature_image") :feature-image (get group "feature_image")
:tags (map (lambda (t) :tags (map (lambda (t)
(~blog-tag-checkbox (~admin/tag-checkbox
:tag-id (get t "id") :tag-id (get t "id")
:checked (get t "checked") :checked (get t "checked")
:img (when (get t "feature_image") :img (when (get t "feature_image")
(~blog-tag-checkbox-image :src (get t "feature_image"))) (~admin/tag-checkbox-image :src (get t "feature_image")))
:name (get t "name"))) :name (get t "name")))
(or all-tags (list)))) (or all-tags (list))))
:delete-form (~blog-tag-group-delete-form :delete-url delete-url :csrf csrf))) :delete-form (~admin/tag-group-delete-form :delete-url delete-url :csrf csrf)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Preview content composition — replaces _h_post_preview_content ;; Preview content composition — replaces _h_post_preview_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-preview-content (&key sx-pretty json-pretty sx-rendered lex-rendered) (defcomp ~admin/preview-content (&key sx-pretty json-pretty sx-rendered lex-rendered)
(let* ((sections (list))) (let* ((sections (list)))
(if (and (not sx-pretty) (not json-pretty) (not sx-rendered) (not lex-rendered)) (if (and (not sx-pretty) (not json-pretty) (not sx-rendered) (not lex-rendered))
(~blog-preview-empty) (~admin/preview-empty)
(~blog-preview-panel :sections (~admin/preview-panel :sections
(<> (<>
(when sx-pretty (when sx-pretty
(~blog-preview-section :title "S-Expression Source" :content sx-pretty)) (~admin/preview-section :title "S-Expression Source" :content sx-pretty))
(when json-pretty (when json-pretty
(~blog-preview-section :title "Lexical JSON" :content json-pretty)) (~admin/preview-section :title "Lexical JSON" :content json-pretty))
(when sx-rendered (when sx-rendered
(~blog-preview-section :title "SX Rendered" (~admin/preview-section :title "SX Rendered"
:content (~blog-preview-rendered :html sx-rendered))) :content (~admin/preview-rendered :html sx-rendered)))
(when lex-rendered (when lex-rendered
(~blog-preview-section :title "Lexical Rendered" (~admin/preview-section :title "Lexical Rendered"
:content (~blog-preview-rendered :html lex-rendered)))))))) :content (~admin/preview-rendered :html lex-rendered))))))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Data introspection composition — replaces _h_post_data_content ;; Data introspection composition — replaces _h_post_data_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-data-value-cell (&key value value-type) (defcomp ~admin/data-value-cell (&key value value-type)
(if (= value-type "nil") (if (= value-type "nil")
(span :class "text-neutral-400" "\u2014") (span :class "text-neutral-400" "\u2014")
(pre :class "whitespace-pre-wrap break-words break-all text-xs" (pre :class "whitespace-pre-wrap break-words break-all text-xs"
@@ -400,7 +400,7 @@
(code value) (code value)
value)))) value))))
(defcomp ~blog-data-scalar-table (&key columns) (defcomp ~admin/data-scalar-table (&key columns)
(div :class "w-full overflow-x-auto sm:overflow-visible" (div :class "w-full overflow-x-auto sm:overflow-visible"
(table :class "w-full table-fixed text-sm border border-neutral-200 rounded-xl overflow-hidden" (table :class "w-full table-fixed text-sm border border-neutral-200 rounded-xl overflow-hidden"
(thead :class "bg-neutral-50/70" (thead :class "bg-neutral-50/70"
@@ -411,10 +411,10 @@
(tr :class "border-t border-neutral-200 align-top" (tr :class "border-t border-neutral-200 align-top"
(td :class "px-3 py-2 whitespace-nowrap text-neutral-600 align-top" (get col "key")) (td :class "px-3 py-2 whitespace-nowrap text-neutral-600 align-top" (get col "key"))
(td :class "px-3 py-2 align-top" (td :class "px-3 py-2 align-top"
(~blog-data-value-cell :value (get col "value") :value-type (get col "type"))))) (~admin/data-value-cell :value (get col "value") :value-type (get col "type")))))
(or columns (list))))))) (or columns (list)))))))
(defcomp ~blog-data-relationship-item (&key index summary children) (defcomp ~admin/data-relationship-item (&key index summary children)
(tr :class "border-t border-neutral-200 align-top" (tr :class "border-t border-neutral-200 align-top"
(td :class "px-2 py-1 whitespace-nowrap align-top" (str index)) (td :class "px-2 py-1 whitespace-nowrap align-top" (str index))
(td :class "px-2 py-1 align-top" (td :class "px-2 py-1 align-top"
@@ -422,11 +422,11 @@
(code summary)) (code summary))
(when children (when children
(div :class "mt-2 pl-3 border-l border-neutral-200" (div :class "mt-2 pl-3 border-l border-neutral-200"
(~blog-data-model-content (~admin/data-model-content
:columns (get children "columns") :columns (get children "columns")
:relationships (get children "relationships"))))))) :relationships (get children "relationships")))))))
(defcomp ~blog-data-relationship (&key name cardinality class-name loaded value) (defcomp ~admin/data-relationship (&key name cardinality class-name loaded value)
(div :class "rounded-xl border border-neutral-200" (div :class "rounded-xl border border-neutral-200"
(div :class "px-3 py-2 bg-neutral-50/70 text-sm font-medium" (div :class "px-3 py-2 bg-neutral-50/70 text-sm font-medium"
"Relationship: " (span :class "font-semibold" name) "Relationship: " (span :class "font-semibold" name)
@@ -448,7 +448,7 @@
(th :class "px-2 py-1 text-left" "Summary"))) (th :class "px-2 py-1 text-left" "Summary")))
(tbody (tbody
(map (lambda (item) (map (lambda (item)
(~blog-data-relationship-item (~admin/data-relationship-item
:index (get item "index") :index (get item "index")
:summary (get item "summary") :summary (get item "summary")
:children (get item "children"))) :children (get item "children")))
@@ -459,17 +459,17 @@
(code (get value "summary"))) (code (get value "summary")))
(when (get value "children") (when (get value "children")
(div :class "pl-3 border-l border-neutral-200" (div :class "pl-3 border-l border-neutral-200"
(~blog-data-model-content (~admin/data-model-content
:columns (get (get value "children") "columns") :columns (get (get value "children") "columns")
:relationships (get (get value "children") "relationships")))))))))) :relationships (get (get value "children") "relationships"))))))))))
(defcomp ~blog-data-model-content (&key columns relationships) (defcomp ~admin/data-model-content (&key columns relationships)
(div :class "space-y-4" (div :class "space-y-4"
(~blog-data-scalar-table :columns columns) (~admin/data-scalar-table :columns columns)
(when (not (empty? (or relationships (list)))) (when (not (empty? (or relationships (list))))
(div :class "space-y-3" (div :class "space-y-3"
(map (lambda (rel) (map (lambda (rel)
(~blog-data-relationship (~admin/data-relationship
:name (get rel "name") :name (get rel "name")
:cardinality (get rel "cardinality") :cardinality (get rel "cardinality")
:class-name (get rel "class_name") :class-name (get rel "class_name")
@@ -477,13 +477,13 @@
:value (get rel "value"))) :value (get rel "value")))
relationships))))) relationships)))))
(defcomp ~blog-data-table-content (&key tablename model-data) (defcomp ~admin/data-table-content (&key tablename model-data)
(if (not model-data) (if (not model-data)
(div :class "px-4 py-8 text-stone-400" "No post data available.") (div :class "px-4 py-8 text-stone-400" "No post data available.")
(div :class "px-4 py-8" (div :class "px-4 py-8"
(div :class "mb-6 text-sm text-neutral-500" (div :class "mb-6 text-sm text-neutral-500"
"Model: " (code "Post") " \u2022 Table: " (code tablename)) "Model: " (code "Post") " \u2022 Table: " (code tablename))
(~blog-data-model-content (~admin/data-model-content
:columns (get model-data "columns") :columns (get model-data "columns")
:relationships (get model-data "relationships"))))) :relationships (get model-data "relationships")))))
@@ -491,7 +491,7 @@
;; Calendar month view for browsing/toggling entries (B1) ;; Calendar month view for browsing/toggling entries (B1)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-cal-entry-associated (&key name toggle-url csrf) (defcomp ~admin/cal-entry-associated (&key name toggle-url csrf)
(div :class "flex items-center gap-1 text-[10px] rounded px-1 py-0.5 bg-green-200 text-green-900" (div :class "flex items-center gap-1 text-[10px] rounded px-1 py-0.5 bg-green-200 text-green-900"
(span :class "truncate flex-1" name) (span :class "truncate flex-1" name)
(button :type "button" :class "flex-shrink-0 hover:text-red-600" (button :type "button" :class "flex-shrink-0 hover:text-red-600"
@@ -505,7 +505,7 @@
:sx-on:afterSwap "document.body.dispatchEvent(new CustomEvent('entryToggled'))" :sx-on:afterSwap "document.body.dispatchEvent(new CustomEvent('entryToggled'))"
(i :class "fa fa-times")))) (i :class "fa fa-times"))))
(defcomp ~blog-cal-entry-unassociated (&key name toggle-url csrf) (defcomp ~admin/cal-entry-unassociated (&key name toggle-url csrf)
(button :type "button" (button :type "button"
:class "w-full text-left text-[10px] rounded px-1 py-0.5 bg-stone-100 text-stone-700 hover:bg-stone-200" :class "w-full text-left text-[10px] rounded px-1 py-0.5 bg-stone-100 text-stone-700 hover:bg-stone-200"
:data-confirm "" :data-confirm-title "Add entry?" :data-confirm "" :data-confirm-title "Add entry?"
@@ -518,7 +518,7 @@
:sx-on:afterSwap "document.body.dispatchEvent(new CustomEvent('entryToggled'))" :sx-on:afterSwap "document.body.dispatchEvent(new CustomEvent('entryToggled'))"
(span :class "truncate block" name))) (span :class "truncate block" name)))
(defcomp ~blog-calendar-view (&key cal-id year month-name (defcomp ~admin/calendar-view (&key cal-id year month-name
current-url prev-month-url prev-year-url current-url prev-month-url prev-year-url
next-month-url next-year-url next-month-url next-year-url
weekday-names days csrf) weekday-names days csrf)
@@ -553,9 +553,9 @@
(div :class "space-y-0.5" (div :class "space-y-0.5"
(map (lambda (e) (map (lambda (e)
(if (get e "is_associated") (if (get e "is_associated")
(~blog-cal-entry-associated (~admin/cal-entry-associated
:name (get e "name") :toggle-url (get e "toggle_url") :csrf csrf) :name (get e "name") :toggle-url (get e "toggle_url") :csrf csrf)
(~blog-cal-entry-unassociated (~admin/cal-entry-unassociated
:name (get e "name") :toggle-url (get e "toggle_url") :csrf csrf))) :name (get e "name") :toggle-url (get e "toggle_url") :csrf csrf)))
entries)))))) entries))))))
(or days (list)))))))) (or days (list))))))))
@@ -564,15 +564,15 @@
;; Nav entries OOB — renders associated entry/calendar items in scroll wrapper (B2) ;; Nav entries OOB — renders associated entry/calendar items in scroll wrapper (B2)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-nav-entries-oob (&key entries calendars) (defcomp ~admin/nav-entries-oob (&key entries calendars)
(let* ((entry-list (or entries (list))) (let* ((entry-list (or entries (list)))
(cal-list (or calendars (list))) (cal-list (or calendars (list)))
(has-items (or (not (empty? entry-list)) (not (empty? cal-list)))) (has-items (or (not (empty? entry-list)) (not (empty? cal-list))))
(nav-cls "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black [.hover-capable_&]:hover:bg-yellow-300 aria-selected:bg-stone-500 aria-selected:text-white [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 p-2") (nav-cls "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black [.hover-capable_&]:hover:bg-yellow-300 aria-selected:bg-stone-500 aria-selected:text-white [.hover-capable_&[aria-selected=true]:hover]:bg-orange-500 p-2")
(scroll-hs "on load or scroll if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end")) (scroll-hs "on load or scroll if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end"))
(if (not has-items) (if (not has-items)
(~blog-nav-entries-empty) (~shared:nav/blog-nav-entries-empty)
(~scroll-nav-wrapper (~shared:misc/scroll-nav-wrapper
:wrapper-id "entries-calendars-nav-wrapper" :wrapper-id "entries-calendars-nav-wrapper"
:container-id "associated-items-container" :container-id "associated-items-container"
:arrow-cls "entries-nav-arrow" :arrow-cls "entries-nav-arrow"
@@ -581,12 +581,12 @@
:right-hs "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft + 200" :right-hs "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft + 200"
:items (<> :items (<>
(map (lambda (e) (map (lambda (e)
(~calendar-entry-nav (~shared:navigation/calendar-entry-nav
:href (get e "href") :nav-class nav-cls :href (get e "href") :nav-class nav-cls
:name (get e "name") :date-str (get e "date_str"))) :name (get e "name") :date-str (get e "date_str")))
entry-list) entry-list)
(map (lambda (c) (map (lambda (c)
(~blog-nav-calendar-item (~shared:nav/blog-nav-calendar-item
:href (get c "href") :nav-cls nav-cls :href (get c "href") :nav-cls nav-cls
:name (get c "name"))) :name (get c "name")))
cal-list)) cal-list))

View File

@@ -1,51 +1,51 @@
;; Blog card components — pure data, no HTML injection ;; Blog card components — pure data, no HTML injection
(defcomp ~blog-like-button (&key like-url hx-headers heart) (defcomp ~cards/like-button (&key like-url hx-headers heart)
(div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl" (div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl"
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart))) (~detail/like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
(defcomp ~blog-draft-status (&key (publish-requested :as boolean) (timestamp :as string?)) (defcomp ~cards/draft-status (&key (publish-requested :as boolean) (timestamp :as string?))
(<> (div :class "flex justify-center gap-2 mt-1" (<> (div :class "flex justify-center gap-2 mt-1"
(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-800" "Draft") (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-800" "Draft")
(when publish-requested (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800" "Publish requested"))) (when publish-requested (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800" "Publish requested")))
(when timestamp (p :class "text-sm text-stone-500" (str "Updated: " timestamp))))) (when timestamp (p :class "text-sm text-stone-500" (str "Updated: " timestamp)))))
(defcomp ~blog-published-status (&key (timestamp :as string)) (defcomp ~cards/published-status (&key (timestamp :as string))
(p :class "text-sm text-stone-500" (str "Published: " timestamp))) (p :class "text-sm text-stone-500" (str "Published: " timestamp)))
;; Tag components — accept data, not HTML ;; Tag components — accept data, not HTML
(defcomp ~blog-tag-icon (&key (src :as string?) (name :as string) (initial :as string)) (defcomp ~cards/tag-icon (&key (src :as string?) (name :as string) (initial :as string))
(if src (if src
(img :src src :alt name :class "h-4 w-4 rounded-full object-cover border border-stone-300 flex-shrink-0") (img :src src :alt name :class "h-4 w-4 rounded-full object-cover border border-stone-300 flex-shrink-0")
(div :class "h-4 w-4 rounded-full text-[8px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0 bg-stone-200 text-stone-600" initial))) (div :class "h-4 w-4 rounded-full text-[8px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0 bg-stone-200 text-stone-600" initial)))
(defcomp ~blog-tag-item (&key src name initial) (defcomp ~cards/tag-item (&key src name initial)
(li (a :class "flex items-center gap-1" (li (a :class "flex items-center gap-1"
(~blog-tag-icon :src src :name name :initial initial) (~cards/tag-icon :src src :name name :initial initial)
(span :class "inline-block rounded-full bg-stone-100 text-stone-600 px-2 py-1 text-sm font-medium border border-stone-200" name)))) (span :class "inline-block rounded-full bg-stone-100 text-stone-600 px-2 py-1 text-sm font-medium border border-stone-200" name))))
;; At-bar — tags + authors row for detail pages ;; At-bar — tags + authors row for detail pages
(defcomp ~blog-at-bar (&key tags authors) (defcomp ~cards/at-bar (&key tags authors)
(when (or tags authors) (when (or tags authors)
(div :class "flex flex-row justify-center gap-3" (div :class "flex flex-row justify-center gap-3"
(when tags (when tags
(div :class "mt-4 flex items-center gap-2" (div "in") (div :class "mt-4 flex items-center gap-2" (div "in")
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (t) (~blog-tag-item :src (get t "src") :name (get t "name") :initial (get t "initial"))) tags)))) (map (lambda (t) (~cards/tag-item :src (get t "src") :name (get t "name") :initial (get t "initial"))) tags))))
(div) (div)
(when authors (when authors
(div :class "mt-4 flex items-center gap-2" (div "by") (div :class "mt-4 flex items-center gap-2" (div "by")
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors))))))) (map (lambda (a) (~cards/author-item :image (get a "image") :name (get a "name"))) authors)))))))
;; Author components ;; Author components
(defcomp ~blog-author-item (&key image name) (defcomp ~cards/author-item (&key image name)
(li :class "flex items-center gap-1" (li :class "flex items-center gap-1"
(when image (img :src image :alt name :class "h-5 w-5 rounded-full object-cover")) (when image (img :src image :alt name :class "h-5 w-5 rounded-full object-cover"))
(span :class "text-stone-700" name))) (span :class "text-stone-700" name)))
;; Card — accepts pure data ;; Card — accepts pure data
(defcomp ~blog-card (&key (slug :as string) (href :as string) (hx-select :as string?) (title :as string) (defcomp ~cards/index (&key (slug :as string) (href :as string) (hx-select :as string?) (title :as string)
(feature-image :as string?) (excerpt :as string?) (feature-image :as string?) (excerpt :as string?)
status (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?) status (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
(liked :as boolean) (like-url :as string?) (csrf-token :as string?) (liked :as boolean) (like-url :as string?) (csrf-token :as string?)
@@ -53,7 +53,7 @@
(tags :as list?) (authors :as list?) widget) (tags :as list?) (authors :as list?) widget)
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(when has-like (when has-like
(~blog-like-button (~cards/like-button
:like-url like-url :like-url like-url
:hx-headers {:X-CSRFToken csrf-token} :hx-headers {:X-CSRFToken csrf-token}
:heart (if liked "❤️" "🤍"))) :heart (if liked "❤️" "🤍")))
@@ -63,8 +63,8 @@
(header :class "mb-2 text-center" (header :class "mb-2 text-center"
(h2 :class "text-4xl font-bold text-stone-900" title) (h2 :class "text-4xl font-bold text-stone-900" title)
(if is-draft (if is-draft
(~blog-draft-status :publish-requested publish-requested :timestamp status-timestamp) (~cards/draft-status :publish-requested publish-requested :timestamp status-timestamp)
(when status-timestamp (~blog-published-status :timestamp status-timestamp)))) (when status-timestamp (~cards/published-status :timestamp status-timestamp))))
(when feature-image (div :class "mb-4" (img :src feature-image :alt "" :class "rounded-lg w-full object-cover"))) (when feature-image (div :class "mb-4" (img :src feature-image :alt "" :class "rounded-lg w-full object-cover")))
(when excerpt (p :class "text-stone-700 text-lg leading-relaxed text-center" excerpt))) (when excerpt (p :class "text-stone-700 text-lg leading-relaxed text-center" excerpt)))
widget widget
@@ -73,14 +73,14 @@
(when tags (when tags
(div :class "mt-4 flex items-center gap-2" (div "in") (div :class "mt-4 flex items-center gap-2" (div "in")
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (t) (~blog-tag-item :src (get t "src") :name (get t "name") :initial (get t "initial"))) tags)))) (map (lambda (t) (~cards/tag-item :src (get t "src") :name (get t "name") :initial (get t "initial"))) tags))))
(div) (div)
(when authors (when authors
(div :class "mt-4 flex items-center gap-2" (div "by") (div :class "mt-4 flex items-center gap-2" (div "by")
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors)))))))) (map (lambda (a) (~cards/author-item :image (get a "image") :name (get a "name"))) authors))))))))
(defcomp ~blog-card-tile (&key (href :as string) (hx-select :as string?) (feature-image :as string?) (title :as string) (defcomp ~cards/tile (&key (href :as string) (hx-select :as string?) (feature-image :as string?) (title :as string)
(is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?) (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?)
(excerpt :as string?) (tags :as list?) (authors :as list?)) (excerpt :as string?) (tags :as list?) (authors :as list?))
(article :class "relative" (article :class "relative"
@@ -91,33 +91,33 @@
(div :class "p-3 text-center" (div :class "p-3 text-center"
(h2 :class "text-lg font-bold text-stone-900" title) (h2 :class "text-lg font-bold text-stone-900" title)
(if is-draft (if is-draft
(~blog-draft-status :publish-requested publish-requested :timestamp status-timestamp) (~cards/draft-status :publish-requested publish-requested :timestamp status-timestamp)
(when status-timestamp (~blog-published-status :timestamp status-timestamp))) (when status-timestamp (~cards/published-status :timestamp status-timestamp)))
(when excerpt (p :class "text-stone-700 text-sm leading-relaxed line-clamp-3 mt-1" excerpt)))) (when excerpt (p :class "text-stone-700 text-sm leading-relaxed line-clamp-3 mt-1" excerpt))))
(when (or tags authors) (when (or tags authors)
(div :class "flex flex-row justify-center gap-3" (div :class "flex flex-row justify-center gap-3"
(when tags (when tags
(div :class "mt-4 flex items-center gap-2" (div "in") (div :class "mt-4 flex items-center gap-2" (div "in")
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (t) (~blog-tag-item :src (get t "src") :name (get t "name") :initial (get t "initial"))) tags)))) (map (lambda (t) (~cards/tag-item :src (get t "src") :name (get t "name") :initial (get t "initial"))) tags))))
(div) (div)
(when authors (when authors
(div :class "mt-4 flex items-center gap-2" (div "by") (div :class "mt-4 flex items-center gap-2" (div "by")
(ul :class "flex flex-wrap gap-2 text-sm" (ul :class "flex flex-wrap gap-2 text-sm"
(map (lambda (a) (~blog-author-item :image (get a "image") :name (get a "name"))) authors)))))))) (map (lambda (a) (~cards/author-item :image (get a "image") :name (get a "name"))) authors))))))))
;; Data-driven blog cards list (replaces Python _blog_cards_sx loop) ;; Data-driven blog cards list (replaces Python _blog_cards_sx loop)
(defcomp ~blog-cards-from-data (&key (posts :as list?) (view :as string?) sentinel) (defcomp ~cards/from-data (&key (posts :as list?) (view :as string?) sentinel)
(<> (<>
(map (lambda (p) (map (lambda (p)
(if (= view "tile") (if (= view "tile")
(~blog-card-tile (~cards/tile
:href (get p "href") :hx-select (get p "hx_select") :href (get p "href") :hx-select (get p "hx_select")
:feature-image (get p "feature_image") :title (get p "title") :feature-image (get p "feature_image") :title (get p "title")
:is-draft (get p "is_draft") :publish-requested (get p "publish_requested") :is-draft (get p "is_draft") :publish-requested (get p "publish_requested")
:status-timestamp (get p "status_timestamp") :status-timestamp (get p "status_timestamp")
:excerpt (get p "excerpt") :tags (get p "tags") :authors (get p "authors")) :excerpt (get p "excerpt") :tags (get p "tags") :authors (get p "authors"))
(~blog-card (~cards/index
:slug (get p "slug") :href (get p "href") :hx-select (get p "hx_select") :slug (get p "slug") :href (get p "href") :hx-select (get p "hx_select")
:title (get p "title") :feature-image (get p "feature_image") :title (get p "title") :feature-image (get p "feature_image")
:excerpt (get p "excerpt") :is-draft (get p "is_draft") :excerpt (get p "excerpt") :is-draft (get p "is_draft")
@@ -131,10 +131,10 @@
sentinel)) sentinel))
;; Data-driven page cards list (replaces Python _page_cards_sx loop) ;; Data-driven page cards list (replaces Python _page_cards_sx loop)
(defcomp ~page-cards-from-data (&key (pages :as list?) sentinel) (defcomp ~cards/page-cards-from-data (&key (pages :as list?) sentinel)
(<> (<>
(map (lambda (pg) (map (lambda (pg)
(~blog-page-card (~cards/page-card
:href (get pg "href") :hx-select (get pg "hx_select") :href (get pg "href") :hx-select (get pg "hx_select")
:title (get pg "title") :title (get pg "title")
:has-calendar (get pg "has_calendar") :has-market (get pg "has_market") :has-calendar (get pg "has_calendar") :has-market (get pg "has_market")
@@ -143,21 +143,21 @@
(or pages (list))) (or pages (list)))
sentinel)) sentinel))
(defcomp ~blog-page-badges (&key has-calendar has-market) (defcomp ~cards/page-badges (&key has-calendar has-market)
(div :class "flex justify-center gap-2 mt-2" (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" (when has-calendar (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800"
(i :class "fa fa-calendar mr-1") "Calendar")) (i :class "fa fa-calendar mr-1") "Calendar"))
(when has-market (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800" (when has-market (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800"
(i :class "fa fa-shopping-bag mr-1") "Market")))) (i :class "fa fa-shopping-bag mr-1") "Market"))))
(defcomp ~blog-page-card (&key (href :as string) (hx-select :as string?) (title :as string) (has-calendar :as boolean) (has-market :as boolean) (pub-timestamp :as string?) (feature-image :as string?) (excerpt :as string?)) (defcomp ~cards/page-card (&key (href :as string) (hx-select :as string?) (title :as string) (has-calendar :as boolean) (has-market :as boolean) (pub-timestamp :as string?) (feature-image :as string?) (excerpt :as string?))
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true" :sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"
:class "block rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden" :class "block rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden"
(header :class "mb-2 text-center" (header :class "mb-2 text-center"
(h2 :class "text-4xl font-bold text-stone-900" title) (h2 :class "text-4xl font-bold text-stone-900" title)
(~blog-page-badges :has-calendar has-calendar :has-market has-market) (~cards/page-badges :has-calendar has-calendar :has-market has-market)
(when pub-timestamp (~blog-published-status :timestamp pub-timestamp))) (when pub-timestamp (~cards/published-status :timestamp pub-timestamp)))
(when feature-image (div :class "mb-4" (img :src feature-image :alt "" :class "rounded-lg w-full object-cover"))) (when feature-image (div :class "mb-4" (img :src feature-image :alt "" :class "rounded-lg w-full object-cover")))
(when excerpt (p :class "text-stone-700 text-lg leading-relaxed text-center" excerpt))))) (when excerpt (p :class "text-stone-700 text-lg leading-relaxed text-center" excerpt)))))

View File

@@ -1,34 +1,34 @@
;; Blog post detail components ;; Blog post detail components
(defcomp ~blog-detail-edit-link (&key (href :as string) (hx-select :as string)) (defcomp ~detail/edit-link (&key (href :as string) (hx-select :as string))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-stone-700 text-white hover:bg-stone-800 transition-colors" :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-stone-700 text-white hover:bg-stone-800 transition-colors"
(i :class "fa fa-pencil mr-1") " Edit")) (i :class "fa fa-pencil mr-1") " Edit"))
(defcomp ~blog-detail-draft (&key publish-requested edit) (defcomp ~detail/draft (&key publish-requested edit)
(div :class "flex items-center justify-center gap-2 mb-3" (div :class "flex items-center justify-center gap-2 mb-3"
(span :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-amber-100 text-amber-800" "Draft") (span :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-amber-100 text-amber-800" "Draft")
(when publish-requested (span :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-blue-100 text-blue-800" "Publish requested")) (when publish-requested (span :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-blue-100 text-blue-800" "Publish requested"))
edit)) edit))
(defcomp ~blog-like-toggle (&key like-url hx-headers heart) (defcomp ~detail/like-toggle (&key like-url hx-headers heart)
(button :sx-post like-url :sx-swap "outerHTML" (button :sx-post like-url :sx-swap "outerHTML"
:sx-headers hx-headers :class "cursor-pointer" heart)) :sx-headers hx-headers :class "cursor-pointer" heart))
(defcomp ~blog-detail-like (&key like-url hx-headers heart) (defcomp ~detail/like (&key like-url hx-headers heart)
(div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl" (div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl"
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart))) (~detail/like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
(defcomp ~blog-detail-excerpt (&key (excerpt :as string)) (defcomp ~detail/excerpt (&key (excerpt :as string))
(div :class "w-full text-center italic text-3xl p-2" excerpt)) (div :class "w-full text-center italic text-3xl p-2" excerpt))
(defcomp ~blog-detail-chrome (&key like excerpt at-bar) (defcomp ~detail/chrome (&key like excerpt at-bar)
(<> like (<> like
excerpt excerpt
(div :class "hidden md:block" at-bar))) (div :class "hidden md:block" at-bar)))
(defcomp ~blog-detail-main (&key draft chrome feature-image html-content sx-content) (defcomp ~detail/main (&key draft chrome feature-image html-content sx-content)
(<> (article :class "relative" (<> (article :class "relative"
draft draft
chrome chrome
@@ -43,34 +43,34 @@
;; Data-driven composition — replaces _post_main_panel_sx ;; Data-driven composition — replaces _post_main_panel_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-post-detail-content (&key (slug :as string) (is-draft :as boolean) (publish-requested :as boolean) (can-edit :as boolean) (edit-href :as string?) (defcomp ~detail/post-detail-content (&key (slug :as string) (is-draft :as boolean) (publish-requested :as boolean) (can-edit :as boolean) (edit-href :as string?)
(is-page :as boolean) (has-user :as boolean) (liked :as boolean) (like-url :as string?) (csrf :as string?) (is-page :as boolean) (has-user :as boolean) (liked :as boolean) (like-url :as string?) (csrf :as string?)
(custom-excerpt :as string?) (tags :as list?) (authors :as list?) (custom-excerpt :as string?) (tags :as list?) (authors :as list?)
(feature-image :as string?) (html-content :as string?) (sx-content :as string?)) (feature-image :as string?) (html-content :as string?) (sx-content :as string?))
(let* ((hx-select "#main-panel") (let* ((hx-select "#main-panel")
(draft-sx (when is-draft (draft-sx (when is-draft
(~blog-detail-draft (~detail/draft
:publish-requested publish-requested :publish-requested publish-requested
:edit (when can-edit :edit (when can-edit
(~blog-detail-edit-link :href edit-href :hx-select hx-select))))) (~detail/edit-link :href edit-href :hx-select hx-select)))))
(chrome-sx (when (not is-page) (chrome-sx (when (not is-page)
(~blog-detail-chrome (~detail/chrome
:like (when has-user :like (when has-user
(~blog-detail-like (~detail/like
:like-url like-url :like-url like-url
:hx-headers {:X-CSRFToken csrf} :hx-headers {:X-CSRFToken csrf}
:heart (if liked "❤️" "🤍"))) :heart (if liked "❤️" "🤍")))
:excerpt (when (not (= custom-excerpt "")) :excerpt (when (not (= custom-excerpt ""))
(~blog-detail-excerpt :excerpt custom-excerpt)) (~detail/excerpt :excerpt custom-excerpt))
:at-bar (~blog-at-bar :tags tags :authors authors))))) :at-bar (~cards/at-bar :tags tags :authors authors)))))
(~blog-detail-main (~detail/main
:draft draft-sx :draft draft-sx
:chrome chrome-sx :chrome chrome-sx
:feature-image feature-image :feature-image feature-image
:html-content html-content :html-content html-content
:sx-content sx-content))) :sx-content sx-content)))
(defcomp ~blog-meta (&key (robots :as string) (page-title :as string) (desc :as string) (canonical :as string?) (og-type :as string) (og-title :as string) (image :as string?) (twitter-card :as string) (twitter-title :as string)) (defcomp ~detail/meta (&key (robots :as string) (page-title :as string) (desc :as string) (canonical :as string?) (og-type :as string) (og-title :as string) (image :as string?) (twitter-card :as string) (twitter-title :as string))
(<> (<>
(meta :name "robots" :content robots) (meta :name "robots" :content robots)
(title page-title) (title page-title)
@@ -86,7 +86,7 @@
(meta :name "twitter:description" :content desc) (meta :name "twitter:description" :content desc)
(when image (meta :name "twitter:image" :content image)))) (when image (meta :name "twitter:image" :content image))))
(defcomp ~blog-home-main (&key html-content sx-content) (defcomp ~detail/home-main (&key html-content sx-content)
(article :class "relative" (article :class "relative"
(if sx-content (if sx-content
(div :class "blog-content p-2" sx-content) (div :class "blog-content p-2" sx-content)

View File

@@ -1,10 +1,10 @@
;; Blog editor components ;; Blog editor components
(defcomp ~blog-editor-error (&key error) (defcomp ~editor/error (&key error)
(div :class "max-w-[768px] mx-auto mt-[16px] rounded-[8px] border border-red-300 bg-red-50 px-[16px] py-[12px] text-[14px] text-red-700" (div :class "max-w-[768px] mx-auto mt-[16px] rounded-[8px] border border-red-300 bg-red-50 px-[16px] py-[12px] text-[14px] text-red-700"
(strong "Save failed:") " " error)) (strong "Save failed:") " " error))
(defcomp ~blog-editor-form (&key (csrf :as string) (title-placeholder :as string) (create-label :as string)) (defcomp ~editor/form (&key (csrf :as string) (title-placeholder :as string) (create-label :as string))
(form :id "post-new-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]" (form :id "post-new-form" :method "post" :class "max-w-[768px] mx-auto pb-[48px]"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :id "lexical-json-input" :name "lexical" :value "") (input :type "hidden" :id "lexical-json-input" :name "lexical" :value "")
@@ -56,7 +56,7 @@
:class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer" create-label)))) :class "px-[20px] py-[6px] bg-stone-700 text-white text-[14px] rounded-[8px] hover:bg-stone-800 transition-colors cursor-pointer" create-label))))
;; Edit form — pre-populated version for /<slug>/admin/edit/ ;; Edit form — pre-populated version for /<slug>/admin/edit/
(defcomp ~blog-editor-edit-form (&key (csrf :as string) (updated-at :as string) (title-val :as string?) (excerpt-val :as string?) (defcomp ~editor/edit-form (&key (csrf :as string) (updated-at :as string) (title-val :as string?) (excerpt-val :as string?)
(feature-image :as string?) (feature-image-caption :as string?) (feature-image :as string?) (feature-image-caption :as string?)
(sx-content-val :as string?) (lexical-json :as string?) (sx-content-val :as string?) (lexical-json :as string?)
(has-sx :as boolean) (title-placeholder :as string) (has-sx :as boolean) (title-placeholder :as string)
@@ -135,7 +135,7 @@
(when footer-extra footer-extra))))) (when footer-extra footer-extra)))))
;; Publish-mode show/hide script for edit form ;; Publish-mode show/hide script for edit form
(defcomp ~blog-editor-publish-js (&key already-emailed) (defcomp ~editor/publish-js (&key already-emailed)
(script (script
"(function() {" "(function() {"
" var statusSel = document.getElementById('status-select');" " var statusSel = document.getElementById('status-select');"
@@ -153,20 +153,20 @@
" sync();" " sync();"
"})();")) "})();"))
(defcomp ~blog-editor-styles (&key (css-href :as string)) (defcomp ~editor/styles (&key (css-href :as string))
(<> (link :rel "stylesheet" :href css-href) (<> (link :rel "stylesheet" :href css-href)
(style (style
"#lexical-editor { display: flow-root; }" "#lexical-editor { display: flow-root; }"
"#lexical-editor [data-kg-card=\"html\"] * { float: none !important; }" "#lexical-editor [data-kg-card=\"html\"] * { float: none !important; }"
"#lexical-editor [data-kg-card=\"html\"] table { width: 100% !important; }"))) "#lexical-editor [data-kg-card=\"html\"] table { width: 100% !important; }")))
(defcomp ~blog-editor-scripts (&key (js-src :as string) (sx-editor-js-src :as string?) (init-js :as string)) (defcomp ~editor/scripts (&key (js-src :as string) (sx-editor-js-src :as string?) (init-js :as string))
(<> (script :src js-src) (<> (script :src js-src)
(when sx-editor-js-src (script :src sx-editor-js-src)) (when sx-editor-js-src (script :src sx-editor-js-src))
(script init-js))) (script init-js)))
;; SX editor styles — comprehensive CSS for the Koenig-style block editor ;; SX editor styles — comprehensive CSS for the Koenig-style block editor
(defcomp ~sx-editor-styles () (defcomp ~editor/sx-editor-styles ()
(style (style
;; Editor container ;; Editor container
".sx-editor { position: relative; font-size: 18px; line-height: 1.6; font-family: Georgia, 'Times New Roman', serif; color: #1c1917; }" ".sx-editor { position: relative; font-size: 18px; line-height: 1.6; font-family: Georgia, 'Times New Roman', serif; color: #1c1917; }"
@@ -308,34 +308,34 @@
;; Editor panel composition — replaces render_editor_panel (new post/page) ;; Editor panel composition — replaces render_editor_panel (new post/page)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-editor-content (&key csrf title-placeholder create-label (defcomp ~editor/content (&key csrf title-placeholder create-label
css-href js-src sx-editor-js-src init-js css-href js-src sx-editor-js-src init-js
save-error) save-error)
(~blog-editor-panel :parts (~layouts/editor-panel :parts
(<> (<>
(when save-error (~blog-editor-error :error save-error)) (when save-error (~editor/error :error save-error))
(~blog-editor-form :csrf csrf :title-placeholder title-placeholder (~editor/form :csrf csrf :title-placeholder title-placeholder
:create-label create-label) :create-label create-label)
(~blog-editor-styles :css-href css-href) (~editor/styles :css-href css-href)
(~sx-editor-styles) (~editor/sx-editor-styles)
(~blog-editor-scripts :js-src js-src :sx-editor-js-src sx-editor-js-src (~editor/scripts :js-src js-src :sx-editor-js-src sx-editor-js-src
:init-js init-js)))) :init-js init-js))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Edit content composition — replaces _h_post_edit_content (existing post) ;; Edit content composition — replaces _h_post_edit_content (existing post)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-edit-content (&key csrf updated-at title-val excerpt-val (defcomp ~editor/edit-content (&key csrf updated-at title-val excerpt-val
feature-image feature-image-caption feature-image feature-image-caption
sx-content-val lexical-json has-sx sx-content-val lexical-json has-sx
title-placeholder status already-emailed title-placeholder status already-emailed
newsletter-options footer-extra newsletter-options footer-extra
css-href js-src sx-editor-js-src init-js css-href js-src sx-editor-js-src init-js
save-error) save-error)
(~blog-editor-panel :parts (~layouts/editor-panel :parts
(<> (<>
(when save-error (~blog-editor-error :error save-error)) (when save-error (~editor/error :error save-error))
(~blog-editor-edit-form (~editor/edit-form
:csrf csrf :updated-at updated-at :csrf csrf :updated-at updated-at
:title-val title-val :excerpt-val excerpt-val :title-val title-val :excerpt-val excerpt-val
:feature-image feature-image :feature-image-caption feature-image-caption :feature-image feature-image :feature-image-caption feature-image-caption
@@ -343,8 +343,8 @@
:has-sx has-sx :title-placeholder title-placeholder :has-sx has-sx :title-placeholder title-placeholder
:status status :already-emailed already-emailed :status status :already-emailed already-emailed
:newsletter-options newsletter-options :footer-extra footer-extra) :newsletter-options newsletter-options :footer-extra footer-extra)
(~blog-editor-publish-js :already-emailed already-emailed) (~editor/publish-js :already-emailed already-emailed)
(~blog-editor-styles :css-href css-href) (~editor/styles :css-href css-href)
(~sx-editor-styles) (~editor/sx-editor-styles)
(~blog-editor-scripts :js-src js-src :sx-editor-js-src sx-editor-js-src (~editor/scripts :js-src js-src :sx-editor-js-src sx-editor-js-src
:init-js init-js)))) :init-js init-js))))

View File

@@ -1,37 +1,37 @@
;; Blog filter components ;; Blog filter components
(defcomp ~blog-action-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (icon-class :as string) (label :as string)) (defcomp ~filters/action-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (icon-class :as string) (label :as string))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class btn-class :title title (i :class icon-class) label)) :class btn-class :title title (i :class icon-class) label))
(defcomp ~blog-drafts-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (label :as string) (draft-count :as number)) (defcomp ~filters/drafts-button (&key (href :as string) (hx-select :as string) (btn-class :as string) (title :as string) (label :as string) (draft-count :as number))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts " :class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts "
(span :class "inline-block bg-stone-500 text-white px-1.5 py-0.5 text-xs font-medium rounded ml-1" draft-count))) (span :class "inline-block bg-stone-500 text-white px-1.5 py-0.5 text-xs font-medium rounded ml-1" draft-count)))
(defcomp ~blog-drafts-button-amber (&key href hx-select btn-class title label draft-count) (defcomp ~filters/drafts-button-amber (&key href hx-select btn-class title label draft-count)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts " :class btn-class :title title (i :class "fa fa-file-text-o mr-1") " Drafts "
(span :class "inline-block bg-amber-500 text-white px-1.5 py-0.5 text-xs font-medium rounded ml-1" draft-count))) (span :class "inline-block bg-amber-500 text-white px-1.5 py-0.5 text-xs font-medium rounded ml-1" draft-count)))
(defcomp ~blog-action-buttons-wrapper (&key inner) (defcomp ~filters/action-buttons-wrapper (&key inner)
(div :class "flex flex-wrap gap-2 px-4 py-3" inner)) (div :class "flex flex-wrap gap-2 px-4 py-3" inner))
(defcomp ~blog-filter-any-topic (&key cls hx-select) (defcomp ~filters/any-topic (&key cls hx-select)
(li (a :class (str "px-3 py-1 rounded border " cls) (li (a :class (str "px-3 py-1 rounded border " cls)
:sx-get "?page=1" :sx-target "#main-panel" :sx-select hx-select :sx-get "?page=1" :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" "Any Topic"))) :sx-swap "outerHTML" :sx-push-url "true" "Any Topic")))
(defcomp ~blog-filter-group-icon-image (&key src name) (defcomp ~filters/group-icon-image (&key src name)
(img :src src :alt name :class "h-6 w-6 rounded-full object-cover border border-stone-300 flex-shrink-0")) (img :src src :alt name :class "h-6 w-6 rounded-full object-cover border border-stone-300 flex-shrink-0"))
(defcomp ~blog-filter-group-icon-color (&key style initial) (defcomp ~filters/group-icon-color (&key style initial)
(div :class "h-6 w-6 rounded-full text-[10px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0" :style style initial)) (div :class "h-6 w-6 rounded-full text-[10px] font-semibold flex items-center justify-center border border-stone-300 flex-shrink-0" :style style initial))
(defcomp ~blog-filter-group-li (&key cls hx-get hx-select icon name count) (defcomp ~filters/group-li (&key cls hx-get hx-select icon name count)
(li (a :class (str "flex items-center gap-2 px-3 py-1 rounded border " cls) (li (a :class (str "flex items-center gap-2 px-3 py-1 rounded border " cls)
:sx-get hx-get :sx-target "#main-panel" :sx-select hx-select :sx-get hx-get :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" :sx-swap "outerHTML" :sx-push-url "true"
@@ -40,19 +40,19 @@
(span :class "flex-1") (span :class "flex-1")
(span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count)))) (span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count))))
(defcomp ~blog-filter-nav (&key items) (defcomp ~filters/nav (&key items)
(nav :class "max-w-3xl mx-auto px-4 pb-4 flex flex-wrap gap-2 text-sm" (nav :class "max-w-3xl mx-auto px-4 pb-4 flex flex-wrap gap-2 text-sm"
(ul :class "divide-y flex flex-col gap-3" items))) (ul :class "divide-y flex flex-col gap-3" items)))
(defcomp ~blog-filter-any-author (&key cls hx-select) (defcomp ~filters/any-author (&key cls hx-select)
(li (a :class (str "px-3 py-1 rounded " cls) (li (a :class (str "px-3 py-1 rounded " cls)
:sx-get "?page=1" :sx-target "#main-panel" :sx-select hx-select :sx-get "?page=1" :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" "Any author"))) :sx-swap "outerHTML" :sx-push-url "true" "Any author")))
(defcomp ~blog-filter-author-icon (&key src name) (defcomp ~filters/author-icon (&key src name)
(img :src src :alt name :class "h-5 w-5 rounded-full object-cover")) (img :src src :alt name :class "h-5 w-5 rounded-full object-cover"))
(defcomp ~blog-filter-author-li (&key cls hx-get hx-select icon name count) (defcomp ~filters/author-li (&key cls hx-get hx-select icon name count)
(li (a :class (str "flex items-center gap-2 px-3 py-1 rounded " cls) (li (a :class (str "flex items-center gap-2 px-3 py-1 rounded " cls)
:sx-get hx-get :sx-target "#main-panel" :sx-select hx-select :sx-get hx-get :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" :sx-swap "outerHTML" :sx-push-url "true"
@@ -61,41 +61,41 @@
(span :class "flex-1") (span :class "flex-1")
(span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count)))) (span :class "inline-block bg-stone-100 text-stone-600 px-2 py-1 text-xs font-medium border border-stone-200" count))))
(defcomp ~blog-filter-summary (&key (text :as string)) (defcomp ~filters/summary (&key (text :as string))
(span :class "text-sm text-stone-600" text)) (span :class "text-sm text-stone-600" text))
;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop) ;; 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) (defcomp ~filters/tag-groups-filter-from-data (&key groups selected-groups hx-select)
(let* ((is-any (empty? (or selected-groups (list)))) (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"))) (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 (~filters/nav
:items (<> :items (<>
(~blog-filter-any-topic :cls any-cls :hx-select hx-select) (~filters/any-topic :cls any-cls :hx-select hx-select)
(map (lambda (g) (map (lambda (g)
(let* ((slug (get g "slug")) (let* ((slug (get g "slug"))
(name (get g "name")) (name (get g "name"))
(is-on (contains? selected-groups slug)) (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")) (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") (icon (if (get g "feature_image")
(~blog-filter-group-icon-image :src (get g "feature_image") :name name) (~filters/group-icon-image :src (get g "feature_image") :name name)
(~blog-filter-group-icon-color :style (get g "style") :initial (get g "initial"))))) (~filters/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 (~filters/group-li :cls cls :hx-get (str "?group=" slug "&page=1") :hx-select hx-select
:icon icon :name name :count (get g "count")))) :icon icon :name name :count (get g "count"))))
(or groups (list))))))) (or groups (list)))))))
;; Data-driven authors filter (replaces Python _authors_filter_sx loop) ;; Data-driven authors filter (replaces Python _authors_filter_sx loop)
(defcomp ~blog-authors-filter-from-data (&key authors selected-authors hx-select) (defcomp ~filters/authors-filter-from-data (&key authors selected-authors hx-select)
(let* ((is-any (empty? (or selected-authors (list)))) (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"))) (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 (~filters/nav
:items (<> :items (<>
(~blog-filter-any-author :cls any-cls :hx-select hx-select) (~filters/any-author :cls any-cls :hx-select hx-select)
(map (lambda (a) (map (lambda (a)
(let* ((slug (get a "slug")) (let* ((slug (get a "slug"))
(is-on (contains? selected-authors 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")) (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") (icon (when (get a "profile_image")
(~blog-filter-author-icon :src (get a "profile_image") :name (get a "name"))))) (~filters/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 (~filters/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")))) :icon icon :name (get a "name") :count (get a "count"))))
(or authors (list))))))) (or authors (list)))))))

View File

@@ -11,7 +11,7 @@
(let ((post (query "blog" "post-by-slug" :slug (trim s)))) (let ((post (query "blog" "post-by-slug" :slug (trim s))))
(when post (when post
(<> (str "<!-- fragment:" (trim s) " -->") (<> (str "<!-- fragment:" (trim s) " -->")
(~link-card (~shared:fragments/link-card
:link (app-url "blog" (str "/" (get post "slug") "/")) :link (app-url "blog" (str "/" (get post "slug") "/"))
:title (get post "title") :title (get post "title")
:image (get post "feature_image") :image (get post "feature_image")
@@ -22,7 +22,7 @@
(when slug (when slug
(let ((post (query "blog" "post-by-slug" :slug slug))) (let ((post (query "blog" "post-by-slug" :slug slug)))
(when post (when post
(~link-card (~shared:fragments/link-card
:link (app-url "blog" (str "/" (get post "slug") "/")) :link (app-url "blog" (str "/" (get post "slug") "/"))
:title (get post "title") :title (get post "title")
:image (get post "feature_image") :image (get post "feature_image")

View File

@@ -30,25 +30,25 @@
(app-url "blog" (str "/" item-slug "/")))) (app-url "blog" (str "/" item-slug "/"))))
(selected (or (= item-slug (or first-seg "")) (selected (or (= item-slug (or first-seg ""))
(= item-slug app)))) (= item-slug app))))
(~blog-nav-item-link (~shared:nav/blog-nav-item-link
:href href :href href
:hx-get href :hx-get href
:selected (if selected "true" "false") :selected (if selected "true" "false")
:nav-cls nav-cls :nav-cls nav-cls
:img (~img-or-placeholder :img (~shared:misc/img-or-placeholder
:src (get item "feature_image") :src (get item "feature_image")
:alt (or (get item "label") item-slug) :alt (or (get item "label") item-slug)
:size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0") :size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0")
:label (or (get item "label") item-slug)))) items) :label (or (get item "label") item-slug)))) items)
;; Hardcoded artdag link ;; Hardcoded artdag link
(~blog-nav-item-link (~shared:nav/blog-nav-item-link
:href (app-url "artdag" "/") :href (app-url "artdag" "/")
:hx-get (app-url "artdag" "/") :hx-get (app-url "artdag" "/")
:selected (if (or (= "artdag" (or first-seg "")) :selected (if (or (= "artdag" (or first-seg ""))
(= "artdag" app)) "true" "false") (= "artdag" app)) "true" "false")
:nav-cls nav-cls :nav-cls nav-cls
:img (~img-or-placeholder :img (~shared:misc/img-or-placeholder
:src nil :alt "art-dag" :src nil :alt "art-dag"
:size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0") :size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0")
:label "art-dag"))) :label "art-dag")))
@@ -69,8 +69,8 @@
(right-hs (str "on click set #" cid ".scrollLeft to #" cid ".scrollLeft + 200"))) (right-hs (str "on click set #" cid ".scrollLeft to #" cid ".scrollLeft + 200")))
(if (empty? items) (if (empty? items)
(~blog-nav-empty :wrapper-id "menu-items-nav-wrapper") (~shared:nav/blog-nav-empty :wrapper-id "menu-items-nav-wrapper")
(~scroll-nav-wrapper (~shared:misc/scroll-nav-wrapper
:wrapper-id "menu-items-nav-wrapper" :wrapper-id "menu-items-nav-wrapper"
:container-id cid :container-id cid
:arrow-cls arrow-cls :arrow-cls arrow-cls

View File

@@ -1,21 +1,21 @@
;; Blog header components ;; Blog header components
(defcomp ~blog-container-nav (&key container-nav) (defcomp ~header/container-nav (&key container-nav)
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id "entries-calendars-nav-wrapper" container-nav)) :id "entries-calendars-nav-wrapper" container-nav))
(defcomp ~blog-admin-label () (defcomp ~header/admin-label ()
(<> (i :class "fa fa-shield-halved" :aria-hidden "true") " admin")) (<> (i :class "fa fa-shield-halved" :aria-hidden "true") " admin"))
(defcomp ~blog-admin-nav-item (&key href nav-btn-class label is-selected select-colours) (defcomp ~header/admin-nav-item (&key href nav-btn-class label is-selected select-colours)
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:aria-selected (when is-selected "true") :aria-selected (when is-selected "true")
:class (str (or nav-btn-class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3") " " (or select-colours "")) :class (str (or nav-btn-class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3") " " (or select-colours ""))
label))) label)))
(defcomp ~blog-sub-settings-label (&key icon label) (defcomp ~header/sub-settings-label (&key icon label)
(<> (i :class icon :aria-hidden "true") " " label)) (<> (i :class icon :aria-hidden "true") " " label))
(defcomp ~blog-sub-admin-label (&key icon label) (defcomp ~header/sub-admin-label (&key icon label)
(<> (i :class icon :aria-hidden "true") (div label))) (<> (i :class icon :aria-hidden "true") (div label)))

View File

@@ -1,9 +1,9 @@
;; Blog index components ;; Blog index components
(defcomp ~blog-no-pages () (defcomp ~index/no-pages ()
(div :class "col-span-full mt-8 text-center text-stone-500" "No pages found.")) (div :class "col-span-full mt-8 text-center text-stone-500" "No pages found."))
(defcomp ~blog-content-type-tabs (&key posts-href pages-href hx-select posts-cls pages-cls) (defcomp ~index/content-type-tabs (&key posts-href pages-href hx-select posts-cls pages-cls)
(div :class "flex justify-center gap-1 px-3 pt-3" (div :class "flex justify-center gap-1 px-3 pt-3"
(a :href posts-href :sx-get posts-href :sx-target "#main-panel" (a :href posts-href :sx-get posts-href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -12,18 +12,18 @@
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "px-4 py-1.5 rounded-t text-sm font-medium transition-colors " pages-cls) "Pages"))) :class (str "px-4 py-1.5 rounded-t text-sm font-medium transition-colors " pages-cls) "Pages")))
(defcomp ~blog-main-panel-pages (&key tabs cards) (defcomp ~index/main-panel-pages (&key tabs cards)
(<> tabs (<> tabs
(div :class "max-w-full px-3 py-3 space-y-3" cards) (div :class "max-w-full px-3 py-3 space-y-3" cards)
(div :class "pb-8"))) (div :class "pb-8")))
(defcomp ~blog-main-panel-posts (&key tabs toggle grid-cls cards) (defcomp ~index/main-panel-posts (&key tabs toggle grid-cls cards)
(<> tabs (<> tabs
toggle toggle
(div :class grid-cls cards) (div :class grid-cls cards)
(div :class "pb-8"))) (div :class "pb-8")))
(defcomp ~blog-aside (&key search action-buttons tag-groups-filter authors-filter) (defcomp ~index/aside (&key search action-buttons tag-groups-filter authors-filter)
(<> search (<> search
action-buttons action-buttons
(div :id "category-summary-desktop" :hxx-swap-oob "outerHTML" (div :id "category-summary-desktop" :hxx-swap-oob "outerHTML"
@@ -36,12 +36,12 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Helper: CSS class for filter item based on selection state ;; Helper: CSS class for filter item based on selection state
(defcomp ~blog-filter-cls (&key is-on) (defcomp ~index/filter-cls (&key is-on)
;; Returns nothing — use inline (if is-on ...) instead ;; Returns nothing — use inline (if is-on ...) instead
nil) nil)
;; Blog index main content — replaces _blog_main_panel_sx ;; Blog index main content — replaces _blog_main_panel_sx
(defcomp ~blog-index-main-content (&key content-type view cards page total-pages (defcomp ~index/main-content (&key content-type view cards page total-pages
current-local-href hx-select blog-url-base) current-local-href hx-select blog-url-base)
(let* ((posts-href (str blog-url-base "/index")) (let* ((posts-href (str blog-url-base "/index"))
(pages-href (str posts-href "?type=pages")) (pages-href (str posts-href "?type=pages"))
@@ -51,13 +51,13 @@
"bg-stone-700 text-white" "bg-stone-100 text-stone-600 hover:bg-stone-200"))) "bg-stone-700 text-white" "bg-stone-100 text-stone-600 hover:bg-stone-200")))
(if (= content-type "pages") (if (= content-type "pages")
;; Pages listing ;; Pages listing
(~blog-main-panel-pages (~index/main-panel-pages
:tabs (~blog-content-type-tabs :tabs (~index/content-type-tabs
:posts-href posts-href :pages-href pages-href :posts-href posts-href :pages-href pages-href
:hx-select hx-select :posts-cls posts-cls :pages-cls pages-cls) :hx-select hx-select :posts-cls posts-cls :pages-cls pages-cls)
:cards (<> :cards (<>
(map (lambda (card) (map (lambda (card)
(~blog-page-card (~cards/page-card
:href (get card "href") :hx-select hx-select :href (get card "href") :hx-select hx-select
:title (get card "title") :title (get card "title")
:has-calendar (get card "has_calendar") :has-calendar (get card "has_calendar")
@@ -67,14 +67,14 @@
:excerpt (get card "excerpt"))) :excerpt (get card "excerpt")))
(or cards (list))) (or cards (list)))
(if (< page total-pages) (if (< page total-pages)
(~sentinel-simple (~shared:misc/sentinel-simple
:id (str "sentinel-" page "-d") :id (str "sentinel-" page "-d")
:next-url (str current-local-href :next-url (str current-local-href
(if (contains? current-local-href "?") "&" "?") (if (contains? current-local-href "?") "&" "?")
"page=" (+ page 1))) "page=" (+ page 1)))
(if (not (empty? (or cards (list)))) (if (not (empty? (or cards (list))))
(~end-of-results) (~shared:misc/end-of-results)
(~blog-no-pages))))) (~index/no-pages)))))
;; Posts listing ;; Posts listing
(let* ((grid-cls (if (= view "tile") (let* ((grid-cls (if (= view "tile")
"max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
@@ -88,19 +88,19 @@
(tile-cls (if (= view "tile") (tile-cls (if (= view "tile")
"bg-stone-200 text-stone-800" "bg-stone-200 text-stone-800"
"text-stone-400 hover:text-stone-600"))) "text-stone-400 hover:text-stone-600")))
(~blog-main-panel-posts (~index/main-panel-posts
:tabs (~blog-content-type-tabs :tabs (~index/content-type-tabs
:posts-href posts-href :pages-href pages-href :posts-href posts-href :pages-href pages-href
:hx-select hx-select :posts-cls posts-cls :pages-cls pages-cls) :hx-select hx-select :posts-cls posts-cls :pages-cls pages-cls)
:toggle (~view-toggle :toggle (~shared:misc/view-toggle
:list-href list-href :tile-href tile-href :hx-select hx-select :list-href list-href :tile-href tile-href :hx-select hx-select
:list-cls list-cls :tile-cls tile-cls :storage-key "blog_view" :list-cls list-cls :tile-cls tile-cls :storage-key "blog_view"
:list-svg (~list-svg) :tile-svg (~tile-svg)) :list-svg (~shared:misc/list-svg) :tile-svg (~shared:misc/tile-svg))
:grid-cls grid-cls :grid-cls grid-cls
:cards (<> :cards (<>
(map (lambda (card) (map (lambda (card)
(if (= view "tile") (if (= view "tile")
(~blog-card-tile (~cards/tile
:href (get card "href") :hx-select hx-select :href (get card "href") :hx-select hx-select
:feature-image (get card "feature_image") :feature-image (get card "feature_image")
:title (get card "title") :is-draft (get card "is_draft") :title (get card "title") :is-draft (get card "is_draft")
@@ -108,7 +108,7 @@
:status-timestamp (get card "status_timestamp") :status-timestamp (get card "status_timestamp")
:excerpt (get card "excerpt") :excerpt (get card "excerpt")
:tags (get card "tags") :authors (get card "authors")) :tags (get card "tags") :authors (get card "authors"))
(~blog-card (~cards/index
:slug (get card "slug") :href (get card "href") :hx-select hx-select :slug (get card "slug") :href (get card "href") :hx-select hx-select
:title (get card "title") :feature-image (get card "feature_image") :title (get card "title") :feature-image (get card "feature_image")
:excerpt (get card "excerpt") :is-draft (get card "is_draft") :excerpt (get card "excerpt") :is-draft (get card "is_draft")
@@ -119,52 +119,52 @@
:tags (get card "tags") :authors (get card "authors") :tags (get card "tags") :authors (get card "authors")
:widget (get card "widget")))) :widget (get card "widget"))))
(or cards (list))) (or cards (list)))
(~blog-index-sentinel (~index/sentinel
:page page :total-pages total-pages :page page :total-pages total-pages
:current-local-href current-local-href))))))) :current-local-href current-local-href)))))))
;; Sentinel for blog index infinite scroll ;; Sentinel for blog index infinite scroll
(defcomp ~blog-index-sentinel (&key page total-pages current-local-href) (defcomp ~index/sentinel (&key page total-pages current-local-href)
(when (< page total-pages) (when (< page total-pages)
(let* ((next-url (str current-local-href "?page=" (+ page 1)))) (let* ((next-url (str current-local-href "?page=" (+ page 1))))
(~sentinel-desktop (~shared:misc/sentinel-desktop
:id (str "sentinel-" page "-d") :id (str "sentinel-" page "-d")
:next-url next-url :next-url next-url
:hyperscript "init if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end on htmx:beforeRequest(event) add .hidden to .js-neterr in me remove .hidden from .js-loading in me remove .opacity-100 from me add .opacity-0 to me set trig to null if event.detail and event.detail.triggeringEvent then set trig to event.detail.triggeringEvent end if trig and trig.type is 'intersect' set scroller to the closest .js-grid-viewport if scroller is null then halt end if scroller.scrollTop < 20 then halt end end def backoff() set ms to me.dataset.retryMs if ms > 30000 then set ms to 30000 end add .hidden to .js-loading in me remove .hidden from .js-neterr in me remove .opacity-0 from me add .opacity-100 to me wait ms ms trigger sentinel:retry set ms to ms * 2 if ms > 30000 then set ms to 30000 end set me.dataset.retryMs to ms end on htmx:sendError call backoff() on htmx:responseError call backoff() on htmx:timeout call backoff()")))) :hyperscript "init if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end on htmx:beforeRequest(event) add .hidden to .js-neterr in me remove .hidden from .js-loading in me remove .opacity-100 from me add .opacity-0 to me set trig to null if event.detail and event.detail.triggeringEvent then set trig to event.detail.triggeringEvent end if trig and trig.type is 'intersect' set scroller to the closest .js-grid-viewport if scroller is null then halt end if scroller.scrollTop < 20 then halt end end def backoff() set ms to me.dataset.retryMs if ms > 30000 then set ms to 30000 end add .hidden to .js-loading in me remove .hidden from .js-neterr in me remove .opacity-0 from me add .opacity-100 to me wait ms ms trigger sentinel:retry set ms to ms * 2 if ms > 30000 then set ms to 30000 end set me.dataset.retryMs to ms end on htmx:sendError call backoff() on htmx:responseError call backoff() on htmx:timeout call backoff()"))))
;; Blog index action buttons — replaces _action_buttons_sx ;; Blog index action buttons — replaces _action_buttons_sx
(defcomp ~blog-index-actions (&key is-admin has-user hx-select draft-count drafts (defcomp ~index/actions (&key is-admin has-user hx-select draft-count drafts
new-post-href new-page-href current-local-href) new-post-href new-page-href current-local-href)
(~blog-action-buttons-wrapper (~filters/action-buttons-wrapper
:inner (<> :inner (<>
(when is-admin (when is-admin
(<> (<>
(~blog-action-button (~filters/action-button
:href new-post-href :hx-select hx-select :href new-post-href :hx-select hx-select
:btn-class "px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors" :btn-class "px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors"
:title "New Post" :icon-class "fa fa-plus mr-1" :label " New Post") :title "New Post" :icon-class "fa fa-plus mr-1" :label " New Post")
(~blog-action-button (~filters/action-button
:href new-page-href :hx-select hx-select :href new-page-href :hx-select hx-select
:btn-class "px-3 py-1 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 transition-colors" :btn-class "px-3 py-1 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 transition-colors"
:title "New Page" :icon-class "fa fa-plus mr-1" :label " New Page"))) :title "New Page" :icon-class "fa fa-plus mr-1" :label " New Page")))
(when (and has-user (or draft-count drafts)) (when (and has-user (or draft-count drafts))
(if drafts (if drafts
(~blog-drafts-button (~filters/drafts-button
:href current-local-href :hx-select hx-select :href current-local-href :hx-select hx-select
:btn-class "px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors" :btn-class "px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors"
:title "Hide Drafts" :label " Drafts " :draft-count (str draft-count)) :title "Hide Drafts" :label " Drafts " :draft-count (str draft-count))
(let* ((on-href (str current-local-href (let* ((on-href (str current-local-href
(if (contains? current-local-href "?") "&" "?") "drafts=1"))) (if (contains? current-local-href "?") "&" "?") "drafts=1")))
(~blog-drafts-button-amber (~filters/drafts-button-amber
:href on-href :hx-select hx-select :href on-href :hx-select hx-select
:btn-class "px-3 py-1 rounded bg-amber-600 text-white text-sm hover:bg-amber-700 transition-colors" :btn-class "px-3 py-1 rounded bg-amber-600 text-white text-sm hover:bg-amber-700 transition-colors"
:title "Show Drafts" :label " Drafts " :draft-count (str draft-count)))))))) :title "Show Drafts" :label " Drafts " :draft-count (str draft-count))))))))
;; Tag groups filter — replaces _tag_groups_filter_sx ;; Tag groups filter — replaces _tag_groups_filter_sx
(defcomp ~blog-index-tag-groups-filter (&key tag-groups is-any-group hx-select) (defcomp ~index/tag-groups-filter (&key tag-groups is-any-group hx-select)
(~blog-filter-nav (~filters/nav
:items (<> :items (<>
(~blog-filter-any-topic (~filters/any-topic
:cls (if is-any-group :cls (if is-any-group
"bg-stone-900 text-white border-stone-900" "bg-stone-900 text-white border-stone-900"
"bg-white text-stone-600 border-stone-300 hover:bg-stone-50") "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")
@@ -178,23 +178,23 @@
(colour (get grp "colour")) (colour (get grp "colour"))
(name (get grp "name")) (name (get grp "name"))
(icon (if fi (icon (if fi
(~blog-filter-group-icon-image :src fi :name name) (~filters/group-icon-image :src fi :name name)
(~blog-filter-group-icon-color (~filters/group-icon-color
:style (if colour :style (if colour
(str "background-color: " colour "; color: white;") (str "background-color: " colour "; color: white;")
"background-color: #e7e5e4; color: #57534e;") "background-color: #e7e5e4; color: #57534e;")
:initial (slice (or name "?") 0 1))))) :initial (slice (or name "?") 0 1)))))
(~blog-filter-group-li (~filters/group-li
:cls cls :hx-get (str "?group=" (get grp "slug") "&page=1") :cls cls :hx-get (str "?group=" (get grp "slug") "&page=1")
:hx-select hx-select :icon icon :hx-select hx-select :icon icon
:name name :count (str (get grp "post_count"))))) :name name :count (str (get grp "post_count")))))
(or tag-groups (list)))))) (or tag-groups (list))))))
;; Authors filter — replaces _authors_filter_sx ;; Authors filter — replaces _authors_filter_sx
(defcomp ~blog-index-authors-filter (&key authors is-any-author hx-select) (defcomp ~index/authors-filter (&key authors is-any-author hx-select)
(~blog-filter-nav (~filters/nav
:items (<> :items (<>
(~blog-filter-any-author (~filters/any-author
:cls (if is-any-author :cls (if is-any-author
"bg-stone-900 text-white border-stone-900" "bg-stone-900 text-white border-stone-900"
"bg-white text-stone-600 border-stone-300 hover:bg-stone-50") "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")
@@ -205,49 +205,49 @@
"bg-stone-900 text-white border-stone-900" "bg-stone-900 text-white border-stone-900"
"bg-white text-stone-600 border-stone-300 hover:bg-stone-50")) "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))
(img (get a "profile_image"))) (img (get a "profile_image")))
(~blog-filter-author-li (~filters/author-li
:cls cls :hx-get (str "?author=" (get a "slug") "&page=1") :cls cls :hx-get (str "?author=" (get a "slug") "&page=1")
:hx-select hx-select :hx-select hx-select
:icon (when img (~blog-filter-author-icon :src img :name (get a "name"))) :icon (when img (~filters/author-icon :src img :name (get a "name")))
:name (get a "name") :name (get a "name")
:count (str (get a "published_post_count"))))) :count (str (get a "published_post_count")))))
(or authors (list)))))) (or authors (list))))))
;; Blog index aside — replaces _blog_aside_sx ;; Blog index aside — replaces _blog_aside_sx
(defcomp ~blog-index-aside-content (&key is-admin has-user hx-select draft-count drafts (defcomp ~index/aside-content (&key is-admin has-user hx-select draft-count drafts
new-post-href new-page-href current-local-href new-post-href new-page-href current-local-href
tag-groups authors is-any-group is-any-author) tag-groups authors is-any-group is-any-author)
(~blog-aside (~index/aside
:search (~search-desktop) :search (~shared:controls/search-desktop)
:action-buttons (~blog-index-actions :action-buttons (~index/actions
:is-admin is-admin :has-user has-user :hx-select hx-select :is-admin is-admin :has-user has-user :hx-select hx-select
:draft-count draft-count :drafts drafts :draft-count draft-count :drafts drafts
:new-post-href new-post-href :new-page-href new-page-href :new-post-href new-post-href :new-page-href new-page-href
:current-local-href current-local-href) :current-local-href current-local-href)
:tag-groups-filter (~blog-index-tag-groups-filter :tag-groups-filter (~index/tag-groups-filter
:tag-groups tag-groups :is-any-group is-any-group :hx-select hx-select) :tag-groups tag-groups :is-any-group is-any-group :hx-select hx-select)
:authors-filter (~blog-index-authors-filter :authors-filter (~index/authors-filter
:authors authors :is-any-author is-any-author :hx-select hx-select))) :authors authors :is-any-author is-any-author :hx-select hx-select)))
;; Blog index mobile filter — replaces _blog_filter_sx ;; Blog index mobile filter — replaces _blog_filter_sx
(defcomp ~blog-index-filter-content (&key is-admin has-user hx-select draft-count drafts (defcomp ~index/filter-content (&key is-admin has-user hx-select draft-count drafts
new-post-href new-page-href current-local-href new-post-href new-page-href current-local-href
tag-groups authors is-any-group is-any-author tag-groups authors is-any-group is-any-author
tg-summary au-summary) tg-summary au-summary)
(~mobile-filter (~shared:controls/mobile-filter
:filter-summary (<> :filter-summary (<>
(~search-mobile) (~shared:controls/search-mobile)
(when (not (= tg-summary "")) (when (not (= tg-summary ""))
(~blog-filter-summary :text tg-summary)) (~filters/summary :text tg-summary))
(when (not (= au-summary "")) (when (not (= au-summary ""))
(~blog-filter-summary :text au-summary))) (~filters/summary :text au-summary)))
:action-buttons (~blog-index-actions :action-buttons (~index/actions
:is-admin is-admin :has-user has-user :hx-select hx-select :is-admin is-admin :has-user has-user :hx-select hx-select
:draft-count draft-count :drafts drafts :draft-count draft-count :drafts drafts
:new-post-href new-post-href :new-page-href new-page-href :new-post-href new-post-href :new-page-href new-page-href
:current-local-href current-local-href) :current-local-href current-local-href)
:filter-details (<> :filter-details (<>
(~blog-index-tag-groups-filter (~index/tag-groups-filter
:tag-groups tag-groups :is-any-group is-any-group :hx-select hx-select) :tag-groups tag-groups :is-any-group is-any-group :hx-select hx-select)
(~blog-index-authors-filter (~index/authors-filter
:authors authors :is-any-author is-any-author :hx-select hx-select)))) :authors authors :is-any-author is-any-author :hx-select hx-select))))

View File

@@ -7,7 +7,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Image card ;; Image card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-image (&key (src :as string) (alt :as string?) (caption :as string?) (width :as string?) (href :as string?)) (defcomp ~kg_cards/kg-image (&key (src :as string) (alt :as string?) (caption :as string?) (width :as string?) (href :as string?))
(figure :class (str "kg-card kg-image-card" (figure :class (str "kg-card kg-image-card"
(if (= width "wide") " kg-width-wide" (if (= width "wide") " kg-width-wide"
(if (= width "full") " kg-width-full" ""))) (if (= width "full") " kg-width-full" "")))
@@ -19,7 +19,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Gallery card ;; Gallery card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-gallery (&key (images :as list) (caption :as string?)) (defcomp ~kg_cards/kg-gallery (&key (images :as list) (caption :as string?))
(figure :class "kg-card kg-gallery-card kg-width-wide" (figure :class "kg-card kg-gallery-card kg-width-wide"
(div :class "kg-gallery-container" (div :class "kg-gallery-container"
(map (lambda (row) (map (lambda (row)
@@ -36,19 +36,19 @@
;; HTML card — wraps user-pasted HTML so the editor can identify the block. ;; HTML card — wraps user-pasted HTML so the editor can identify the block.
;; Content is native sx children (no longer an opaque HTML string). ;; Content is native sx children (no longer an opaque HTML string).
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-html (&rest children) (defcomp ~kg_cards/kg-html (&rest children)
(div :class "kg-card kg-html-card" children)) (div :class "kg-card kg-html-card" children))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Markdown card — rendered markdown content, editor can identify the block. ;; Markdown card — rendered markdown content, editor can identify the block.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-md (&rest children) (defcomp ~kg_cards/kg-md (&rest children)
(div :class "kg-card kg-md-card" children)) (div :class "kg-card kg-md-card" children))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Embed card ;; Embed card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-embed (&key (html :as string) (caption :as string?)) (defcomp ~kg_cards/kg-embed (&key (html :as string) (caption :as string?))
(figure :class "kg-card kg-embed-card" (figure :class "kg-card kg-embed-card"
(~rich-text :html html) (~rich-text :html html)
(when caption (figcaption caption)))) (when caption (figcaption caption))))
@@ -56,7 +56,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Bookmark card ;; Bookmark card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-bookmark (&key (url :as string) (title :as string?) (description :as string?) (icon :as string?) (author :as string?) (publisher :as string?) (thumbnail :as string?) (caption :as string?)) (defcomp ~kg_cards/kg-bookmark (&key (url :as string) (title :as string?) (description :as string?) (icon :as string?) (author :as string?) (publisher :as string?) (thumbnail :as string?) (caption :as string?))
(figure :class "kg-card kg-bookmark-card" (figure :class "kg-card kg-bookmark-card"
(a :class "kg-bookmark-container" :href url (a :class "kg-bookmark-container" :href url
(div :class "kg-bookmark-content" (div :class "kg-bookmark-content"
@@ -75,7 +75,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Callout card ;; Callout card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-callout (&key (color :as string?) (emoji :as string?) (content :as string?)) (defcomp ~kg_cards/kg-callout (&key (color :as string?) (emoji :as string?) (content :as string?))
(div :class (str "kg-card kg-callout-card kg-callout-card-" (or color "grey")) (div :class (str "kg-card kg-callout-card kg-callout-card-" (or color "grey"))
(when emoji (div :class "kg-callout-emoji" emoji)) (when emoji (div :class "kg-callout-emoji" emoji))
(div :class "kg-callout-text" (or content "")))) (div :class "kg-callout-text" (or content ""))))
@@ -83,14 +83,14 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Button card ;; Button card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-button (&key (url :as string) (text :as string?) (alignment :as string?)) (defcomp ~kg_cards/kg-button (&key (url :as string) (text :as string?) (alignment :as string?))
(div :class (str "kg-card kg-button-card kg-align-" (or alignment "center")) (div :class (str "kg-card kg-button-card kg-align-" (or alignment "center"))
(a :href url :class "kg-btn kg-btn-accent" (or text "")))) (a :href url :class "kg-btn kg-btn-accent" (or text ""))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Toggle card (accordion) ;; Toggle card (accordion)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-toggle (&key (heading :as string?) (content :as string?)) (defcomp ~kg_cards/kg-toggle (&key (heading :as string?) (content :as string?))
(div :class "kg-card kg-toggle-card" :data-kg-toggle-state "close" (div :class "kg-card kg-toggle-card" :data-kg-toggle-state "close"
(div :class "kg-toggle-heading" (div :class "kg-toggle-heading"
(h4 :class "kg-toggle-heading-text" (or heading "")) (h4 :class "kg-toggle-heading-text" (or heading ""))
@@ -101,7 +101,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Audio card ;; Audio card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-audio (&key (src :as string) (title :as string?) (duration :as string?) (thumbnail :as string?)) (defcomp ~kg_cards/kg-audio (&key (src :as string) (title :as string?) (duration :as string?) (thumbnail :as string?))
(div :class "kg-card kg-audio-card" (div :class "kg-card kg-audio-card"
(if thumbnail (if thumbnail
(img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail") (img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail")
@@ -124,7 +124,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Video card ;; Video card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-video (&key (src :as string) (caption :as string?) (width :as string?) (thumbnail :as string?) (loop :as boolean?)) (defcomp ~kg_cards/kg-video (&key (src :as string) (caption :as string?) (width :as string?) (thumbnail :as string?) (loop :as boolean?))
(figure :class (str "kg-card kg-video-card" (figure :class (str "kg-card kg-video-card"
(if (= width "wide") " kg-width-wide" (if (= width "wide") " kg-width-wide"
(if (= width "full") " kg-width-full" ""))) (if (= width "full") " kg-width-full" "")))
@@ -136,7 +136,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; File card ;; File card
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-file (&key (src :as string) (filename :as string?) (title :as string?) (filesize :as string?) (caption :as string?)) (defcomp ~kg_cards/kg-file (&key (src :as string) (filename :as string?) (title :as string?) (filesize :as string?) (caption :as string?))
(div :class "kg-card kg-file-card" (div :class "kg-card kg-file-card"
(a :class "kg-file-card-container" :href src :download (or filename "") (a :class "kg-file-card-container" :href src :download (or filename "")
(div :class "kg-file-card-contents" (div :class "kg-file-card-contents"
@@ -149,5 +149,5 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Paywall marker ;; Paywall marker
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~kg-paywall () (defcomp ~kg_cards/kg-paywall ()
(~rich-text :html "<!--members-only-->")) (~rich-text :html "<!--members-only-->"))

View File

@@ -3,8 +3,8 @@
;; --- Blog header (invisible row for blog-header-child swap target) --- ;; --- Blog header (invisible row for blog-header-child swap target) ---
(defcomp ~blog-header (&key oob) (defcomp ~layouts/header (&key oob)
(~menu-row-sx :id "blog-row" :level 1 (~shared:layout/menu-row-sx :id "blog-row" :level 1
:link-label-content (div) :link-label-content (div)
:child-id "blog-header-child" :oob oob)) :child-id "blog-header-child" :oob oob))
@@ -12,10 +12,10 @@
(defmacro ~blog-settings-header-auto (oob) (defmacro ~blog-settings-header-auto (oob)
(quasiquote (quasiquote
(~menu-row-sx :id "root-settings-row" :level 1 (~shared:layout/menu-row-sx :id "root-settings-row" :level 1
:link-href (url-for "settings.defpage_settings_home") :link-href (url-for "settings.defpage_settings_home")
:link-label-content (~blog-admin-label) :link-label-content (~header/admin-label)
:nav (~blog-settings-nav) :nav (~layouts/settings-nav)
:child-id "root-settings-header-child" :child-id "root-settings-header-child"
:oob (unquote oob)))) :oob (unquote oob))))
@@ -23,9 +23,9 @@
(defmacro ~blog-sub-settings-header-auto (row-id child-id endpoint icon label oob) (defmacro ~blog-sub-settings-header-auto (row-id child-id endpoint icon label oob)
(quasiquote (quasiquote
(~menu-row-sx :id (unquote row-id) :level 2 (~shared:layout/menu-row-sx :id (unquote row-id) :level 2
:link-href (url-for (unquote endpoint)) :link-href (url-for (unquote endpoint))
:link-label-content (~blog-sub-settings-label :link-label-content (~header/sub-settings-label
:icon (str "fa fa-" (unquote icon)) :icon (str "fa fa-" (unquote icon))
:label (unquote label)) :label (unquote label))
:child-id (unquote child-id) :child-id (unquote child-id)
@@ -35,47 +35,47 @@
;; Blog layout (root + blog header) ;; Blog layout (root + blog header)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-layout-full () (defcomp ~layouts/full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-header))) (~layouts/header)))
(defcomp ~blog-layout-oob () (defcomp ~layouts/oob ()
(<> (~blog-header :oob true) (<> (~layouts/header :oob true)
(~clear-oob-div :id "blog-header-child") (~shared:layout/clear-oob-div :id "blog-header-child")
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Settings layout (root + settings header) ;; Settings layout (root + settings header)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-settings-layout-full () (defcomp ~layouts/settings-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-settings-header-auto))) (~blog-settings-header-auto)))
(defcomp ~blog-settings-layout-oob () (defcomp ~layouts/settings-layout-oob ()
(<> (~blog-settings-header-auto true) (<> (~blog-settings-header-auto true)
(~clear-oob-div :id "root-settings-header-child") (~shared:layout/clear-oob-div :id "root-settings-header-child")
(~root-header-auto true))) (~root-header-auto true)))
(defcomp ~blog-settings-layout-mobile () (defcomp ~layouts/settings-layout-mobile ()
(~blog-settings-nav)) (~layouts/settings-nav))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Cache layout (root + settings + cache sub-header) ;; Cache layout (root + settings + cache sub-header)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-cache-layout-full () (defcomp ~layouts/cache-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-settings-header-auto) (~blog-settings-header-auto)
(~blog-sub-settings-header-auto (~blog-sub-settings-header-auto
"cache-row" "cache-header-child" "cache-row" "cache-header-child"
"settings.defpage_cache_page" "refresh" "Cache"))) "settings.defpage_cache_page" "refresh" "Cache")))
(defcomp ~blog-cache-layout-oob () (defcomp ~layouts/cache-layout-oob ()
(<> (~blog-sub-settings-header-auto (<> (~blog-sub-settings-header-auto
"cache-row" "cache-header-child" "cache-row" "cache-header-child"
"settings.defpage_cache_page" "refresh" "Cache" true) "settings.defpage_cache_page" "refresh" "Cache" true)
(~clear-oob-div :id "cache-header-child") (~shared:layout/clear-oob-div :id "cache-header-child")
(~blog-settings-header-auto true) (~blog-settings-header-auto true)
(~root-header-auto true))) (~root-header-auto true)))
@@ -83,18 +83,18 @@
;; Snippets layout (root + settings + snippets sub-header) ;; Snippets layout (root + settings + snippets sub-header)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-snippets-layout-full () (defcomp ~layouts/snippets-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-settings-header-auto) (~blog-settings-header-auto)
(~blog-sub-settings-header-auto (~blog-sub-settings-header-auto
"snippets-row" "snippets-header-child" "snippets-row" "snippets-header-child"
"snippets.defpage_snippets_page" "puzzle-piece" "Snippets"))) "snippets.defpage_snippets_page" "puzzle-piece" "Snippets")))
(defcomp ~blog-snippets-layout-oob () (defcomp ~layouts/snippets-layout-oob ()
(<> (~blog-sub-settings-header-auto (<> (~blog-sub-settings-header-auto
"snippets-row" "snippets-header-child" "snippets-row" "snippets-header-child"
"snippets.defpage_snippets_page" "puzzle-piece" "Snippets" true) "snippets.defpage_snippets_page" "puzzle-piece" "Snippets" true)
(~clear-oob-div :id "snippets-header-child") (~shared:layout/clear-oob-div :id "snippets-header-child")
(~blog-settings-header-auto true) (~blog-settings-header-auto true)
(~root-header-auto true))) (~root-header-auto true)))
@@ -102,18 +102,18 @@
;; Menu Items layout (root + settings + menu-items sub-header) ;; Menu Items layout (root + settings + menu-items sub-header)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-menu-items-layout-full () (defcomp ~layouts/menu-items-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-settings-header-auto) (~blog-settings-header-auto)
(~blog-sub-settings-header-auto (~blog-sub-settings-header-auto
"menu_items-row" "menu_items-header-child" "menu_items-row" "menu_items-header-child"
"menu_items.defpage_menu_items_page" "bars" "Menu Items"))) "menu_items.defpage_menu_items_page" "bars" "Menu Items")))
(defcomp ~blog-menu-items-layout-oob () (defcomp ~layouts/menu-items-layout-oob ()
(<> (~blog-sub-settings-header-auto (<> (~blog-sub-settings-header-auto
"menu_items-row" "menu_items-header-child" "menu_items-row" "menu_items-header-child"
"menu_items.defpage_menu_items_page" "bars" "Menu Items" true) "menu_items.defpage_menu_items_page" "bars" "Menu Items" true)
(~clear-oob-div :id "menu_items-header-child") (~shared:layout/clear-oob-div :id "menu_items-header-child")
(~blog-settings-header-auto true) (~blog-settings-header-auto true)
(~root-header-auto true))) (~root-header-auto true)))
@@ -121,18 +121,18 @@
;; Tag Groups layout (root + settings + tag-groups sub-header) ;; Tag Groups layout (root + settings + tag-groups sub-header)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-tag-groups-layout-full () (defcomp ~layouts/tag-groups-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-settings-header-auto) (~blog-settings-header-auto)
(~blog-sub-settings-header-auto (~blog-sub-settings-header-auto
"tag-groups-row" "tag-groups-header-child" "tag-groups-row" "tag-groups-header-child"
"blog.tag_groups_admin.defpage_tag_groups_page" "tags" "Tag Groups"))) "blog.tag_groups_admin.defpage_tag_groups_page" "tags" "Tag Groups")))
(defcomp ~blog-tag-groups-layout-oob () (defcomp ~layouts/tag-groups-layout-oob ()
(<> (~blog-sub-settings-header-auto (<> (~blog-sub-settings-header-auto
"tag-groups-row" "tag-groups-header-child" "tag-groups-row" "tag-groups-header-child"
"blog.tag_groups_admin.defpage_tag_groups_page" "tags" "Tag Groups" true) "blog.tag_groups_admin.defpage_tag_groups_page" "tags" "Tag Groups" true)
(~clear-oob-div :id "tag-groups-header-child") (~shared:layout/clear-oob-div :id "tag-groups-header-child")
(~blog-settings-header-auto true) (~blog-settings-header-auto true)
(~root-header-auto true))) (~root-header-auto true)))
@@ -140,31 +140,31 @@
;; Tag Group Edit layout (root + settings + tag-groups sub-header with id) ;; Tag Group Edit layout (root + settings + tag-groups sub-header with id)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-tag-group-edit-layout-full () (defcomp ~layouts/tag-group-edit-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~blog-settings-header-auto) (~blog-settings-header-auto)
(~menu-row-sx :id "tag-groups-row" :level 2 (~shared:layout/menu-row-sx :id "tag-groups-row" :level 2
:link-href (url-for "blog.tag_groups_admin.defpage_tag_group_edit" :link-href (url-for "blog.tag_groups_admin.defpage_tag_group_edit"
:id (request-view-args "id")) :id (request-view-args "id"))
:link-label-content (~blog-sub-settings-label :link-label-content (~header/sub-settings-label
:icon "fa fa-tags" :label "Tag Groups") :icon "fa fa-tags" :label "Tag Groups")
:child-id "tag-groups-header-child"))) :child-id "tag-groups-header-child")))
(defcomp ~blog-tag-group-edit-layout-oob () (defcomp ~layouts/tag-group-edit-layout-oob ()
(<> (~menu-row-sx :id "tag-groups-row" :level 2 (<> (~shared:layout/menu-row-sx :id "tag-groups-row" :level 2
:link-href (url-for "blog.tag_groups_admin.defpage_tag_group_edit" :link-href (url-for "blog.tag_groups_admin.defpage_tag_group_edit"
:id (request-view-args "id")) :id (request-view-args "id"))
:link-label-content (~blog-sub-settings-label :link-label-content (~header/sub-settings-label
:icon "fa fa-tags" :label "Tag Groups") :icon "fa fa-tags" :label "Tag Groups")
:child-id "tag-groups-header-child" :child-id "tag-groups-header-child"
:oob true) :oob true)
(~clear-oob-div :id "tag-groups-header-child") (~shared:layout/clear-oob-div :id "tag-groups-header-child")
(~blog-settings-header-auto true) (~blog-settings-header-auto true)
(~root-header-auto true))) (~root-header-auto true)))
;; --- Settings nav links — uses IO primitives --- ;; --- Settings nav links — uses IO primitives ---
(defcomp ~blog-settings-nav () (defcomp ~layouts/settings-nav ()
(let* ((sc (select-colours)) (let* ((sc (select-colours))
(links (list (links (list
(dict :endpoint "menu_items.defpage_menu_items_page" :icon "fa fa-bars" :label "Menu Items") (dict :endpoint "menu_items.defpage_menu_items_page" :icon "fa fa-bars" :label "Menu Items")
@@ -172,7 +172,7 @@
(dict :endpoint "blog.tag_groups_admin.defpage_tag_groups_page" :icon "fa fa-tags" :label "Tag Groups") (dict :endpoint "blog.tag_groups_admin.defpage_tag_groups_page" :icon "fa fa-tags" :label "Tag Groups")
(dict :endpoint "settings.defpage_cache_page" :icon "fa fa-refresh" :label "Cache")))) (dict :endpoint "settings.defpage_cache_page" :icon "fa fa-refresh" :label "Cache"))))
(<> (map (lambda (lnk) (<> (map (lambda (lnk)
(~nav-link (~shared:layout/nav-link
:href (url-for (get lnk "endpoint")) :href (url-for (get lnk "endpoint"))
:icon (get lnk "icon") :icon (get lnk "icon")
:label (get lnk "label") :label (get lnk "label")
@@ -181,5 +181,5 @@
;; --- Editor panel wrapper --- ;; --- Editor panel wrapper ---
(defcomp ~blog-editor-panel (&key parts) (defcomp ~layouts/editor-panel (&key parts)
(<> parts)) (<> parts))

View File

@@ -1,6 +1,6 @@
;; Menu item form and page search components ;; Menu item form and page search components
(defcomp ~page-search-item (&key id title slug feature-image) (defcomp ~menu_items/page-search-item (&key id title slug feature-image)
(div :class "flex items-center gap-3 p-3 hover:bg-stone-50 cursor-pointer border-b last:border-b-0" (div :class "flex items-center gap-3 p-3 hover:bg-stone-50 cursor-pointer border-b last:border-b-0"
:data-page-id id :data-page-title title :data-page-slug slug :data-page-id id :data-page-title title :data-page-slug slug
:data-page-image (or feature-image "") :data-page-image (or feature-image "")
@@ -11,50 +11,50 @@
(div :class "font-medium truncate" title) (div :class "font-medium truncate" title)
(div :class "text-xs text-stone-500 truncate" slug)))) (div :class "text-xs text-stone-500 truncate" slug))))
(defcomp ~page-search-results (&key items sentinel) (defcomp ~menu_items/page-search-results (&key items sentinel)
(div :class "border border-stone-200 rounded-md max-h-64 overflow-y-auto" (div :class "border border-stone-200 rounded-md max-h-64 overflow-y-auto"
items sentinel)) items sentinel))
(defcomp ~page-search-sentinel (&key url query next-page) (defcomp ~menu_items/page-search-sentinel (&key url query next-page)
(div :sx-get url :sx-trigger "intersect once" :sx-swap "outerHTML" (div :sx-get url :sx-trigger "intersect once" :sx-swap "outerHTML"
:sx-vals (str "{\"q\": \"" query "\", \"page\": " next-page "}") :sx-vals (str "{\"q\": \"" query "\", \"page\": " next-page "}")
:class "p-3 text-center text-sm text-stone-400" :class "p-3 text-center text-sm text-stone-400"
(i :class "fa fa-spinner fa-spin") " Loading more...")) (i :class "fa fa-spinner fa-spin") " Loading more..."))
(defcomp ~page-search-empty (&key query) (defcomp ~menu_items/page-search-empty (&key query)
(div :class "p-3 text-center text-stone-400 border border-stone-200 rounded-md" (div :class "p-3 text-center text-stone-400 border border-stone-200 rounded-md"
(str "No pages found matching \"" query "\""))) (str "No pages found matching \"" query "\"")))
;; Data-driven page search results (replaces Python render_page_search_results loop) ;; 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) (defcomp ~menu_items/page-search-results-from-data (&key pages query has-more search-url next-page)
(if (and (not pages) query) (if (and (not pages) query)
(~page-search-empty :query query) (~menu_items/page-search-empty :query query)
(when pages (when pages
(~page-search-results (~menu_items/page-search-results
:items (<> (map (lambda (p) :items (<> (map (lambda (p)
(~page-search-item (~menu_items/page-search-item
:id (get p "id") :title (get p "title") :id (get p "id") :title (get p "title")
:slug (get p "slug") :feature-image (get p "feature_image"))) :slug (get p "slug") :feature-image (get p "feature_image")))
pages)) pages))
:sentinel (when has-more :sentinel (when has-more
(~page-search-sentinel :url search-url :query query :next-page next-page)))))) (~menu_items/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) ;; 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) (defcomp ~menu_items/menu-nav-from-data (&key items nav-cls container-id arrow-cls scroll-hs)
(if (not items) (if (not items)
(~blog-nav-empty :wrapper-id "menu-items-nav-wrapper") (~shared:nav/blog-nav-empty :wrapper-id "menu-items-nav-wrapper")
(~scroll-nav-wrapper :wrapper-id "menu-items-nav-wrapper" :container-id container-id (~shared:misc/scroll-nav-wrapper :wrapper-id "menu-items-nav-wrapper" :container-id container-id
:arrow-cls arrow-cls :arrow-cls arrow-cls
:left-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft - 200") :left-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft - 200")
:scroll-hs scroll-hs :scroll-hs scroll-hs
:right-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft + 200") :right-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft + 200")
:items (<> (map (lambda (item) :items (<> (map (lambda (item)
(let* ((img (~img-or-placeholder :src (get item "feature_image") :alt (get item "label") (let* ((img (~shared:misc/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"))) :size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0")))
(if (= (get item "slug") "cart") (if (= (get item "slug") "cart")
(~blog-nav-item-plain :href (get item "href") :selected (get item "selected") (~shared:nav/blog-nav-item-plain :href (get item "href") :selected (get item "selected")
:nav-cls nav-cls :img img :label (get item "label")) :nav-cls nav-cls :img img :label (get item "label"))
(~blog-nav-item-link :href (get item "href") :hx-get (get item "hx_get") (~shared:nav/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"))))) :selected (get item "selected") :nav-cls nav-cls :img img :label (get item "label")))))
items)) items))
:oob true))) :oob true)))

View File

@@ -1,6 +1,6 @@
;; Blog settings panel components (features, markets, associated entries) ;; Blog settings panel components (features, markets, associated entries)
(defcomp ~blog-features-form (&key (features-url :as string) (calendar-checked :as boolean) (market-checked :as boolean) (hs-trigger :as string)) (defcomp ~settings/features-form (&key (features-url :as string) (calendar-checked :as boolean) (market-checked :as boolean) (hs-trigger :as string))
(form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML" (form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML"
:sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3" :sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3"
(label :class "flex items-center gap-3 cursor-pointer" (label :class "flex items-center gap-3 cursor-pointer"
@@ -18,33 +18,33 @@
(i :class "fa fa-shopping-bag text-green-600 mr-1") (i :class "fa fa-shopping-bag text-green-600 mr-1")
" Market \u2014 enable product catalog on this page")))) " Market \u2014 enable product catalog on this page"))))
(defcomp ~blog-sumup-form (&key sumup-url merchant-code placeholder sumup-configured checkout-prefix) (defcomp ~settings/sumup-form (&key sumup-url merchant-code placeholder sumup-configured checkout-prefix)
(div :class "mt-4 pt-4 border-t border-stone-100" (div :class "mt-4 pt-4 border-t border-stone-100"
(~sumup-settings-form :update-url sumup-url :merchant-code merchant-code (~shared:misc/sumup-settings-form :update-url sumup-url :merchant-code merchant-code
:placeholder placeholder :sumup-configured sumup-configured :placeholder placeholder :sumup-configured sumup-configured
:checkout-prefix checkout-prefix :panel-id "features-panel"))) :checkout-prefix checkout-prefix :panel-id "features-panel")))
(defcomp ~blog-features-panel (&key form sumup) (defcomp ~settings/features-panel (&key form sumup)
(div :id "features-panel" :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200" (div :id "features-panel" :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200"
(h3 :class "text-lg font-semibold text-stone-800" "Page Features") (h3 :class "text-lg font-semibold text-stone-800" "Page Features")
form sumup)) form sumup))
;; Markets panel ;; Markets panel
(defcomp ~blog-market-item (&key (name :as string) (slug :as string) (delete-url :as string) (confirm-text :as string)) (defcomp ~settings/market-item (&key (name :as string) (slug :as string) (delete-url :as string) (confirm-text :as string))
(li :class "flex items-center justify-between p-3 bg-stone-50 rounded" (li :class "flex items-center justify-between p-3 bg-stone-50 rounded"
(div (span :class "font-medium" name) (div (span :class "font-medium" name)
(span :class "text-stone-400 text-sm ml-2" (str "/" slug "/"))) (span :class "text-stone-400 text-sm ml-2" (str "/" slug "/")))
(button :sx-delete delete-url :sx-target "#markets-panel" :sx-swap "outerHTML" (button :sx-delete delete-url :sx-target "#markets-panel" :sx-swap "outerHTML"
:sx-confirm confirm-text :class "text-red-600 hover:text-red-800 text-sm" "Delete"))) :sx-confirm confirm-text :class "text-red-600 hover:text-red-800 text-sm" "Delete")))
(defcomp ~blog-markets-list (&key items) (defcomp ~settings/markets-list (&key items)
(ul :class "space-y-2 mb-4" items)) (ul :class "space-y-2 mb-4" items))
(defcomp ~blog-markets-empty () (defcomp ~settings/markets-empty ()
(p :class "text-stone-500 mb-4 text-sm" "No markets yet.")) (p :class "text-stone-500 mb-4 text-sm" "No markets yet."))
(defcomp ~blog-markets-panel (&key list create-url) (defcomp ~settings/markets-panel (&key list create-url)
(div :id "markets-panel" (div :id "markets-panel"
(h3 :class "text-lg font-semibold mb-3" "Markets") (h3 :class "text-lg font-semibold mb-3" "Markets")
list list
@@ -59,17 +59,17 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Features panel composition — replaces render_features_panel ;; Features panel composition — replaces render_features_panel
(defcomp ~blog-features-panel-content (&key features-url calendar-checked market-checked (defcomp ~settings/features-panel-content (&key features-url calendar-checked market-checked
show-sumup sumup-url merchant-code placeholder show-sumup sumup-url merchant-code placeholder
sumup-configured checkout-prefix) sumup-configured checkout-prefix)
(~blog-features-panel (~settings/features-panel
:form (~blog-features-form :form (~settings/features-form
:features-url features-url :features-url features-url
:calendar-checked calendar-checked :calendar-checked calendar-checked
:market-checked market-checked :market-checked market-checked
:hs-trigger "on change trigger submit on closest <form/>") :hs-trigger "on change trigger submit on closest <form/>")
:sumup (when show-sumup :sumup (when show-sumup
(~blog-sumup-form (~settings/sumup-form
:sumup-url sumup-url :sumup-url sumup-url
:merchant-code merchant-code :merchant-code merchant-code
:placeholder placeholder :placeholder placeholder
@@ -77,13 +77,13 @@
:checkout-prefix checkout-prefix)))) :checkout-prefix checkout-prefix))))
;; Markets panel composition — replaces render_markets_panel ;; Markets panel composition — replaces render_markets_panel
(defcomp ~blog-markets-panel-content (&key markets create-url) (defcomp ~settings/markets-panel-content (&key markets create-url)
(~blog-markets-panel (~settings/markets-panel
:list (if (empty? (or markets (list))) :list (if (empty? (or markets (list)))
(~blog-markets-empty) (~settings/markets-empty)
(~blog-markets-list (~settings/markets-list
:items (map (lambda (m) :items (map (lambda (m)
(~blog-market-item (~settings/market-item
:name (get m "name") :name (get m "name")
:slug (get m "slug") :slug (get m "slug")
:delete-url (get m "delete_url") :delete-url (get m "delete_url")
@@ -93,11 +93,11 @@
;; Associated entries ;; Associated entries
(defcomp ~blog-entry-image (&key (src :as string?) (title :as string)) (defcomp ~settings/entry-image (&key (src :as string?) (title :as string))
(if src (img :src src :alt title :class "w-8 h-8 rounded-full object-cover flex-shrink-0") (if src (img :src src :alt title :class "w-8 h-8 rounded-full object-cover flex-shrink-0")
(div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"))) (div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0")))
(defcomp ~blog-associated-entry (&key (confirm-text :as string) (toggle-url :as string) hx-headers img (name :as string) (date-str :as string)) (defcomp ~settings/associated-entry (&key (confirm-text :as string) (toggle-url :as string) hx-headers img (name :as string) (date-str :as string))
(button :type "button" (button :type "button"
:class "w-full text-left p-3 rounded border bg-green-50 border-green-300 transition hover:bg-green-100" :class "w-full text-left p-3 rounded border bg-green-50 border-green-300 transition hover:bg-green-100"
:data-confirm "" :data-confirm-title "Remove entry?" :data-confirm "" :data-confirm-title "Remove entry?"
@@ -115,14 +115,14 @@
(div :class "text-xs text-stone-600 mt-1" date-str)) (div :class "text-xs text-stone-600 mt-1" date-str))
(i :class "fa fa-times-circle text-green-600 text-lg flex-shrink-0")))) (i :class "fa fa-times-circle text-green-600 text-lg flex-shrink-0"))))
(defcomp ~blog-associated-entries-content (&key items) (defcomp ~settings/associated-entries-content (&key items)
(div :class "space-y-1" items)) (div :class "space-y-1" items))
(defcomp ~blog-associated-entries-empty () (defcomp ~settings/associated-entries-empty ()
(div :class "text-sm text-stone-400" (div :class "text-sm text-stone-400"
"No entries associated yet. Browse calendars below to add entries.")) "No entries associated yet. Browse calendars below to add entries."))
(defcomp ~blog-associated-entries-panel (&key content) (defcomp ~settings/associated-entries-panel (&key content)
(div :id "associated-entries-list" :class "border rounded-lg p-4 bg-white" (div :id "associated-entries-list" :class "border rounded-lg p-4 bg-white"
(h3 :class "text-lg font-semibold mb-4" "Associated Entries") (h3 :class "text-lg font-semibold mb-4" "Associated Entries")
content)) content))
@@ -131,17 +131,17 @@
;; Associated entries composition — replaces _render_associated_entries ;; Associated entries composition — replaces _render_associated_entries
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-associated-entries-from-data (&key entries csrf) (defcomp ~settings/associated-entries-from-data (&key entries csrf)
(~blog-associated-entries-panel (~settings/associated-entries-panel
:content (if (empty? (or entries (list))) :content (if (empty? (or entries (list)))
(~blog-associated-entries-empty) (~settings/associated-entries-empty)
(~blog-associated-entries-content (~settings/associated-entries-content
:items (map (lambda (e) :items (map (lambda (e)
(~blog-associated-entry (~settings/associated-entry
:confirm-text (get e "confirm_text") :confirm-text (get e "confirm_text")
:toggle-url (get e "toggle_url") :toggle-url (get e "toggle_url")
:hx-headers {:X-CSRFToken csrf} :hx-headers {:X-CSRFToken csrf}
:img (~blog-entry-image :src (get e "cal_image") :title (get e "cal_title")) :img (~settings/entry-image :src (get e "cal_image") :title (get e "cal_title"))
:name (get e "name") :name (get e "name")
:date-str (get e "date_str"))) :date-str (get e "date_str")))
(or entries (list))))))) (or entries (list)))))))
@@ -150,7 +150,7 @@
;; Entries browser composition — replaces _h_post_entries_content ;; Entries browser composition — replaces _h_post_entries_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-calendar-browser-item (&key (name :as string) (title :as string) (image :as string?) (view-url :as string)) (defcomp ~settings/calendar-browser-item (&key (name :as string) (title :as string) (image :as string?) (view-url :as string))
(details :class "border rounded-lg bg-white" :data-toggle-group "calendar-browser" (details :class "border rounded-lg bg-white" :data-toggle-group "calendar-browser"
(summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3" (summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3"
(if image (if image
@@ -163,7 +163,7 @@
(div :class "p-4 border-t" :sx-get view-url :sx-trigger "intersect once" :sx-swap "innerHTML" (div :class "p-4 border-t" :sx-get view-url :sx-trigger "intersect once" :sx-swap "innerHTML"
(div :class "text-sm text-stone-400" "Loading calendar...")))) (div :class "text-sm text-stone-400" "Loading calendar..."))))
(defcomp ~blog-entries-browser-content (&key entries-panel calendars) (defcomp ~settings/entries-browser-content (&key entries-panel calendars)
(div :id "post-entries-content" :class "space-y-6 p-4" (div :id "post-entries-content" :class "space-y-6 p-4"
entries-panel entries-panel
(div :class "space-y-3" (div :class "space-y-3"
@@ -171,7 +171,7 @@
(if (empty? (or calendars (list))) (if (empty? (or calendars (list)))
(div :class "text-sm text-stone-400" "No calendars found.") (div :class "text-sm text-stone-400" "No calendars found.")
(map (lambda (cal) (map (lambda (cal)
(~blog-calendar-browser-item (~settings/calendar-browser-item
:name (get cal "name") :name (get cal "name")
:title (get cal "title") :title (get cal "title")
:image (get cal "image") :image (get cal "image")
@@ -182,17 +182,17 @@
;; Post settings form composition — replaces _h_post_settings_content ;; Post settings form composition — replaces _h_post_settings_content
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~blog-settings-field-label (&key (text :as string) (field-for :as string)) (defcomp ~settings/field-label (&key (text :as string) (field-for :as string))
(label :for field-for (label :for field-for
:class "block text-[13px] font-medium text-stone-500 mb-[4px]" text)) :class "block text-[13px] font-medium text-stone-500 mb-[4px]" text))
(defcomp ~blog-settings-section (&key (title :as string) content (is-open :as boolean)) (defcomp ~settings/section (&key (title :as string) content (is-open :as boolean))
(details :class "border border-stone-200 rounded-[8px] overflow-hidden" :open is-open (details :class "border border-stone-200 rounded-[8px] overflow-hidden" :open is-open
(summary :class "px-[16px] py-[10px] bg-stone-50 text-[14px] font-medium text-stone-600 cursor-pointer select-none hover:bg-stone-100 transition-colors" (summary :class "px-[16px] py-[10px] bg-stone-50 text-[14px] font-medium text-stone-600 cursor-pointer select-none hover:bg-stone-100 transition-colors"
title) title)
(div :class "px-[16px] py-[12px] space-y-[12px]" content))) (div :class "px-[16px] py-[12px] space-y-[12px]" content)))
(defcomp ~blog-settings-form-content (&key csrf updated-at is-page save-success (defcomp ~settings/form-content (&key csrf updated-at is-page save-success
slug published-at featured visibility email-only slug published-at featured visibility email-only
tags feature-image-alt tags feature-image-alt
meta-title meta-description canonical-url meta-title meta-description canonical-url
@@ -209,19 +209,19 @@
(input :type "hidden" :name "updated_at" :value (or updated-at "")) (input :type "hidden" :name "updated_at" :value (or updated-at ""))
(div :class "space-y-[12px] mt-[16px]" (div :class "space-y-[12px] mt-[16px]"
;; General ;; General
(~blog-settings-section :title "General" :is-open true :content (~settings/section :title "General" :is-open true :content
(<> (<>
(div (~blog-settings-field-label :text "Slug" :field-for "settings-slug") (div (~settings/field-label :text "Slug" :field-for "settings-slug")
(input :type "text" :name "slug" :id "settings-slug" :value (or slug "") (input :type "text" :name "slug" :id "settings-slug" :value (or slug "")
:placeholder slug-placeholder :class input-cls)) :placeholder slug-placeholder :class input-cls))
(div (~blog-settings-field-label :text "Published at" :field-for "settings-published_at") (div (~settings/field-label :text "Published at" :field-for "settings-published_at")
(input :type "datetime-local" :name "published_at" :id "settings-published_at" (input :type "datetime-local" :name "published_at" :id "settings-published_at"
:value (or published-at "") :class input-cls)) :value (or published-at "") :class input-cls))
(div (label :class "inline-flex items-center gap-[8px] cursor-pointer" (div (label :class "inline-flex items-center gap-[8px] cursor-pointer"
(input :type "checkbox" :name "featured" :id "settings-featured" :checked featured (input :type "checkbox" :name "featured" :id "settings-featured" :checked featured
:class "rounded border-stone-300 text-stone-600 focus:ring-stone-300") :class "rounded border-stone-300 text-stone-600 focus:ring-stone-300")
(span :class "text-[14px] text-stone-600" featured-label))) (span :class "text-[14px] text-stone-600" featured-label)))
(div (~blog-settings-field-label :text "Visibility" :field-for "settings-visibility") (div (~settings/field-label :text "Visibility" :field-for "settings-visibility")
(select :name "visibility" :id "settings-visibility" :class input-cls (select :name "visibility" :id "settings-visibility" :class input-cls
(option :value "public" :selected (= visibility "public") "Public") (option :value "public" :selected (= visibility "public") "Public")
(option :value "members" :selected (= visibility "members") "Members") (option :value "members" :selected (= visibility "members") "Members")
@@ -231,57 +231,57 @@
:class "rounded border-stone-300 text-stone-600 focus:ring-stone-300") :class "rounded border-stone-300 text-stone-600 focus:ring-stone-300")
(span :class "text-[14px] text-stone-600" "Email only"))))) (span :class "text-[14px] text-stone-600" "Email only")))))
;; Tags ;; Tags
(~blog-settings-section :title "Tags" :content (~settings/section :title "Tags" :content
(div (~blog-settings-field-label :text "Tags (comma-separated)" :field-for "settings-tags") (div (~settings/field-label :text "Tags (comma-separated)" :field-for "settings-tags")
(input :type "text" :name "tags" :id "settings-tags" :value (or tags "") (input :type "text" :name "tags" :id "settings-tags" :value (or tags "")
:placeholder "news, updates, featured" :class input-cls) :placeholder "news, updates, featured" :class input-cls)
(p :class "text-[12px] text-stone-400 mt-[4px]" "Unknown tags will be created automatically."))) (p :class "text-[12px] text-stone-400 mt-[4px]" "Unknown tags will be created automatically.")))
;; Feature Image ;; Feature Image
(~blog-settings-section :title "Feature Image" :content (~settings/section :title "Feature Image" :content
(div (~blog-settings-field-label :text "Alt text" :field-for "settings-feature_image_alt") (div (~settings/field-label :text "Alt text" :field-for "settings-feature_image_alt")
(input :type "text" :name "feature_image_alt" :id "settings-feature_image_alt" (input :type "text" :name "feature_image_alt" :id "settings-feature_image_alt"
:value (or feature-image-alt "") :placeholder "Describe the feature image" :class input-cls))) :value (or feature-image-alt "") :placeholder "Describe the feature image" :class input-cls)))
;; SEO / Meta ;; SEO / Meta
(~blog-settings-section :title "SEO / Meta" :content (~settings/section :title "SEO / Meta" :content
(<> (<>
(div (~blog-settings-field-label :text "Meta title" :field-for "settings-meta_title") (div (~settings/field-label :text "Meta title" :field-for "settings-meta_title")
(input :type "text" :name "meta_title" :id "settings-meta_title" :value (or meta-title "") (input :type "text" :name "meta_title" :id "settings-meta_title" :value (or meta-title "")
:placeholder "SEO title" :maxlength "300" :class input-cls) :placeholder "SEO title" :maxlength "300" :class input-cls)
(p :class "text-[12px] text-stone-400 mt-[2px]" "Recommended: 70 characters. Max: 300.")) (p :class "text-[12px] text-stone-400 mt-[2px]" "Recommended: 70 characters. Max: 300."))
(div (~blog-settings-field-label :text "Meta description" :field-for "settings-meta_description") (div (~settings/field-label :text "Meta description" :field-for "settings-meta_description")
(textarea :name "meta_description" :id "settings-meta_description" :rows "2" (textarea :name "meta_description" :id "settings-meta_description" :rows "2"
:placeholder "SEO description" :maxlength "500" :class textarea-cls :placeholder "SEO description" :maxlength "500" :class textarea-cls
(or meta-description "")) (or meta-description ""))
(p :class "text-[12px] text-stone-400 mt-[2px]" "Recommended: 156 characters.")) (p :class "text-[12px] text-stone-400 mt-[2px]" "Recommended: 156 characters."))
(div (~blog-settings-field-label :text "Canonical URL" :field-for "settings-canonical_url") (div (~settings/field-label :text "Canonical URL" :field-for "settings-canonical_url")
(input :type "url" :name "canonical_url" :id "settings-canonical_url" (input :type "url" :name "canonical_url" :id "settings-canonical_url"
:value (or canonical-url "") :placeholder "https://example.com/original-post" :class input-cls)))) :value (or canonical-url "") :placeholder "https://example.com/original-post" :class input-cls))))
;; Facebook / OpenGraph ;; Facebook / OpenGraph
(~blog-settings-section :title "Facebook / OpenGraph" :content (~settings/section :title "Facebook / OpenGraph" :content
(<> (<>
(div (~blog-settings-field-label :text "OG title" :field-for "settings-og_title") (div (~settings/field-label :text "OG title" :field-for "settings-og_title")
(input :type "text" :name "og_title" :id "settings-og_title" :value (or og-title "") :class input-cls)) (input :type "text" :name "og_title" :id "settings-og_title" :value (or og-title "") :class input-cls))
(div (~blog-settings-field-label :text "OG description" :field-for "settings-og_description") (div (~settings/field-label :text "OG description" :field-for "settings-og_description")
(textarea :name "og_description" :id "settings-og_description" :rows "2" :class textarea-cls (textarea :name "og_description" :id "settings-og_description" :rows "2" :class textarea-cls
(or og-description ""))) (or og-description "")))
(div (~blog-settings-field-label :text "OG image URL" :field-for "settings-og_image") (div (~settings/field-label :text "OG image URL" :field-for "settings-og_image")
(input :type "url" :name "og_image" :id "settings-og_image" :value (or og-image "") (input :type "url" :name "og_image" :id "settings-og_image" :value (or og-image "")
:placeholder "https://..." :class input-cls)))) :placeholder "https://..." :class input-cls))))
;; X / Twitter ;; X / Twitter
(~blog-settings-section :title "X / Twitter" :content (~settings/section :title "X / Twitter" :content
(<> (<>
(div (~blog-settings-field-label :text "Twitter title" :field-for "settings-twitter_title") (div (~settings/field-label :text "Twitter title" :field-for "settings-twitter_title")
(input :type "text" :name "twitter_title" :id "settings-twitter_title" (input :type "text" :name "twitter_title" :id "settings-twitter_title"
:value (or twitter-title "") :class input-cls)) :value (or twitter-title "") :class input-cls))
(div (~blog-settings-field-label :text "Twitter description" :field-for "settings-twitter_description") (div (~settings/field-label :text "Twitter description" :field-for "settings-twitter_description")
(textarea :name "twitter_description" :id "settings-twitter_description" :rows "2" :class textarea-cls (textarea :name "twitter_description" :id "settings-twitter_description" :rows "2" :class textarea-cls
(or twitter-description ""))) (or twitter-description "")))
(div (~blog-settings-field-label :text "Twitter image URL" :field-for "settings-twitter_image") (div (~settings/field-label :text "Twitter image URL" :field-for "settings-twitter_image")
(input :type "url" :name "twitter_image" :id "settings-twitter_image" (input :type "url" :name "twitter_image" :id "settings-twitter_image"
:value (or twitter-image "") :placeholder "https://..." :class input-cls)))) :value (or twitter-image "") :placeholder "https://..." :class input-cls))))
;; Advanced ;; Advanced
(~blog-settings-section :title "Advanced" :content (~settings/section :title "Advanced" :content
(div (~blog-settings-field-label :text "Custom template" :field-for "settings-custom_template") (div (~settings/field-label :text "Custom template" :field-for "settings-custom_template")
(input :type "text" :name "custom_template" :id "settings-custom_template" (input :type "text" :name "custom_template" :id "settings-custom_template"
:value (or custom-template "") :placeholder tmpl-placeholder :class input-cls)))) :value (or custom-template "") :placeholder tmpl-placeholder :class input-cls))))
(div :class "flex items-center gap-[16px] mt-[24px] pt-[16px] border-t border-stone-200" (div :class "flex items-center gap-[16px] mt-[24px] pt-[16px] border-t border-stone-200"

View File

@@ -9,7 +9,7 @@
:auth :admin :auth :admin
:layout :blog :layout :blog
:data (editor-data) :data (editor-data)
:content (~blog-editor-content :content (~editor/content
:csrf csrf :title-placeholder title-placeholder :csrf csrf :title-placeholder title-placeholder
:create-label create-label :css-href css-href :create-label create-label :css-href css-href
:js-src js-src :sx-editor-js-src sx-editor-js-src :js-src js-src :sx-editor-js-src sx-editor-js-src
@@ -20,7 +20,7 @@
:auth :admin :auth :admin
:layout :blog :layout :blog
:data (editor-page-data) :data (editor-page-data)
:content (~blog-editor-content :content (~editor/content
:csrf csrf :title-placeholder title-placeholder :csrf csrf :title-placeholder title-placeholder
:create-label create-label :css-href css-href :create-label create-label :css-href css-href
:js-src js-src :sx-editor-js-src sx-editor-js-src :js-src js-src :sx-editor-js-src sx-editor-js-src
@@ -33,21 +33,21 @@
:auth :admin :auth :admin
:layout (:post-admin :selected "admin") :layout (:post-admin :selected "admin")
:data (post-admin-data slug) :data (post-admin-data slug)
:content (~blog-admin-placeholder)) :content (~admin/placeholder))
(defpage post-data (defpage post-data
:path "/<slug>/admin/data/" :path "/<slug>/admin/data/"
:auth :admin :auth :admin
:layout (:post-admin :selected "data") :layout (:post-admin :selected "data")
:data (post-data-data slug) :data (post-data-data slug)
:content (~blog-data-table-content :tablename tablename :model-data model-data)) :content (~admin/data-table-content :tablename tablename :model-data model-data))
(defpage post-preview (defpage post-preview
:path "/<slug>/admin/preview/" :path "/<slug>/admin/preview/"
:auth :admin :auth :admin
:layout (:post-admin :selected "preview") :layout (:post-admin :selected "preview")
:data (post-preview-data slug) :data (post-preview-data slug)
:content (~blog-preview-content :content (~admin/preview-content
:sx-pretty sx-pretty :json-pretty json-pretty :sx-pretty sx-pretty :json-pretty json-pretty
:sx-rendered sx-rendered :lex-rendered lex-rendered)) :sx-rendered sx-rendered :lex-rendered lex-rendered))
@@ -56,8 +56,8 @@
:auth :admin :auth :admin
:layout (:post-admin :selected "entries") :layout (:post-admin :selected "entries")
:data (post-entries-data slug) :data (post-entries-data slug)
:content (~blog-entries-browser-content :content (~settings/entries-browser-content
:entries-panel (~blog-associated-entries-from-data :entries entries :csrf csrf) :entries-panel (~settings/associated-entries-from-data :entries entries :csrf csrf)
:calendars calendars)) :calendars calendars))
(defpage post-settings (defpage post-settings
@@ -65,7 +65,7 @@
:auth :post_author :auth :post_author
:layout (:post-admin :selected "settings") :layout (:post-admin :selected "settings")
:data (post-settings-data slug) :data (post-settings-data slug)
:content (~blog-settings-form-content :content (~settings/form-content
:csrf csrf :updated-at updated-at :is-page is-page :csrf csrf :updated-at updated-at :is-page is-page
:save-success save-success :slug settings-slug :save-success save-success :slug settings-slug
:published-at published-at :featured featured :published-at published-at :featured featured
@@ -82,7 +82,7 @@
:auth :post_author :auth :post_author
:layout (:post-admin :selected "edit") :layout (:post-admin :selected "edit")
:data (post-edit-data slug) :data (post-edit-data slug)
:content (~blog-edit-content :content (~editor/edit-content
:csrf csrf :updated-at updated-at :csrf csrf :updated-at updated-at
:title-val title-val :excerpt-val excerpt-val :title-val title-val :excerpt-val excerpt-val
:feature-image feature-image :feature-image-caption feature-image-caption :feature-image feature-image :feature-image-caption feature-image-caption
@@ -111,7 +111,7 @@
:auth :admin :auth :admin
:layout :blog-cache :layout :blog-cache
:data (service "blog-page" "cache-data") :data (service "blog-page" "cache-data")
:content (~blog-cache-panel :clear-url clear-url :csrf csrf)) :content (~admin/cache-panel :clear-url clear-url :csrf csrf))
; --- Snippets --- ; --- Snippets ---
@@ -120,7 +120,7 @@
:auth :login :auth :login
:layout :blog-snippets :layout :blog-snippets
:data (service "blog-page" "snippets-data") :data (service "blog-page" "snippets-data")
:content (~blog-snippets-content :content (~admin/snippets-content
:snippets snippets :is-admin is-admin :csrf csrf)) :snippets snippets :is-admin is-admin :csrf csrf))
; --- Menu Items --- ; --- Menu Items ---
@@ -130,7 +130,7 @@
:auth :admin :auth :admin
:layout :blog-menu-items :layout :blog-menu-items
:data (service "blog-page" "menu-items-data") :data (service "blog-page" "menu-items-data")
:content (~blog-menu-items-content :content (~admin/menu-items-content
:menu-items menu-items :new-url new-url :csrf csrf)) :menu-items menu-items :new-url new-url :csrf csrf))
; --- Tag Groups --- ; --- Tag Groups ---
@@ -140,7 +140,7 @@
:auth :admin :auth :admin
:layout :blog-tag-groups :layout :blog-tag-groups
:data (service "blog-page" "tag-groups-data") :data (service "blog-page" "tag-groups-data")
:content (~blog-tag-groups-content :content (~admin/tag-groups-content
:groups groups :unassigned-tags unassigned-tags :groups groups :unassigned-tags unassigned-tags
:create-url create-url :csrf csrf)) :create-url create-url :csrf csrf))
@@ -149,6 +149,6 @@
:auth :admin :auth :admin
:layout :blog-tag-group-edit :layout :blog-tag-group-edit
:data (service "blog-page" "tag-group-edit-data" :id id) :data (service "blog-page" "tag-group-edit-data" :id id)
:content (~blog-tag-group-edit-content :content (~admin/tag-group-edit-content
:group group :all-tags all-tags :group group :all-tags all-tags
:save-url save-url :delete-url delete-url :csrf csrf)) :save-url save-url :delete-url delete-url :csrf csrf))

View File

@@ -167,7 +167,7 @@ class TestCards:
result = lexical_to_sx(_doc({ result = lexical_to_sx(_doc({
"type": "image", "src": "photo.jpg", "alt": "test" "type": "image", "src": "photo.jpg", "alt": "test"
})) }))
assert '(~kg-image :src "photo.jpg" :alt "test")' == result assert '(~kg_cards/kg-image :src "photo.jpg" :alt "test")' == result
def test_image_wide_with_caption(self): def test_image_wide_with_caption(self):
result = lexical_to_sx(_doc({ result = lexical_to_sx(_doc({
@@ -189,7 +189,7 @@ class TestCards:
"type": "bookmark", "url": "https://example.com", "type": "bookmark", "url": "https://example.com",
"metadata": {"title": "Example", "description": "A site"} "metadata": {"title": "Example", "description": "A site"}
})) }))
assert "(~kg-bookmark " in result assert "(~kg_cards/kg-bookmark " in result
assert ':url "https://example.com"' in result assert ':url "https://example.com"' in result
assert ':title "Example"' in result assert ':title "Example"' in result
@@ -199,7 +199,7 @@ class TestCards:
"calloutEmoji": "💡", "calloutEmoji": "💡",
"children": [_text("Note")] "children": [_text("Note")]
})) }))
assert "(~kg-callout " in result assert "(~kg_cards/kg-callout " in result
assert ':color "blue"' in result assert ':color "blue"' in result
def test_button(self): def test_button(self):
@@ -207,7 +207,7 @@ class TestCards:
"type": "button", "buttonText": "Click", "type": "button", "buttonText": "Click",
"buttonUrl": "https://example.com" "buttonUrl": "https://example.com"
})) }))
assert "(~kg-button " in result assert "(~kg_cards/kg-button " in result
assert ':text "Click"' in result assert ':text "Click"' in result
def test_toggle(self): def test_toggle(self):
@@ -215,28 +215,28 @@ class TestCards:
"type": "toggle", "heading": "FAQ", "type": "toggle", "heading": "FAQ",
"children": [_text("Answer")] "children": [_text("Answer")]
})) }))
assert "(~kg-toggle " in result assert "(~kg_cards/kg-toggle " in result
assert ':heading "FAQ"' in result assert ':heading "FAQ"' in result
def test_html(self): def test_html(self):
result = lexical_to_sx(_doc({ result = lexical_to_sx(_doc({
"type": "html", "html": "<div>custom</div>" "type": "html", "html": "<div>custom</div>"
})) }))
assert result == '(~kg-html (div "custom"))' assert result == '(~kg_cards/kg-html (div "custom"))'
def test_embed(self): def test_embed(self):
result = lexical_to_sx(_doc({ result = lexical_to_sx(_doc({
"type": "embed", "html": "<iframe></iframe>", "type": "embed", "html": "<iframe></iframe>",
"caption": "Video" "caption": "Video"
})) }))
assert "(~kg-embed " in result assert "(~kg_cards/kg-embed " in result
assert ':caption "Video"' in result assert ':caption "Video"' in result
def test_markdown(self): def test_markdown(self):
result = lexical_to_sx(_doc({ result = lexical_to_sx(_doc({
"type": "markdown", "markdown": "**bold** text" "type": "markdown", "markdown": "**bold** text"
})) }))
assert result.startswith("(~kg-md ") assert result.startswith("(~kg_cards/kg-md ")
assert "(p " in result assert "(p " in result
assert "(strong " in result assert "(strong " in result
@@ -244,14 +244,14 @@ class TestCards:
result = lexical_to_sx(_doc({ result = lexical_to_sx(_doc({
"type": "video", "src": "v.mp4", "cardWidth": "wide" "type": "video", "src": "v.mp4", "cardWidth": "wide"
})) }))
assert "(~kg-video " in result assert "(~kg_cards/kg-video " in result
assert ':width "wide"' in result assert ':width "wide"' in result
def test_audio(self): def test_audio(self):
result = lexical_to_sx(_doc({ result = lexical_to_sx(_doc({
"type": "audio", "src": "s.mp3", "title": "Song", "duration": 195 "type": "audio", "src": "s.mp3", "title": "Song", "duration": 195
})) }))
assert "(~kg-audio " in result assert "(~kg_cards/kg-audio " in result
assert ':duration "3:15"' in result assert ':duration "3:15"' in result
def test_file(self): def test_file(self):
@@ -259,13 +259,13 @@ class TestCards:
"type": "file", "src": "f.pdf", "fileName": "doc.pdf", "type": "file", "src": "f.pdf", "fileName": "doc.pdf",
"fileSize": 2100000 "fileSize": 2100000
})) }))
assert "(~kg-file " in result assert "(~kg_cards/kg-file " in result
assert ':filename "doc.pdf"' in result assert ':filename "doc.pdf"' in result
assert "MB" in result assert "MB" in result
def test_paywall(self): def test_paywall(self):
result = lexical_to_sx(_doc({"type": "paywall"})) result = lexical_to_sx(_doc({"type": "paywall"}))
assert result == "(~kg-paywall)" assert result == "(~kg_cards/kg-paywall)"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -1,12 +1,12 @@
;; Cart calendar entry components ;; Cart calendar entry components
(defcomp ~cart-cal-entry (&key (name :as string) (date-str :as string) (cost :as string)) (defcomp ~calendar/cal-entry (&key (name :as string) (date-str :as string) (cost :as string))
(li :class "flex items-start justify-between text-sm" (li :class "flex items-start justify-between text-sm"
(div (div :class "font-medium" name) (div (div :class "font-medium" name)
(div :class "text-xs text-stone-500" date-str)) (div :class "text-xs text-stone-500" date-str))
(div :class "ml-4 font-medium" cost))) (div :class "ml-4 font-medium" cost)))
(defcomp ~cart-cal-section (&key items) (defcomp ~calendar/cal-section (&key items)
(div :class "mt-6 border-t border-stone-200 pt-4" (div :class "mt-6 border-t border-stone-200 pt-4"
(h2 :class "text-base font-semibold mb-2" "Calendar bookings") (h2 :class "text-base font-semibold mb-2" "Calendar bookings")
(ul :class "space-y-2" items))) (ul :class "space-y-2" items)))

View File

@@ -4,6 +4,6 @@
;; Renders the "orders" link for the account dashboard nav. ;; Renders the "orders" link for the account dashboard nav.
(defhandler account-nav-item (&key) (defhandler account-nav-item (&key)
(~account-nav-item (~shared:fragments/account-nav-item
:href (app-url "cart" "/orders/") :href (app-url "cart" "/orders/")
:label "orders")) :label "orders"))

View File

@@ -10,7 +10,7 @@
(count (+ (or (get summary "count") 0) (count (+ (or (get summary "count") 0)
(or (get summary "calendar_count") 0) (or (get summary "calendar_count") 0)
(or (get summary "ticket_count") 0)))) (or (get summary "ticket_count") 0))))
(~cart-mini (~shared:fragments/cart-mini
:cart-count count :cart-count count
:blog-url (app-url "blog" "") :blog-url (app-url "blog" "")
:cart-url (app-url "cart" "") :cart-url (app-url "cart" "")

View File

@@ -1,14 +1,14 @@
;; Cart header components ;; Cart header components
(defcomp ~cart-page-label-img (&key src) (defcomp ~header/page-label-img (&key src)
(img :src src :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0")) (img :src src :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
(defcomp ~cart-page-label (&key feature-image title) (defcomp ~header/page-label (&key feature-image title)
(<> (when feature-image (<> (when feature-image
(~cart-page-label-img :src feature-image)) (~header/page-label-img :src feature-image))
(span title))) (span title)))
(defcomp ~cart-all-carts-link (&key href) (defcomp ~header/all-carts-link (&key href)
(a :href href :class "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition" (a :href href :class "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition"
(i :class "fa fa-arrow-left text-xs" :aria-hidden "true") "All carts")) (i :class "fa fa-arrow-left text-xs" :aria-hidden "true") "All carts"))

View File

@@ -1,29 +1,29 @@
;; Cart item components ;; Cart item components
(defcomp ~cart-item-img (&key (src :as string) (alt :as string)) (defcomp ~items/img (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100" :loading "lazy")) (img :src src :alt alt :class "w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100" :loading "lazy"))
(defcomp ~cart-item-price (&key (text :as string)) (defcomp ~items/price (&key (text :as string))
(p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (p :class "text-sm sm:text-base font-semibold text-stone-900" text))
(defcomp ~cart-item-price-was (&key (text :as string)) (defcomp ~items/price-was (&key (text :as string))
(p :class "text-xs text-stone-400 line-through" text)) (p :class "text-xs text-stone-400 line-through" text))
(defcomp ~cart-item-no-price () (defcomp ~items/no-price ()
(p :class "text-xs text-stone-500" "No price")) (p :class "text-xs text-stone-500" "No price"))
(defcomp ~cart-item-deleted () (defcomp ~items/deleted ()
(p :class "mt-2 inline-flex items-center gap-1 text-[0.65rem] sm:text-xs font-medium text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5" (p :class "mt-2 inline-flex items-center gap-1 text-[0.65rem] sm:text-xs font-medium text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5"
(i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true") (i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true")
" This item is no longer available or price has changed")) " This item is no longer available or price has changed"))
(defcomp ~cart-item-brand (&key (brand :as string)) (defcomp ~items/brand (&key (brand :as string))
(p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand)) (p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand))
(defcomp ~cart-item-line-total (&key (text :as string)) (defcomp ~items/line-total (&key (text :as string))
(p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (p :class "text-sm sm:text-base font-semibold text-stone-900" text))
(defcomp ~cart-item (&key (id :as string) img (prod-url :as string) (title :as string) brand deleted price (qty-url :as string) (csrf :as string) (minus :as string) (qty :as string) (plus :as string) line-total) (defcomp ~items/index (&key (id :as string) img (prod-url :as string) (title :as string) brand deleted price (qty-url :as string) (csrf :as string) (minus :as string) (qty :as string) (plus :as string) line-total)
(article :id id :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5" (article :id id :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5"
(div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img)) (div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img))
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -47,14 +47,14 @@
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+"))) (button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+")))
(div :class "flex items-center justify-between sm:justify-end gap-3" (when line-total line-total)))))) (div :class "flex items-center justify-between sm:justify-end gap-3" (when line-total line-total))))))
(defcomp ~cart-page-panel (&key items cal tickets summary) (defcomp ~items/page-panel (&key items cal tickets summary)
(div :class "max-w-full px-3 py-3 space-y-3" (div :class "max-w-full px-3 py-3 space-y-3"
(div :id "cart" (div :id "cart"
(div (section :class "space-y-3 sm:space-y-4" items cal tickets) (div (section :class "space-y-3 sm:space-y-4" items cal tickets)
summary)))) summary))))
;; Assembled cart item from serialized data — replaces Python _cart_item_sx ;; Assembled cart item from serialized data — replaces Python _cart_item_sx
(defcomp ~cart-item-from-data (&key (item :as dict)) (defcomp ~items/from-data (&key (item :as dict))
(let* ((slug (or (get item "slug") "")) (let* ((slug (or (get item "slug") ""))
(title (or (get item "title") "")) (title (or (get item "title") ""))
(image (get item "image")) (image (get item "image"))
@@ -71,48 +71,48 @@
(qty-url (or (get item "qty_url") "")) (qty-url (or (get item "qty_url") ""))
(csrf (csrf-token)) (csrf (csrf-token))
(line-total (when unit-price (* unit-price quantity)))) (line-total (when unit-price (* unit-price quantity))))
(~cart-item (~items/index
:id (str "cart-item-" slug) :id (str "cart-item-" slug)
:img (if image :img (if image
(~cart-item-img :src image :alt title) (~items/img :src image :alt title)
(~img-or-placeholder :src nil (~shared:misc/img-or-placeholder :src nil
:size-cls "w-24 h-24 sm:w-32 sm:h-28 rounded-xl border border-dashed border-stone-300" :size-cls "w-24 h-24 sm:w-32 sm:h-28 rounded-xl border border-dashed border-stone-300"
:placeholder-text "No image")) :placeholder-text "No image"))
:prod-url prod-url :prod-url prod-url
:title title :title title
:brand (when brand (~cart-item-brand :brand brand)) :brand (when brand (~items/brand :brand brand))
:deleted (when is-deleted (~cart-item-deleted)) :deleted (when is-deleted (~items/deleted))
:price (if unit-price :price (if unit-price
(<> (<>
(~cart-item-price :text (str symbol (format-decimal unit-price 2))) (~items/price :text (str symbol (format-decimal unit-price 2)))
(when (and special-price (!= special-price regular-price)) (when (and special-price (!= special-price regular-price))
(~cart-item-price-was :text (str symbol (format-decimal regular-price 2))))) (~items/price-was :text (str symbol (format-decimal regular-price 2)))))
(~cart-item-no-price)) (~items/no-price))
:qty-url qty-url :csrf csrf :qty-url qty-url :csrf csrf
:minus (str (- quantity 1)) :minus (str (- quantity 1))
:qty (str quantity) :qty (str quantity)
:plus (str (+ quantity 1)) :plus (str (+ quantity 1))
:line-total (when line-total :line-total (when line-total
(~cart-item-line-total :text (str "Line total: " symbol (format-decimal line-total 2))))))) (~items/line-total :text (str "Line total: " symbol (format-decimal line-total 2)))))))
;; Assembled calendar entries section — replaces Python _calendar_entries_sx ;; Assembled calendar entries section — replaces Python _calendar_entries_sx
(defcomp ~cart-cal-section-from-data (&key (entries :as list)) (defcomp ~items/cal-section-from-data (&key (entries :as list))
(when (not (empty? entries)) (when (not (empty? entries))
(~cart-cal-section (~calendar/cal-section
:items (map (lambda (e) :items (map (lambda (e)
(let* ((name (or (get e "name") "")) (let* ((name (or (get e "name") ""))
(date-str (or (get e "date_str") ""))) (date-str (or (get e "date_str") "")))
(~cart-cal-entry (~calendar/cal-entry
:name name :date-str date-str :name name :date-str date-str
:cost (str "\u00a3" (format-decimal (or (get e "cost") 0) 2))))) :cost (str "\u00a3" (format-decimal (or (get e "cost") 0) 2)))))
entries)))) entries))))
;; Assembled ticket groups section — replaces Python _ticket_groups_sx ;; Assembled ticket groups section — replaces Python _ticket_groups_sx
(defcomp ~cart-tickets-section-from-data (&key (ticket-groups :as list)) (defcomp ~items/tickets-section-from-data (&key (ticket-groups :as list))
(when (not (empty? ticket-groups)) (when (not (empty? ticket-groups))
(let* ((csrf (csrf-token)) (let* ((csrf (csrf-token))
(qty-url (url-for "cart_global.update_ticket_quantity"))) (qty-url (url-for "cart_global.update_ticket_quantity")))
(~cart-tickets-section (~tickets/section
:items (map (lambda (tg) :items (map (lambda (tg)
(let* ((name (or (get tg "entry_name") "")) (let* ((name (or (get tg "entry_name") ""))
(tt-name (get tg "ticket_type_name")) (tt-name (get tg "ticket_type_name"))
@@ -122,14 +122,14 @@
(entry-id (str (or (get tg "entry_id") ""))) (entry-id (str (or (get tg "entry_id") "")))
(tt-id (get tg "ticket_type_id")) (tt-id (get tg "ticket_type_id"))
(date-str (or (get tg "date_str") ""))) (date-str (or (get tg "date_str") "")))
(~cart-ticket-article (~tickets/article
:name name :name name
:type-name (when tt-name (~cart-ticket-type-name :name tt-name)) :type-name (when tt-name (~tickets/type-name :name tt-name))
:date-str date-str :date-str date-str
:price (str "\u00a3" (format-decimal price 2)) :price (str "\u00a3" (format-decimal price 2))
:qty-url qty-url :csrf csrf :qty-url qty-url :csrf csrf
:entry-id entry-id :entry-id entry-id
:type-hidden (when tt-id (~cart-ticket-type-hidden :value (str tt-id))) :type-hidden (when tt-id (~tickets/type-hidden :value (str tt-id)))
:minus (str (max (- quantity 1) 0)) :minus (str (max (- quantity 1) 0))
:qty (str quantity) :qty (str quantity)
:plus (str (+ quantity 1)) :plus (str (+ quantity 1))
@@ -137,29 +137,29 @@
ticket-groups))))) ticket-groups)))))
;; Assembled cart summary — replaces Python _cart_summary_sx ;; Assembled cart summary — replaces Python _cart_summary_sx
(defcomp ~cart-summary-from-data (&key (item-count :as number) (grand-total :as number) (symbol :as string) (is-logged-in :as boolean) (checkout-action :as string) (login-href :as string) (user-email :as string?)) (defcomp ~items/summary-from-data (&key (item-count :as number) (grand-total :as number) (symbol :as string) (is-logged-in :as boolean) (checkout-action :as string) (login-href :as string) (user-email :as string?))
(~cart-summary-panel (~summary/panel
:item-count (str item-count) :item-count (str item-count)
:subtotal (str symbol (format-decimal grand-total 2)) :subtotal (str symbol (format-decimal grand-total 2))
:checkout (if is-logged-in :checkout (if is-logged-in
(~cart-checkout-form (~summary/checkout-form
:action checkout-action :csrf (csrf-token) :action checkout-action :csrf (csrf-token)
:label (str " Checkout as " user-email)) :label (str " Checkout as " user-email))
(~cart-checkout-signin :href login-href)))) (~summary/checkout-signin :href login-href))))
;; Assembled page cart content — replaces Python _page_cart_main_panel_sx ;; Assembled page cart content — replaces Python _page_cart_main_panel_sx
(defcomp ~cart-page-cart-content (&key (cart-items :as list?) (cal-entries :as list?) (ticket-groups :as list?) summary) (defcomp ~items/page-cart-content (&key (cart-items :as list?) (cal-entries :as list?) (ticket-groups :as list?) summary)
(if (and (empty? (or cart-items (list))) (if (and (empty? (or cart-items (list)))
(empty? (or cal-entries (list))) (empty? (or cal-entries (list)))
(empty? (or ticket-groups (list)))) (empty? (or ticket-groups (list))))
(div :class "max-w-full px-3 py-3 space-y-3" (div :class "max-w-full px-3 py-3 space-y-3"
(div :id "cart" (div :id "cart"
(div :class "rounded-2xl border border-dashed border-stone-300 bg-white/80 p-6 sm:p-8 text-center" (div :class "rounded-2xl border border-dashed border-stone-300 bg-white/80 p-6 sm:p-8 text-center"
(~empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center")))) (~shared:misc/empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center"))))
(~cart-page-panel (~items/page-panel
:items (map (lambda (item) (~cart-item-from-data :item item)) (or cart-items (list))) :items (map (lambda (item) (~items/from-data :item item)) (or cart-items (list)))
:cal (when (not (empty? (or cal-entries (list)))) :cal (when (not (empty? (or cal-entries (list))))
(~cart-cal-section-from-data :entries cal-entries)) (~items/cal-section-from-data :entries cal-entries))
:tickets (when (not (empty? (or ticket-groups (list)))) :tickets (when (not (empty? (or ticket-groups (list))))
(~cart-tickets-section-from-data :ticket-groups ticket-groups)) (~items/tickets-section-from-data :ticket-groups ticket-groups))
:summary summary))) :summary summary)))

View File

@@ -10,17 +10,17 @@
(quasiquote (quasiquote
(let ((__cpctx (cart-page-ctx))) (let ((__cpctx (cart-page-ctx)))
(<> (<>
(~menu-row-sx :id "cart-row" :level 1 :colour "sky" (~shared:layout/menu-row-sx :id "cart-row" :level 1 :colour "sky"
:link-href (get __cpctx "cart-url") :link-href (get __cpctx "cart-url")
:link-label "cart" :icon "fa fa-shopping-cart" :link-label "cart" :icon "fa fa-shopping-cart"
:child-id "cart-header-child") :child-id "cart-header-child")
(~header-child-sx :id "cart-header-child" (~shared:layout/header-child-sx :id "cart-header-child"
:inner (~menu-row-sx :id "page-cart-row" :level 2 :colour "sky" :inner (~shared:layout/menu-row-sx :id "page-cart-row" :level 2 :colour "sky"
:link-href (get __cpctx "page-cart-url") :link-href (get __cpctx "page-cart-url")
:link-label-content (~cart-page-label :link-label-content (~header/page-label
:feature-image (get __cpctx "feature-image") :feature-image (get __cpctx "feature-image")
:title (get __cpctx "title")) :title (get __cpctx "title"))
:nav (~cart-all-carts-link :href (get __cpctx "cart-url")) :nav (~header/all-carts-link :href (get __cpctx "cart-url"))
:oob (unquote oob))))))) :oob (unquote oob)))))))
(defmacro ~cart-page-header-oob () (defmacro ~cart-page-header-oob ()
@@ -28,14 +28,14 @@
(quasiquote (quasiquote
(let ((__cpctx (cart-page-ctx))) (let ((__cpctx (cart-page-ctx)))
(<> (<>
(~menu-row-sx :id "page-cart-row" :level 2 :colour "sky" (~shared:layout/menu-row-sx :id "page-cart-row" :level 2 :colour "sky"
:link-href (get __cpctx "page-cart-url") :link-href (get __cpctx "page-cart-url")
:link-label-content (~cart-page-label :link-label-content (~header/page-label
:feature-image (get __cpctx "feature-image") :feature-image (get __cpctx "feature-image")
:title (get __cpctx "title")) :title (get __cpctx "title"))
:nav (~cart-all-carts-link :href (get __cpctx "cart-url")) :nav (~header/all-carts-link :href (get __cpctx "cart-url"))
:oob true) :oob true)
(~menu-row-sx :id "cart-row" :level 1 :colour "sky" (~shared:layout/menu-row-sx :id "cart-row" :level 1 :colour "sky"
:link-href (get __cpctx "cart-url") :link-href (get __cpctx "cart-url")
:link-label "cart" :icon "fa fa-shopping-cart" :link-label "cart" :icon "fa fa-shopping-cart"
:child-id "cart-header-child" :child-id "cart-header-child"
@@ -45,12 +45,12 @@
;; cart-page layout: root + cart row + page-cart row ;; cart-page layout: root + cart row + page-cart row
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~cart-page-layout-full () (defcomp ~layouts/page-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (~cart-page-header-auto)))) :inner (~cart-page-header-auto))))
(defcomp ~cart-page-layout-oob () (defcomp ~layouts/page-layout-oob ()
(<> (~cart-page-header-oob) (<> (~cart-page-header-oob)
(~root-header-auto true))) (~root-header-auto true)))
@@ -59,14 +59,14 @@
;; Uses (post-header-ctx) — requires :data handler to populate g._defpage_ctx ;; Uses (post-header-ctx) — requires :data handler to populate g._defpage_ctx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~cart-admin-layout-full (&key selected) (defcomp ~layouts/admin-layout-full (&key selected)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (~post-header-auto nil)))) :inner (~post-header-auto nil))))
(defcomp ~cart-admin-layout-oob (&key selected) (defcomp ~layouts/admin-layout-oob (&key selected)
(<> (~post-header-auto true) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child" (~shared:layout/oob-header-sx :parent-id "post-header-child"
:row (~post-admin-header-auto nil selected)) :row (~post-admin-header-auto nil selected))
(~root-header-auto true))) (~root-header-auto true)))
@@ -74,63 +74,63 @@
;; orders-within-cart: root + auth-simple + orders ;; orders-within-cart: root + auth-simple + orders
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~cart-orders-layout-full (&key list-url) (defcomp ~layouts/orders-layout-full (&key list-url)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~auth-header-row-simple-auto) :inner (<> (~auth-header-row-simple-auto)
(~header-child-sx :id "auth-header-child" (~shared:layout/header-child-sx :id "auth-header-child"
:inner (~orders-header-row :list-url list-url)))))) :inner (~shared:auth/orders-header-row :list-url list-url))))))
(defcomp ~cart-orders-layout-oob (&key list-url) (defcomp ~layouts/orders-layout-oob (&key list-url)
(<> (~auth-header-row-simple-auto true) (<> (~auth-header-row-simple-auto true)
(~oob-header-sx (~shared:layout/oob-header-sx
:parent-id "auth-header-child" :parent-id "auth-header-child"
:row (~orders-header-row :list-url list-url)) :row (~shared:auth/orders-header-row :list-url list-url))
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; order-detail-within-cart: root + auth-simple + orders + order ;; order-detail-within-cart: root + auth-simple + orders + order
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~cart-order-detail-layout-full (&key list-url detail-url order-label) (defcomp ~layouts/order-detail-layout-full (&key list-url detail-url order-label)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~auth-header-row-simple-auto) :inner (<> (~auth-header-row-simple-auto)
(~header-child-sx :id "auth-header-child" (~shared:layout/header-child-sx :id "auth-header-child"
:inner (<> (~orders-header-row :list-url list-url) :inner (<> (~shared:auth/orders-header-row :list-url list-url)
(~header-child-sx :id "orders-header-child" (~shared:layout/header-child-sx :id "orders-header-child"
:inner (~menu-row-sx :id "order-row" :level 3 :colour "sky" :inner (~shared:layout/menu-row-sx :id "order-row" :level 3 :colour "sky"
:link-href detail-url :link-href detail-url
:link-label order-label :link-label order-label
:icon "fa fa-gbp")))))))) :icon "fa fa-gbp"))))))))
(defcomp ~cart-order-detail-layout-oob (&key detail-url order-label) (defcomp ~layouts/order-detail-layout-oob (&key detail-url order-label)
(<> (~oob-header-sx (<> (~shared:layout/oob-header-sx
:parent-id "orders-header-child" :parent-id "orders-header-child"
:row (~menu-row-sx :id "order-row" :level 3 :colour "sky" :row (~shared:layout/menu-row-sx :id "order-row" :level 3 :colour "sky"
:link-href detail-url :link-label order-label :link-href detail-url :link-label order-label
:icon "fa fa-gbp" :oob true)) :icon "fa fa-gbp" :oob true))
(~root-header-auto true))) (~root-header-auto true)))
;; --- orders rows wrapper (for infinite scroll) --- ;; --- orders rows wrapper (for infinite scroll) ---
(defcomp ~cart-orders-rows (&key rows next-scroll) (defcomp ~layouts/orders-rows (&key rows next-scroll)
(<> rows next-scroll)) (<> rows next-scroll))
;; Composition defcomp — replaces Python loop in render_orders_rows ;; Composition defcomp — replaces Python loop in render_orders_rows
(defcomp ~cart-orders-rows-content (&key orders detail-url-prefix page total-pages next-url) (defcomp ~layouts/orders-rows-content (&key orders detail-url-prefix page total-pages next-url)
(~cart-orders-rows (~layouts/orders-rows
:rows (map (lambda (od) :rows (map (lambda (od)
(~order-row-pair :order od :detail-url-prefix detail-url-prefix)) (~shared:orders/row-pair :order od :detail-url-prefix detail-url-prefix))
(or orders (list))) (or orders (list)))
:next-scroll (if (< page total-pages) :next-scroll (if (< page total-pages)
(~infinite-scroll :url next-url :page page (~shared:controls/infinite-scroll :url next-url :page page
:total-pages total-pages :id-prefix "orders" :colspan 5) :total-pages total-pages :id-prefix "orders" :colspan 5)
(~order-end-row)))) (~shared:orders/end-row))))
;; Composition defcomp — replaces conditional composition in render_checkout_error_page ;; Composition defcomp — replaces conditional composition in render_checkout_error_page
(defcomp ~cart-checkout-error-from-data (&key msg order-id back-url) (defcomp ~layouts/checkout-error-from-data (&key msg order-id back-url)
(~checkout-error-content (~shared:orders/checkout-error-content
:msg msg :msg msg
:order (when order-id (~checkout-error-order-id :oid (str "#" order-id))) :order (when order-id (~shared:orders/checkout-error-order-id :oid (str "#" order-id)))
:back-url back-url)) :back-url back-url))

View File

@@ -1,20 +1,20 @@
;; Cart overview components ;; Cart overview components
(defcomp ~cart-badge (&key (icon :as string) (text :as string)) (defcomp ~overview/badge (&key (icon :as string) (text :as string))
(span :class "inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100" (span :class "inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-stone-100"
(i :class icon :aria-hidden "true") text)) (i :class icon :aria-hidden "true") text))
(defcomp ~cart-badges-wrap (&key badges) (defcomp ~overview/badges-wrap (&key badges)
(div :class "mt-1 flex flex-wrap gap-2 text-xs text-stone-600" (div :class "mt-1 flex flex-wrap gap-2 text-xs text-stone-600"
badges)) badges))
(defcomp ~cart-group-card-img (&key (src :as string) (alt :as string)) (defcomp ~overview/group-card-img (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "h-16 w-16 rounded-xl object-cover border border-stone-200 flex-shrink-0")) (img :src src :alt alt :class "h-16 w-16 rounded-xl object-cover border border-stone-200 flex-shrink-0"))
(defcomp ~cart-mp-subtitle (&key (title :as string)) (defcomp ~overview/mp-subtitle (&key (title :as string))
(p :class "text-xs text-stone-500 truncate" title)) (p :class "text-xs text-stone-500 truncate" title))
(defcomp ~cart-group-card (&key (href :as string) img (display-title :as string) subtitle badges (total :as string)) (defcomp ~overview/group-card (&key (href :as string) img (display-title :as string) subtitle badges (total :as string))
(a :href href :class "block rounded-2xl border border-stone-200 bg-white shadow-sm hover:shadow-md hover:border-stone-300 transition p-4 sm:p-5" (a :href href :class "block rounded-2xl border border-stone-200 bg-white shadow-sm hover:shadow-md hover:border-stone-300 transition p-4 sm:p-5"
(div :class "flex items-start gap-4" (div :class "flex items-start gap-4"
img img
@@ -25,7 +25,7 @@
(div :class "text-lg font-bold text-stone-900" total) (div :class "text-lg font-bold text-stone-900" total)
(div :class "mt-1 text-xs text-emerald-700 font-medium" "View cart \u2192"))))) (div :class "mt-1 text-xs text-emerald-700 font-medium" "View cart \u2192")))))
(defcomp ~cart-orphan-card (&key badges (total :as string)) (defcomp ~overview/orphan-card (&key badges (total :as string))
(div :class "rounded-2xl border border-dashed border-amber-300 bg-amber-50/60 p-4 sm:p-5" (div :class "rounded-2xl border border-dashed border-amber-300 bg-amber-50/60 p-4 sm:p-5"
(div :class "flex items-start gap-4" (div :class "flex items-start gap-4"
(div :class "h-16 w-16 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0" (div :class "h-16 w-16 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0"
@@ -36,17 +36,17 @@
(div :class "text-right flex-shrink-0" (div :class "text-right flex-shrink-0"
(div :class "text-lg font-bold text-stone-900" total))))) (div :class "text-lg font-bold text-stone-900" total)))))
(defcomp ~cart-overview-panel (&key cards) (defcomp ~overview/panel (&key cards)
(div :class "max-w-full px-3 py-3 space-y-3" (div :class "max-w-full px-3 py-3 space-y-3"
(div :class "space-y-4" cards))) (div :class "space-y-4" cards)))
(defcomp ~cart-empty () (defcomp ~overview/empty ()
(div :class "max-w-full px-3 py-3 space-y-3" (div :class "max-w-full px-3 py-3 space-y-3"
(div :class "rounded-2xl border border-dashed border-stone-300 bg-white/80 p-6 sm:p-8 text-center" (div :class "rounded-2xl border border-dashed border-stone-300 bg-white/80 p-6 sm:p-8 text-center"
(~empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center")))) (~shared:misc/empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center"))))
;; Assembled page group card — replaces Python _page_group_card_sx ;; Assembled page group card — replaces Python _page_group_card_sx
(defcomp ~cart-page-group-card-from-data (&key (grp :as dict) (cart-url-base :as string)) (defcomp ~overview/page-group-card-from-data (&key (grp :as dict) (cart-url-base :as string))
(let* ((post (get grp "post")) (let* ((post (get grp "post"))
(product-count (or (get grp "product_count") 0)) (product-count (or (get grp "product_count") 0))
(calendar-count (or (get grp "calendar_count") 0)) (calendar-count (or (get grp "calendar_count") 0))
@@ -55,13 +55,13 @@
(market-place (get grp "market_place")) (market-place (get grp "market_place"))
(badges (<> (badges (<>
(when (> product-count 0) (when (> product-count 0)
(~cart-badge :icon "fa fa-box-open" (~overview/badge :icon "fa fa-box-open"
:text (str product-count " item" (pluralize product-count)))) :text (str product-count " item" (pluralize product-count))))
(when (> calendar-count 0) (when (> calendar-count 0)
(~cart-badge :icon "fa fa-calendar" (~overview/badge :icon "fa fa-calendar"
:text (str calendar-count " booking" (pluralize calendar-count)))) :text (str calendar-count " booking" (pluralize calendar-count))))
(when (> ticket-count 0) (when (> ticket-count 0)
(~cart-badge :icon "fa fa-ticket" (~overview/badge :icon "fa fa-ticket"
:text (str ticket-count " ticket" (pluralize ticket-count))))))) :text (str ticket-count " ticket" (pluralize ticket-count)))))))
(if post (if post
(let* ((slug (or (get post "slug") "")) (let* ((slug (or (get post "slug") ""))
@@ -69,26 +69,26 @@
(feature-image (get post "feature_image")) (feature-image (get post "feature_image"))
(mp-name (if market-place (or (get market-place "name") "") "")) (mp-name (if market-place (or (get market-place "name") "") ""))
(display-title (if (!= mp-name "") mp-name title))) (display-title (if (!= mp-name "") mp-name title)))
(~cart-group-card (~overview/group-card
:href (str cart-url-base "/" slug "/") :href (str cart-url-base "/" slug "/")
:img (if feature-image :img (if feature-image
(~cart-group-card-img :src feature-image :alt title) (~overview/group-card-img :src feature-image :alt title)
(~img-or-placeholder :src nil :size-cls "h-16 w-16 rounded-xl" (~shared:misc/img-or-placeholder :src nil :size-cls "h-16 w-16 rounded-xl"
:placeholder-icon "fa fa-store text-xl")) :placeholder-icon "fa fa-store text-xl"))
:display-title display-title :display-title display-title
:subtitle (when (!= mp-name "") :subtitle (when (!= mp-name "")
(~cart-mp-subtitle :title title)) (~overview/mp-subtitle :title title))
:badges (~cart-badges-wrap :badges badges) :badges (~overview/badges-wrap :badges badges)
:total (str "\u00a3" (format-decimal total 2)))) :total (str "\u00a3" (format-decimal total 2))))
(~cart-orphan-card (~overview/orphan-card
:badges (~cart-badges-wrap :badges badges) :badges (~overview/badges-wrap :badges badges)
:total (str "\u00a3" (format-decimal total 2)))))) :total (str "\u00a3" (format-decimal total 2))))))
;; Assembled cart overview content — replaces Python _overview_main_panel_sx ;; Assembled cart overview content — replaces Python _overview_main_panel_sx
(defcomp ~cart-overview-content (&key (page-groups :as list) (cart-url-base :as string)) (defcomp ~overview/content (&key (page-groups :as list) (cart-url-base :as string))
(if (empty? page-groups) (if (empty? page-groups)
(~cart-empty) (~overview/empty)
(~cart-overview-panel (~overview/panel
:cards (map (lambda (grp) :cards (map (lambda (grp)
(~cart-page-group-card-from-data :grp grp :cart-url-base cart-url-base)) (~overview/page-group-card-from-data :grp grp :cart-url-base cart-url-base))
page-groups)))) page-groups))))

View File

@@ -1,13 +1,13 @@
;; Cart payments components ;; Cart payments components
(defcomp ~cart-payments-panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix) (defcomp ~payments/panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix)
(section :class "p-4 max-w-lg mx-auto" (section :class "p-4 max-w-lg mx-auto"
(~sumup-settings-form :update-url update-url :csrf csrf :merchant-code merchant-code (~shared:misc/sumup-settings-form :update-url update-url :csrf csrf :merchant-code merchant-code
:placeholder placeholder :input-cls input-cls :sumup-configured sumup-configured :placeholder placeholder :input-cls input-cls :sumup-configured sumup-configured
:checkout-prefix checkout-prefix :sx-select "#payments-panel"))) :checkout-prefix checkout-prefix :sx-select "#payments-panel")))
;; Assembled cart admin overview content ;; Assembled cart admin overview content
(defcomp ~cart-admin-content () (defcomp ~payments/admin-content ()
(let* ((payments-href (url-for "defpage_cart_payments"))) (let* ((payments-href (url-for "defpage_cart_payments")))
(div :id "main-panel" (div :id "main-panel"
(div :class "flex items-center justify-between p-3 border-b" (div :class "flex items-center justify-between p-3 border-b"
@@ -15,13 +15,13 @@
(a :href payments-href :class "text-sm underline" "configure"))))) (a :href payments-href :class "text-sm underline" "configure")))))
;; Assembled cart payments content ;; Assembled cart payments content
(defcomp ~cart-payments-content (&key page-config) (defcomp ~payments/content (&key page-config)
(let* ((sumup-configured (and page-config (get page-config "sumup_api_key"))) (let* ((sumup-configured (and page-config (get page-config "sumup_api_key")))
(merchant-code (or (get page-config "sumup_merchant_code") "")) (merchant-code (or (get page-config "sumup_merchant_code") ""))
(checkout-prefix (or (get page-config "sumup_checkout_prefix") "")) (checkout-prefix (or (get page-config "sumup_checkout_prefix") ""))
(placeholder (if sumup-configured "--------" "sup_sk_...")) (placeholder (if sumup-configured "--------" "sup_sk_..."))
(input-cls "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500")) (input-cls "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"))
(~cart-payments-panel (~payments/panel
:update-url (url-for "page_admin.update_sumup") :update-url (url-for "page_admin.update_sumup")
:csrf (csrf-token) :csrf (csrf-token)
:merchant-code merchant-code :merchant-code merchant-code

View File

@@ -1,17 +1,17 @@
;; Cart summary / checkout components ;; Cart summary / checkout components
(defcomp ~cart-checkout-form (&key (action :as string) (csrf :as string) (label :as string)) (defcomp ~summary/checkout-form (&key (action :as string) (csrf :as string) (label :as string))
(form :method "post" :action action :class "w-full" (form :method "post" :action action :class "w-full"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "w-full inline-flex items-center justify-center px-4 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition" (button :type "submit" :class "w-full inline-flex items-center justify-center px-4 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition"
(i :class "fa-solid fa-credit-card mr-2" :aria-hidden "true") label))) (i :class "fa-solid fa-credit-card mr-2" :aria-hidden "true") label)))
(defcomp ~cart-checkout-signin (&key (href :as string)) (defcomp ~summary/checkout-signin (&key (href :as string))
(div :class "w-full flex" (div :class "w-full flex"
(a :href href :class "w-full cursor-pointer flex flex-row items-center justify-center p-3 gap-2 rounded bg-stone-200 text-black hover:bg-stone-300 transition" (a :href href :class "w-full cursor-pointer flex flex-row items-center justify-center p-3 gap-2 rounded bg-stone-200 text-black hover:bg-stone-300 transition"
(i :class "fa-solid fa-key") (span "sign in or register to checkout")))) (i :class "fa-solid fa-key") (span "sign in or register to checkout"))))
(defcomp ~cart-summary-panel (&key (item-count :as string) (subtotal :as string) checkout) (defcomp ~summary/panel (&key (item-count :as string) (subtotal :as string) checkout)
(aside :id "cart-summary" :class "lg:pl-2" (aside :id "cart-summary" :class "lg:pl-2"
(div :class "rounded-2xl bg-white shadow-sm border border-stone-200 p-4 sm:p-5" (div :class "rounded-2xl bg-white shadow-sm border border-stone-200 p-4 sm:p-5"
(h2 :class "text-sm sm:text-base font-semibold text-stone-900 mb-3 sm:mb-4" "Order summary") (h2 :class "text-sm sm:text-base font-semibold text-stone-900 mb-3 sm:mb-4" "Order summary")

View File

@@ -1,12 +1,12 @@
;; Cart ticket components ;; Cart ticket components
(defcomp ~cart-ticket-type-name (&key (name :as string)) (defcomp ~tickets/type-name (&key (name :as string))
(p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" name)) (p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" name))
(defcomp ~cart-ticket-type-hidden (&key (value :as string)) (defcomp ~tickets/type-hidden (&key (value :as string))
(input :type "hidden" :name "ticket_type_id" :value value)) (input :type "hidden" :name "ticket_type_id" :value value))
(defcomp ~cart-ticket-article (&key (name :as string) type-name (date-str :as string) (price :as string) (qty-url :as string) (csrf :as string) (entry-id :as string) type-hidden (minus :as string) (qty :as string) (plus :as string) (line-total :as string)) (defcomp ~tickets/article (&key (name :as string) type-name (date-str :as string) (price :as string) (qty-url :as string) (csrf :as string) (entry-id :as string) type-hidden (minus :as string) (qty :as string) (plus :as string) (line-total :as string))
(article :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4" (article :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3" (div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3"
@@ -35,7 +35,7 @@
(div :class "flex items-center justify-between sm:justify-end gap-3" (div :class "flex items-center justify-between sm:justify-end gap-3"
(p :class "text-sm sm:text-base font-semibold text-stone-900" line-total)))))) (p :class "text-sm sm:text-base font-semibold text-stone-900" line-total))))))
(defcomp ~cart-tickets-section (&key items) (defcomp ~tickets/section (&key items)
(div :class "mt-6 border-t border-stone-200 pt-4" (div :class "mt-6 border-t border-stone-200 pt-4"
(h2 :class "text-base font-semibold mb-2" (h2 :class "text-base font-semibold mb-2"
(i :class "fa fa-ticket mr-1" :aria-hidden "true") " Event tickets") (i :class "fa fa-ticket mr-1" :aria-hidden "true") " Event tickets")

View File

@@ -6,7 +6,7 @@
:auth :public :auth :public
:layout :root :layout :root
:data (service "cart-page" "overview-data") :data (service "cart-page" "overview-data")
:content (~cart-overview-content :content (~overview/content
:page-groups page-groups :page-groups page-groups
:cart-url-base cart-url-base)) :cart-url-base cart-url-base))
@@ -15,11 +15,11 @@
:auth :public :auth :public
:layout :cart-page :layout :cart-page
:data (service "cart-page" "page-cart-data") :data (service "cart-page" "page-cart-data")
:content (~cart-page-cart-content :content (~items/page-cart-content
:cart-items cart-items :cart-items cart-items
:cal-entries cal-entries :cal-entries cal-entries
:ticket-groups ticket-groups :ticket-groups ticket-groups
:summary (~cart-summary-from-data :summary (~items/summary-from-data
:item-count (get summary "item_count") :item-count (get summary "item_count")
:grand-total (get summary "grand_total") :grand-total (get summary "grand_total")
:symbol (get summary "symbol") :symbol (get summary "symbol")
@@ -33,12 +33,12 @@
:auth :admin :auth :admin
:layout :cart-admin :layout :cart-admin
:data (service "cart-page" "admin-data") :data (service "cart-page" "admin-data")
:content (~cart-admin-content)) :content (~payments/admin-content))
(defpage cart-payments (defpage cart-payments
:path "/<page_slug>/admin/payments/" :path "/<page_slug>/admin/payments/"
:auth :admin :auth :admin
:layout (:cart-admin :selected "payments") :layout (:cart-admin :selected "payments")
:data (service "cart-page" "payments-admin-data") :data (service "cart-page" "payments-admin-data")
:content (~cart-payments-content :content (~payments/content
:page-config page-config)) :page-config page-config))

View File

@@ -1,6 +1,6 @@
;; Events admin components ;; Events admin components
(defcomp ~events-calendar-admin-panel (&key description-content csrf description) (defcomp ~admin/calendar-admin-panel (&key description-content csrf description)
(section :class "max-w-3xl mx-auto p-4 space-y-10" (section :class "max-w-3xl mx-auto p-4 space-y-10"
(div (div
(h2 :class "text-xl font-semibold" "Calendar configuration") (h2 :class "text-xl font-semibold" "Calendar configuration")
@@ -19,45 +19,45 @@
(div (button :class "px-3 py-2 rounded bg-stone-800 text-white" "Save")))) (div (button :class "px-3 py-2 rounded bg-stone-800 text-white" "Save"))))
(hr :class "border-stone-200"))) (hr :class "border-stone-200")))
(defcomp ~events-entry-admin-link (&key href) (defcomp ~admin/entry-admin-link (&key href)
(a :href href :class "inline-flex items-center gap-1 px-2 py-1 text-xs text-stone-500 hover:text-stone-700 hover:bg-stone-100 rounded" (a :href href :class "inline-flex items-center gap-1 px-2 py-1 text-xs text-stone-500 hover:text-stone-700 hover:bg-stone-100 rounded"
(i :class "fa fa-cog" :aria-hidden "true") " Admin")) (i :class "fa fa-cog" :aria-hidden "true") " Admin"))
(defcomp ~events-entry-field (&key label content) (defcomp ~admin/entry-field (&key label content)
(div :class "flex flex-col mb-4" (div :class "flex flex-col mb-4"
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label) (div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label)
content)) content))
(defcomp ~events-entry-name-field (&key name) (defcomp ~admin/entry-name-field (&key name)
(div :class "mt-1 text-lg font-medium" name)) (div :class "mt-1 text-lg font-medium" name))
(defcomp ~events-entry-slot-assigned (&key slot-name flex-label) (defcomp ~admin/entry-slot-assigned (&key slot-name flex-label)
(div :class "mt-1" (div :class "mt-1"
(span :class "px-2 py-1 rounded text-sm bg-blue-100 text-blue-700" slot-name) (span :class "px-2 py-1 rounded text-sm bg-blue-100 text-blue-700" slot-name)
(span :class "ml-2 text-xs text-stone-500" flex-label))) (span :class "ml-2 text-xs text-stone-500" flex-label)))
(defcomp ~events-entry-slot-none () (defcomp ~admin/entry-slot-none ()
(div :class "mt-1" (span :class "text-sm text-stone-400" "No slot assigned"))) (div :class "mt-1" (span :class "text-sm text-stone-400" "No slot assigned")))
(defcomp ~events-entry-time-field (&key time-str) (defcomp ~admin/entry-time-field (&key time-str)
(div :class "mt-1" time-str)) (div :class "mt-1" time-str))
(defcomp ~events-entry-state-field (&key entry-id badge) (defcomp ~admin/entry-state-field (&key entry-id badge)
(div :class "mt-1" (div :id (str "entry-state-" entry-id) badge))) (div :class "mt-1" (div :id (str "entry-state-" entry-id) badge)))
(defcomp ~events-entry-cost-field (&key cost) (defcomp ~admin/entry-cost-field (&key cost)
(div :class "mt-1" (span :class "font-medium text-green-600" cost))) (div :class "mt-1" (span :class "font-medium text-green-600" cost)))
(defcomp ~events-entry-tickets-field (&key entry-id tickets-config) (defcomp ~admin/entry-tickets-field (&key entry-id tickets-config)
(div :class "mt-1" :id (str "entry-tickets-" entry-id) tickets-config)) (div :class "mt-1" :id (str "entry-tickets-" entry-id) tickets-config))
(defcomp ~events-entry-date-field (&key date-str) (defcomp ~admin/entry-date-field (&key date-str)
(div :class "mt-1" date-str)) (div :class "mt-1" date-str))
(defcomp ~events-entry-posts-field (&key entry-id posts-panel) (defcomp ~admin/entry-posts-field (&key entry-id posts-panel)
(div :class "mt-1" :id (str "entry-posts-" entry-id) posts-panel)) (div :class "mt-1" :id (str "entry-posts-" entry-id) posts-panel))
(defcomp ~events-entry-panel (&key entry-id list-container name slot time state cost (defcomp ~admin/entry-panel (&key entry-id list-container name slot time state cost
tickets buy date posts options pre-action edit-url) tickets buy date posts options pre-action edit-url)
(section :id (str "entry-" entry-id) :class list-container (section :id (str "entry-" entry-id) :class list-container
name slot time state cost name slot time state cost
@@ -68,21 +68,21 @@
:sx-get edit-url :sx-target (str "#entry-" entry-id) :sx-swap "outerHTML" :sx-get edit-url :sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
"Edit")))) "Edit"))))
(defcomp ~events-entry-title (&key name badge) (defcomp ~admin/entry-title (&key name badge)
(<> (i :class "fa fa-clock") " " name " " badge)) (<> (i :class "fa fa-clock") " " name " " badge))
(defcomp ~events-entry-times (&key time-str) (defcomp ~admin/entry-times (&key time-str)
(div :class "text-sm text-gray-600" time-str)) (div :class "text-sm text-gray-600" time-str))
(defcomp ~events-entry-optioned-oob (&key entry-id title state) (defcomp ~admin/entry-optioned-oob (&key entry-id title state)
(<> (div :id (str "entry-title-" entry-id) :sx-swap-oob "innerHTML" title) (<> (div :id (str "entry-title-" entry-id) :sx-swap-oob "innerHTML" title)
(div :id (str "entry-state-" entry-id) :sx-swap-oob "innerHTML" state))) (div :id (str "entry-state-" entry-id) :sx-swap-oob "innerHTML" state)))
(defcomp ~events-entry-options (&key entry-id buttons) (defcomp ~admin/entry-options (&key entry-id buttons)
(div :id (str "calendar_entry_options_" entry-id) :class "flex flex-col md:flex-row gap-1" (div :id (str "calendar_entry_options_" entry-id) :class "flex flex-col md:flex-row gap-1"
buttons)) buttons))
(defcomp ~events-entry-option-button (&key url target csrf btn-type action-btn confirm-title confirm-text (defcomp ~admin/entry-option-button (&key url target csrf btn-type action-btn confirm-title confirm-text
label is-btn) label is-btn)
(form :sx-post url :sx-select target :sx-target target :sx-swap "outerHTML" (form :sx-post url :sx-select target :sx-target target :sx-swap "outerHTML"
:sx-trigger (if is-btn "confirmed" nil) :sx-trigger (if is-btn "confirmed" nil)

View File

@@ -1,34 +1,34 @@
;; Events calendar components ;; Events calendar components
(defcomp ~events-calendar-nav-arrow (&key (pill-cls :as string) (href :as string) (label :as string)) (defcomp ~calendar/nav-arrow (&key (pill-cls :as string) (href :as string) (label :as string))
(a :class (str pill-cls " text-xl") :href href (a :class (str pill-cls " text-xl") :href href
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label)) :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label))
(defcomp ~events-calendar-month-label (&key (month-name :as string) (year :as string)) (defcomp ~calendar/month-label (&key (month-name :as string) (year :as string))
(div :class "px-3 font-medium" (str month-name " " year))) (div :class "px-3 font-medium" (str month-name " " year)))
(defcomp ~events-calendar-weekday (&key (name :as string)) (defcomp ~calendar/weekday (&key (name :as string))
(div :class "py-1" name)) (div :class "py-1" name))
(defcomp ~events-calendar-day-short (&key (day-str :as string)) (defcomp ~calendar/day-short (&key (day-str :as string))
(span :class "sm:hidden text-[16px] text-stone-500" day-str)) (span :class "sm:hidden text-[16px] text-stone-500" day-str))
(defcomp ~events-calendar-day-num (&key (pill-cls :as string) (href :as string) (num :as string)) (defcomp ~calendar/day-num (&key (pill-cls :as string) (href :as string) (num :as string))
(a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" num)) :sx-swap "outerHTML" :sx-push-url "true" num))
(defcomp ~events-calendar-entry-badge (&key (bg-cls :as string) (name :as string) (state-label :as string)) (defcomp ~calendar/entry-badge (&key (bg-cls :as string) (name :as string) (state-label :as string))
(div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls) (div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls)
(span :class "truncate" name) (span :class "truncate" name)
(span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label))) (span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label)))
(defcomp ~events-calendar-cell (&key (cell-cls :as string) day-short day-num badges) (defcomp ~calendar/cell (&key (cell-cls :as string) day-short day-num badges)
(div :class cell-cls (div :class cell-cls
(div :class "flex justify-between items-center" (div :class "flex justify-between items-center"
(div :class "flex flex-col" day-short day-num)) (div :class "flex flex-col" day-short day-num))
(div :class "mt-1 space-y-0.5" badges))) (div :class "mt-1 space-y-0.5" badges)))
(defcomp ~events-calendar-grid (&key arrows weekdays cells) (defcomp ~calendar/grid (&key arrows weekdays cells)
(section :class "bg-orange-100" (section :class "bg-orange-100"
(header :class "flex items-center justify-center mt-2" (header :class "flex items-center justify-center mt-2"
(nav :class "flex items-center gap-2 text-2xl" arrows)) (nav :class "flex items-center gap-2 text-2xl" arrows))
@@ -37,36 +37,36 @@
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells)))) (div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells))))
;; Calendar grid from data — all iteration in sx ;; Calendar grid from data — all iteration in sx
(defcomp ~events-calendar-grid-from-data (&key (pill-cls :as string) (month-name :as string) (year :as string) (defcomp ~calendar/grid-from-data (&key (pill-cls :as string) (month-name :as string) (year :as string)
(prev-year-href :as string) (prev-month-href :as string) (prev-year-href :as string) (prev-month-href :as string)
(next-month-href :as string) (next-year-href :as string) (next-month-href :as string) (next-year-href :as string)
(weekday-names :as list) (cells :as list)) (weekday-names :as list) (cells :as list))
(~events-calendar-grid (~calendar/grid
:arrows (<> :arrows (<>
(~events-calendar-nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab") (~calendar/nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab")
(~events-calendar-nav-arrow :pill-cls pill-cls :href prev-month-href :label "\u2039") (~calendar/nav-arrow :pill-cls pill-cls :href prev-month-href :label "\u2039")
(~events-calendar-month-label :month-name month-name :year year) (~calendar/month-label :month-name month-name :year year)
(~events-calendar-nav-arrow :pill-cls pill-cls :href next-month-href :label "\u203a") (~calendar/nav-arrow :pill-cls pill-cls :href next-month-href :label "\u203a")
(~events-calendar-nav-arrow :pill-cls pill-cls :href next-year-href :label "\u00bb")) (~calendar/nav-arrow :pill-cls pill-cls :href next-year-href :label "\u00bb"))
:weekdays (<> (map (lambda (wd) (~events-calendar-weekday :name wd)) :weekdays (<> (map (lambda (wd) (~calendar/weekday :name wd))
(or weekday-names (list)))) (or weekday-names (list))))
:cells (<> (map (lambda (cell) :cells (<> (map (lambda (cell)
(~events-calendar-cell (~calendar/cell
:cell-cls (get cell "cell-cls") :cell-cls (get cell "cell-cls")
:day-short (when (get cell "day-str") :day-short (when (get cell "day-str")
(~events-calendar-day-short :day-str (get cell "day-str"))) (~calendar/day-short :day-str (get cell "day-str")))
:day-num (when (get cell "day-href") :day-num (when (get cell "day-href")
(~events-calendar-day-num :pill-cls pill-cls (~calendar/day-num :pill-cls pill-cls
:href (get cell "day-href") :num (get cell "day-num"))) :href (get cell "day-href") :num (get cell "day-num")))
:badges (when (get cell "badges") :badges (when (get cell "badges")
(<> (map (lambda (b) (<> (map (lambda (b)
(~events-calendar-entry-badge (~calendar/entry-badge
:bg-cls (get b "bg-cls") :name (get b "name") :bg-cls (get b "bg-cls") :name (get b "name")
:state-label (get b "state-label"))) :state-label (get b "state-label")))
(get cell "badges")))))) (get cell "badges"))))))
(or cells (list)))))) (or cells (list))))))
(defcomp ~events-calendar-description-display (&key (description :as string?) (edit-url :as string)) (defcomp ~calendar/description-display (&key (description :as string?) (edit-url :as string))
(div :id "calendar-description" (div :id "calendar-description"
(if description (if description
(p :class "text-stone-700 whitespace-pre-line break-all" description) (p :class "text-stone-700 whitespace-pre-line break-all" description)
@@ -75,12 +75,12 @@
:sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML" :sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML"
(i :class "fas fa-edit")))) (i :class "fas fa-edit"))))
(defcomp ~events-calendar-description-title-oob (&key (description :as string)) (defcomp ~calendar/description-title-oob (&key (description :as string))
(div :id "calendar-description-title" :sx-swap-oob "outerHTML" (div :id "calendar-description-title" :sx-swap-oob "outerHTML"
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block" :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
description)) description))
(defcomp ~events-calendar-description-edit-form (&key (save-url :as string) (cancel-url :as string) (csrf :as string) (description :as string?)) (defcomp ~calendar/description-edit-form (&key (save-url :as string) (cancel-url :as string) (csrf :as string) (description :as string?))
(div :id "calendar-description" (div :id "calendar-description"
(form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML" (form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)

View File

@@ -1,18 +1,18 @@
;; Events day components ;; Events day components
(defcomp ~events-day-entry-link (&key (href :as string) (name :as string) (time-str :as string)) (defcomp ~day/entry-link (&key (href :as string) (name :as string) (time-str :as string))
(a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0" (a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" time-str)))) (div :class "text-xs text-stone-600 truncate" time-str))))
(defcomp ~events-day-entries-nav (&key inner) (defcomp ~day/entries-nav (&key inner)
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id "day-entries-nav-wrapper" :id "day-entries-nav-wrapper"
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" (div :class "flex overflow-x-auto gap-1 scrollbar-thin"
inner))) inner)))
(defcomp ~events-day-table (&key (list-container :as string) rows (pre-action :as string) (add-url :as string)) (defcomp ~day/table (&key (list-container :as string) rows (pre-action :as string) (add-url :as string))
(section :id "day-entries" :class list-container (section :id "day-entries" :class list-container
(table :class "w-full text-sm border table-fixed" (table :class "w-full text-sm border table-fixed"
(thead :class "bg-stone-100" (thead :class "bg-stone-100"
@@ -29,95 +29,95 @@
:sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML" :sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
"+ Add entry")))) "+ Add entry"))))
(defcomp ~events-day-empty-row () (defcomp ~day/empty-row ()
(tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet."))) (tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet.")))
(defcomp ~events-day-row-name (&key (href :as string) (pill-cls :as string) (name :as string)) (defcomp ~day/row-name (&key (href :as string) (pill-cls :as string) (name :as string))
(td :class "p-2 align-top w-2/6" (div :class "font-medium" (td :class "p-2 align-top w-2/6" (div :class "font-medium"
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" name)))) :sx-swap "outerHTML" :sx-push-url "true" name))))
(defcomp ~events-day-row-slot (&key (href :as string) (pill-cls :as string) (slot-name :as string) (time-str :as string)) (defcomp ~day/row-slot (&key (href :as string) (pill-cls :as string) (slot-name :as string) (time-str :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium" (td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium"
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel" (a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" slot-name) :sx-swap "outerHTML" :sx-push-url "true" slot-name)
(span :class "text-stone-600 font-normal" time-str)))) (span :class "text-stone-600 font-normal" time-str))))
(defcomp ~events-day-row-time (&key (start :as string) (end :as string)) (defcomp ~day/row-time (&key (start :as string) (end :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end)))) (td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end))))
(defcomp ~events-day-row-state (&key (state-id :as string) badge) (defcomp ~day/row-state (&key (state-id :as string) badge)
(td :class "p-2 align-top w-1/6" (div :id state-id badge))) (td :class "p-2 align-top w-1/6" (div :id state-id badge)))
(defcomp ~events-day-row-cost (&key (cost-str :as string)) (defcomp ~day/row-cost (&key (cost-str :as string))
(td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str))) (td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str)))
(defcomp ~events-day-row-tickets (&key (price-str :as string) (count-str :as string)) (defcomp ~day/row-tickets (&key (price-str :as string) (count-str :as string))
(td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1" (td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1"
(div :class "font-medium text-green-600" price-str) (div :class "font-medium text-green-600" price-str)
(div :class "text-stone-600" count-str)))) (div :class "text-stone-600" count-str))))
(defcomp ~events-day-row-no-tickets () (defcomp ~day/row-no-tickets ()
(td :class "p-2 align-top w-1/6" (span :class "text-xs text-stone-400" "No tickets"))) (td :class "p-2 align-top w-1/6" (span :class "text-xs text-stone-400" "No tickets")))
(defcomp ~events-day-row-actions () (defcomp ~day/row-actions ()
(td :class "p-2 align-top w-1/6")) (td :class "p-2 align-top w-1/6"))
(defcomp ~events-day-row (&key (tr-cls :as string) name slot state cost tickets actions) (defcomp ~day/row (&key (tr-cls :as string) name slot state cost tickets actions)
(tr :class tr-cls name slot state cost tickets actions)) (tr :class tr-cls name slot state cost tickets actions))
(defcomp ~events-day-admin-panel () (defcomp ~day/admin-panel ()
(div :class "p-4 text-sm text-stone-500" "Admin options")) (div :class "p-4 text-sm text-stone-500" "Admin options"))
(defcomp ~events-day-entries-nav-oob-empty () (defcomp ~day/entries-nav-oob-empty ()
(div :id "day-entries-nav-wrapper" :sx-swap-oob "true")) (div :id "day-entries-nav-wrapper" :sx-swap-oob "true"))
(defcomp ~events-day-entries-nav-oob (&key items) (defcomp ~day/entries-nav-oob (&key items)
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id "day-entries-nav-wrapper" :sx-swap-oob "true" :id "day-entries-nav-wrapper" :sx-swap-oob "true"
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items))) (div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
(defcomp ~events-day-nav-entry (&key (href :as string) (nav-btn :as string) (name :as string) (time-str :as string)) (defcomp ~day/nav-entry (&key (href :as string) (nav-btn :as string) (name :as string) (time-str :as string))
(a :href href :class nav-btn (a :href href :class nav-btn
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" time-str)))) (div :class "text-xs text-stone-600 truncate" time-str))))
;; Day table from data — all row iteration in sx ;; Day table from data — all row iteration in sx
(defcomp ~events-day-table-from-data (&key (list-container :as string) (pre-action :as string) (add-url :as string) (tr-cls :as string) (pill-cls :as string) (rows :as list?)) (defcomp ~day/table-from-data (&key (list-container :as string) (pre-action :as string) (add-url :as string) (tr-cls :as string) (pill-cls :as string) (rows :as list?))
(~events-day-table (~day/table
:list-container list-container :list-container list-container
:rows (if (empty? (or rows (list))) :rows (if (empty? (or rows (list)))
(~events-day-empty-row) (~day/empty-row)
(<> (map (lambda (r) (<> (map (lambda (r)
(~events-day-row (~day/row
:tr-cls tr-cls :tr-cls tr-cls
:name (~events-day-row-name :name (~day/row-name
:href (get r "href") :pill-cls pill-cls :name (get r "name")) :href (get r "href") :pill-cls pill-cls :name (get r "name"))
:slot (if (get r "slot-name") :slot (if (get r "slot-name")
(~events-day-row-slot (~day/row-slot
:href (get r "slot-href") :pill-cls pill-cls :href (get r "slot-href") :pill-cls pill-cls
:slot-name (get r "slot-name") :time-str (get r "slot-time")) :slot-name (get r "slot-name") :time-str (get r "slot-time"))
(~events-day-row-time :start (get r "start") :end (get r "end"))) (~day/row-time :start (get r "start") :end (get r "end")))
:state (~events-day-row-state :state (~day/row-state
:state-id (get r "state-id") :state-id (get r "state-id")
:badge (~entry-state-badge :state (get r "state"))) :badge (~entries/entry-state-badge :state (get r "state")))
:cost (~events-day-row-cost :cost-str (get r "cost-str")) :cost (~day/row-cost :cost-str (get r "cost-str"))
:tickets (if (get r "has-tickets") :tickets (if (get r "has-tickets")
(~events-day-row-tickets (~day/row-tickets
:price-str (get r "price-str") :count-str (get r "count-str")) :price-str (get r "price-str") :count-str (get r "count-str"))
(~events-day-row-no-tickets)) (~day/row-no-tickets))
:actions (~events-day-row-actions))) :actions (~day/row-actions)))
(or rows (list))))) (or rows (list)))))
:pre-action pre-action :add-url add-url)) :pre-action pre-action :add-url add-url))
;; Day entries nav OOB from data ;; Day entries nav OOB from data
(defcomp ~events-day-entries-nav-oob-from-data (&key (nav-btn :as string) (entries :as list?)) (defcomp ~day/entries-nav-oob-from-data (&key (nav-btn :as string) (entries :as list?))
(if (empty? (or entries (list))) (if (empty? (or entries (list)))
(~events-day-entries-nav-oob-empty) (~day/entries-nav-oob-empty)
(~events-day-entries-nav-oob (~day/entries-nav-oob
:items (<> (map (lambda (e) :items (<> (map (lambda (e)
(~events-day-nav-entry (~day/nav-entry
:href (get e "href") :nav-btn nav-btn :href (get e "href") :nav-btn nav-btn
:name (get e "name") :time-str (get e "time-str"))) :name (get e "name") :time-str (get e "time-str")))
entries))))) entries)))))

View File

@@ -4,8 +4,8 @@
;; State badges — cond maps state string to class + label ;; State badges — cond maps state string to class + label
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~entry-state-badge (&key state) (defcomp ~entries/entry-state-badge (&key state)
(~badge (~shared:misc/badge
:cls (cond :cls (cond
((= state "confirmed") "bg-emerald-100 text-emerald-800") ((= state "confirmed") "bg-emerald-100 text-emerald-800")
((= state "provisional") "bg-amber-100 text-amber-800") ((= state "provisional") "bg-amber-100 text-amber-800")
@@ -21,7 +21,7 @@
((= state "declined") "Declined") ((= state "declined") "Declined")
(true (or state "Unknown"))))) (true (or state "Unknown")))))
(defcomp ~entry-state-badge-lg (&key state) (defcomp ~entries/entry-state-badge-lg (&key state)
(span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium " (span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium "
(cond (cond
((= state "confirmed") "bg-emerald-100 text-emerald-800") ((= state "confirmed") "bg-emerald-100 text-emerald-800")
@@ -38,8 +38,8 @@
((= state "declined") "Declined") ((= state "declined") "Declined")
(true (or state "Unknown"))))) (true (or state "Unknown")))))
(defcomp ~ticket-state-badge (&key state) (defcomp ~entries/ticket-state-badge (&key state)
(~badge (~shared:misc/badge
:cls (cond :cls (cond
((= state "confirmed") "bg-emerald-100 text-emerald-800") ((= state "confirmed") "bg-emerald-100 text-emerald-800")
((= state "checked_in") "bg-blue-100 text-blue-800") ((= state "checked_in") "bg-blue-100 text-blue-800")
@@ -53,7 +53,7 @@
((= state "cancelled") "Cancelled") ((= state "cancelled") "Cancelled")
(true (or state "Unknown"))))) (true (or state "Unknown")))))
(defcomp ~ticket-state-badge-lg (&key state) (defcomp ~entries/ticket-state-badge-lg (&key state)
(span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium " (span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium "
(cond (cond
((= state "confirmed") "bg-emerald-100 text-emerald-800") ((= state "confirmed") "bg-emerald-100 text-emerald-800")
@@ -73,36 +73,36 @@
;; Entry card components ;; Entry card components
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-entry-title-linked (&key href name) (defcomp ~entries/entry-title-linked (&key href name)
(a :href href :class "hover:text-emerald-700" (a :href href :class "hover:text-emerald-700"
(h2 :class "text-lg font-semibold text-stone-900" name))) (h2 :class "text-lg font-semibold text-stone-900" name)))
(defcomp ~events-entry-title-plain (&key name) (defcomp ~entries/entry-title-plain (&key name)
(h2 :class "text-lg font-semibold text-stone-900" name)) (h2 :class "text-lg font-semibold text-stone-900" name))
(defcomp ~events-entry-title-tile-linked (&key href name) (defcomp ~entries/entry-title-tile-linked (&key href name)
(a :href href :class "hover:text-emerald-700" (a :href href :class "hover:text-emerald-700"
(h2 :class "text-base font-semibold text-stone-900 line-clamp-2" name))) (h2 :class "text-base font-semibold text-stone-900 line-clamp-2" name)))
(defcomp ~events-entry-title-tile-plain (&key name) (defcomp ~entries/entry-title-tile-plain (&key name)
(h2 :class "text-base font-semibold text-stone-900 line-clamp-2" name)) (h2 :class "text-base font-semibold text-stone-900 line-clamp-2" name))
(defcomp ~events-entry-page-badge (&key href title) (defcomp ~entries/entry-page-badge (&key href title)
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" title)) (a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" title))
(defcomp ~events-entry-cal-badge (&key name) (defcomp ~entries/entry-cal-badge (&key name)
(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700" name)) (span :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700" name))
(defcomp ~events-entry-time-linked (&key href date-str) (defcomp ~entries/entry-time-linked (&key href date-str)
(<> (a :href href :class "hover:text-stone-700" date-str) " · ")) (<> (a :href href :class "hover:text-stone-700" date-str) " · "))
(defcomp ~events-entry-time-plain (&key date-str) (defcomp ~entries/entry-time-plain (&key date-str)
(<> (span date-str) " · ")) (<> (span date-str) " · "))
(defcomp ~events-entry-cost (&key cost) (defcomp ~entries/entry-cost (&key cost)
(div :class "mt-1 text-sm font-medium text-green-600" cost)) (div :class "mt-1 text-sm font-medium text-green-600" cost))
(defcomp ~events-entry-card (&key title badges time-parts cost widget) (defcomp ~entries/entry-card (&key title badges time-parts cost widget)
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-4" (article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-4"
(div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-3" (div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-3"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -112,7 +112,7 @@
cost) cost)
widget))) widget)))
(defcomp ~events-entry-card-tile (&key title badges time cost widget) (defcomp ~entries/entry-card-tile (&key title badges time cost widget)
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 overflow-hidden" (article :class "rounded-xl bg-white shadow-sm border border-stone-200 overflow-hidden"
(div :class "p-3" (div :class "p-3"
title title
@@ -121,20 +121,20 @@
cost) cost)
widget)) widget))
(defcomp ~events-entry-tile-widget-wrapper (&key widget) (defcomp ~entries/entry-tile-widget-wrapper (&key widget)
(div :class "border-t border-stone-100 px-3 py-2" widget)) (div :class "border-t border-stone-100 px-3 py-2" widget))
(defcomp ~events-entry-widget-wrapper (&key widget) (defcomp ~entries/entry-widget-wrapper (&key widget)
(div :class "shrink-0" widget)) (div :class "shrink-0" widget))
(defcomp ~events-date-separator (&key date-str) (defcomp ~entries/date-separator (&key date-str)
(div :class "pt-2 pb-1" (div :class "pt-2 pb-1"
(h3 :class "text-sm font-semibold text-stone-500 uppercase tracking-wide" date-str))) (h3 :class "text-sm font-semibold text-stone-500 uppercase tracking-wide" date-str)))
(defcomp ~events-grid (&key grid-cls cards) (defcomp ~entries/grid (&key grid-cls cards)
(div :class grid-cls cards)) (div :class grid-cls cards))
(defcomp ~events-main-panel-body (&key toggle body) (defcomp ~entries/main-panel-body (&key toggle body)
(<> toggle body (div :class "pb-8"))) (<> toggle body (div :class "pb-8")))
@@ -143,46 +143,46 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Ticket widget from data — replaces _ticket_widget_html Python composition ;; Ticket widget from data — replaces _ticket_widget_html Python composition
(defcomp ~events-tw-widget-from-data (&key entry-id price qty ticket-url csrf) (defcomp ~entries/tw-widget-from-data (&key entry-id price qty ticket-url csrf)
(~events-tw-widget :entry-id (str entry-id) :price price (~page/tw-widget :entry-id (str entry-id) :price price
:inner (if (= (or qty 0) 0) :inner (if (= (or qty 0) 0)
(~events-tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id) (~page/tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id)
:csrf csrf :entry-id (str entry-id) :count-val "1" :csrf csrf :entry-id (str entry-id) :count-val "1"
:btn (~events-tw-cart-plus)) :btn (~page/tw-cart-plus))
(<> (<>
(~events-tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id) (~page/tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id)
:csrf csrf :entry-id (str entry-id) :count-val (str (- qty 1)) :csrf csrf :entry-id (str entry-id) :count-val (str (- qty 1))
:btn (~events-tw-minus)) :btn (~page/tw-minus))
(~events-tw-cart-icon :qty (str qty)) (~page/tw-cart-icon :qty (str qty))
(~events-tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id) (~page/tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id)
:csrf csrf :entry-id (str entry-id) :count-val (str (+ qty 1)) :csrf csrf :entry-id (str entry-id) :count-val (str (+ qty 1))
:btn (~events-tw-plus)))))) :btn (~page/tw-plus))))))
;; Entry card (list view) from data ;; Entry card (list view) from data
(defcomp ~events-entry-card-from-data (&key entry-href name day-href (defcomp ~entries/entry-card-from-data (&key entry-href name day-href
page-badge-href page-badge-title cal-name page-badge-href page-badge-title cal-name
date-str start-time end-time is-page-scoped date-str start-time end-time is-page-scoped
cost has-ticket ticket-data) cost has-ticket ticket-data)
(~events-entry-card (~entries/entry-card
:title (if entry-href :title (if entry-href
(~events-entry-title-linked :href entry-href :name name) (~entries/entry-title-linked :href entry-href :name name)
(~events-entry-title-plain :name name)) (~entries/entry-title-plain :name name))
:badges (<> :badges (<>
(when page-badge-title (when page-badge-title
(~events-entry-page-badge :href page-badge-href :title page-badge-title)) (~entries/entry-page-badge :href page-badge-href :title page-badge-title))
(when cal-name (when cal-name
(~events-entry-cal-badge :name cal-name))) (~entries/entry-cal-badge :name cal-name)))
:time-parts (<> :time-parts (<>
(when (and day-href (not is-page-scoped)) (when (and day-href (not is-page-scoped))
(~events-entry-time-linked :href day-href :date-str date-str)) (~entries/entry-time-linked :href day-href :date-str date-str))
(when (and (not day-href) (not is-page-scoped) date-str) (when (and (not day-href) (not is-page-scoped) date-str)
(~events-entry-time-plain :date-str date-str)) (~entries/entry-time-plain :date-str date-str))
start-time start-time
(when end-time (str " \u2013 " end-time))) (when end-time (str " \u2013 " end-time)))
:cost (when cost (~events-entry-cost :cost cost)) :cost (when cost (~entries/entry-cost :cost cost))
:widget (when has-ticket :widget (when has-ticket
(~events-entry-widget-wrapper (~entries/entry-widget-wrapper
:widget (~events-tw-widget-from-data :widget (~entries/tw-widget-from-data
:entry-id (get ticket-data "entry-id") :entry-id (get ticket-data "entry-id")
:price (get ticket-data "price") :price (get ticket-data "price")
:qty (get ticket-data "qty") :qty (get ticket-data "qty")
@@ -190,24 +190,24 @@
:csrf (get ticket-data "csrf")))))) :csrf (get ticket-data "csrf"))))))
;; Entry card (tile view) from data ;; Entry card (tile view) from data
(defcomp ~events-entry-card-tile-from-data (&key entry-href name day-href (defcomp ~entries/entry-card-tile-from-data (&key entry-href name day-href
page-badge-href page-badge-title cal-name page-badge-href page-badge-title cal-name
date-str time-str date-str time-str
cost has-ticket ticket-data) cost has-ticket ticket-data)
(~events-entry-card-tile (~entries/entry-card-tile
:title (if entry-href :title (if entry-href
(~events-entry-title-tile-linked :href entry-href :name name) (~entries/entry-title-tile-linked :href entry-href :name name)
(~events-entry-title-tile-plain :name name)) (~entries/entry-title-tile-plain :name name))
:badges (<> :badges (<>
(when page-badge-title (when page-badge-title
(~events-entry-page-badge :href page-badge-href :title page-badge-title)) (~entries/entry-page-badge :href page-badge-href :title page-badge-title))
(when cal-name (when cal-name
(~events-entry-cal-badge :name cal-name))) (~entries/entry-cal-badge :name cal-name)))
:time time-str :time time-str
:cost (when cost (~events-entry-cost :cost cost)) :cost (when cost (~entries/entry-cost :cost cost))
:widget (when has-ticket :widget (when has-ticket
(~events-entry-tile-widget-wrapper (~entries/entry-tile-widget-wrapper
:widget (~events-tw-widget-from-data :widget (~entries/tw-widget-from-data
:entry-id (get ticket-data "entry-id") :entry-id (get ticket-data "entry-id")
:price (get ticket-data "price") :price (get ticket-data "price")
:qty (get ticket-data "qty") :qty (get ticket-data "qty")
@@ -215,13 +215,13 @@
:csrf (get ticket-data "csrf")))))) :csrf (get ticket-data "csrf"))))))
;; Entry cards list (with date separators + sentinel) from data ;; Entry cards list (with date separators + sentinel) from data
(defcomp ~events-entry-cards-from-data (&key items view page has-more next-url) (defcomp ~entries/entry-cards-from-data (&key items view page has-more next-url)
(<> (<>
(map (lambda (item) (map (lambda (item)
(if (get item "is-separator") (if (get item "is-separator")
(~events-date-separator :date-str (get item "date-str")) (~entries/date-separator :date-str (get item "date-str"))
(if (= view "tile") (if (= view "tile")
(~events-entry-card-tile-from-data (~entries/entry-card-tile-from-data
:entry-href (get item "entry-href") :name (get item "name") :entry-href (get item "entry-href") :name (get item "name")
:day-href (get item "day-href") :day-href (get item "day-href")
:page-badge-href (get item "page-badge-href") :page-badge-href (get item "page-badge-href")
@@ -230,7 +230,7 @@
:date-str (get item "date-str") :time-str (get item "time-str") :date-str (get item "date-str") :time-str (get item "time-str")
:cost (get item "cost") :has-ticket (get item "has-ticket") :cost (get item "cost") :has-ticket (get item "has-ticket")
:ticket-data (get item "ticket-data")) :ticket-data (get item "ticket-data"))
(~events-entry-card-from-data (~entries/entry-card-from-data
:entry-href (get item "entry-href") :name (get item "name") :entry-href (get item "entry-href") :name (get item "name")
:day-href (get item "day-href") :day-href (get item "day-href")
:page-badge-href (get item "page-badge-href") :page-badge-href (get item "page-badge-href")
@@ -243,20 +243,20 @@
:ticket-data (get item "ticket-data"))))) :ticket-data (get item "ticket-data")))))
(or items (list))) (or items (list)))
(when has-more (when has-more
(~sentinel-simple :id (str "sentinel-" page) :next-url next-url)))) (~shared:misc/sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
;; Events main panel (toggle + cards grid) from data ;; Events main panel (toggle + cards grid) from data
(defcomp ~events-main-panel-from-data (&key toggle items view page has-more next-url) (defcomp ~entries/main-panel-from-data (&key toggle items view page has-more next-url)
(~events-main-panel-body (~entries/main-panel-body
:toggle toggle :toggle toggle
:body (if items :body (if items
(~events-grid (~entries/grid
:grid-cls (if (= view "tile") :grid-cls (if (= view "tile")
"max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
"max-w-full px-3 py-3 space-y-3") "max-w-full px-3 py-3 space-y-3")
:cards (~events-entry-cards-from-data :cards (~entries/entry-cards-from-data
:items items :view view :page page :items items :view view :page page
:has-more has-more :next-url next-url)) :has-more has-more :next-url next-url))
(~empty-state :icon "fa fa-calendar-xmark" (~shared:misc/empty-state :icon "fa fa-calendar-xmark"
:message "No upcoming events" :message "No upcoming events"
:cls "px-3 py-12 text-center text-stone-400")))) :cls "px-3 py-12 text-center text-stone-400"))))

View File

@@ -5,25 +5,25 @@
;; Slot picker option (shared by entry-edit and entry-add) ;; Slot picker option (shared by entry-edit and entry-add)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-slot-option (&key value data-start data-end data-flexible data-cost selected label) (defcomp ~forms/slot-option (&key value data-start data-end data-flexible data-cost selected label)
(option :value value :data-start data-start :data-end data-end (option :value value :data-start data-start :data-end data-end
:data-flexible data-flexible :data-cost data-cost :data-flexible data-flexible :data-cost data-cost
:selected selected :selected selected
label)) label))
(defcomp ~events-slot-picker (&key id options) (defcomp ~forms/slot-picker (&key id options)
(select :id id :name "slot_id" :class "w-full border p-2 rounded" (select :id id :name "slot_id" :class "w-full border p-2 rounded"
:data-slot-picker "" :required "required" :data-slot-picker "" :required "required"
options)) options))
(defcomp ~events-no-slots () (defcomp ~forms/no-slots ()
(div :class "text-sm text-stone-500" "No slots defined for this day.")) (div :class "text-sm text-stone-500" "No slots defined for this day."))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Entry edit form (_types/entry/_edit.html) ;; Entry edit form (_types/entry/_edit.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-entry-edit-form (&key entry-id list-container put-url cancel-url csrf (defcomp ~forms/entry-edit-form (&key entry-id list-container put-url cancel-url csrf
name-val slot-picker name-val slot-picker
start-val end-val cost-display start-val end-val cost-display
ticket-price-val ticket-count-val ticket-price-val ticket-count-val
@@ -115,7 +115,7 @@
;; Post search results (_types/entry/_post_search_results.html) ;; Post search results (_types/entry/_post_search_results.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-post-search-item (&key post-url entry-id csrf post-id (defcomp ~forms/post-search-item (&key post-url entry-id csrf post-id
img title) img title)
(form :sx-post post-url :sx-target (str "#entry-posts-" entry-id) :sx-swap "innerHTML" (form :sx-post post-url :sx-target (str "#entry-posts-" entry-id) :sx-swap "innerHTML"
:class "p-2 hover:bg-stone-50 cursor-pointer rounded text-sm border-b" :class "p-2 hover:bg-stone-50 cursor-pointer rounded text-sm border-b"
@@ -129,7 +129,7 @@
:data-confirm-cancel-text "Cancel" :data-confirm-cancel-text "Cancel"
img (span title)))) img (span title))))
(defcomp ~events-post-search-sentinel (&key page next-url) (defcomp ~forms/post-search-sentinel (&key page next-url)
(div :id (str "post-search-sentinel-" page) (div :id (str "post-search-sentinel-" page)
:sx-get next-url :sx-get next-url
:sx-trigger "intersect once delay:250ms, sentinel:retry" :sx-trigger "intersect once delay:250ms, sentinel:retry"
@@ -172,7 +172,7 @@
(div :class "text-xs text-center text-stone-400 js-loading" "Loading more...") (div :class "text-xs text-center text-stone-400 js-loading" "Loading more...")
(div :class "text-xs text-center text-stone-400 js-neterr hidden" "Connection error. Retrying..."))) (div :class "text-xs text-center text-stone-400 js-neterr hidden" "Connection error. Retrying...")))
(defcomp ~events-post-search-end () (defcomp ~forms/post-search-end ()
(div :class "py-2 text-xs text-center text-stone-400" "End of results")) (div :class "py-2 text-xs text-center text-stone-400" "End of results"))
@@ -180,17 +180,17 @@
;; Slot edit form (_types/slot/_edit.html) ;; Slot edit form (_types/slot/_edit.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-day-checkbox (&key name label checked) (defcomp ~forms/day-checkbox (&key name label checked)
(label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-100" (label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-100"
(input :type "checkbox" :name name :value "1" :data-day name :checked checked) (input :type "checkbox" :name name :value "1" :data-day name :checked checked)
(span label))) (span label)))
(defcomp ~events-day-all-checkbox (&key checked) (defcomp ~forms/day-all-checkbox (&key checked)
(label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-200" (label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-200"
(input :type "checkbox" :data-day-all "" :checked checked) (input :type "checkbox" :data-day-all "" :checked checked)
(span "All"))) (span "All")))
(defcomp ~events-slot-edit-form (&key slot-id list-container put-url cancel-url csrf (defcomp ~forms/slot-edit-form (&key slot-id list-container put-url cancel-url csrf
name-val cost-val start-val end-val desc-val name-val cost-val start-val end-val desc-val
days flexible-checked days flexible-checked
action-btn cancel-btn) action-btn cancel-btn)
@@ -271,7 +271,7 @@
;; Slot add form (_types/slots/_add.html) ;; Slot add form (_types/slots/_add.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-slot-add-form (&key post-url csrf days action-btn cancel-btn cancel-url) (defcomp ~forms/slot-add-form (&key post-url csrf days action-btn cancel-btn cancel-url)
(form :sx-post post-url :sx-target "#slots-table" :sx-select "#slots-table" (form :sx-post post-url :sx-target "#slots-table" :sx-select "#slots-table"
:sx-disinherit "sx-select" :sx-swap "outerHTML" :sx-disinherit "sx-select" :sx-swap "outerHTML"
:sx-headers csrf :class "space-y-3" :sx-headers csrf :class "space-y-3"
@@ -312,7 +312,7 @@
:data-confirm-cancel-text "Cancel" :data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save slot")))) (i :class "fa fa-save") " Save slot"))))
(defcomp ~events-slot-add-button (&key pre-action add-url) (defcomp ~forms/slot-add-button (&key pre-action add-url)
(button :type "button" :class pre-action (button :type "button" :class pre-action
:sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML" :sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
"+ Add slot")) "+ Add slot"))
@@ -323,20 +323,20 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Day checkboxes from data — replaces Python loop ;; Day checkboxes from data — replaces Python loop
(defcomp ~events-day-checkboxes-from-data (&key days-data all-checked) (defcomp ~forms/day-checkboxes-from-data (&key days-data all-checked)
(<> (<>
(~events-day-all-checkbox :checked (when all-checked "checked")) (~forms/day-all-checkbox :checked (when all-checked "checked"))
(map (lambda (d) (map (lambda (d)
(~events-day-checkbox (~forms/day-checkbox
:name (get d "name") :name (get d "name")
:label (get d "label") :label (get d "label")
:checked (when (get d "checked") "checked"))) :checked (when (get d "checked") "checked")))
(or days-data (list))))) (or days-data (list)))))
;; Slot options from data — replaces _slot_options_html Python loop ;; Slot options from data — replaces _slot_options_html Python loop
(defcomp ~events-slot-options-from-data (&key slots) (defcomp ~forms/slot-options-from-data (&key slots)
(<> (map (lambda (s) (<> (map (lambda (s)
(~events-slot-option (~forms/slot-option
:value (get s "value") :value (get s "value")
:data-start (get s "data-start") :data-start (get s "data-start")
:data-end (get s "data-end") :data-end (get s "data-end")
@@ -347,32 +347,32 @@
(or slots (list))))) (or slots (list)))))
;; Slot picker from data — wraps picker + options ;; Slot picker from data — wraps picker + options
(defcomp ~events-slot-picker-from-data (&key id slots) (defcomp ~forms/slot-picker-from-data (&key id slots)
(if (empty? (or slots (list))) (if (empty? (or slots (list)))
(~events-no-slots) (~forms/no-slots)
(~events-slot-picker (~forms/slot-picker
:id id :id id
:options (~events-slot-options-from-data :slots slots)))) :options (~forms/slot-options-from-data :slots slots))))
;; Slot edit form from data ;; Slot edit form from data
(defcomp ~events-slot-edit-form-from-data (&key slot-id list-container put-url cancel-url csrf (defcomp ~forms/slot-edit-form-from-data (&key slot-id list-container put-url cancel-url csrf
name-val cost-val start-val end-val desc-val name-val cost-val start-val end-val desc-val
days-data all-checked flexible-checked days-data all-checked flexible-checked
action-btn cancel-btn) action-btn cancel-btn)
(~events-slot-edit-form (~forms/slot-edit-form
:slot-id slot-id :list-container list-container :slot-id slot-id :list-container list-container
:put-url put-url :cancel-url cancel-url :csrf csrf :put-url put-url :cancel-url cancel-url :csrf csrf
:name-val name-val :cost-val cost-val :start-val start-val :name-val name-val :cost-val cost-val :start-val start-val
:end-val end-val :desc-val desc-val :end-val end-val :desc-val desc-val
:days (~events-day-checkboxes-from-data :days-data days-data :all-checked all-checked) :days (~forms/day-checkboxes-from-data :days-data days-data :all-checked all-checked)
:flexible-checked flexible-checked :flexible-checked flexible-checked
:action-btn action-btn :cancel-btn cancel-btn)) :action-btn action-btn :cancel-btn cancel-btn))
;; Slot add form from data ;; Slot add form from data
(defcomp ~events-slot-add-form-from-data (&key post-url csrf days-data action-btn cancel-btn cancel-url) (defcomp ~forms/slot-add-form-from-data (&key post-url csrf days-data action-btn cancel-btn cancel-url)
(~events-slot-add-form (~forms/slot-add-form
:post-url post-url :csrf csrf :post-url post-url :csrf csrf
:days (~events-day-checkboxes-from-data :days-data days-data) :days (~forms/day-checkboxes-from-data :days-data days-data)
:action-btn action-btn :cancel-btn cancel-btn :cancel-url cancel-url)) :action-btn action-btn :cancel-btn cancel-btn :cancel-url cancel-url))
@@ -380,7 +380,7 @@
;; Entry add form (_types/day/_add.html) ;; Entry add form (_types/day/_add.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-entry-add-form (&key post-url csrf slot-picker (defcomp ~forms/entry-add-form (&key post-url csrf slot-picker
action-btn cancel-btn cancel-url) action-btn cancel-btn cancel-url)
(<> (<>
(div :id "entry-errors" :class "mt-2 text-sm text-red-600") (div :id "entry-errors" :class "mt-2 text-sm text-red-600")
@@ -446,7 +446,7 @@
:data-confirm-cancel-text "Cancel" :data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save entry"))))) (i :class "fa fa-save") " Save entry")))))
(defcomp ~events-entry-add-button (&key pre-action add-url) (defcomp ~forms/entry-add-button (&key pre-action add-url)
(button :type "button" :class pre-action (button :type "button" :class pre-action
:sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML" :sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
"+ Add entry")) "+ Add entry"))
@@ -456,7 +456,7 @@
;; Ticket type edit form (_types/ticket_type/_edit.html) ;; Ticket type edit form (_types/ticket_type/_edit.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-type-edit-form (&key ticket-id list-container put-url cancel-url csrf (defcomp ~forms/ticket-type-edit-form (&key ticket-id list-container put-url cancel-url csrf
name-val cost-val count-val name-val cost-val count-val
action-btn cancel-btn) action-btn cancel-btn)
(section :id (str "ticket-" ticket-id) :class list-container (section :id (str "ticket-" ticket-id) :class list-container
@@ -509,7 +509,7 @@
;; Ticket type add form (_types/ticket_types/_add.html) ;; Ticket type add form (_types/ticket_types/_add.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-type-add-form (&key post-url csrf action-btn cancel-btn cancel-url) (defcomp ~forms/ticket-type-add-form (&key post-url csrf action-btn cancel-btn cancel-url)
(form :sx-post post-url :sx-target "#tickets-table" :sx-select "#tickets-table" (form :sx-post post-url :sx-target "#tickets-table" :sx-select "#tickets-table"
:sx-disinherit "sx-select" :sx-swap "outerHTML" :sx-disinherit "sx-select" :sx-swap "outerHTML"
:sx-headers csrf :class "space-y-3" :sx-headers csrf :class "space-y-3"
@@ -540,7 +540,7 @@
:data-confirm-cancel-text "Cancel" :data-confirm-cancel-text "Cancel"
(i :class "fa fa-save") " Save ticket type")))) (i :class "fa fa-save") " Save ticket type"))))
(defcomp ~events-ticket-type-add-button (&key action-btn add-url) (defcomp ~forms/ticket-type-add-button (&key action-btn add-url)
(button :class action-btn (button :class action-btn
:sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML" :sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
(i :class "fa fa-plus") " Add ticket type")) (i :class "fa fa-plus") " Add ticket type"))
@@ -550,6 +550,6 @@
;; Entry admin nav — placeholder ;; Entry admin nav — placeholder
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-admin-placeholder-nav () (defcomp ~forms/admin-placeholder-nav ()
(div :class "relative nav-group" (div :class "relative nav-group"
(span :class "block px-3 py-2 text-stone-400 text-sm italic" "Admin options"))) (span :class "block px-3 py-2 text-stone-400 text-sm italic" "Admin options")))

View File

@@ -5,14 +5,14 @@
;; Container cards entries (fragments/container_cards_entries.html) ;; Container cards entries (fragments/container_cards_entries.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-frag-entry-card (&key href name date-str time-str) (defcomp ~fragments/frag-entry-card (&key href name date-str time-str)
(a :href href (a :href href
:class "flex flex-col gap-1 px-3 py-2 bg-stone-50 hover:bg-stone-100 rounded border border-stone-200 transition text-sm whitespace-nowrap flex-shrink-0 min-w-[180px]" :class "flex flex-col gap-1 px-3 py-2 bg-stone-50 hover:bg-stone-100 rounded border border-stone-200 transition text-sm whitespace-nowrap flex-shrink-0 min-w-[180px]"
(div :class "font-medium text-stone-900 truncate" name) (div :class "font-medium text-stone-900 truncate" name)
(div :class "text-xs text-stone-600" date-str) (div :class "text-xs text-stone-600" date-str)
(div :class "text-xs text-stone-500" time-str))) (div :class "text-xs text-stone-500" time-str)))
(defcomp ~events-frag-entries-widget (&key cards) (defcomp ~fragments/frag-entries-widget (&key cards)
(div :class "mt-4 mb-2" (div :class "mt-4 mb-2"
(h3 :class "text-sm font-semibold text-stone-700 mb-2 px-2" "Events:") (h3 :class "text-sm font-semibold text-stone-700 mb-2 px-2" "Events:")
(div :class "overflow-x-auto scrollbar-hide" :style "scroll-behavior: smooth;" (div :class "overflow-x-auto scrollbar-hide" :style "scroll-behavior: smooth;"
@@ -23,7 +23,7 @@
;; Account page tickets (fragments/account_page_tickets.html) ;; Account page tickets (fragments/account_page_tickets.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-frag-ticket-item (&key href entry-name date-str calendar-name type-name badge) (defcomp ~fragments/frag-ticket-item (&key href entry-name date-str calendar-name type-name badge)
(div :class "py-4 first:pt-0 last:pb-0" (div :class "py-4 first:pt-0 last:pb-0"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "min-w-0 flex-1" (div :class "min-w-0 flex-1"
@@ -35,13 +35,13 @@
type-name)) type-name))
(div :class "flex-shrink-0" badge)))) (div :class "flex-shrink-0" badge))))
(defcomp ~events-frag-tickets-panel (&key items) (defcomp ~fragments/frag-tickets-panel (&key items)
(div :class "w-full max-w-3xl mx-auto px-4 py-6" (div :class "w-full max-w-3xl mx-auto px-4 py-6"
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6" (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
(h1 :class "text-xl font-semibold tracking-tight" "Tickets") (h1 :class "text-xl font-semibold tracking-tight" "Tickets")
items))) items)))
(defcomp ~events-frag-tickets-list (&key items) (defcomp ~fragments/frag-tickets-list (&key items)
(div :class "divide-y divide-stone-100" items)) (div :class "divide-y divide-stone-100" items))
@@ -49,7 +49,7 @@
;; Account page bookings (fragments/account_page_bookings.html) ;; Account page bookings (fragments/account_page_bookings.html)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-frag-booking-item (&key name date-str calendar-name cost-str badge) (defcomp ~fragments/frag-booking-item (&key name date-str calendar-name cost-str badge)
(div :class "py-4 first:pt-0 last:pb-0" (div :class "py-4 first:pt-0 last:pb-0"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "min-w-0 flex-1" (div :class "min-w-0 flex-1"
@@ -60,13 +60,13 @@
cost-str)) cost-str))
(div :class "flex-shrink-0" badge)))) (div :class "flex-shrink-0" badge))))
(defcomp ~events-frag-bookings-panel (&key items) (defcomp ~fragments/frag-bookings-panel (&key items)
(div :class "w-full max-w-3xl mx-auto px-4 py-6" (div :class "w-full max-w-3xl mx-auto px-4 py-6"
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6" (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
(h1 :class "text-xl font-semibold tracking-tight" "Bookings") (h1 :class "text-xl font-semibold tracking-tight" "Bookings")
items))) items)))
(defcomp ~events-frag-bookings-list (&key items) (defcomp ~fragments/frag-bookings-list (&key items)
(div :class "divide-y divide-stone-100" items)) (div :class "divide-y divide-stone-100" items))
@@ -75,12 +75,12 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Container cards: list of widgets, each with entries ;; Container cards: list of widgets, each with entries
(defcomp ~events-frag-container-cards-from-data (&key widgets) (defcomp ~fragments/frag-container-cards-from-data (&key widgets)
(<> (map (lambda (w) (<> (map (lambda (w)
(if (get w "entries") (if (get w "entries")
(~events-frag-entries-widget (~fragments/frag-entries-widget
:cards (<> (map (lambda (e) :cards (<> (map (lambda (e)
(~events-frag-entry-card (~fragments/frag-entry-card
:href (get e "href") :name (get e "name") :href (get e "href") :name (get e "name")
:date-str (get e "date-str") :time-str (get e "time-str"))) :date-str (get e "date-str") :time-str (get e "time-str")))
(get w "entries")))) (get w "entries"))))
@@ -88,43 +88,43 @@
(or widgets (list))))) (or widgets (list)))))
;; Ticket item from data — composes badge + optional spans ;; Ticket item from data — composes badge + optional spans
(defcomp ~events-frag-ticket-item-from-data (&key href entry-name date-str calendar-name type-name state) (defcomp ~fragments/frag-ticket-item-from-data (&key href entry-name date-str calendar-name type-name state)
(~events-frag-ticket-item (~fragments/frag-ticket-item
:href href :entry-name entry-name :date-str date-str :href href :entry-name entry-name :date-str date-str
:calendar-name (when calendar-name (span "\u00b7 " calendar-name)) :calendar-name (when calendar-name (span "\u00b7 " calendar-name))
:type-name (when type-name (span "\u00b7 " type-name)) :type-name (when type-name (span "\u00b7 " type-name))
:badge (~status-pill :status state))) :badge (~shared:controls/status-pill :status state)))
;; Tickets panel from data — full panel with list iteration ;; Tickets panel from data — full panel with list iteration
(defcomp ~events-frag-tickets-panel-from-data (&key tickets) (defcomp ~fragments/frag-tickets-panel-from-data (&key tickets)
(~events-frag-tickets-panel (~fragments/frag-tickets-panel
:items (if (empty? (or tickets (list))) :items (if (empty? (or tickets (list)))
(~empty-state :message "No tickets yet." :cls "text-sm text-stone-500") (~shared:misc/empty-state :message "No tickets yet." :cls "text-sm text-stone-500")
(~events-frag-tickets-list (~fragments/frag-tickets-list
:items (<> (map (lambda (t) :items (<> (map (lambda (t)
(~events-frag-ticket-item-from-data (~fragments/frag-ticket-item-from-data
:href (get t "href") :entry-name (get t "entry-name") :href (get t "href") :entry-name (get t "entry-name")
:date-str (get t "date-str") :calendar-name (get t "calendar-name") :date-str (get t "date-str") :calendar-name (get t "calendar-name")
:type-name (get t "type-name") :state (get t "state"))) :type-name (get t "type-name") :state (get t "state")))
tickets)))))) tickets))))))
;; Booking item from data — composes badge + optional spans ;; Booking item from data — composes badge + optional spans
(defcomp ~events-frag-booking-item-from-data (&key name date-str end-time calendar-name cost-str state) (defcomp ~fragments/frag-booking-item-from-data (&key name date-str end-time calendar-name cost-str state)
(~events-frag-booking-item (~fragments/frag-booking-item
:name name :name name
:date-str (<> date-str (when end-time (span "\u2013 " end-time))) :date-str (<> date-str (when end-time (span "\u2013 " end-time)))
:calendar-name (when calendar-name (span "\u00b7 " calendar-name)) :calendar-name (when calendar-name (span "\u00b7 " calendar-name))
:cost-str (when cost-str (span "\u00b7 \u00a3" cost-str)) :cost-str (when cost-str (span "\u00b7 \u00a3" cost-str))
:badge (~status-pill :status state))) :badge (~shared:controls/status-pill :status state)))
;; Bookings panel from data — full panel with list iteration ;; Bookings panel from data — full panel with list iteration
(defcomp ~events-frag-bookings-panel-from-data (&key bookings) (defcomp ~fragments/frag-bookings-panel-from-data (&key bookings)
(~events-frag-bookings-panel (~fragments/frag-bookings-panel
:items (if (empty? (or bookings (list))) :items (if (empty? (or bookings (list)))
(~empty-state :message "No bookings yet." :cls "text-sm text-stone-500") (~shared:misc/empty-state :message "No bookings yet." :cls "text-sm text-stone-500")
(~events-frag-bookings-list (~fragments/frag-bookings-list
:items (<> (map (lambda (b) :items (<> (map (lambda (b)
(~events-frag-booking-item-from-data (~fragments/frag-booking-item-from-data
:href (get b "href") :name (get b "name") :href (get b "href") :name (get b "name")
:date-str (get b "date-str") :end-time (get b "end-time") :date-str (get b "date-str") :end-time (get b "end-time")
:calendar-name (get b "calendar-name") :cost-str (get b "cost-str") :calendar-name (get b "calendar-name") :cost-str (get b "cost-str")

View File

@@ -8,12 +8,12 @@
(nav-class (or (get styles "nav_button") "")) (nav-class (or (get styles "nav_button") ""))
(hx-select "#main-panel, #search-mobile, #search-count-mobile, #search-desktop, #search-count-desktop, #menu-items-nav-wrapper")) (hx-select "#main-panel, #search-mobile, #search-count-mobile, #search-desktop, #search-count-desktop, #menu-items-nav-wrapper"))
(<> (<>
(~nav-group-link (~shared:misc/nav-group-link
:href (app-url "account" "/tickets/") :href (app-url "account" "/tickets/")
:hx-select hx-select :hx-select hx-select
:nav-class nav-class :nav-class nav-class
:label "tickets") :label "tickets")
(~nav-group-link (~shared:misc/nav-group-link
:href (app-url "account" "/bookings/") :href (app-url "account" "/bookings/")
:hx-select hx-select :hx-select hx-select
:nav-class nav-class :nav-class nav-class

View File

@@ -10,13 +10,13 @@
(cond (cond
(= slug "tickets") (= slug "tickets")
(let ((tickets (service "calendar" "user-tickets" :user-id uid))) (let ((tickets (service "calendar" "user-tickets" :user-id uid)))
(~events-frag-tickets-panel (~fragments/frag-tickets-panel
:items (if (empty? tickets) :items (if (empty? tickets)
(~empty-state :message "No tickets yet." (~shared:misc/empty-state :message "No tickets yet."
:cls "text-sm text-stone-500") :cls "text-sm text-stone-500")
(~events-frag-tickets-list (~fragments/frag-tickets-list
:items (<> (map (fn (t) :items (<> (map (fn (t)
(~events-frag-ticket-item (~fragments/frag-ticket-item
:href (app-url "events" :href (app-url "events"
(str "/tickets/" (get t "code") "/")) (str "/tickets/" (get t "code") "/"))
:entry-name (get t "entry_name") :entry-name (get t "entry_name")
@@ -25,18 +25,18 @@
(span (str "\u00b7 " (get t "calendar_name")))) (span (str "\u00b7 " (get t "calendar_name"))))
:type-name (when (get t "ticket_type_name") :type-name (when (get t "ticket_type_name")
(span (str "\u00b7 " (get t "ticket_type_name")))) (span (str "\u00b7 " (get t "ticket_type_name"))))
:badge (~status-pill :status (or (get t "state") "")))) :badge (~shared:controls/status-pill :status (or (get t "state") ""))))
tickets)))))) tickets))))))
(= slug "bookings") (= slug "bookings")
(let ((bookings (service "calendar" "user-bookings" :user-id uid))) (let ((bookings (service "calendar" "user-bookings" :user-id uid)))
(~events-frag-bookings-panel (~fragments/frag-bookings-panel
:items (if (empty? bookings) :items (if (empty? bookings)
(~empty-state :message "No bookings yet." (~shared:misc/empty-state :message "No bookings yet."
:cls "text-sm text-stone-500") :cls "text-sm text-stone-500")
(~events-frag-bookings-list (~fragments/frag-bookings-list
:items (<> (map (fn (b) :items (<> (map (fn (b)
(~events-frag-booking-item (~fragments/frag-booking-item
:name (get b "name") :name (get b "name")
:date-str (str (format-date (get b "start_at") "%d %b %Y, %H:%M") :date-str (str (format-date (get b "start_at") "%d %b %Y, %H:%M")
(if (get b "end_at") (if (get b "end_at")
@@ -46,5 +46,5 @@
(span (str "\u00b7 " (get b "calendar_name")))) (span (str "\u00b7 " (get b "calendar_name"))))
:cost-str (when (get b "cost") :cost-str (when (get b "cost")
(span (str "\u00b7 \u00a3" (get b "cost")))) (span (str "\u00b7 \u00a3" (get b "cost"))))
:badge (~status-pill :status (or (get b "state") "")))) :badge (~shared:controls/status-pill :status (or (get b "state") ""))))
bookings)))))))))) bookings))))))))))

View File

@@ -19,13 +19,13 @@
(post-slug (or (nth slugs i) ""))) (post-slug (or (nth slugs i) "")))
(<> (str "<!-- card-widget:" pid " -->") (<> (str "<!-- card-widget:" pid " -->")
(when (not (empty? entries)) (when (not (empty? entries))
(~events-frag-entries-widget (~fragments/frag-entries-widget
:cards (<> (map (fn (e) :cards (<> (map (fn (e)
(let ((time-str (str (format-date (get e "start_at") "%H:%M") (let ((time-str (str (format-date (get e "start_at") "%H:%M")
(if (get e "end_at") (if (get e "end_at")
(str " \u2013 " (format-date (get e "end_at") "%H:%M")) (str " \u2013 " (format-date (get e "end_at") "%H:%M"))
"")))) ""))))
(~events-frag-entry-card (~fragments/frag-entry-card
:href (app-url "events" :href (app-url "events"
(str "/" post-slug (str "/" post-slug
"/" (get e "calendar_slug") "/" (get e "calendar_slug")

View File

@@ -53,7 +53,7 @@
(if (get entry "end_at") (if (get entry "end_at")
(str " " (format-date (get entry "end_at") "%H:%M")) (str " " (format-date (get entry "end_at") "%H:%M"))
"")))) ""))))
(~calendar-entry-nav (~shared:navigation/calendar-entry-nav
:href (app-url "events" entry-path) :href (app-url "events" entry-path)
:name (get entry "name") :name (get entry "name")
:date-str date-str :date-str date-str
@@ -61,7 +61,7 @@
;; Infinite scroll sentinel ;; Infinite scroll sentinel
(when (and has-more (not (empty? purl))) (when (and has-more (not (empty? purl)))
(~htmx-sentinel (~shared:misc/htmx-sentinel
:id (str "entries-load-sentinel-" pg) :id (str "entries-load-sentinel-" pg)
:hx-get (str purl "?page=" (+ pg 1)) :hx-get (str purl "?page=" (+ pg 1))
:hx-trigger "intersect once" :hx-trigger "intersect once"
@@ -74,7 +74,7 @@
(is-selected (if (not (empty? cur-cal)) (is-selected (if (not (empty? cur-cal))
(= (get cal "slug") cur-cal) (= (get cal "slug") cur-cal)
false))) false)))
(~calendar-link-nav (~shared:navigation/calendar-link-nav
:href href :href href
:name (get cal "name") :name (get cal "name")
:nav-class nav-class :nav-class nav-class

View File

@@ -16,7 +16,7 @@
:container-type "page" :container-type "page"
:container-id (get post "id"))) :container-id (get post "id")))
(cal-names (join ", " (map (fn (c) (get c "name")) calendars)))) (cal-names (join ", " (map (fn (c) (get c "name")) calendars))))
(~link-card (~shared:fragments/link-card
:title (get post "title") :title (get post "title")
:image (get post "feature_image") :image (get post "feature_image")
:subtitle cal-names :subtitle cal-names
@@ -28,7 +28,7 @@
:container-type "page" :container-type "page"
:container-id (get post "id"))) :container-id (get post "id")))
(cal-names (join ", " (map (fn (c) (get c "name")) calendars)))) (cal-names (join ", " (map (fn (c) (get c "name")) calendars))))
(~link-card (~shared:fragments/link-card
:title (get post "title") :title (get post "title")
:image (get post "feature_image") :image (get post "feature_image")
:subtitle cal-names :subtitle cal-names

View File

@@ -1,12 +1,12 @@
;; Events header components ;; Events header components
(defcomp ~events-calendars-label () (defcomp ~header/calendars-label ()
(<> (i :class "fa fa-calendar" :aria-hidden "true") (div "Calendars"))) (<> (i :class "fa fa-calendar" :aria-hidden "true") (div "Calendars")))
(defcomp ~events-markets-label () (defcomp ~header/markets-label ()
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div "Markets"))) (<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div "Markets")))
(defcomp ~events-calendar-label (&key name description) (defcomp ~header/calendar-label (&key name description)
(div :class "flex flex-col md:flex-row md:gap-2 items-center min-w-0" (div :class "flex flex-col md:flex-row md:gap-2 items-center min-w-0"
(div :class "flex flex-row items-center gap-2" (div :class "flex flex-row items-center gap-2"
(i :class "fa fa-calendar") (i :class "fa fa-calendar")
@@ -15,16 +15,16 @@
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block" :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
description))) description)))
(defcomp ~events-day-label (&key date-str) (defcomp ~header/day-label (&key date-str)
(div :class "flex gap-1 items-center" (div :class "flex gap-1 items-center"
(i :class "fa fa-calendar-day") (i :class "fa fa-calendar-day")
(span date-str))) (span date-str)))
(defcomp ~events-entry-label (&key entry-id title times) (defcomp ~header/entry-label (&key entry-id title times)
(div :id (str "entry-title-" entry-id) :class "flex gap-1 items-center" (div :id (str "entry-title-" entry-id) :class "flex gap-1 items-center"
title times)) title times))
(defcomp ~events-slot-label (&key name description) (defcomp ~header/slot-label (&key name description)
(div :class "flex flex-col md:flex-row md:gap-2 items-center" (div :class "flex flex-col md:flex-row md:gap-2 items-center"
(div :class "flex flex-row items-center gap-2" (div :class "flex flex-row items-center gap-2"
(i :class "fa fa-clock") (i :class "fa fa-clock")

View File

@@ -11,20 +11,20 @@
(let ((__cal (events-calendar-ctx)) (let ((__cal (events-calendar-ctx))
(__sc (select-colours))) (__sc (select-colours)))
(when (get __cal "slug") (when (get __cal "slug")
(~menu-row-sx :id "calendar-row" :level 3 (~shared:layout/menu-row-sx :id "calendar-row" :level 3
:link-href (url-for "calendar.get" :link-href (url-for "calendar.get"
:calendar-slug (get __cal "slug")) :calendar-slug (get __cal "slug"))
:link-label-content (~events-calendar-label :link-label-content (~header/calendar-label
:name (get __cal "name") :name (get __cal "name")
:description (get __cal "description")) :description (get __cal "description"))
:nav (<> :nav (<>
(~nav-link :href (url-for "defpage_slots_listing" (~shared:layout/nav-link :href (url-for "defpage_slots_listing"
:calendar-slug (get __cal "slug")) :calendar-slug (get __cal "slug"))
:icon "fa fa-clock" :label "Slots" :icon "fa fa-clock" :label "Slots"
:select-colours __sc) :select-colours __sc)
(let ((__rights (app-rights))) (let ((__rights (app-rights)))
(when (get __rights "admin") (when (get __rights "admin")
(~nav-link :href (url-for "defpage_calendar_admin" (~shared:layout/nav-link :href (url-for "defpage_calendar_admin"
:calendar-slug (get __cal "slug")) :calendar-slug (get __cal "slug"))
:icon "fa fa-cog" :icon "fa fa-cog"
:select-colours __sc)))) :select-colours __sc))))
@@ -37,13 +37,13 @@
(let ((__cal (events-calendar-ctx)) (let ((__cal (events-calendar-ctx))
(__sc (select-colours))) (__sc (select-colours)))
(when (get __cal "slug") (when (get __cal "slug")
(~menu-row-sx :id "calendar-admin-row" :level 4 (~shared:layout/menu-row-sx :id "calendar-admin-row" :level 4
:link-label "admin" :icon "fa fa-cog" :link-label "admin" :icon "fa fa-cog"
:nav (<> :nav (<>
(~nav-link :href (url-for "defpage_slots_listing" (~shared:layout/nav-link :href (url-for "defpage_slots_listing"
:calendar-slug (get __cal "slug")) :calendar-slug (get __cal "slug"))
:label "slots" :select-colours __sc) :label "slots" :select-colours __sc)
(~nav-link :href (url-for "calendar.admin.calendar_description_edit" (~shared:layout/nav-link :href (url-for "calendar.admin.calendar_description_edit"
:calendar-slug (get __cal "slug")) :calendar-slug (get __cal "slug"))
:label "description" :select-colours __sc)) :label "description" :select-colours __sc))
:child-id "calendar-admin-header-child" :child-id "calendar-admin-header-child"
@@ -55,13 +55,13 @@
(let ((__day (events-day-ctx)) (let ((__day (events-day-ctx))
(__cal (events-calendar-ctx))) (__cal (events-calendar-ctx)))
(when (get __day "date-str") (when (get __day "date-str")
(~menu-row-sx :id "day-row" :level 4 (~shared:layout/menu-row-sx :id "day-row" :level 4
:link-href (url-for "calendar.day.show_day" :link-href (url-for "calendar.day.show_day"
:calendar-slug (get __cal "slug") :calendar-slug (get __cal "slug")
:year (get __day "year") :year (get __day "year")
:month (get __day "month") :month (get __day "month")
:day (get __day "day")) :day (get __day "day"))
:link-label-content (~events-day-label :link-label-content (~header/day-label
:date-str (get __day "date-str")) :date-str (get __day "date-str"))
:nav (get __day "nav") :nav (get __day "nav")
:child-id "day-header-child" :child-id "day-header-child"
@@ -73,7 +73,7 @@
(let ((__day (events-day-ctx)) (let ((__day (events-day-ctx))
(__cal (events-calendar-ctx))) (__cal (events-calendar-ctx)))
(when (get __day "date-str") (when (get __day "date-str")
(~menu-row-sx :id "day-admin-row" :level 5 (~shared:layout/menu-row-sx :id "day-admin-row" :level 5
:link-href (url-for "defpage_day_admin" :link-href (url-for "defpage_day_admin"
:calendar-slug (get __cal "slug") :calendar-slug (get __cal "slug")
:year (get __day "year") :year (get __day "year")
@@ -88,12 +88,12 @@
(quasiquote (quasiquote
(let ((__ectx (events-entry-ctx))) (let ((__ectx (events-entry-ctx)))
(when (get __ectx "id") (when (get __ectx "id")
(~menu-row-sx :id "entry-row" :level 5 (~shared:layout/menu-row-sx :id "entry-row" :level 5
:link-href (get __ectx "link-href") :link-href (get __ectx "link-href")
:link-label-content (~events-entry-label :link-label-content (~header/entry-label
:entry-id (get __ectx "id") :entry-id (get __ectx "id")
:title (~events-entry-title :name (get __ectx "name")) :title (~admin/entry-title :name (get __ectx "name"))
:times (~events-entry-times :time-str (get __ectx "time-str"))) :times (~admin/entry-times :time-str (get __ectx "time-str")))
:nav (get __ectx "nav") :nav (get __ectx "nav")
:child-id "entry-header-child" :child-id "entry-header-child"
:oob (unquote oob)))))) :oob (unquote oob))))))
@@ -103,11 +103,11 @@
(quasiquote (quasiquote
(let ((__ectx (events-entry-ctx))) (let ((__ectx (events-entry-ctx)))
(when (get __ectx "id") (when (get __ectx "id")
(~menu-row-sx :id "entry-admin-row" :level 6 (~shared:layout/menu-row-sx :id "entry-admin-row" :level 6
:link-href (get __ectx "admin-href") :link-href (get __ectx "admin-href")
:link-label "admin" :icon "fa fa-cog" :link-label "admin" :icon "fa fa-cog"
:nav (when (get __ectx "is-admin") :nav (when (get __ectx "is-admin")
(~nav-link :href (get __ectx "ticket-types-href") (~shared:layout/nav-link :href (get __ectx "ticket-types-href")
:label "ticket_types" :label "ticket_types"
:select-colours (get __ectx "select-colours"))) :select-colours (get __ectx "select-colours")))
:child-id "entry-admin-header-child" :child-id "entry-admin-header-child"
@@ -118,8 +118,8 @@
(quasiquote (quasiquote
(let ((__slot (events-slot-ctx))) (let ((__slot (events-slot-ctx)))
(when (get __slot "name") (when (get __slot "name")
(~menu-row-sx :id "slot-row" :level 5 (~shared:layout/menu-row-sx :id "slot-row" :level 5
:link-label-content (~events-slot-label :link-label-content (~header/slot-label
:name (get __slot "name") :name (get __slot "name")
:description (get __slot "description")) :description (get __slot "description"))
:child-id "slot-header-child" :child-id "slot-header-child"
@@ -131,12 +131,12 @@
(let ((__ectx (events-entry-ctx)) (let ((__ectx (events-entry-ctx))
(__cal (events-calendar-ctx))) (__cal (events-calendar-ctx)))
(when (get __ectx "id") (when (get __ectx "id")
(~menu-row-sx :id "ticket_types-row" :level 7 (~shared:layout/menu-row-sx :id "ticket_types-row" :level 7
:link-href (get __ectx "ticket-types-href") :link-href (get __ectx "ticket-types-href")
:link-label-content (<> :link-label-content (<>
(i :class "fa fa-ticket") (i :class "fa fa-ticket")
(div :class "shrink-0" "ticket types")) (div :class "shrink-0" "ticket types"))
:nav (~events-admin-placeholder-nav) :nav (~forms/admin-placeholder-nav)
:child-id "ticket_type-header-child" :child-id "ticket_type-header-child"
:oob (unquote oob)))))) :oob (unquote oob))))))
@@ -145,22 +145,22 @@
(quasiquote (quasiquote
(let ((__tt (events-ticket-type-ctx))) (let ((__tt (events-ticket-type-ctx)))
(when (get __tt "id") (when (get __tt "id")
(~menu-row-sx :id "ticket_type-row" :level 8 (~shared:layout/menu-row-sx :id "ticket_type-row" :level 8
:link-href (get __tt "link-href") :link-href (get __tt "link-href")
:link-label-content (div :class "flex flex-col md:flex-row md:gap-2 items-center" :link-label-content (div :class "flex flex-col md:flex-row md:gap-2 items-center"
(div :class "flex flex-row items-center gap-2" (div :class "flex flex-row items-center gap-2"
(i :class "fa fa-ticket") (i :class "fa fa-ticket")
(div :class "shrink-0" (get __tt "name")))) (div :class "shrink-0" (get __tt "name"))))
:nav (~events-admin-placeholder-nav) :nav (~forms/admin-placeholder-nav)
:child-id "ticket_type-header-child-inner" :child-id "ticket_type-header-child-inner"
:oob (unquote oob)))))) :oob (unquote oob))))))
(defmacro ~events-markets-header-auto (oob) (defmacro ~events-markets-header-auto (oob)
"Markets section header row." "Markets section header row."
(quasiquote (quasiquote
(~menu-row-sx :id "markets-row" :level 3 (~shared:layout/menu-row-sx :id "markets-row" :level 3
:link-href (url-for "defpage_events_markets") :link-href (url-for "defpage_events_markets")
:link-label-content (~events-markets-label) :link-label-content (~header/markets-label)
:child-id "markets-header-child" :child-id "markets-header-child"
:oob (unquote oob)))) :oob (unquote oob))))
@@ -168,218 +168,218 @@
;; OOB clear helpers — clear deeper header rows not present at this level ;; OOB clear helpers — clear deeper header rows not present at this level
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-clear-oob-cal-admin () (defcomp ~layouts/clear-oob-cal-admin ()
"Clear OOB divs for cal-admin level (keeps down to calendar-admin)." "Clear OOB divs for cal-admin level (keeps down to calendar-admin)."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-row")
(~clear-oob-div :id "entry-header-child") (~shared:layout/clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-row")
(~clear-oob-div :id "day-admin-header-child") (~shared:layout/clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-row")
(~clear-oob-div :id "day-header-child") (~shared:layout/clear-oob-div :id "day-header-child")
(~clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child"))) (~shared:layout/clear-oob-div :id "calendars-header-child")))
(defcomp ~events-clear-oob-slot () (defcomp ~layouts/clear-oob-slot ()
"Clear OOB divs for slot level." "Clear OOB divs for slot level."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-row")
(~clear-oob-div :id "entry-header-child") (~shared:layout/clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-row")
(~clear-oob-div :id "day-admin-header-child") (~shared:layout/clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-row")
(~clear-oob-div :id "day-header-child") (~shared:layout/clear-oob-div :id "day-header-child")
(~clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child"))) (~shared:layout/clear-oob-div :id "calendars-header-child")))
(defcomp ~events-clear-oob-day-admin () (defcomp ~layouts/clear-oob-day-admin ()
"Clear OOB divs for day-admin level." "Clear OOB divs for day-admin level."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-row")
(~clear-oob-div :id "entry-header-child") (~shared:layout/clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child"))) (~shared:layout/clear-oob-div :id "calendars-header-child")))
(defcomp ~events-clear-oob-entry () (defcomp ~layouts/clear-oob-entry ()
"Clear OOB divs for entry level (public, no admin rows)." "Clear OOB divs for entry level (public, no admin rows)."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-row")
(~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-row")
(~clear-oob-div :id "day-admin-header-child") (~shared:layout/clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-row")
(~clear-oob-div :id "calendar-admin-header-child") (~shared:layout/clear-oob-div :id "calendar-admin-header-child")
(~clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child") (~shared:layout/clear-oob-div :id "calendars-header-child")
(~clear-oob-div :id "post-admin-row") (~shared:layout/clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child"))) (~shared:layout/clear-oob-div :id "post-admin-header-child")))
(defcomp ~events-clear-oob-entry-admin () (defcomp ~layouts/clear-oob-entry-admin ()
"Clear OOB divs for entry-admin level." "Clear OOB divs for entry-admin level."
(<> (<>
(~clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-row")
(~clear-oob-div :id "calendars-header-child"))) (~shared:layout/clear-oob-div :id "calendars-header-child")))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; OOB clear helpers for renders.py — clear all deeper IDs except kept ones ;; OOB clear helpers for renders.py — clear all deeper IDs except kept ones
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-clear-deeper-post () (defcomp ~layouts/clear-deeper-post ()
"Clear all events IDs deeper than post level." "Clear all events IDs deeper than post level."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child") (~shared:layout/clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-header-child")
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child")
(~clear-oob-div :id "calendar-row") (~clear-oob-div :id "calendar-header-child") (~shared:layout/clear-oob-div :id "calendar-row") (~shared:layout/clear-oob-div :id "calendar-header-child")
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child") (~shared:layout/clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-header-child")
(~clear-oob-div :id "post-admin-row") (~clear-oob-div :id "post-admin-header-child"))) (~shared:layout/clear-oob-div :id "post-admin-row") (~shared:layout/clear-oob-div :id "post-admin-header-child")))
(defcomp ~events-clear-deeper-post-admin () (defcomp ~layouts/clear-deeper-post-admin ()
"Clear all events IDs deeper than post-admin level." "Clear all events IDs deeper than post-admin level."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child") (~shared:layout/clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-header-child")
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child")
(~clear-oob-div :id "calendar-row") (~clear-oob-div :id "calendar-header-child") (~shared:layout/clear-oob-div :id "calendar-row") (~shared:layout/clear-oob-div :id "calendar-header-child")
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child"))) (~shared:layout/clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-header-child")))
(defcomp ~events-clear-deeper-calendar () (defcomp ~layouts/clear-deeper-calendar ()
"Clear all events IDs deeper than calendar level." "Clear all events IDs deeper than calendar level."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child") (~shared:layout/clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-header-child")
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child")
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child") (~shared:layout/clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-header-child")
(~clear-oob-div :id "post-admin-row") (~clear-oob-div :id "post-admin-header-child"))) (~shared:layout/clear-oob-div :id "post-admin-row") (~shared:layout/clear-oob-div :id "post-admin-header-child")))
(defcomp ~events-clear-deeper-day () (defcomp ~layouts/clear-deeper-day ()
"Clear all events IDs deeper than day level." "Clear all events IDs deeper than day level."
(<> (<>
(~clear-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") (~shared:layout/clear-oob-div :id "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child")
(~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child")
(~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child")
(~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child")
(~clear-oob-div :id "calendars-row") (~clear-oob-div :id "calendars-header-child") (~shared:layout/clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-header-child")
(~clear-oob-div :id "post-admin-row") (~clear-oob-div :id "post-admin-header-child"))) (~shared:layout/clear-oob-div :id "post-admin-row") (~shared:layout/clear-oob-div :id "post-admin-header-child")))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Calendar admin layout: root + post + child(post-admin + cal + cal-admin) ;; Calendar admin layout: root + post + child(post-admin + cal + cal-admin)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-cal-admin-layout-full () (defcomp ~layouts/cal-admin-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars") (~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
(~events-calendar-admin-header-auto nil))))) (~events-calendar-admin-header-auto nil)))))
(defcomp ~events-cal-admin-layout-oob () (defcomp ~layouts/cal-admin-layout-oob ()
(<> (~post-admin-header-auto true "calendars") (<> (~post-admin-header-auto true "calendars")
(~events-calendar-header-auto true) (~events-calendar-header-auto true)
(~oob-header-sx :parent-id "calendar-header-child" (~shared:layout/oob-header-sx :parent-id "calendar-header-child"
:row (~events-calendar-admin-header-auto nil)) :row (~events-calendar-admin-header-auto nil))
(~events-clear-oob-cal-admin) (~layouts/clear-oob-cal-admin)
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Slots layout: same full as cal-admin ;; Slots layout: same full as cal-admin
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-slots-layout-full () (defcomp ~layouts/slots-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars") (~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
(~events-calendar-admin-header-auto nil))))) (~events-calendar-admin-header-auto nil)))))
(defcomp ~events-slots-layout-oob () (defcomp ~layouts/slots-layout-oob ()
(<> (~post-admin-header-auto true "calendars") (<> (~post-admin-header-auto true "calendars")
(~events-calendar-admin-header-auto true) (~events-calendar-admin-header-auto true)
(~events-clear-oob-cal-admin) (~layouts/clear-oob-cal-admin)
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Slot detail layout: root + post + child(admin + cal + cal-admin + slot) ;; Slot detail layout: root + post + child(admin + cal + cal-admin + slot)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-slot-layout-full () (defcomp ~layouts/slot-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars") (~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
(~events-calendar-admin-header-auto nil) (~events-calendar-admin-header-auto nil)
(~events-slot-header-auto nil))))) (~events-slot-header-auto nil)))))
(defcomp ~events-slot-layout-oob () (defcomp ~layouts/slot-layout-oob ()
(<> (~post-admin-header-auto true "calendars") (<> (~post-admin-header-auto true "calendars")
(~events-calendar-admin-header-auto true) (~events-calendar-admin-header-auto true)
(~oob-header-sx :parent-id "calendar-admin-header-child" (~shared:layout/oob-header-sx :parent-id "calendar-admin-header-child"
:row (~events-slot-header-auto nil)) :row (~events-slot-header-auto nil))
(~events-clear-oob-slot) (~layouts/clear-oob-slot)
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Day admin layout: root + post + child(admin + cal + day + day-admin) ;; Day admin layout: root + post + child(admin + cal + day + day-admin)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-day-admin-layout-full () (defcomp ~layouts/day-admin-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars") (~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
(~events-day-header-auto nil) (~events-day-header-auto nil)
(~events-day-admin-header-auto nil))))) (~events-day-admin-header-auto nil)))))
(defcomp ~events-day-admin-layout-oob () (defcomp ~layouts/day-admin-layout-oob ()
(<> (~post-admin-header-auto true "calendars") (<> (~post-admin-header-auto true "calendars")
(~events-calendar-header-auto true) (~events-calendar-header-auto true)
(~oob-header-sx :parent-id "day-header-child" (~shared:layout/oob-header-sx :parent-id "day-header-child"
:row (~events-day-admin-header-auto nil)) :row (~events-day-admin-header-auto nil))
(~events-clear-oob-day-admin) (~layouts/clear-oob-day-admin)
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Entry layout: root + child(post + cal + day + entry) — public, no admin ;; Entry layout: root + child(post + cal + day + entry) — public, no admin
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-entry-layout-full () (defcomp ~layouts/entry-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
(~events-day-header-auto nil) (~events-day-header-auto nil)
(~events-entry-header-auto nil))))) (~events-entry-header-auto nil)))))
(defcomp ~events-entry-layout-oob () (defcomp ~layouts/entry-layout-oob ()
(<> (~events-day-header-auto true) (<> (~events-day-header-auto true)
(~oob-header-sx :parent-id "day-header-child" (~shared:layout/oob-header-sx :parent-id "day-header-child"
:row (~events-entry-header-auto nil)) :row (~events-entry-header-auto nil))
(~events-clear-oob-entry) (~layouts/clear-oob-entry)
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Entry admin layout: root + post + child(admin + cal + day + entry + entry-admin) ;; Entry admin layout: root + post + child(admin + cal + day + entry + entry-admin)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-entry-admin-layout-full () (defcomp ~layouts/entry-admin-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~post-admin-header-auto nil "calendars") (~post-admin-header-auto nil "calendars")
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
@@ -387,21 +387,21 @@
(~events-entry-header-auto nil) (~events-entry-header-auto nil)
(~events-entry-admin-header-auto nil))))) (~events-entry-admin-header-auto nil)))))
(defcomp ~events-entry-admin-layout-oob () (defcomp ~layouts/entry-admin-layout-oob ()
(<> (~post-admin-header-auto true "calendars") (<> (~post-admin-header-auto true "calendars")
(~events-entry-header-auto true) (~events-entry-header-auto true)
(~oob-header-sx :parent-id "entry-header-child" (~shared:layout/oob-header-sx :parent-id "entry-header-child"
:row (~events-entry-admin-header-auto nil)) :row (~events-entry-admin-header-auto nil))
(~events-clear-oob-entry-admin) (~layouts/clear-oob-entry-admin)
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Ticket types layout: root + child(post + cal + day + entry + entry-admin + ticket-types) ;; Ticket types layout: root + child(post + cal + day + entry + entry-admin + ticket-types)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-types-layout-full () (defcomp ~layouts/ticket-types-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
(~events-day-header-auto nil) (~events-day-header-auto nil)
@@ -409,9 +409,9 @@
(~events-entry-admin-header-auto nil) (~events-entry-admin-header-auto nil)
(~events-ticket-types-header-auto nil))))) (~events-ticket-types-header-auto nil)))))
(defcomp ~events-ticket-types-layout-oob () (defcomp ~layouts/ticket-types-layout-oob ()
(<> (~events-entry-admin-header-auto true) (<> (~events-entry-admin-header-auto true)
(~oob-header-sx :parent-id "entry-admin-header-child" (~shared:layout/oob-header-sx :parent-id "entry-admin-header-child"
:row (~events-ticket-types-header-auto nil)) :row (~events-ticket-types-header-auto nil))
(~root-header-auto true))) (~root-header-auto true)))
@@ -419,9 +419,9 @@
;; Ticket type layout: all headers down to ticket-type ;; Ticket type layout: all headers down to ticket-type
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-ticket-type-layout-full () (defcomp ~layouts/ticket-type-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~events-calendar-header-auto nil) (~events-calendar-header-auto nil)
(~events-day-header-auto nil) (~events-day-header-auto nil)
@@ -430,9 +430,9 @@
(~events-ticket-types-header-auto nil) (~events-ticket-types-header-auto nil)
(~events-ticket-type-header-auto nil))))) (~events-ticket-type-header-auto nil)))))
(defcomp ~events-ticket-type-layout-oob () (defcomp ~layouts/ticket-type-layout-oob ()
(<> (~events-ticket-types-header-auto true) (<> (~events-ticket-types-header-auto true)
(~oob-header-sx :parent-id "ticket_types-header-child" (~shared:layout/oob-header-sx :parent-id "ticket_types-header-child"
:row (~events-ticket-type-header-auto nil)) :row (~events-ticket-type-header-auto nil))
(~root-header-auto true))) (~root-header-auto true)))
@@ -440,14 +440,14 @@
;; Markets layout: root + child(post + markets) ;; Markets layout: root + child(post + markets)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~events-markets-layout-full () (defcomp ~layouts/markets-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~events-markets-header-auto nil))))) (~events-markets-header-auto nil)))))
(defcomp ~events-markets-layout-oob () (defcomp ~layouts/markets-layout-oob ()
(<> (~post-header-auto true) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child" (~shared:layout/oob-header-sx :parent-id "post-header-child"
:row (~events-markets-header-auto nil)) :row (~events-markets-header-auto nil))
(~root-header-auto true))) (~root-header-auto true)))

View File

@@ -1,15 +1,15 @@
;; Events page-level components (slots, ticket types, buy form, cart, posts nav) ;; Events page-level components (slots, ticket types, buy form, cart, posts nav)
(defcomp ~events-slot-days-pills (&key days-inner) (defcomp ~page/slot-days-pills (&key days-inner)
(div :class "flex flex-wrap gap-1" days-inner)) (div :class "flex flex-wrap gap-1" days-inner))
(defcomp ~events-slot-day-pill (&key day) (defcomp ~page/slot-day-pill (&key day)
(span :class "px-2 py-0.5 rounded-full text-xs bg-slate-200" day)) (span :class "px-2 py-0.5 rounded-full text-xs bg-slate-200" day))
(defcomp ~events-slot-no-days () (defcomp ~page/slot-no-days ()
(span :class "text-xs text-slate-400" "No days")) (span :class "text-xs text-slate-400" "No days"))
(defcomp ~events-slot-panel (&key slot-id list-container days flexible time-str cost-str pre-action edit-url) (defcomp ~page/slot-panel (&key slot-id list-container days flexible time-str cost-str pre-action edit-url)
(section :id (str "slot-" slot-id) :class list-container (section :id (str "slot-" slot-id) :class list-container
(div :class "flex flex-col" (div :class "flex flex-col"
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Days") (div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Days")
@@ -27,15 +27,15 @@
(button :type "button" :class pre-action :sx-get edit-url (button :type "button" :class pre-action :sx-get edit-url
:sx-target (str "#slot-" slot-id) :sx-swap "outerHTML" "Edit"))) :sx-target (str "#slot-" slot-id) :sx-swap "outerHTML" "Edit")))
(defcomp ~events-slot-description-oob (&key description) (defcomp ~page/slot-description-oob (&key description)
(div :id "slot-description-title" :sx-swap-oob "outerHTML" (div :id "slot-description-title" :sx-swap-oob "outerHTML"
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block" :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
description)) description))
(defcomp ~events-slots-empty-row () (defcomp ~page/slots-empty-row ()
(tr (td :colspan "5" :class "p-3 text-stone-500" "No slots yet."))) (tr (td :colspan "5" :class "p-3 text-stone-500" "No slots yet.")))
(defcomp ~events-slots-row (&key tr-cls slot-href pill-cls hx-select slot-name description (defcomp ~page/slots-row (&key tr-cls slot-href pill-cls hx-select slot-name description
flexible days time-str cost-str action-btn del-url csrf-hdr) flexible days time-str cost-str action-btn del-url csrf-hdr)
(tr :class tr-cls (tr :class tr-cls
(td :class "p-2 align-top w-1/6" (td :class "p-2 align-top w-1/6"
@@ -57,7 +57,7 @@
:sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed" :sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed"
(i :class "fa-solid fa-trash"))))) (i :class "fa-solid fa-trash")))))
(defcomp ~events-slots-table (&key list-container rows pre-action add-url) (defcomp ~page/slots-table (&key list-container rows pre-action add-url)
(section :id "slots-table" :class list-container (section :id "slots-table" :class list-container
(table :class "w-full text-sm border table-fixed" (table :class "w-full text-sm border table-fixed"
(thead :class "bg-stone-100" (thead :class "bg-stone-100"
@@ -78,61 +78,61 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Days pills from data — replaces Python loop ;; Days pills from data — replaces Python loop
(defcomp ~events-days-pills-from-data (&key days) (defcomp ~page/days-pills-from-data (&key days)
(if (empty? (or days (list))) (if (empty? (or days (list)))
(~events-slot-no-days) (~page/slot-no-days)
(~events-slot-days-pills (~page/slot-days-pills
:days-inner (<> (map (lambda (d) (~events-slot-day-pill :day d)) days))))) :days-inner (<> (map (lambda (d) (~page/slot-day-pill :day d)) days)))))
;; Slot panel from data ;; Slot panel from data
(defcomp ~events-slot-panel-from-data (&key slot-id list-container days (defcomp ~page/slot-panel-from-data (&key slot-id list-container days
flexible time-str cost-str flexible time-str cost-str
pre-action edit-url description oob) pre-action edit-url description oob)
(<> (<>
(~events-slot-panel (~page/slot-panel
:slot-id slot-id :list-container list-container :slot-id slot-id :list-container list-container
:days (~events-days-pills-from-data :days days) :days (~page/days-pills-from-data :days days)
:flexible flexible :time-str time-str :cost-str cost-str :flexible flexible :time-str time-str :cost-str cost-str
:pre-action pre-action :edit-url edit-url) :pre-action pre-action :edit-url edit-url)
(when oob (when oob
(~events-slot-description-oob :description (or description ""))))) (~page/slot-description-oob :description (or description "")))))
;; Slots table from data ;; Slots table from data
(defcomp ~events-slots-table-from-data (&key list-container slots pre-action add-url (defcomp ~page/slots-table-from-data (&key list-container slots pre-action add-url
tr-cls pill-cls action-btn hx-select csrf-hdr) tr-cls pill-cls action-btn hx-select csrf-hdr)
(~events-slots-table (~page/slots-table
:list-container list-container :list-container list-container
:rows (if (empty? (or slots (list))) :rows (if (empty? (or slots (list)))
(~events-slots-empty-row) (~page/slots-empty-row)
(<> (map (lambda (s) (<> (map (lambda (s)
(~events-slots-row (~page/slots-row
:tr-cls tr-cls :slot-href (get s "slot-href") :tr-cls tr-cls :slot-href (get s "slot-href")
:pill-cls pill-cls :hx-select hx-select :pill-cls pill-cls :hx-select hx-select
:slot-name (get s "slot-name") :description (get s "description") :slot-name (get s "slot-name") :description (get s "description")
:flexible (get s "flexible") :flexible (get s "flexible")
:days (~events-days-pills-from-data :days (get s "days")) :days (~page/days-pills-from-data :days (get s "days"))
:time-str (get s "time-str") :time-str (get s "time-str")
:cost-str (get s "cost-str") :action-btn action-btn :cost-str (get s "cost-str") :action-btn action-btn
:del-url (get s "del-url") :csrf-hdr csrf-hdr)) :del-url (get s "del-url") :csrf-hdr csrf-hdr))
(or slots (list))))) (or slots (list)))))
:pre-action pre-action :add-url add-url)) :pre-action pre-action :add-url add-url))
(defcomp ~events-ticket-type-col (&key label value) (defcomp ~page/ticket-type-col (&key label value)
(div :class "flex flex-col" (div :class "flex flex-col"
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label) (div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label)
(div :class "mt-1" value))) (div :class "mt-1" value)))
(defcomp ~events-ticket-type-panel (&key ticket-id list-container c1 c2 c3 pre-action edit-url) (defcomp ~page/ticket-type-panel (&key ticket-id list-container c1 c2 c3 pre-action edit-url)
(section :id (str "ticket-" ticket-id) :class list-container (section :id (str "ticket-" ticket-id) :class list-container
(div :class "grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm" (div :class "grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm"
c1 c2 c3) c1 c2 c3)
(button :type "button" :class pre-action :sx-get edit-url (button :type "button" :class pre-action :sx-get edit-url
:sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML" "Edit"))) :sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML" "Edit")))
(defcomp ~events-ticket-types-empty-row () (defcomp ~page/ticket-types-empty-row ()
(tr (td :colspan "4" :class "p-3 text-stone-500" "No ticket types yet."))) (tr (td :colspan "4" :class "p-3 text-stone-500" "No ticket types yet.")))
(defcomp ~events-ticket-types-row (&key tr-cls tt-href pill-cls hx-select tt-name cost-str count (defcomp ~page/ticket-types-row (&key tr-cls tt-href pill-cls hx-select tt-name cost-str count
action-btn del-url csrf-hdr) action-btn del-url csrf-hdr)
(tr :class tr-cls (tr :class tr-cls
(td :class "p-2 align-top w-1/3" (td :class "p-2 align-top w-1/3"
@@ -151,7 +151,7 @@
:sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed" :sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed"
(i :class "fa-solid fa-trash"))))) (i :class "fa-solid fa-trash")))))
(defcomp ~events-ticket-types-table (&key list-container rows action-btn add-url) (defcomp ~page/ticket-types-table (&key list-container rows action-btn add-url)
(section :id "tickets-table" :class list-container (section :id "tickets-table" :class list-container
(table :class "w-full text-sm border table-fixed" (table :class "w-full text-sm border table-fixed"
(thead :class "bg-stone-100" (thead :class "bg-stone-100"
@@ -164,7 +164,7 @@
(button :class action-btn :sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML" (button :class action-btn :sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
(i :class "fa fa-plus") " Add ticket type")))) (i :class "fa fa-plus") " Add ticket type"))))
(defcomp ~events-ticket-config-display (&key price-str count-str show-js) (defcomp ~page/ticket-config-display (&key price-str count-str show-js)
(div :class "space-y-2" (div :class "space-y-2"
(div :class "flex items-center gap-2" (div :class "flex items-center gap-2"
(span :class "text-sm font-medium text-stone-700" "Price:") (span :class "text-sm font-medium text-stone-700" "Price:")
@@ -175,13 +175,13 @@
(button :type "button" :class "text-xs text-blue-600 hover:text-blue-800 underline" (button :type "button" :class "text-xs text-blue-600 hover:text-blue-800 underline"
:onclick show-js "Edit ticket config"))) :onclick show-js "Edit ticket config")))
(defcomp ~events-ticket-config-none (&key show-js) (defcomp ~page/ticket-config-none (&key show-js)
(div :class "space-y-2" (div :class "space-y-2"
(span :class "text-sm text-stone-400" "No tickets configured") (span :class "text-sm text-stone-400" "No tickets configured")
(button :type "button" :class "block text-xs text-blue-600 hover:text-blue-800 underline" (button :type "button" :class "block text-xs text-blue-600 hover:text-blue-800 underline"
:onclick show-js "Configure tickets"))) :onclick show-js "Configure tickets")))
(defcomp ~events-ticket-config-form (&key entry-id hidden-cls update-url csrf price-val count-val hide-js) (defcomp ~page/ticket-config-form (&key entry-id hidden-cls update-url csrf price-val count-val hide-js)
(form :id (str "ticket-form-" entry-id) :class (str hidden-cls " space-y-3 mt-2 p-3 border rounded bg-stone-50") (form :id (str "ticket-form-" entry-id) :class (str hidden-cls " space-y-3 mt-2 p-3 border rounded bg-stone-50")
:sx-post update-url :sx-target (str "#entry-tickets-" entry-id) :sx-swap "innerHTML" :sx-post update-url :sx-target (str "#entry-tickets-" entry-id) :sx-swap "innerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
@@ -203,12 +203,12 @@
:onclick hide-js "Cancel")))) :onclick hide-js "Cancel"))))
;; Data-driven buy form — Python passes pre-resolved data, .sx does layout + iteration ;; Data-driven buy form — Python passes pre-resolved data, .sx does layout + iteration
(defcomp ~events-buy-form (&key entry-id info-sold info-remaining info-basket (defcomp ~page/buy-form (&key entry-id info-sold info-remaining info-basket
ticket-types user-ticket-counts-by-type ticket-types user-ticket-counts-by-type
user-ticket-count price-str adjust-url csrf state user-ticket-count price-str adjust-url csrf state
my-tickets-href) my-tickets-href)
(if (!= state "confirmed") (if (!= state "confirmed")
(~events-buy-not-confirmed :entry-id (str entry-id)) (~page/buy-not-confirmed :entry-id (str entry-id))
(let ((eid-s (str entry-id)) (let ((eid-s (str entry-id))
(target (str "#ticket-buy-" entry-id))) (target (str "#ticket-buy-" entry-id)))
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-stone-200 bg-white p-4" (div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-stone-200 bg-white p-4"
@@ -234,19 +234,19 @@
(div :class "flex items-center justify-between p-3 rounded-lg bg-stone-50 border border-stone-100" (div :class "flex items-center justify-between p-3 rounded-lg bg-stone-50 border border-stone-100"
(div (div :class "font-medium text-sm" (get tt "name")) (div (div :class "font-medium text-sm" (get tt "name"))
(div :class "text-xs text-stone-500" (get tt "cost_str"))) (div :class "text-xs text-stone-500" (get tt "cost_str")))
(~events-adjust-inline :csrf csrf :adjust-url adjust-url :target target (~page/adjust-inline :csrf csrf :adjust-url adjust-url :target target
:entry-id eid-s :count tt-count :ticket-type-id tt-id :entry-id eid-s :count tt-count :ticket-type-id tt-id
:my-tickets-href my-tickets-href)))) :my-tickets-href my-tickets-href))))
ticket-types)) ticket-types))
(<> (div :class "flex items-center justify-between mb-4" (<> (div :class "flex items-center justify-between mb-4"
(div (span :class "font-medium text-green-600" price-str) (div (span :class "font-medium text-green-600" price-str)
(span :class "text-sm text-stone-500 ml-2" "per ticket"))) (span :class "text-sm text-stone-500 ml-2" "per ticket")))
(~events-adjust-inline :csrf csrf :adjust-url adjust-url :target target (~page/adjust-inline :csrf csrf :adjust-url adjust-url :target target
:entry-id eid-s :count (if user-ticket-count user-ticket-count 0) :entry-id eid-s :count (if user-ticket-count user-ticket-count 0)
:ticket-type-id nil :my-tickets-href my-tickets-href))))))) :ticket-type-id nil :my-tickets-href my-tickets-href)))))))
;; Inline +/- controls (used by both default and per-type) ;; Inline +/- controls (used by both default and per-type)
(defcomp ~events-adjust-inline (&key csrf adjust-url target entry-id count ticket-type-id my-tickets-href) (defcomp ~page/adjust-inline (&key csrf adjust-url target entry-id count ticket-type-id my-tickets-href)
(if (= count 0) (if (= count 0)
(form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" :class "flex items-center" (form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" :class "flex items-center"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
@@ -279,13 +279,13 @@
:class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl"
"+"))))) "+")))))
(defcomp ~events-buy-not-confirmed (&key entry-id) (defcomp ~page/buy-not-confirmed (&key entry-id)
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-stone-200 bg-stone-50 p-4 text-sm text-stone-500" (div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-stone-200 bg-stone-50 p-4 text-sm text-stone-500"
(i :class "fa fa-ticket mr-1" :aria-hidden "true") (i :class "fa fa-ticket mr-1" :aria-hidden "true")
"Tickets available once this event is confirmed.")) "Tickets available once this event is confirmed."))
(defcomp ~events-buy-result (&key entry-id tickets remaining my-tickets-href) (defcomp ~page/buy-result (&key entry-id tickets remaining my-tickets-href)
(let ((count (len tickets)) (let ((count (len tickets))
(suffix (if (= count 1) "" "s"))) (suffix (if (= count 1) "" "s")))
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-emerald-200 bg-emerald-50 p-4" (div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-emerald-200 bg-emerald-50 p-4"
@@ -308,21 +308,21 @@
"View all my tickets"))))) "View all my tickets")))))
;; Single response wrappers for POST routes (include OOB cart icon) ;; Single response wrappers for POST routes (include OOB cart icon)
(defcomp ~events-buy-response (&key entry-id tickets remaining my-tickets-href (defcomp ~page/buy-response (&key entry-id tickets remaining my-tickets-href
cart-count blog-href cart-href logo) cart-count blog-href cart-href logo)
(<> (<>
(~events-cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo) (~page/cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo)
(~events-buy-result :entry-id entry-id :tickets tickets :remaining remaining (~page/buy-result :entry-id entry-id :tickets tickets :remaining remaining
:my-tickets-href my-tickets-href))) :my-tickets-href my-tickets-href)))
(defcomp ~events-adjust-response (&key cart-count blog-href cart-href logo (defcomp ~page/adjust-response (&key cart-count blog-href cart-href logo
entry-id info-sold info-remaining info-basket entry-id info-sold info-remaining info-basket
ticket-types user-ticket-counts-by-type ticket-types user-ticket-counts-by-type
user-ticket-count price-str adjust-url csrf state user-ticket-count price-str adjust-url csrf state
my-tickets-href) my-tickets-href)
(<> (<>
(~events-cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo) (~page/cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo)
(~events-buy-form :entry-id entry-id :info-sold info-sold :info-remaining info-remaining (~page/buy-form :entry-id entry-id :info-sold info-sold :info-remaining info-remaining
:info-basket info-basket :ticket-types ticket-types :info-basket info-basket :ticket-types ticket-types
:user-ticket-counts-by-type user-ticket-counts-by-type :user-ticket-counts-by-type user-ticket-counts-by-type
:user-ticket-count user-ticket-count :price-str price-str :user-ticket-count user-ticket-count :price-str price-str
@@ -330,18 +330,18 @@
:my-tickets-href my-tickets-href))) :my-tickets-href my-tickets-href)))
;; Unified OOB cart icon — picks logo or badge based on count ;; Unified OOB cart icon — picks logo or badge based on count
(defcomp ~events-cart-icon (&key cart-count blog-href cart-href logo) (defcomp ~page/cart-icon (&key cart-count blog-href cart-href logo)
(if (= cart-count 0) (if (= cart-count 0)
(~events-cart-icon-logo :blog-href blog-href :logo logo) (~page/cart-icon-logo :blog-href blog-href :logo logo)
(~events-cart-icon-badge :cart-href cart-href :count (str cart-count)))) (~page/cart-icon-badge :cart-href cart-href :count (str cart-count))))
(defcomp ~events-cart-icon-logo (&key blog-href logo) (defcomp ~page/cart-icon-logo (&key blog-href logo)
(div :id "cart-mini" :sx-swap-oob "true" (div :id "cart-mini" :sx-swap-oob "true"
(div :class "h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0" (div :class "h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0"
(a :href blog-href :class "h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1" (a :href blog-href :class "h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1"
(img :src logo :class "h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0"))))) (img :src logo :class "h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0")))))
(defcomp ~events-cart-icon-badge (&key cart-href count) (defcomp ~page/cart-icon-badge (&key cart-href count)
(div :id "cart-mini" :sx-swap-oob "true" (div :id "cart-mini" :sx-swap-oob "true"
(a :href cart-href :class "relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700" (a :href cart-href :class "relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700"
(i :class "fa fa-shopping-cart text-5xl" :aria-hidden "true") (i :class "fa fa-shopping-cart text-5xl" :aria-hidden "true")
@@ -349,37 +349,37 @@
count)))) count))))
;; Inline ticket widget (for all-events/page-summary cards) ;; Inline ticket widget (for all-events/page-summary cards)
(defcomp ~events-tw-form (&key ticket-url target csrf entry-id count-val btn) (defcomp ~page/tw-form (&key ticket-url target csrf entry-id count-val btn)
(form :action ticket-url :method "post" :sx-post ticket-url :sx-target target :sx-swap "outerHTML" (form :action ticket-url :method "post" :sx-post ticket-url :sx-target target :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "entry_id" :value entry-id) (input :type "hidden" :name "entry_id" :value entry-id)
(input :type "hidden" :name "count" :value count-val) (input :type "hidden" :name "count" :value count-val)
btn)) btn))
(defcomp ~events-tw-cart-plus () (defcomp ~page/tw-cart-plus ()
(button :type "submit" :class "relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1" (button :type "submit" :class "relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1"
(i :class "fa fa-cart-plus text-2xl" :aria-hidden "true"))) (i :class "fa fa-cart-plus text-2xl" :aria-hidden "true")))
(defcomp ~events-tw-minus () (defcomp ~page/tw-minus ()
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "-")) (button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "-"))
(defcomp ~events-tw-plus () (defcomp ~page/tw-plus ()
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+")) (button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+"))
(defcomp ~events-tw-cart-icon (&key qty) (defcomp ~page/tw-cart-icon (&key qty)
(span :class "relative inline-flex items-center justify-center text-emerald-700" (span :class "relative inline-flex items-center justify-center text-emerald-700"
(span :class "relative inline-flex items-center justify-center" (span :class "relative inline-flex items-center justify-center"
(i :class "fa-solid fa-shopping-cart text-xl" :aria-hidden "true") (i :class "fa-solid fa-shopping-cart text-xl" :aria-hidden "true")
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none" (span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
(span :class "flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold" qty))))) (span :class "flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold" qty)))))
(defcomp ~events-tw-widget (&key entry-id price inner) (defcomp ~page/tw-widget (&key entry-id price inner)
(div :id (str "page-ticket-" entry-id) :class "flex items-center gap-2" (div :id (str "page-ticket-" entry-id) :class "flex items-center gap-2"
(span :class "text-green-600 font-medium text-sm" price) (span :class "text-green-600 font-medium text-sm" price)
inner)) inner))
;; Entry posts panel ;; Entry posts panel
(defcomp ~events-entry-posts-panel (&key posts search-url entry-id) (defcomp ~page/entry-posts-panel (&key posts search-url entry-id)
(div :class "space-y-2" (div :class "space-y-2"
posts posts
(div :class "mt-3 pt-3 border-t" (div :class "mt-3 pt-3 border-t"
@@ -390,13 +390,13 @@
:sx-target (str "#post-search-results-" entry-id) :sx-swap "innerHTML" :name "q") :sx-target (str "#post-search-results-" entry-id) :sx-swap "innerHTML" :name "q")
(div :id (str "post-search-results-" entry-id) :class "mt-2 max-h-96 overflow-y-auto border rounded")))) (div :id (str "post-search-results-" entry-id) :class "mt-2 max-h-96 overflow-y-auto border rounded"))))
(defcomp ~events-entry-posts-list (&key items) (defcomp ~page/entry-posts-list (&key items)
(div :class "space-y-2" items)) (div :class "space-y-2" items))
(defcomp ~events-entry-posts-none () (defcomp ~page/entry-posts-none ()
(p :class "text-sm text-stone-400" "No posts associated")) (p :class "text-sm text-stone-400" "No posts associated"))
(defcomp ~events-entry-post-item (&key img title del-url entry-id csrf-hdr) (defcomp ~page/entry-post-item (&key img title del-url entry-id csrf-hdr)
(div :class "flex items-center justify-between gap-3 p-2 bg-stone-50 rounded border" (div :class "flex items-center justify-between gap-3 p-2 bg-stone-50 rounded border"
img (span :class "text-sm flex-1" title) img (span :class "text-sm flex-1" title)
(button :type "button" :class "text-xs text-red-600 hover:text-red-800 flex-shrink-0" (button :type "button" :class "text-xs text-red-600 hover:text-red-800 flex-shrink-0"
@@ -409,41 +409,41 @@
:sx-headers csrf-hdr :sx-headers csrf-hdr
(i :class "fa fa-times") " Remove"))) (i :class "fa fa-times") " Remove")))
(defcomp ~events-post-img (&key src alt) (defcomp ~page/post-img (&key src alt)
(img :src src :alt alt :class "w-8 h-8 rounded-full object-cover flex-shrink-0")) (img :src src :alt alt :class "w-8 h-8 rounded-full object-cover flex-shrink-0"))
(defcomp ~events-post-img-placeholder () (defcomp ~page/post-img-placeholder ()
(div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0")) (div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"))
;; Entry posts nav OOB ;; Entry posts nav OOB
(defcomp ~events-entry-posts-nav-oob-empty () (defcomp ~page/entry-posts-nav-oob-empty ()
(div :id "entry-posts-nav-wrapper" :sx-swap-oob "true")) (div :id "entry-posts-nav-wrapper" :sx-swap-oob "true"))
(defcomp ~events-entry-posts-nav-oob (&key items) (defcomp ~page/entry-posts-nav-oob (&key items)
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id "entry-posts-nav-wrapper" :sx-swap-oob "true" :id "entry-posts-nav-wrapper" :sx-swap-oob "true"
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items))) (div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
(defcomp ~events-entry-nav-post (&key href nav-btn img title) (defcomp ~page/entry-nav-post (&key href nav-btn img title)
(a :href href :class nav-btn img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title)))) (a :href href :class nav-btn img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
;; Post nav entries OOB ;; Post nav entries OOB
(defcomp ~events-post-nav-oob-empty () (defcomp ~page/post-nav-oob-empty ()
(div :id "entries-calendars-nav-wrapper" :sx-swap-oob "true")) (div :id "entries-calendars-nav-wrapper" :sx-swap-oob "true"))
(defcomp ~events-post-nav-entry (&key href nav-btn name time-str) (defcomp ~page/post-nav-entry (&key href nav-btn name time-str)
(a :href href :class nav-btn (a :href href :class nav-btn
(div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0") (div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0")
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" time-str)))) (div :class "text-xs text-stone-600 truncate" time-str))))
(defcomp ~events-post-nav-calendar (&key href nav-btn name) (defcomp ~page/post-nav-calendar (&key href nav-btn name)
(a :href href :class nav-btn (a :href href :class nav-btn
(i :class "fa fa-calendar" :aria-hidden "true") (i :class "fa fa-calendar" :aria-hidden "true")
(div name))) (div name)))
(defcomp ~events-post-nav-wrapper (&key items hyperscript) (defcomp ~page/post-nav-wrapper (&key items hyperscript)
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id "entries-calendars-nav-wrapper" :sx-swap-oob "true" :id "entries-calendars-nav-wrapper" :sx-swap-oob "true"
(button :class "entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded" (button :class "entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
@@ -461,7 +461,7 @@
(i :class "fa fa-chevron-right")))) (i :class "fa fa-chevron-right"))))
;; Entry nav post link (with image) ;; Entry nav post link (with image)
(defcomp ~events-entry-nav-post-link (&key href img title) (defcomp ~page/entry-nav-post-link (&key href img title)
(a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0" (a :href href :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0"
img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title)))) img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
@@ -471,60 +471,60 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Post image helper from data ;; Post image helper from data
(defcomp ~events-post-img-from-data (&key src alt) (defcomp ~page/post-img-from-data (&key src alt)
(if src (if src
(~events-post-img :src src :alt alt) (~page/post-img :src src :alt alt)
(~events-post-img-placeholder))) (~page/post-img-placeholder)))
;; Entry posts nav OOB from data ;; Entry posts nav OOB from data
(defcomp ~events-entry-posts-nav-oob-from-data (&key nav-btn posts) (defcomp ~page/entry-posts-nav-oob-from-data (&key nav-btn posts)
(if (empty? (or posts (list))) (if (empty? (or posts (list)))
(~events-entry-posts-nav-oob-empty) (~page/entry-posts-nav-oob-empty)
(~events-entry-posts-nav-oob (~page/entry-posts-nav-oob
:items (<> (map (lambda (p) :items (<> (map (lambda (p)
(~events-entry-nav-post (~page/entry-nav-post
:href (get p "href") :nav-btn nav-btn :href (get p "href") :nav-btn nav-btn
:img (~events-post-img-from-data :src (get p "img") :alt (get p "title")) :img (~page/post-img-from-data :src (get p "img") :alt (get p "title"))
:title (get p "title"))) :title (get p "title")))
posts))))) posts)))))
;; Entry posts nav (non-OOB) from data — for desktop nav embedding ;; Entry posts nav (non-OOB) from data — for desktop nav embedding
(defcomp ~events-entry-posts-nav-inner-from-data (&key posts) (defcomp ~page/entry-posts-nav-inner-from-data (&key posts)
(when (not (empty? (or posts (list)))) (when (not (empty? (or posts (list))))
(~events-entry-posts-nav-oob (~page/entry-posts-nav-oob
:items (<> (map (lambda (p) :items (<> (map (lambda (p)
(~events-entry-nav-post-link (~page/entry-nav-post-link
:href (get p "href") :href (get p "href")
:img (~events-post-img-from-data :src (get p "img") :alt (get p "title")) :img (~page/post-img-from-data :src (get p "img") :alt (get p "title"))
:title (get p "title"))) :title (get p "title")))
posts))))) posts)))))
;; Post nav entries+calendars OOB from data ;; Post nav entries+calendars OOB from data
(defcomp ~events-post-nav-wrapper-from-data (&key nav-btn entries calendars hyperscript) (defcomp ~page/post-nav-wrapper-from-data (&key nav-btn entries calendars hyperscript)
(if (and (empty? (or entries (list))) (empty? (or calendars (list)))) (if (and (empty? (or entries (list))) (empty? (or calendars (list))))
(~events-post-nav-oob-empty) (~page/post-nav-oob-empty)
(~events-post-nav-wrapper (~page/post-nav-wrapper
:items (<> :items (<>
(map (lambda (e) (map (lambda (e)
(~events-post-nav-entry (~page/post-nav-entry
:href (get e "href") :nav-btn nav-btn :href (get e "href") :nav-btn nav-btn
:name (get e "name") :time-str (get e "time-str"))) :name (get e "name") :time-str (get e "time-str")))
(or entries (list))) (or entries (list)))
(map (lambda (c) (map (lambda (c)
(~events-post-nav-calendar (~page/post-nav-calendar
:href (get c "href") :nav-btn nav-btn :name (get c "name"))) :href (get c "href") :nav-btn nav-btn :name (get c "name")))
(or calendars (list)))) (or calendars (list))))
:hyperscript hyperscript))) :hyperscript hyperscript)))
;; Entry posts panel from data ;; Entry posts panel from data
(defcomp ~events-entry-posts-panel-from-data (&key entry-id posts search-url) (defcomp ~page/entry-posts-panel-from-data (&key entry-id posts search-url)
(~events-entry-posts-panel (~page/entry-posts-panel
:posts (if (empty? (or posts (list))) :posts (if (empty? (or posts (list)))
(~events-entry-posts-none) (~page/entry-posts-none)
(~events-entry-posts-list (~page/entry-posts-list
:items (<> (map (lambda (p) :items (<> (map (lambda (p)
(~events-entry-post-item (~page/entry-post-item
:img (~events-post-img-from-data :src (get p "img") :alt (get p "title")) :img (~page/post-img-from-data :src (get p "img") :alt (get p "title"))
:title (get p "title") :title (get p "title")
:del-url (get p "del-url") :entry-id entry-id :del-url (get p "del-url") :entry-id entry-id
:csrf-hdr (get p "csrf-hdr"))) :csrf-hdr (get p "csrf-hdr")))
@@ -532,11 +532,11 @@
:search-url search-url :entry-id entry-id)) :search-url search-url :entry-id entry-id))
;; CRUD list/panel from data — shared by calendars + markets ;; CRUD list/panel from data — shared by calendars + markets
(defcomp ~events-crud-list-from-data (&key items empty-msg list-id) (defcomp ~page/crud-list-from-data (&key items empty-msg list-id)
(if (empty? (or items (list))) (if (empty? (or items (list)))
(~empty-state :message empty-msg :cls "text-gray-500 mt-4") (~shared:misc/empty-state :message empty-msg :cls "text-gray-500 mt-4")
(<> (map (lambda (item) (<> (map (lambda (item)
(~crud-item (~shared:misc/crud-item
:href (get item "href") :name (get item "name") :slug (get item "slug") :href (get item "href") :name (get item "name") :slug (get item "slug")
:del-url (get item "del-url") :csrf-hdr (get item "csrf-hdr") :del-url (get item "del-url") :csrf-hdr (get item "csrf-hdr")
:list-id list-id :list-id list-id
@@ -544,84 +544,84 @@
:confirm-text (get item "confirm-text"))) :confirm-text (get item "confirm-text")))
items)))) items))))
(defcomp ~events-crud-panel-from-data (&key can-create create-url csrf errors-id list-id (defcomp ~page/crud-panel-from-data (&key can-create create-url csrf errors-id list-id
placeholder btn-label items empty-msg) placeholder btn-label items empty-msg)
(~crud-panel (~shared:misc/crud-panel
:form (when can-create :form (when can-create
(~crud-create-form (~shared:misc/crud-create-form
:create-url create-url :csrf csrf :errors-id errors-id :create-url create-url :csrf csrf :errors-id errors-id
:list-id list-id :placeholder placeholder :btn-label btn-label)) :list-id list-id :placeholder placeholder :btn-label btn-label))
:list (~events-crud-list-from-data :items items :empty-msg empty-msg :list-id list-id) :list (~page/crud-list-from-data :items items :empty-msg empty-msg :list-id list-id)
:list-id list-id)) :list-id list-id))
;; Post nav admin cog ;; Post nav admin cog
(defcomp ~events-post-nav-admin-cog (&key href aclass) (defcomp ~page/post-nav-admin-cog (&key href aclass)
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href :class aclass (a :href href :class aclass
(i :class "fa fa-cog" :aria-hidden "true")))) (i :class "fa fa-cog" :aria-hidden "true"))))
;; Post nav from data — calendar links + container nav + admin ;; Post nav from data — calendar links + container nav + admin
(defcomp ~events-post-nav-from-data (&key calendars container-nav select-colours (defcomp ~page/post-nav-from-data (&key calendars container-nav select-colours
has-admin admin-href aclass) has-admin admin-href aclass)
(<> (<>
(map (lambda (c) (map (lambda (c)
(~nav-link :href (get c "href") :icon "fa fa-calendar" (~shared:layout/nav-link :href (get c "href") :icon "fa fa-calendar"
:label (get c "name") :select-colours select-colours :label (get c "name") :select-colours select-colours
:is-selected (get c "is-selected"))) :is-selected (get c "is-selected")))
(or calendars (list))) (or calendars (list)))
(when container-nav container-nav) (when container-nav container-nav)
(when has-admin (when has-admin
(~events-post-nav-admin-cog :href admin-href :aclass aclass)))) (~page/post-nav-admin-cog :href admin-href :aclass aclass))))
;; Calendar nav from data — slots + admin link ;; Calendar nav from data — slots + admin link
(defcomp ~events-calendar-nav-from-data (&key slots-href admin-href select-colours is-admin) (defcomp ~page/calendar-nav-from-data (&key slots-href admin-href select-colours is-admin)
(<> (<>
(~nav-link :href slots-href :icon "fa fa-clock" (~shared:layout/nav-link :href slots-href :icon "fa fa-clock"
:label "Slots" :select-colours select-colours) :label "Slots" :select-colours select-colours)
(when is-admin (when is-admin
(~nav-link :href admin-href :icon "fa fa-cog" (~shared:layout/nav-link :href admin-href :icon "fa fa-cog"
:select-colours select-colours)))) :select-colours select-colours))))
;; Calendar admin nav from data ;; Calendar admin nav from data
(defcomp ~events-calendar-admin-nav-from-data (&key links select-colours) (defcomp ~page/calendar-admin-nav-from-data (&key links select-colours)
(<> (map (lambda (l) (<> (map (lambda (l)
(~nav-link :href (get l "href") :label (get l "label") (~shared:layout/nav-link :href (get l "href") :label (get l "label")
:select-colours select-colours)) :select-colours select-colours))
(or links (list))))) (or links (list)))))
;; Day nav from data — confirmed entries + admin link ;; Day nav from data — confirmed entries + admin link
(defcomp ~events-day-nav-from-data (&key entries is-admin admin-href) (defcomp ~page/day-nav-from-data (&key entries is-admin admin-href)
(<> (<>
(when (not (empty? (or entries (list)))) (when (not (empty? (or entries (list))))
(~events-day-entries-nav (~day/entries-nav
:inner (<> (map (lambda (e) :inner (<> (map (lambda (e)
(~events-day-entry-link (~day/entry-link
:href (get e "href") :name (get e "name") :time-str (get e "time-str"))) :href (get e "href") :name (get e "name") :time-str (get e "time-str")))
entries)))) entries))))
(when is-admin (when is-admin
(~nav-link :href admin-href :icon "fa fa-cog")))) (~shared:layout/nav-link :href admin-href :icon "fa fa-cog"))))
;; Post search results from data ;; Post search results from data
(defcomp ~events-post-search-results-from-data (&key items page next-url has-more) (defcomp ~page/post-search-results-from-data (&key items page next-url has-more)
(<> (<>
(map (lambda (item) (map (lambda (item)
(~events-post-search-item (~forms/post-search-item
:post-url (get item "post-url") :entry-id (get item "entry-id") :post-url (get item "post-url") :entry-id (get item "entry-id")
:csrf (get item "csrf") :post-id (get item "post-id") :csrf (get item "csrf") :post-id (get item "post-id")
:img (~events-post-img-from-data :src (get item "img") :alt (get item "title")) :img (~page/post-img-from-data :src (get item "img") :alt (get item "title"))
:title (get item "title"))) :title (get item "title")))
(or items (list))) (or items (list)))
(cond (cond
(has-more (~events-post-search-sentinel :page page :next-url next-url)) (has-more (~forms/post-search-sentinel :page page :next-url next-url))
((not (empty? (or items (list)))) (~events-post-search-end)) ((not (empty? (or items (list)))) (~forms/post-search-end))
(true "")))) (true ""))))
;; Entry options from data — state-driven button composition ;; Entry options from data — state-driven button composition
(defcomp ~events-entry-options-from-data (&key entry-id state buttons) (defcomp ~page/entry-options-from-data (&key entry-id state buttons)
(~events-entry-options (~admin/entry-options
:entry-id entry-id :entry-id entry-id
:buttons (<> (map (lambda (b) :buttons (<> (map (lambda (b)
(~events-entry-option-button (~admin/entry-option-button
:url (get b "url") :target (str "#calendar_entry_options_" entry-id) :url (get b "url") :target (str "#calendar_entry_options_" entry-id)
:csrf (get b "csrf") :btn-type (get b "btn-type") :csrf (get b "csrf") :btn-type (get b "btn-type")
:action-btn (get b "action-btn") :action-btn (get b "action-btn")

View File

@@ -1,12 +1,12 @@
;; Events payments components ;; Events payments components
(defcomp ~events-payments-panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix) (defcomp ~payments/panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix)
(section :class "p-4 max-w-lg mx-auto" (section :class "p-4 max-w-lg mx-auto"
(~sumup-settings-form :update-url update-url :csrf csrf :merchant-code merchant-code (~shared:misc/sumup-settings-form :update-url update-url :csrf csrf :merchant-code merchant-code
:placeholder placeholder :input-cls input-cls :sumup-configured sumup-configured :placeholder placeholder :input-cls input-cls :sumup-configured sumup-configured
:checkout-prefix checkout-prefix :sx-select "#payments-panel"))) :checkout-prefix checkout-prefix :sx-select "#payments-panel")))
(defcomp ~events-markets-create-form (&key create-url csrf) (defcomp ~payments/markets-create-form (&key create-url csrf)
(<> (<>
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600") (div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url (form :class "mt-4 flex gap-2 items-end" :sx-post create-url
@@ -20,15 +20,15 @@
:placeholder "e.g. Farm Shop, Bakery")) :placeholder "e.g. Farm Shop, Bakery"))
(button :type "submit" :class "border rounded px-3 py-2" "Add market")))) (button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
(defcomp ~events-markets-panel (&key form list) (defcomp ~payments/markets-panel (&key form list)
(section :class "p-4" (section :class "p-4"
form form
(div :id "markets-list" :class "mt-6" list))) (div :id "markets-list" :class "mt-6" list)))
(defcomp ~events-markets-empty () (defcomp ~payments/markets-empty ()
(p :class "text-gray-500 mt-4" "No markets yet. Create one above.")) (p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
(defcomp ~events-markets-item (&key href market-name market-slug del-url csrf-hdr) (defcomp ~payments/markets-item (&key href market-name market-slug del-url csrf-hdr)
(div :class "mt-6 border rounded-lg p-4" (div :class "mt-6 border rounded-lg p-4"
(div :class "flex items-center justify-between gap-3" (div :class "flex items-center justify-between gap-3"
(a :class "flex items-baseline gap-3" :href href (a :class "flex items-baseline gap-3" :href href

View File

@@ -1,6 +1,6 @@
;; Events ticket components ;; Events ticket components
(defcomp ~events-ticket-card (&key (href :as string) (entry-name :as string) (type-name :as string?) (time-str :as string?) (cal-name :as string?) badge (code-prefix :as string)) (defcomp ~tickets/card (&key (href :as string) (entry-name :as string) (type-name :as string?) (time-str :as string?) (cal-name :as string?) badge (code-prefix :as string))
(a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition" (a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -12,7 +12,7 @@
badge badge
(span :class "text-xs text-stone-400 font-mono" (str code-prefix "...")))))) (span :class "text-xs text-stone-400 font-mono" (str code-prefix "..."))))))
(defcomp ~events-tickets-panel (&key (list-container :as string) (has-tickets :as boolean) cards) (defcomp ~tickets/panel (&key (list-container :as string) (has-tickets :as boolean) cards)
(section :id "tickets-list" :class list-container (section :id "tickets-list" :class list-container
(h1 :class "text-2xl font-bold mb-6" "My Tickets") (h1 :class "text-2xl font-bold mb-6" "My Tickets")
(if has-tickets (if has-tickets
@@ -22,7 +22,7 @@
(p :class "text-lg" "No tickets yet") (p :class "text-lg" "No tickets yet")
(p :class "text-sm mt-1" "Tickets will appear here after you purchase them."))))) (p :class "text-sm mt-1" "Tickets will appear here after you purchase them.")))))
(defcomp ~events-ticket-detail (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) badge (defcomp ~tickets/detail (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) badge
(type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (cal-name :as string?) (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (cal-name :as string?)
(type-desc :as string?) (checkin-str :as string?) (qr-script :as string)) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string))
(section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto") (section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto")
@@ -54,25 +54,25 @@
(script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js") (script :src "https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js")
(script qr-script))) (script qr-script)))
(defcomp ~events-ticket-admin-stat (&key (border :as string) (bg :as string) (text-cls :as string) (label-cls :as string) (value :as string) (label :as string)) (defcomp ~tickets/admin-stat (&key (border :as string) (bg :as string) (text-cls :as string) (label-cls :as string) (value :as string) (label :as string))
(div :class (str "rounded-xl border " border " " bg " p-4 text-center") (div :class (str "rounded-xl border " border " " bg " p-4 text-center")
(div :class (str "text-2xl font-bold " text-cls) value) (div :class (str "text-2xl font-bold " text-cls) value)
(div :class (str "text-xs " label-cls " uppercase tracking-wide") label))) (div :class (str "text-xs " label-cls " uppercase tracking-wide") label)))
(defcomp ~events-ticket-admin-date (&key (date-str :as string)) (defcomp ~tickets/admin-date (&key (date-str :as string))
(div :class "text-xs text-stone-500" date-str)) (div :class "text-xs text-stone-500" date-str))
(defcomp ~events-ticket-admin-checkin-form (&key (checkin-url :as string) (code :as string) (csrf :as string)) (defcomp ~tickets/admin-checkin-form (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML" (form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition" (button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition"
(i :class "fa fa-check mr-1" :aria-hidden "true") "Check in"))) (i :class "fa fa-check mr-1" :aria-hidden "true") "Check in")))
(defcomp ~events-ticket-admin-checked-in (&key (time-str :as string)) (defcomp ~tickets/admin-checked-in (&key (time-str :as string))
(span :class "text-xs text-blue-600" (span :class "text-xs text-blue-600"
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str))) (i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))
(defcomp ~events-ticket-admin-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge action) (defcomp ~tickets/admin-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge action)
(tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code) (tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code)
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short)) (td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date) (td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
@@ -80,7 +80,7 @@
(td :class "px-4 py-3" badge) (td :class "px-4 py-3" badge)
(td :class "px-4 py-3" action))) (td :class "px-4 py-3" action)))
(defcomp ~events-ticket-admin-panel (&key (list-container :as string) stats (lookup-url :as string) (has-tickets :as boolean) rows) (defcomp ~tickets/admin-panel (&key (list-container :as string) stats (lookup-url :as string) (has-tickets :as boolean) rows)
(section :id "ticket-admin" :class list-container (section :id "ticket-admin" :class list-container
(h1 :class "text-2xl font-bold mb-6" "Ticket Admin") (h1 :class "text-2xl font-bold mb-6" "Ticket Admin")
(div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats) (div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats)
@@ -113,11 +113,11 @@
(tbody :class "divide-y divide-stone-100" rows)) (tbody :class "divide-y divide-stone-100" rows))
(div :class "px-6 py-8 text-center text-stone-500" "No tickets yet")))))) (div :class "px-6 py-8 text-center text-stone-500" "No tickets yet"))))))
(defcomp ~events-checkin-error (&key (message :as string)) (defcomp ~tickets/checkin-error (&key (message :as string))
(div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800" (div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message)) (i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
(defcomp ~events-checkin-success-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge (time-str :as string)) (defcomp ~tickets/checkin-success-row (&key (code :as string) (code-short :as string) (entry-name :as string) date (type-name :as string) badge (time-str :as string))
(tr :class "bg-blue-50" :id (str "ticket-row-" code) (tr :class "bg-blue-50" :id (str "ticket-row-" code)
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short)) (td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date) (td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
@@ -127,65 +127,65 @@
(span :class "text-xs text-blue-600" (span :class "text-xs text-blue-600"
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str))))) (i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))))
(defcomp ~events-lookup-error (&key (message :as string)) (defcomp ~tickets/lookup-error (&key (message :as string))
(div :class "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800" (div :class "rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800"
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message)) (i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
(defcomp ~events-lookup-info (&key (entry-name :as string)) (defcomp ~tickets/lookup-info (&key (entry-name :as string))
(div :class "font-semibold text-lg" entry-name)) (div :class "font-semibold text-lg" entry-name))
(defcomp ~events-lookup-type (&key (type-name :as string)) (defcomp ~tickets/lookup-type (&key (type-name :as string))
(div :class "text-sm text-stone-600" type-name)) (div :class "text-sm text-stone-600" type-name))
(defcomp ~events-lookup-date (&key (date-str :as string)) (defcomp ~tickets/lookup-date (&key (date-str :as string))
(div :class "text-sm text-stone-500 mt-1" date-str)) (div :class "text-sm text-stone-500 mt-1" date-str))
(defcomp ~events-lookup-cal (&key (cal-name :as string)) (defcomp ~tickets/lookup-cal (&key (cal-name :as string))
(div :class "text-xs text-stone-400 mt-0.5" cal-name)) (div :class "text-xs text-stone-400 mt-0.5" cal-name))
(defcomp ~events-lookup-status (&key badge (code :as string)) (defcomp ~tickets/lookup-status (&key badge (code :as string))
(div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code))) (div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code)))
(defcomp ~events-lookup-checkin-time (&key (date-str :as string)) (defcomp ~tickets/lookup-checkin-time (&key (date-str :as string))
(div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str))) (div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str)))
(defcomp ~events-lookup-checkin-btn (&key (checkin-url :as string) (code :as string) (csrf :as string)) (defcomp ~tickets/lookup-checkin-btn (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML" (form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" (button :type "submit"
:class "px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold text-lg" :class "px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold text-lg"
(i :class "fa fa-check mr-2" :aria-hidden "true") "Check In"))) (i :class "fa fa-check mr-2" :aria-hidden "true") "Check In")))
(defcomp ~events-lookup-checked-in () (defcomp ~tickets/lookup-checked-in ()
(div :class "text-blue-600 text-center" (div :class "text-blue-600 text-center"
(i :class "fa fa-check-circle text-3xl" :aria-hidden "true") (i :class "fa fa-check-circle text-3xl" :aria-hidden "true")
(div :class "text-sm font-medium mt-1" "Checked In"))) (div :class "text-sm font-medium mt-1" "Checked In")))
(defcomp ~events-lookup-cancelled () (defcomp ~tickets/lookup-cancelled ()
(div :class "text-red-600 text-center" (div :class "text-red-600 text-center"
(i :class "fa fa-times-circle text-3xl" :aria-hidden "true") (i :class "fa fa-times-circle text-3xl" :aria-hidden "true")
(div :class "text-sm font-medium mt-1" "Cancelled"))) (div :class "text-sm font-medium mt-1" "Cancelled")))
(defcomp ~events-lookup-card (&key info (code :as string) action) (defcomp ~tickets/lookup-card (&key info (code :as string) action)
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-4" (div :class "rounded-lg border border-stone-200 bg-stone-50 p-4"
(div :class "flex items-start justify-between gap-4" (div :class "flex items-start justify-between gap-4"
(div :class "flex-1" info) (div :class "flex-1" info)
(div :id (str "checkin-action-" code) action)))) (div :id (str "checkin-action-" code) action))))
(defcomp ~events-entry-tickets-admin-row (&key (code :as string) (code-short :as string) (type-name :as string) badge action) (defcomp ~tickets/entry-tickets-admin-row (&key (code :as string) (code-short :as string) (type-name :as string) badge action)
(tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code) (tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code)
(td :class "px-4 py-2 font-mono text-xs" code-short) (td :class "px-4 py-2 font-mono text-xs" code-short)
(td :class "px-4 py-2" type-name) (td :class "px-4 py-2" type-name)
(td :class "px-4 py-2" badge) (td :class "px-4 py-2" badge)
(td :class "px-4 py-2" action))) (td :class "px-4 py-2" action)))
(defcomp ~events-entry-tickets-admin-checkin (&key (checkin-url :as string) (code :as string) (csrf :as string)) (defcomp ~tickets/entry-tickets-admin-checkin (&key (checkin-url :as string) (code :as string) (csrf :as string))
(form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML" (form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700" (button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
"Check in"))) "Check in")))
(defcomp ~events-entry-tickets-admin-table (&key rows) (defcomp ~tickets/entry-tickets-admin-table (&key rows)
(div :class "overflow-x-auto rounded-xl border border-stone-200" (div :class "overflow-x-auto rounded-xl border border-stone-200"
(table :class "w-full text-sm" (table :class "w-full text-sm"
(thead :class "bg-stone-50" (thead :class "bg-stone-50"
@@ -195,10 +195,10 @@
(th :class "px-4 py-2 text-left font-medium text-stone-600" "Actions"))) (th :class "px-4 py-2 text-left font-medium text-stone-600" "Actions")))
(tbody :class "divide-y divide-stone-100" rows)))) (tbody :class "divide-y divide-stone-100" rows))))
(defcomp ~events-entry-tickets-admin-empty () (defcomp ~tickets/entry-tickets-admin-empty ()
(div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry")) (div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry"))
(defcomp ~events-entry-tickets-admin-panel (&key (entry-name :as string) (count-label :as string) body) (defcomp ~tickets/entry-tickets-admin-panel (&key (entry-name :as string) (count-label :as string) body)
(div :class "space-y-4" (div :class "space-y-4"
(div :class "flex items-center justify-between" (div :class "flex items-center justify-between"
(h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name)) (h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name))
@@ -211,72 +211,72 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; My tickets panel from data ;; My tickets panel from data
(defcomp ~events-tickets-panel-from-data (&key (list-container :as string) (tickets :as list?)) (defcomp ~tickets/panel-from-data (&key (list-container :as string) (tickets :as list?))
(~events-tickets-panel (~tickets/panel
:list-container list-container :list-container list-container
:has-tickets (not (empty? (or tickets (list)))) :has-tickets (not (empty? (or tickets (list))))
:cards (<> (map (lambda (t) :cards (<> (map (lambda (t)
(~events-ticket-card (~tickets/card
:href (get t "href") :entry-name (get t "entry-name") :href (get t "href") :entry-name (get t "entry-name")
:type-name (get t "type-name") :time-str (get t "time-str") :type-name (get t "type-name") :time-str (get t "time-str")
:cal-name (get t "cal-name") :cal-name (get t "cal-name")
:badge (~ticket-state-badge :state (get t "state")) :badge (~entries/ticket-state-badge :state (get t "state"))
:code-prefix (get t "code-prefix"))) :code-prefix (get t "code-prefix")))
(or tickets (list)))))) (or tickets (list))))))
;; Ticket detail from data — uses lg badge variant ;; Ticket detail from data — uses lg badge variant
(defcomp ~events-ticket-detail-from-data (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string) (defcomp ~tickets/detail-from-data (&key (list-container :as string) (back-href :as string) (header-bg :as string) (entry-name :as string)
(state :as string) (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?) (state :as string) (type-name :as string?) (code :as string) (time-date :as string?) (time-range :as string?)
(cal-name :as string?) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string)) (cal-name :as string?) (type-desc :as string?) (checkin-str :as string?) (qr-script :as string))
(~events-ticket-detail (~tickets/detail
:list-container list-container :back-href back-href :list-container list-container :back-href back-href
:header-bg header-bg :entry-name entry-name :header-bg header-bg :entry-name entry-name
:badge (~ticket-state-badge-lg :state state) :badge (~entries/ticket-state-badge-lg :state state)
:type-name type-name :code code :type-name type-name :code code
:time-date time-date :time-range time-range :time-date time-date :time-range time-range
:cal-name cal-name :type-desc type-desc :cal-name cal-name :type-desc type-desc
:checkin-str checkin-str :qr-script qr-script)) :checkin-str checkin-str :qr-script qr-script))
;; Ticket admin row from data — conditional action column ;; Ticket admin row from data — conditional action column
(defcomp ~events-ticket-admin-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?) (defcomp ~tickets/admin-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?)
(type-name :as string) (state :as string) (checkin-url :as string) (csrf :as string) (type-name :as string) (state :as string) (checkin-url :as string) (csrf :as string)
(checked-in-time :as string?)) (checked-in-time :as string?))
(~events-ticket-admin-row (~tickets/admin-row
:code code :code-short code-short :code code :code-short code-short
:entry-name entry-name :entry-name entry-name
:date (when date-str (~events-ticket-admin-date :date-str date-str)) :date (when date-str (~tickets/admin-date :date-str date-str))
:type-name type-name :type-name type-name
:badge (~ticket-state-badge :state state) :badge (~entries/ticket-state-badge :state state)
:action (cond :action (cond
((or (= state "confirmed") (= state "reserved")) ((or (= state "confirmed") (= state "reserved"))
(~events-ticket-admin-checkin-form (~tickets/admin-checkin-form
:checkin-url checkin-url :code code :csrf csrf)) :checkin-url checkin-url :code code :csrf csrf))
((= state "checked_in") ((= state "checked_in")
(~events-ticket-admin-checked-in :time-str (or checked-in-time ""))) (~tickets/admin-checked-in :time-str (or checked-in-time "")))
(true nil)))) (true nil))))
;; Ticket admin panel from data ;; Ticket admin panel from data
(defcomp ~events-ticket-admin-panel-from-data (&key (list-container :as string) (lookup-url :as string) (tickets :as list?) (defcomp ~tickets/admin-panel-from-data (&key (list-container :as string) (lookup-url :as string) (tickets :as list?)
(total :as number?) (confirmed :as number?) (checked-in :as number?) (reserved :as number?)) (total :as number?) (confirmed :as number?) (checked-in :as number?) (reserved :as number?))
(~events-ticket-admin-panel (~tickets/admin-panel
:list-container list-container :list-container list-container
:stats (<> :stats (<>
(~events-ticket-admin-stat :border "border-stone-200" :bg "" (~tickets/admin-stat :border "border-stone-200" :bg ""
:text-cls "text-stone-900" :label-cls "text-stone-500" :text-cls "text-stone-900" :label-cls "text-stone-500"
:value (str (or total 0)) :label "Total") :value (str (or total 0)) :label "Total")
(~events-ticket-admin-stat :border "border-emerald-200" :bg "bg-emerald-50" (~tickets/admin-stat :border "border-emerald-200" :bg "bg-emerald-50"
:text-cls "text-emerald-700" :label-cls "text-emerald-600" :text-cls "text-emerald-700" :label-cls "text-emerald-600"
:value (str (or confirmed 0)) :label "Confirmed") :value (str (or confirmed 0)) :label "Confirmed")
(~events-ticket-admin-stat :border "border-blue-200" :bg "bg-blue-50" (~tickets/admin-stat :border "border-blue-200" :bg "bg-blue-50"
:text-cls "text-blue-700" :label-cls "text-blue-600" :text-cls "text-blue-700" :label-cls "text-blue-600"
:value (str (or checked-in 0)) :label "Checked In") :value (str (or checked-in 0)) :label "Checked In")
(~events-ticket-admin-stat :border "border-amber-200" :bg "bg-amber-50" (~tickets/admin-stat :border "border-amber-200" :bg "bg-amber-50"
:text-cls "text-amber-700" :label-cls "text-amber-600" :text-cls "text-amber-700" :label-cls "text-amber-600"
:value (str (or reserved 0)) :label "Reserved")) :value (str (or reserved 0)) :label "Reserved"))
:lookup-url lookup-url :lookup-url lookup-url
:has-tickets (not (empty? (or tickets (list)))) :has-tickets (not (empty? (or tickets (list))))
:rows (<> (map (lambda (t) :rows (<> (map (lambda (t)
(~events-ticket-admin-row-from-data (~tickets/admin-row-from-data
:code (get t "code") :code-short (get t "code-short") :code (get t "code") :code-short (get t "code-short")
:entry-name (get t "entry-name") :date-str (get t "date-str") :entry-name (get t "entry-name") :date-str (get t "date-str")
:type-name (get t "type-name") :state (get t "state") :type-name (get t "type-name") :state (get t "state")
@@ -285,45 +285,45 @@
(or tickets (list)))))) (or tickets (list))))))
;; Entry tickets admin from data ;; Entry tickets admin from data
(defcomp ~events-entry-tickets-admin-from-data (&key (entry-name :as string) (count-label :as string) (tickets :as list?) (csrf :as string)) (defcomp ~tickets/entry-tickets-admin-from-data (&key (entry-name :as string) (count-label :as string) (tickets :as list?) (csrf :as string))
(~events-entry-tickets-admin-panel (~tickets/entry-tickets-admin-panel
:entry-name entry-name :count-label count-label :entry-name entry-name :count-label count-label
:body (if (empty? (or tickets (list))) :body (if (empty? (or tickets (list)))
(~events-entry-tickets-admin-empty) (~tickets/entry-tickets-admin-empty)
(~events-entry-tickets-admin-table (~tickets/entry-tickets-admin-table
:rows (<> (map (lambda (t) :rows (<> (map (lambda (t)
(~events-entry-tickets-admin-row (~tickets/entry-tickets-admin-row
:code (get t "code") :code-short (get t "code-short") :code (get t "code") :code-short (get t "code-short")
:type-name (get t "type-name") :type-name (get t "type-name")
:badge (~ticket-state-badge :state (get t "state")) :badge (~entries/ticket-state-badge :state (get t "state"))
:action (cond :action (cond
((or (= (get t "state") "confirmed") (= (get t "state") "reserved")) ((or (= (get t "state") "confirmed") (= (get t "state") "reserved"))
(~events-entry-tickets-admin-checkin (~tickets/entry-tickets-admin-checkin
:checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf)) :checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf))
((= (get t "state") "checked_in") ((= (get t "state") "checked_in")
(~events-ticket-admin-checked-in :time-str (or (get t "checked-in-time") ""))) (~tickets/admin-checked-in :time-str (or (get t "checked-in-time") "")))
(true nil)))) (true nil))))
(or tickets (list)))))))) (or tickets (list))))))))
;; Checkin success row from data ;; Checkin success row from data
(defcomp ~events-checkin-success-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?) (type-name :as string) (time-str :as string)) (defcomp ~tickets/checkin-success-row-from-data (&key (code :as string) (code-short :as string) (entry-name :as string) (date-str :as string?) (type-name :as string) (time-str :as string))
(~events-checkin-success-row (~tickets/checkin-success-row
:code code :code-short code-short :code code :code-short code-short
:entry-name entry-name :entry-name entry-name
:date (when date-str (~events-ticket-admin-date :date-str date-str)) :date (when date-str (~tickets/admin-date :date-str date-str))
:type-name type-name :type-name type-name
:badge (~ticket-state-badge :state "checked_in") :badge (~entries/ticket-state-badge :state "checked_in")
:time-str time-str)) :time-str time-str))
;; Ticket types table from data ;; Ticket types table from data
(defcomp ~events-ticket-types-table-from-data (&key (list-container :as string) (ticket-types :as list?) (action-btn :as string) (add-url :as string) (defcomp ~tickets/types-table-from-data (&key (list-container :as string) (ticket-types :as list?) (action-btn :as string) (add-url :as string)
(tr-cls :as string) (pill-cls :as string) (hx-select :as string) (csrf-hdr :as string)) (tr-cls :as string) (pill-cls :as string) (hx-select :as string) (csrf-hdr :as string))
(~events-ticket-types-table (~page/ticket-types-table
:list-container list-container :list-container list-container
:rows (if (empty? (or ticket-types (list))) :rows (if (empty? (or ticket-types (list)))
(~events-ticket-types-empty-row) (~page/ticket-types-empty-row)
(<> (map (lambda (tt) (<> (map (lambda (tt)
(~events-ticket-types-row (~page/ticket-types-row
:tr-cls tr-cls :tt-href (get tt "tt-href") :tr-cls tr-cls :tt-href (get tt "tt-href")
:pill-cls pill-cls :hx-select hx-select :pill-cls pill-cls :hx-select hx-select
:tt-name (get tt "tt-name") :cost-str (get tt "cost-str") :tt-name (get tt "tt-name") :cost-str (get tt "cost-str")
@@ -333,23 +333,23 @@
:action-btn action-btn :add-url add-url)) :action-btn action-btn :add-url add-url))
;; Lookup result from data ;; Lookup result from data
(defcomp ~events-lookup-result-from-data (&key (entry-name :as string) (type-name :as string?) (date-str :as string?) (cal-name :as string?) (defcomp ~tickets/lookup-result-from-data (&key (entry-name :as string) (type-name :as string?) (date-str :as string?) (cal-name :as string?)
(state :as string) (code :as string) (checked-in-str :as string?) (state :as string) (code :as string) (checked-in-str :as string?)
(checkin-url :as string) (csrf :as string)) (checkin-url :as string) (csrf :as string))
(~events-lookup-card (~tickets/lookup-card
:info (<> :info (<>
(~events-lookup-info :entry-name entry-name) (~tickets/lookup-info :entry-name entry-name)
(when type-name (~events-lookup-type :type-name type-name)) (when type-name (~tickets/lookup-type :type-name type-name))
(when date-str (~events-lookup-date :date-str date-str)) (when date-str (~tickets/lookup-date :date-str date-str))
(when cal-name (~events-lookup-cal :cal-name cal-name)) (when cal-name (~tickets/lookup-cal :cal-name cal-name))
(~events-lookup-status (~tickets/lookup-status
:badge (~ticket-state-badge :state state) :code code) :badge (~entries/ticket-state-badge :state state) :code code)
(when checked-in-str (when checked-in-str
(~events-lookup-checkin-time :date-str checked-in-str))) (~tickets/lookup-checkin-time :date-str checked-in-str)))
:code code :code code
:action (cond :action (cond
((or (= state "confirmed") (= state "reserved")) ((or (= state "confirmed") (= state "reserved"))
(~events-lookup-checkin-btn :checkin-url checkin-url :code code :csrf csrf)) (~tickets/lookup-checkin-btn :checkin-url checkin-url :code code :csrf csrf))
((= state "checked_in") (~events-lookup-checked-in)) ((= state "checked_in") (~tickets/lookup-checked-in))
((= state "cancelled") (~events-lookup-cancelled)) ((= state "cancelled") (~tickets/lookup-cancelled))
(true nil)))) (true nil))))

View File

@@ -7,8 +7,8 @@
:auth :admin :auth :admin
:layout :events-calendar-admin :layout :events-calendar-admin
:data (calendar-admin-data calendar-slug) :data (calendar-admin-data calendar-slug)
:content (~events-calendar-admin-panel :content (~admin/calendar-admin-panel
:description-content (~events-calendar-description-display :description-content (~calendar/description-display
:description cal-description :edit-url desc-edit-url) :description cal-description :edit-url desc-edit-url)
:csrf csrf :description cal-description)) :csrf csrf :description cal-description))
@@ -18,7 +18,7 @@
:auth :admin :auth :admin
:layout :events-day-admin :layout :events-day-admin
:data (day-admin-data calendar-slug year month day) :data (day-admin-data calendar-slug year month day)
:content (~events-day-admin-panel)) :content (~day/admin-panel))
;; Slots listing ;; Slots listing
(defpage slots-listing (defpage slots-listing
@@ -26,25 +26,25 @@
:auth :public :auth :public
:layout :events-slots :layout :events-slots
:data (slots-data calendar-slug) :data (slots-data calendar-slug)
:content (~events-slots-table :content (~page/slots-table
:list-container list-container :list-container list-container
:rows (if has-slots :rows (if has-slots
(<> (map (fn (s) (<> (map (fn (s)
(~events-slots-row (~page/slots-row
:tr-cls tr-cls :slot-href (get s "slot-href") :tr-cls tr-cls :slot-href (get s "slot-href")
:pill-cls pill-cls :hx-select hx-select :pill-cls pill-cls :hx-select hx-select
:slot-name (get s "name") :description (get s "description") :slot-name (get s "name") :description (get s "description")
:flexible (get s "flexible") :flexible (get s "flexible")
:days (if (get s "has-days") :days (if (get s "has-days")
(~events-slot-days-pills :days-inner (~page/slot-days-pills :days-inner
(<> (map (fn (d) (~events-slot-day-pill :day d)) (get s "day-list")))) (<> (map (fn (d) (~page/slot-day-pill :day d)) (get s "day-list"))))
(~events-slot-no-days)) (~page/slot-no-days))
:time-str (get s "time-str") :time-str (get s "time-str")
:cost-str (get s "cost-str") :action-btn action-btn :cost-str (get s "cost-str") :action-btn action-btn
:del-url (get s "del-url") :del-url (get s "del-url")
:csrf-hdr csrf-hdr)) :csrf-hdr csrf-hdr))
slots-list)) slots-list))
(~events-slots-empty-row)) (~page/slots-empty-row))
:pre-action pre-action :add-url add-url)) :pre-action pre-action :add-url add-url))
;; Slot detail ;; Slot detail
@@ -53,13 +53,13 @@
:auth :admin :auth :admin
:layout :events-slot :layout :events-slot
:data (slot-data calendar-slug slot-id) :data (slot-data calendar-slug slot-id)
:content (~events-slot-panel :content (~page/slot-panel
:slot-id slot-id-str :slot-id slot-id-str
:list-container list-container :list-container list-container
:days (if has-days :days (if has-days
(~events-slot-days-pills :days-inner (~page/slot-days-pills :days-inner
(<> (map (fn (d) (~events-slot-day-pill :day d)) day-list))) (<> (map (fn (d) (~page/slot-day-pill :day d)) day-list)))
(~events-slot-no-days)) (~page/slot-no-days))
:flexible flexible :flexible flexible
:time-str time-str :cost-str cost-str :time-str time-str :cost-str cost-str
:pre-action pre-action :edit-url edit-url)) :pre-action pre-action :edit-url edit-url))
@@ -70,29 +70,29 @@
:auth :admin :auth :admin
:layout :events-entry :layout :events-entry
:data (entry-data calendar-slug entry-id) :data (entry-data calendar-slug entry-id)
:content (~events-entry-panel :content (~admin/entry-panel
:entry-id entry-id-str :list-container list-container :entry-id entry-id-str :list-container list-container
:name (~events-entry-field :label "Name" :name (~admin/entry-field :label "Name"
:content (~events-entry-name-field :name entry-name)) :content (~admin/entry-name-field :name entry-name))
:slot (~events-entry-field :label "Slot" :slot (~admin/entry-field :label "Slot"
:content (if has-slot :content (if has-slot
(~events-entry-slot-assigned :slot-name slot-name :flex-label flex-label) (~admin/entry-slot-assigned :slot-name slot-name :flex-label flex-label)
(~events-entry-slot-none))) (~admin/entry-slot-none)))
:time (~events-entry-field :label "Time Period" :time (~admin/entry-field :label "Time Period"
:content (~events-entry-time-field :time-str time-str)) :content (~admin/entry-time-field :time-str time-str))
:state (~events-entry-field :label "State" :state (~admin/entry-field :label "State"
:content (~events-entry-state-field :entry-id entry-id-str :content (~admin/entry-state-field :entry-id entry-id-str
:badge (~badge :cls state-badge-cls :label state-badge-label))) :badge (~shared:misc/badge :cls state-badge-cls :label state-badge-label)))
:cost (~events-entry-field :label "Cost" :cost (~admin/entry-field :label "Cost"
:content (~events-entry-cost-field :cost cost-str)) :content (~admin/entry-cost-field :cost cost-str))
:tickets (~events-entry-field :label "Tickets" :tickets (~admin/entry-field :label "Tickets"
:content (~events-entry-tickets-field :entry-id entry-id-str :content (~admin/entry-tickets-field :entry-id entry-id-str
:tickets-config tickets-config)) :tickets-config tickets-config))
:buy buy-form :buy buy-form
:date (~events-entry-field :label "Date" :date (~admin/entry-field :label "Date"
:content (~events-entry-date-field :date-str date-str)) :content (~admin/entry-date-field :date-str date-str))
:posts (~events-entry-field :label "Associated Posts" :posts (~admin/entry-field :label "Associated Posts"
:content (~events-entry-posts-field :entry-id entry-id-str :content (~admin/entry-posts-field :entry-id entry-id-str
:posts-panel posts-panel)) :posts-panel posts-panel))
:options options-html :options options-html
:pre-action pre-action :edit-url edit-url) :pre-action pre-action :edit-url edit-url)
@@ -104,9 +104,9 @@
:auth :admin :auth :admin
:layout :events-entry-admin :layout :events-entry-admin
:data (entry-admin-data calendar-slug entry-id year month day) :data (entry-admin-data calendar-slug entry-id year month day)
:content (~nav-link :href ticket-types-href :label "ticket_types" :content (~shared:layout/nav-link :href ticket-types-href :label "ticket_types"
:select-colours select-colours :aclass nav-btn :is-selected false) :select-colours select-colours :aclass nav-btn :is-selected false)
:menu (~events-admin-placeholder-nav)) :menu (~forms/admin-placeholder-nav))
;; Ticket types listing ;; Ticket types listing
(defpage ticket-types-listing (defpage ticket-types-listing
@@ -114,11 +114,11 @@
:auth :public :auth :public
:layout :events-ticket-types :layout :events-ticket-types
:data (ticket-types-data calendar-slug entry-id year month day) :data (ticket-types-data calendar-slug entry-id year month day)
:content (~events-ticket-types-table :content (~page/ticket-types-table
:list-container list-container :list-container list-container
:rows (if has-types :rows (if has-types
(<> (map (fn (tt) (<> (map (fn (tt)
(~events-ticket-types-row (~page/ticket-types-row
:tr-cls tr-cls :tt-href (get tt "tt-href") :tr-cls tr-cls :tt-href (get tt "tt-href")
:pill-cls pill-cls :hx-select hx-select :pill-cls pill-cls :hx-select hx-select
:tt-name (get tt "tt-name") :cost-str (get tt "cost-str") :tt-name (get tt "tt-name") :cost-str (get tt "cost-str")
@@ -126,9 +126,9 @@
:del-url (get tt "del-url") :del-url (get tt "del-url")
:csrf-hdr csrf-hdr)) :csrf-hdr csrf-hdr))
types-list)) types-list))
(~events-ticket-types-empty-row)) (~page/ticket-types-empty-row))
:action-btn action-btn :add-url add-url) :action-btn action-btn :add-url add-url)
:menu (~events-admin-placeholder-nav)) :menu (~forms/admin-placeholder-nav))
;; Ticket type detail ;; Ticket type detail
(defpage ticket-type-detail (defpage ticket-type-detail
@@ -136,13 +136,13 @@
:auth :admin :auth :admin
:layout :events-ticket-type :layout :events-ticket-type
:data (ticket-type-data calendar-slug entry-id ticket-type-id year month day) :data (ticket-type-data calendar-slug entry-id ticket-type-id year month day)
:content (~events-ticket-type-panel :content (~page/ticket-type-panel
:ticket-id ticket-id :list-container list-container :ticket-id ticket-id :list-container list-container
:c1 (~events-ticket-type-col :label "Name" :value tt-name) :c1 (~page/ticket-type-col :label "Name" :value tt-name)
:c2 (~events-ticket-type-col :label "Cost" :value cost-str) :c2 (~page/ticket-type-col :label "Cost" :value cost-str)
:c3 (~events-ticket-type-col :label "Count" :value count-str) :c3 (~page/ticket-type-col :label "Count" :value count-str)
:pre-action pre-action :edit-url edit-url) :pre-action pre-action :edit-url edit-url)
:menu (~events-admin-placeholder-nav)) :menu (~forms/admin-placeholder-nav))
;; My tickets ;; My tickets
(defpage my-tickets (defpage my-tickets
@@ -150,16 +150,16 @@
:auth :public :auth :public
:layout :root :layout :root
:data (tickets-data) :data (tickets-data)
:content (~events-tickets-panel :content (~tickets/panel
:list-container list-container :list-container list-container
:has-tickets has-tickets :has-tickets has-tickets
:cards (when has-tickets :cards (when has-tickets
(<> (map (fn (t) (<> (map (fn (t)
(~events-ticket-card (~tickets/card
:href (get t "href") :entry-name (get t "entry-name") :href (get t "href") :entry-name (get t "entry-name")
:type-name (get t "type-name") :time-str (get t "time-str") :type-name (get t "type-name") :time-str (get t "time-str")
:cal-name (get t "cal-name") :cal-name (get t "cal-name")
:badge (~badge :cls (get t "badge-cls") :label (get t "badge-label")) :badge (~shared:misc/badge :cls (get t "badge-cls") :label (get t "badge-label"))
:code-prefix (get t "code-prefix"))) :code-prefix (get t "code-prefix")))
tickets-list))))) tickets-list)))))
@@ -169,7 +169,7 @@
:auth :public :auth :public
:layout :root :layout :root
:data (ticket-detail-data code) :data (ticket-detail-data code)
:content (~events-ticket-detail :content (~tickets/detail
:list-container list-container :back-href back-href :list-container list-container :back-href back-href
:header-bg header-bg :entry-name entry-name :header-bg header-bg :entry-name entry-name
:badge (span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium " badge-cls) :badge (span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium " badge-cls)
@@ -185,10 +185,10 @@
:auth :admin :auth :admin
:layout :root :layout :root
:data (ticket-admin-data) :data (ticket-admin-data)
:content (~events-ticket-admin-panel :content (~tickets/admin-panel
:list-container list-container :list-container list-container
:stats (<> (map (fn (s) :stats (<> (map (fn (s)
(~events-ticket-admin-stat (~tickets/admin-stat
:border (get s "border") :bg (get s "bg") :border (get s "border") :bg (get s "bg")
:text-cls (get s "text-cls") :label-cls (get s "label-cls") :text-cls (get s "text-cls") :label-cls (get s "label-cls")
:value (get s "value") :label (get s "label"))) :value (get s "value") :label (get s "label")))
@@ -196,18 +196,18 @@
:lookup-url lookup-url :has-tickets has-tickets :lookup-url lookup-url :has-tickets has-tickets
:rows (when has-tickets :rows (when has-tickets
(<> (map (fn (t) (<> (map (fn (t)
(~events-ticket-admin-row (~tickets/admin-row
:code (get t "code") :code-short (get t "code-short") :code (get t "code") :code-short (get t "code-short")
:entry-name (get t "entry-name") :entry-name (get t "entry-name")
:date (when (get t "date-str") :date (when (get t "date-str")
(~events-ticket-admin-date :date-str (get t "date-str"))) (~tickets/admin-date :date-str (get t "date-str")))
:type-name (get t "type-name") :type-name (get t "type-name")
:badge (~badge :cls (get t "badge-cls") :label (get t "badge-label")) :badge (~shared:misc/badge :cls (get t "badge-cls") :label (get t "badge-label"))
:action (if (get t "can-checkin") :action (if (get t "can-checkin")
(~events-ticket-admin-checkin-form (~tickets/admin-checkin-form
:checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf) :checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf)
(when (get t "is-checked-in") (when (get t "is-checked-in")
(~events-ticket-admin-checked-in :time-str (get t "checkin-time")))))) (~tickets/admin-checked-in :time-str (get t "checkin-time"))))))
admin-tickets))))) admin-tickets)))))
;; Markets ;; Markets
@@ -216,20 +216,20 @@
:auth :public :auth :public
:layout :events-markets :layout :events-markets
:data (markets-data) :data (markets-data)
:content (~crud-panel :content (~shared:misc/crud-panel
:list-id "markets-list" :list-id "markets-list"
:form (when can-create :form (when can-create
(~crud-create-form :create-url create-url :csrf csrf (~shared:misc/crud-create-form :create-url create-url :csrf csrf
:errors-id "market-create-errors" :list-id "markets-list" :errors-id "market-create-errors" :list-id "markets-list"
:placeholder "e.g. Farm Shop, Bakery" :btn-label "Add market")) :placeholder "e.g. Farm Shop, Bakery" :btn-label "Add market"))
:list (if markets-list :list (if markets-list
(<> (map (fn (m) (<> (map (fn (m)
(~crud-item :href (get m "href") :name (get m "name") (~shared:misc/crud-item :href (get m "href") :name (get m "name")
:slug (get m "slug") :del-url (get m "del-url") :slug (get m "slug") :del-url (get m "del-url")
:csrf-hdr (get m "csrf-hdr") :csrf-hdr (get m "csrf-hdr")
:list-id "markets-list" :list-id "markets-list"
:confirm-title "Delete market?" :confirm-title "Delete market?"
:confirm-text "Products will be hidden (soft delete)")) :confirm-text "Products will be hidden (soft delete)"))
markets-list)) markets-list))
(~empty-state :message "No markets yet. Create one above." (~shared:misc/empty-state :message "No markets yet. Create one above."
:cls "text-gray-500 mt-4")))) :cls "text-gray-500 mt-4"))))

View File

@@ -117,7 +117,7 @@ def _cart_icon_oob(count: int) -> str:
def _cart_icon_ctx(count: int) -> dict: def _cart_icon_ctx(count: int) -> dict:
"""Return data dict for the ~events-cart-icon component.""" """Return data dict for the ~page/cart-icon component."""
from quart import g from quart import g
blog_url_fn = getattr(g, "blog_url", None) blog_url_fn = getattr(g, "blog_url", None)

View File

@@ -1,7 +1,7 @@
;; Auth components (choose username — federation-specific) ;; Auth components (choose username — federation-specific)
;; Login and check-email components are shared: see shared/sx/templates/auth.sx ;; Login and check-email components are shared: see shared/sx/templates/auth.sx
(defcomp ~federation-choose-username (&key (domain :as string) error (csrf :as string) (username :as string) (check-url :as string)) (defcomp ~auth/choose-username (&key (domain :as string) error (csrf :as string) (username :as string) (check-url :as string))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-2" "Choose your username") (h1 :class "text-2xl font-bold mb-2" "Choose your username")
(p :class "text-stone-600 mb-6" "This will be your identity on the fediverse: " (p :class "text-stone-600 mb-6" "This will be your identity on the fediverse: "

View File

@@ -12,7 +12,7 @@
(let ((actor (service "federation" "get-actor-by-username" :username u))) (let ((actor (service "federation" "get-actor-by-username" :username u)))
(<> (str "<!-- fragment:" u " -->") (<> (str "<!-- fragment:" u " -->")
(when (not (nil? actor)) (when (not (nil? actor))
(~link-card (~shared:fragments/link-card
:link (app-url "federation" :link (app-url "federation"
(str "/users/" (get actor "preferred_username"))) (str "/users/" (get actor "preferred_username")))
:title (or (get actor "display_name") :title (or (get actor "display_name")
@@ -28,7 +28,7 @@
(let ((actor (service "federation" "get-actor-by-username" (let ((actor (service "federation" "get-actor-by-username"
:username lookup))) :username lookup)))
(when (not (nil? actor)) (when (not (nil? actor))
(~link-card (~shared:fragments/link-card
:link (app-url "federation" :link (app-url "federation"
(str "/users/" (get actor "preferred_username"))) (str "/users/" (get actor "preferred_username")))
:title (or (get actor "display_name") :title (or (get actor "display_name")

View File

@@ -2,16 +2,16 @@
;; Registered via register_sx_layout("social", ...) in __init__.py. ;; Registered via register_sx_layout("social", ...) in __init__.py.
;; Full page: root header + social header in header-child ;; Full page: root header + social header in header-child
(defcomp ~social-layout-full () (defcomp ~layouts/social-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (~federation-social-header :inner (~social/header
:nav (~federation-social-nav :actor (federation-actor-ctx)))))) :nav (~social/nav :actor (federation-actor-ctx))))))
;; OOB (HTMX): social header oob + root header oob ;; OOB (HTMX): social header oob + root header oob
(defcomp ~social-layout-oob () (defcomp ~layouts/social-layout-oob ()
(<> (~oob-header-sx (<> (~shared:layout/oob-header-sx
:parent-id "root-header-child" :parent-id "root-header-child"
:row (~federation-social-header :row (~social/header
:nav (~federation-social-nav :actor (federation-actor-ctx)))) :nav (~social/nav :actor (federation-actor-ctx))))
(~root-header-auto true))) (~root-header-auto true)))

View File

@@ -1,9 +1,9 @@
;; Notification components ;; Notification components
(defcomp ~federation-notification-preview (&key (preview :as string)) (defcomp ~notifications/preview (&key (preview :as string))
(div :class "text-sm text-stone-500 mt-1 truncate" preview)) (div :class "text-sm text-stone-500 mt-1 truncate" preview))
(defcomp ~federation-notification-card (&key (cls :as string) avatar (from-name :as string) (from-username :as string) (from-domain :as string) (action-text :as string) preview (time :as string)) (defcomp ~notifications/card (&key (cls :as string) avatar (from-name :as string) (from-username :as string) (from-domain :as string) (action-text :as string) preview (time :as string))
(div :class cls (div :class cls
(div :class "flex items-start gap-3" (div :class "flex items-start gap-3"
avatar avatar
@@ -15,14 +15,14 @@
preview preview
(div :class "text-xs text-stone-400 mt-1" time))))) (div :class "text-xs text-stone-400 mt-1" time)))))
(defcomp ~federation-notifications-list (&key (items :as list)) (defcomp ~notifications/list (&key (items :as list))
(div :class "space-y-2" items)) (div :class "space-y-2" items))
(defcomp ~federation-notifications-page (&key notifs) (defcomp ~notifications/page (&key notifs)
(h1 :class "text-2xl font-bold mb-6" "Notifications") notifs) (h1 :class "text-2xl font-bold mb-6" "Notifications") notifs)
;; Assembled notification card — replaces Python _notification_sx ;; Assembled notification card — replaces Python _notification_sx
(defcomp ~federation-notification-from-data (&key (notif :as dict)) (defcomp ~notifications/from-data (&key (notif :as dict))
(let* ((from-name (or (get notif "from_actor_name") "?")) (let* ((from-name (or (get notif "from_actor_name") "?"))
(from-username (or (get notif "from_actor_username") "")) (from-username (or (get notif "from_actor_username") ""))
(from-domain (or (get notif "from_actor_domain") "")) (from-domain (or (get notif "from_actor_domain") ""))
@@ -44,9 +44,9 @@
((= ntype "mention") "mentioned you") ((= ntype "mention") "mentioned you")
((= ntype "reply") "replied to your post") ((= ntype "reply") "replied to your post")
(true "")))) (true ""))))
(~federation-notification-card (~notifications/card
:cls (str "bg-white rounded-lg shadow-sm border border-stone-200 p-4" border) :cls (str "bg-white rounded-lg shadow-sm border border-stone-200 p-4" border)
:avatar (~avatar :avatar (~shared:misc/avatar
:src from-icon :src from-icon
:cls (if from-icon "w-8 h-8 rounded-full" :cls (if from-icon "w-8 h-8 rounded-full"
"w-8 h-8 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xs") "w-8 h-8 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xs")
@@ -55,15 +55,15 @@
:from-username (escape from-username) :from-username (escape from-username)
:from-domain (if from-domain (str "@" (escape from-domain)) "") :from-domain (if from-domain (str "@" (escape from-domain)) "")
:action-text action-text :action-text action-text
:preview (when preview (~federation-notification-preview :preview (escape preview))) :preview (when preview (~notifications/preview :preview (escape preview)))
:time created))) :time created)))
;; Assembled notifications content — replaces Python _notifications_content_sx ;; Assembled notifications content — replaces Python _notifications_content_sx
(defcomp ~federation-notifications-content (&key (notifications :as list)) (defcomp ~notifications/content (&key (notifications :as list))
(~federation-notifications-page (~notifications/page
:notifs (if (empty? notifications) :notifs (if (empty? notifications)
(~empty-state :message "No notifications yet." :cls "text-stone-500") (~shared:misc/empty-state :message "No notifications yet." :cls "text-stone-500")
(~federation-notifications-list (~notifications/list
:items (map (lambda (n) :items (map (lambda (n)
(~federation-notification-from-data :notif n)) (~notifications/from-data :notif n))
notifications))))) notifications)))))

View File

@@ -1,6 +1,6 @@
;; Profile and actor timeline components ;; Profile and actor timeline components
(defcomp ~federation-actor-profile-header (&key avatar (display-name :as string) (username :as string) (domain :as string) summary follow) (defcomp ~profile/actor-profile-header (&key avatar (display-name :as string) (username :as string) (domain :as string) summary follow)
(div :class "bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6" (div :class "bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6"
(div :class "flex items-center gap-4" (div :class "flex items-center gap-4"
avatar avatar
@@ -10,39 +10,39 @@
summary) summary)
follow))) follow)))
(defcomp ~federation-actor-timeline-layout (&key header timeline) (defcomp ~profile/actor-timeline-layout (&key header timeline)
header header
(div :id "timeline" timeline)) (div :id "timeline" timeline))
(defcomp ~federation-follow-form (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string) (cls :as string)) (defcomp ~profile/follow-form (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string) (cls :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action (form :method "post" :action action
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class cls label)))) (button :type "submit" :class cls label))))
(defcomp ~federation-profile-summary (&key (summary :as string)) (defcomp ~profile/summary (&key (summary :as string))
(div :class "text-sm text-stone-600 mt-2" (~rich-text :html summary))) (div :class "text-sm text-stone-600 mt-2" (~rich-text :html summary)))
;; Public profile page ;; Public profile page
(defcomp ~federation-activity-obj-type (&key (obj-type :as string)) (defcomp ~profile/activity-obj-type (&key (obj-type :as string))
(span :class "text-sm text-stone-500" obj-type)) (span :class "text-sm text-stone-500" obj-type))
(defcomp ~federation-activity-card (&key (activity-type :as string) (published :as string) obj-type) (defcomp ~profile/activity-card (&key (activity-type :as string) (published :as string) obj-type)
(div :class "bg-white rounded-lg shadow p-4" (div :class "bg-white rounded-lg shadow p-4"
(div :class "flex justify-between items-start" (div :class "flex justify-between items-start"
(span :class "font-medium" activity-type) (span :class "font-medium" activity-type)
(span :class "text-sm text-stone-400" published)) (span :class "text-sm text-stone-400" published))
obj-type)) obj-type))
(defcomp ~federation-activities-list (&key (items :as list)) (defcomp ~profile/activities-list (&key (items :as list))
(div :class "space-y-4" items)) (div :class "space-y-4" items))
(defcomp ~federation-activities-empty () (defcomp ~profile/activities-empty ()
(p :class "text-stone-500" "No activities yet.")) (p :class "text-stone-500" "No activities yet."))
(defcomp ~federation-profile-page (&key (display-name :as string) (username :as string) (domain :as string) summary (activities-heading :as string) activities) (defcomp ~profile/page (&key (display-name :as string) (username :as string) (domain :as string) summary (activities-heading :as string) activities)
(div :class "py-8" (div :class "py-8"
(div :class "bg-white rounded-lg shadow p-6 mb-6" (div :class "bg-white rounded-lg shadow p-6 mb-6"
(h1 :class "text-2xl font-bold" display-name) (h1 :class "text-2xl font-bold" display-name)
@@ -51,11 +51,11 @@
(h2 :class "text-xl font-bold mb-4" activities-heading) (h2 :class "text-xl font-bold mb-4" activities-heading)
activities)) activities))
(defcomp ~federation-profile-summary-text (&key (text :as string)) (defcomp ~profile/summary-text (&key (text :as string))
(p :class "mt-2" text)) (p :class "mt-2" text))
;; Assembled actor timeline content — replaces Python _actor_timeline_content_sx ;; Assembled actor timeline content — replaces Python _actor_timeline_content_sx
(defcomp ~federation-actor-timeline-content (&key (remote-actor :as dict) (items :as list) (is-following :as boolean) actor) (defcomp ~profile/actor-timeline-content (&key (remote-actor :as dict) (items :as list) (is-following :as boolean) actor)
(let* ((display-name (or (get remote-actor "display_name") (get remote-actor "preferred_username") "")) (let* ((display-name (or (get remote-actor "display_name") (get remote-actor "preferred_username") ""))
(icon-url (get remote-actor "icon_url")) (icon-url (get remote-actor "icon_url"))
(summary (get remote-actor "summary")) (summary (get remote-actor "summary"))
@@ -63,9 +63,9 @@
(csrf (csrf-token)) (csrf (csrf-token))
(initial (if (and (not icon-url) display-name) (initial (if (and (not icon-url) display-name)
(upper (slice display-name 0 1)) "?"))) (upper (slice display-name 0 1)) "?")))
(~federation-actor-timeline-layout (~profile/actor-timeline-layout
:header (~federation-actor-profile-header :header (~profile/actor-profile-header
:avatar (~avatar :avatar (~shared:misc/avatar
:src icon-url :src icon-url
:cls (if icon-url "w-16 h-16 rounded-full" :cls (if icon-url "w-16 h-16 rounded-full"
"w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl") "w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl")
@@ -73,18 +73,18 @@
:display-name (escape display-name) :display-name (escape display-name)
:username (escape (or (get remote-actor "preferred_username") "")) :username (escape (or (get remote-actor "preferred_username") ""))
:domain (escape (or (get remote-actor "domain") "")) :domain (escape (or (get remote-actor "domain") ""))
:summary (when summary (~federation-profile-summary :summary summary)) :summary (when summary (~profile/summary :summary summary))
:follow (when actor :follow (when actor
(if is-following (if is-following
(~federation-follow-form (~profile/follow-form
:action (url-for "social.unfollow") :csrf csrf :actor-url actor-url :action (url-for "social.unfollow") :csrf csrf :actor-url actor-url
:label "Unfollow" :label "Unfollow"
:cls "border border-stone-300 rounded px-4 py-2 hover:bg-stone-100") :cls "border border-stone-300 rounded px-4 py-2 hover:bg-stone-100")
(~federation-follow-form (~profile/follow-form
:action (url-for "social.follow") :csrf csrf :actor-url actor-url :action (url-for "social.follow") :csrf csrf :actor-url actor-url
:label "Follow" :label "Follow"
:cls "bg-stone-800 text-white rounded px-4 py-2 hover:bg-stone-700")))) :cls "bg-stone-800 text-white rounded px-4 py-2 hover:bg-stone-700"))))
:timeline (~federation-timeline-items :timeline (~social/timeline-items
:items items :timeline-type "actor" :actor actor :items items :timeline-type "actor" :actor actor
:next-url (when (not (empty? items)) :next-url (when (not (empty? items))
(url-for "social.actor_timeline_page" (url-for "social.actor_timeline_page"
@@ -92,14 +92,14 @@
:before (get (last items) "before_cursor"))))))) :before (get (last items) "before_cursor")))))))
;; Data-driven activities list (replaces Python loop in render_profile_page) ;; Data-driven activities list (replaces Python loop in render_profile_page)
(defcomp ~federation-activities-from-data (&key (activities :as list)) (defcomp ~profile/activities-from-data (&key (activities :as list))
(if (empty? (or activities (list))) (if (empty? (or activities (list)))
(~federation-activities-empty) (~profile/activities-empty)
(~federation-activities-list (~profile/activities-list
:items (<> (map (lambda (a) :items (<> (map (lambda (a)
(~federation-activity-card (~profile/activity-card
:activity-type (get a "activity_type") :activity-type (get a "activity_type")
:published (get a "published") :published (get a "published")
:obj-type (when (get a "object_type") :obj-type (when (get a "object_type")
(~federation-activity-obj-type :obj-type (get a "object_type"))))) (~profile/activity-obj-type :obj-type (get a "object_type")))))
activities))))) activities)))))

View File

@@ -1,37 +1,37 @@
;; Search and actor card components ;; Search and actor card components
;; Aliases — delegate to shared ~avatar ;; Aliases — delegate to shared ~shared:misc/avatar
(defcomp ~federation-actor-avatar-img (&key (src :as string) (cls :as string)) (defcomp ~search/actor-avatar-img (&key (src :as string) (cls :as string))
(~avatar :src src :cls cls)) (~shared:misc/avatar :src src :cls cls))
(defcomp ~federation-actor-avatar-placeholder (&key (cls :as string) (initial :as string)) (defcomp ~search/actor-avatar-placeholder (&key (cls :as string) (initial :as string))
(~avatar :cls cls :initial initial)) (~shared:misc/avatar :cls cls :initial initial))
(defcomp ~federation-actor-name-link (&key (href :as string) (name :as string)) (defcomp ~search/actor-name-link (&key (href :as string) (name :as string))
(a :href href :class "font-semibold text-stone-900 hover:underline" name)) (a :href href :class "font-semibold text-stone-900 hover:underline" name))
(defcomp ~federation-actor-name-link-external (&key (href :as string) (name :as string)) (defcomp ~search/actor-name-link-external (&key (href :as string) (name :as string))
(a :href href :target "_blank" :rel "noopener" (a :href href :target "_blank" :rel "noopener"
:class "font-semibold text-stone-900 hover:underline" name)) :class "font-semibold text-stone-900 hover:underline" name))
(defcomp ~federation-actor-summary (&key (summary :as string)) (defcomp ~search/actor-summary (&key (summary :as string))
(div :class "text-sm text-stone-600 mt-1 truncate" (~rich-text :html summary))) (div :class "text-sm text-stone-600 mt-1 truncate" (~rich-text :html summary)))
(defcomp ~federation-unfollow-button (&key (action :as string) (csrf :as string) (actor-url :as string)) (defcomp ~search/unfollow-button (&key (action :as string) (csrf :as string) (actor-url :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML" (form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class "text-sm border border-stone-300 rounded px-3 py-1 hover:bg-stone-100" "Unfollow")))) (button :type "submit" :class "text-sm border border-stone-300 rounded px-3 py-1 hover:bg-stone-100" "Unfollow"))))
(defcomp ~federation-follow-button (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string)) (defcomp ~search/follow-button (&key (action :as string) (csrf :as string) (actor-url :as string) (label :as string))
(div :class "flex-shrink-0" (div :class "flex-shrink-0"
(form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML" (form :method "post" :action action :sx-post action :sx-target "closest article" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "actor_url" :value actor-url) (input :type "hidden" :name "actor_url" :value actor-url)
(button :type "submit" :class "text-sm bg-stone-800 text-white rounded px-3 py-1 hover:bg-stone-700" label)))) (button :type "submit" :class "text-sm bg-stone-800 text-white rounded px-3 py-1 hover:bg-stone-700" label))))
(defcomp ~federation-actor-card (&key (cls :as string) (id :as string) avatar name (username :as string) (domain :as string) summary button) (defcomp ~search/actor-card (&key (cls :as string) (id :as string) avatar name (username :as string) (domain :as string) summary button)
(article :class cls :id id (article :class cls :id id
avatar avatar
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
@@ -41,7 +41,7 @@
button)) button))
;; Data-driven actor card (replaces Python _actor_card_sx loop) ;; Data-driven actor card (replaces Python _actor_card_sx loop)
(defcomp ~federation-actor-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string) (follow-url :as string) (unfollow-url :as string) (list-type :as string)) (defcomp ~search/actor-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string) (follow-url :as string) (unfollow-url :as string) (list-type :as string))
(let* ((icon-url (get d "icon_url")) (let* ((icon-url (get d "icon_url"))
(display-name (get d "display_name")) (display-name (get d "display_name"))
(username (get d "username")) (username (get d "username"))
@@ -49,42 +49,42 @@
(actor-url (get d "actor_url")) (actor-url (get d "actor_url"))
(safe-id (get d "safe_id")) (safe-id (get d "safe_id"))
(initial (or (get d "initial") "?")) (initial (or (get d "initial") "?"))
(avatar (~avatar (avatar (~shared:misc/avatar
:src icon-url :src icon-url
:cls (if icon-url "w-12 h-12 rounded-full" :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") "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))) :initial (when (not icon-url) initial)))
(name-sx (if (get d "external_link") (name-sx (if (get d "external_link")
(~federation-actor-name-link-external :href (get d "name_href") :name display-name) (~search/actor-name-link-external :href (get d "name_href") :name display-name)
(~federation-actor-name-link :href (get d "name_href") :name display-name))) (~search/actor-name-link :href (get d "name_href") :name display-name)))
(summary-sx (when (get d "summary") (summary-sx (when (get d "summary")
(~federation-actor-summary :summary (get d "summary")))) (~search/actor-summary :summary (get d "summary"))))
(is-followed (get d "is_followed")) (is-followed (get d "is_followed"))
(button (when has-actor (button (when has-actor
(if (or (= list-type "following") is-followed) (if (or (= list-type "following") is-followed)
(~federation-unfollow-button :action unfollow-url :csrf csrf :actor-url actor-url) (~search/unfollow-button :action unfollow-url :csrf csrf :actor-url actor-url)
(~federation-follow-button :action follow-url :csrf csrf :actor-url actor-url (~search/follow-button :action follow-url :csrf csrf :actor-url actor-url
:label (if (= list-type "followers") "Follow Back" "Follow")))))) :label (if (= list-type "followers") "Follow Back" "Follow"))))))
(~federation-actor-card (~search/actor-card
:cls "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4" :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) :id (str "actor-" safe-id)
:avatar avatar :name name-sx :username username :domain domain :avatar avatar :name name-sx :username username :domain domain
:summary summary-sx :button button))) :summary summary-sx :button button)))
;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops) ;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops)
(defcomp ~federation-actor-list-from-data (&key (actors :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string) (defcomp ~search/actor-list-from-data (&key (actors :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
(follow-url :as string) (unfollow-url :as string) (list-type :as string)) (follow-url :as string) (unfollow-url :as string) (list-type :as string))
(<> (<>
(map (lambda (d) (map (lambda (d)
(~federation-actor-card-from-data :d d :has-actor has-actor :csrf csrf (~search/actor-card-from-data :d d :has-actor has-actor :csrf csrf
:follow-url follow-url :unfollow-url unfollow-url :list-type list-type)) :follow-url follow-url :unfollow-url unfollow-url :list-type list-type))
(or actors (list))) (or actors (list)))
(when next-url (~federation-scroll-sentinel :url next-url)))) (when next-url (~social/scroll-sentinel :url next-url))))
(defcomp ~federation-search-info (&key (cls :as string) (text :as string)) (defcomp ~search/info (&key (cls :as string) (text :as string))
(p :class cls text)) (p :class cls text))
(defcomp ~federation-search-page (&key (search-url :as string) (search-page-url :as string) (query :as string) info results) (defcomp ~search/page (&key (search-url :as string) (search-page-url :as string) (query :as string) info results)
(h1 :class "text-2xl font-bold mb-6" "Search") (h1 :class "text-2xl font-bold mb-6" "Search")
(form :method "get" :action search-url :class "mb-6" (form :method "get" :action search-url :class "mb-6"
:sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url :sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url
@@ -97,7 +97,7 @@
(div :id "search-results" results)) (div :id "search-results" results))
;; Following / Followers list page ;; Following / Followers list page
(defcomp ~federation-actor-list-page (&key (title :as string) (count-str :as string) items) (defcomp ~search/actor-list-page (&key (title :as string) (count-str :as string) items)
(h1 :class "text-2xl font-bold mb-6" title " " (h1 :class "text-2xl font-bold mb-6" title " "
(span :class "text-stone-400 font-normal" count-str)) (span :class "text-stone-400 font-normal" count-str))
(div :id "actor-list" items)) (div :id "actor-list" items))
@@ -106,7 +106,7 @@
;; Assembled actor card — replaces Python _actor_card_sx ;; Assembled actor card — replaces Python _actor_card_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-actor-card-from-data (&key (a :as dict) actor (followed-urls :as list) (list-type :as string)) (defcomp ~search/actor-card-from-data (&key (a :as dict) actor (followed-urls :as list) (list-type :as string))
(let* ((display-name (or (get a "display_name") (get a "preferred_username") "")) (let* ((display-name (or (get a "display_name") (get a "preferred_username") ""))
(username (or (get a "preferred_username") "")) (username (or (get a "preferred_username") ""))
(domain (or (get a "domain") "")) (domain (or (get a "domain") ""))
@@ -119,81 +119,81 @@
(upper (slice (or display-name username) 0 1)) "?")) (upper (slice (or display-name username) 0 1)) "?"))
(csrf (csrf-token)) (csrf (csrf-token))
(is-followed (contains? (or followed-urls (list)) actor-url))) (is-followed (contains? (or followed-urls (list)) actor-url)))
(~federation-actor-card (~search/actor-card
:cls "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4" :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) :id (str "actor-" safe-id)
:avatar (~avatar :avatar (~shared:misc/avatar
:src icon-url :src icon-url
:cls (if icon-url "w-12 h-12 rounded-full" :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") "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)) :initial (when (not icon-url) initial))
:name (if (and (or (= list-type "following") (= list-type "search")) aid) :name (if (and (or (= list-type "following") (= list-type "search")) aid)
(~federation-actor-name-link (~search/actor-name-link
:href (url-for "social.defpage_actor_timeline" :id aid) :href (url-for "social.defpage_actor_timeline" :id aid)
:name (escape display-name)) :name (escape display-name))
(~federation-actor-name-link-external (~search/actor-name-link-external
:href (str "https://" domain "/@" username) :href (str "https://" domain "/@" username)
:name (escape display-name))) :name (escape display-name)))
:username (escape username) :username (escape username)
:domain (escape domain) :domain (escape domain)
:summary (when summary (~federation-actor-summary :summary summary)) :summary (when summary (~search/actor-summary :summary summary))
:button (when actor :button (when actor
(if (or (= list-type "following") is-followed) (if (or (= list-type "following") is-followed)
(~federation-unfollow-button (~search/unfollow-button
:action (url-for "social.unfollow") :csrf csrf :actor-url actor-url) :action (url-for "social.unfollow") :csrf csrf :actor-url actor-url)
(~federation-follow-button (~search/follow-button
:action (url-for "social.follow") :csrf csrf :actor-url actor-url :action (url-for "social.follow") :csrf csrf :actor-url actor-url
:label (if (= list-type "followers") "Follow Back" "Follow"))))))) :label (if (= list-type "followers") "Follow Back" "Follow")))))))
;; Assembled search content — replaces Python _search_content_sx ;; Assembled search content — replaces Python _search_content_sx
(defcomp ~federation-search-content (&key (query :as string?) (actors :as list) (total :as number) (followed-urls :as list) actor) (defcomp ~search/content (&key (query :as string?) (actors :as list) (total :as number) (followed-urls :as list) actor)
(~federation-search-page (~search/page
:search-url (url-for "social.defpage_search") :search-url (url-for "social.defpage_search")
:search-page-url (url-for "social.search_page") :search-page-url (url-for "social.search_page")
:query (escape (or query "")) :query (escape (or query ""))
:info (cond :info (cond
((and query (> total 0)) ((and query (> total 0))
(~federation-search-info (~search/info
:cls "text-sm text-stone-500 mb-4" :cls "text-sm text-stone-500 mb-4"
:text (str total " result" (pluralize total) " for " (escape query)))) :text (str total " result" (pluralize total) " for " (escape query))))
(query (query
(~federation-search-info (~search/info
:cls "text-stone-500 mb-4" :cls "text-stone-500 mb-4"
:text (str "No results found for " (escape query)))) :text (str "No results found for " (escape query))))
(true nil)) (true nil))
:results (when (not (empty? actors)) :results (when (not (empty? actors))
(<> (<>
(map (lambda (a) (map (lambda (a)
(~federation-actor-card-from-data (~search/actor-card-from-data
:a a :actor actor :followed-urls followed-urls :list-type "search")) :a a :actor actor :followed-urls followed-urls :list-type "search"))
actors) actors)
(when (>= (len actors) 20) (when (>= (len actors) 20)
(~federation-scroll-sentinel (~social/scroll-sentinel
:url (url-for "social.search_page" :q query :page 2))))))) :url (url-for "social.search_page" :q query :page 2)))))))
;; Assembled following/followers content — replaces Python _following_content_sx etc. ;; Assembled following/followers content — replaces Python _following_content_sx etc.
(defcomp ~federation-following-content (&key (actors :as list) (total :as number) actor) (defcomp ~search/following-content (&key (actors :as list) (total :as number) actor)
(~federation-actor-list-page (~search/actor-list-page
:title "Following" :count-str (str "(" total ")") :title "Following" :count-str (str "(" total ")")
:items (when (not (empty? actors)) :items (when (not (empty? actors))
(<> (<>
(map (lambda (a) (map (lambda (a)
(~federation-actor-card-from-data (~search/actor-card-from-data
:a a :actor actor :followed-urls (list) :list-type "following")) :a a :actor actor :followed-urls (list) :list-type "following"))
actors) actors)
(when (>= (len actors) 20) (when (>= (len actors) 20)
(~federation-scroll-sentinel (~social/scroll-sentinel
:url (url-for "social.following_list_page" :page 2))))))) :url (url-for "social.following_list_page" :page 2)))))))
(defcomp ~federation-followers-content (&key (actors :as list) (total :as number) (followed-urls :as list) actor) (defcomp ~search/followers-content (&key (actors :as list) (total :as number) (followed-urls :as list) actor)
(~federation-actor-list-page (~search/actor-list-page
:title "Followers" :count-str (str "(" total ")") :title "Followers" :count-str (str "(" total ")")
:items (when (not (empty? actors)) :items (when (not (empty? actors))
(<> (<>
(map (lambda (a) (map (lambda (a)
(~federation-actor-card-from-data (~search/actor-card-from-data
:a a :actor actor :followed-urls followed-urls :list-type "followers")) :a a :actor actor :followed-urls followed-urls :list-type "followers"))
actors) actors)
(when (>= (len actors) 20) (when (>= (len actors) 20)
(~federation-scroll-sentinel (~social/scroll-sentinel
:url (url-for "social.followers_list_page" :page 2))))))) :url (url-for "social.followers_list_page" :page 2)))))))

View File

@@ -2,46 +2,46 @@
;; --- Navigation --- ;; --- Navigation ---
(defcomp ~federation-nav-choose-username (&key (url :as string)) (defcomp ~social/nav-choose-username (&key (url :as string))
(nav :class "flex gap-3 text-sm items-center" (nav :class "flex gap-3 text-sm items-center"
(a :href url :class "px-2 py-1 rounded hover:bg-stone-200 font-bold" "Choose username"))) (a :href url :class "px-2 py-1 rounded hover:bg-stone-200 font-bold" "Choose username")))
(defcomp ~federation-nav-notification-link (&key (href :as string) (cls :as string) (count-url :as string)) (defcomp ~social/nav-notification-link (&key (href :as string) (cls :as string) (count-url :as string))
(a :href href :class cls "Notifications" (a :href href :class cls "Notifications"
(span :sx-get count-url :sx-trigger "load, every 30s" :sx-swap "innerHTML" (span :sx-get count-url :sx-trigger "load, every 30s" :sx-swap "innerHTML"
:class "absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden"))) :class "absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden")))
(defcomp ~federation-nav-bar (&key items) (defcomp ~social/nav-bar (&key items)
(nav :class "flex gap-3 text-sm items-center flex-wrap" items)) (nav :class "flex gap-3 text-sm items-center flex-wrap" items))
(defcomp ~federation-social-header (&key nav) (defcomp ~social/header (&key nav)
(div :id "social-row" :class "flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-sky-400" (div :id "social-row" :class "flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-sky-400"
(div :class "w-full flex flex-row items-center gap-2 flex-wrap" nav))) (div :class "w-full flex flex-row items-center gap-2 flex-wrap" nav)))
;; --- Post card --- ;; --- Post card ---
(defcomp ~federation-boost-label (&key (name :as string)) (defcomp ~social/boost-label (&key (name :as string))
(div :class "text-sm text-stone-500 mb-2" "Boosted by " name)) (div :class "text-sm text-stone-500 mb-2" "Boosted by " name))
;; Aliases — delegate to shared ~avatar ;; Aliases — delegate to shared ~shared:misc/avatar
(defcomp ~federation-avatar-img (&key (src :as string) (cls :as string)) (defcomp ~social/avatar-img (&key (src :as string) (cls :as string))
(~avatar :src src :cls cls)) (~shared:misc/avatar :src src :cls cls))
(defcomp ~federation-avatar-placeholder (&key (cls :as string) (initial :as string)) (defcomp ~social/avatar-placeholder (&key (cls :as string) (initial :as string))
(~avatar :cls cls :initial initial)) (~shared:misc/avatar :cls cls :initial initial))
(defcomp ~federation-content (&key (content :as string) (summary :as string?)) (defcomp ~social/content (&key (content :as string) (summary :as string?))
(if summary (if summary
(details :class "mt-2" (details :class "mt-2"
(summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary)) (summary :class "text-stone-500 cursor-pointer" "CW: " (~rich-text :html summary))
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content))) (div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))
(div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content)))) (div :class "mt-2 prose prose-sm prose-stone max-w-none" (~rich-text :html content))))
(defcomp ~federation-original-link (&key (url :as string)) (defcomp ~social/original-link (&key (url :as string))
(a :href url :target "_blank" :rel "noopener" (a :href url :target "_blank" :rel "noopener"
:class "text-sm text-stone-400 hover:underline mt-1 inline-block" "original")) :class "text-sm text-stone-400 hover:underline mt-1 inline-block" "original"))
(defcomp ~federation-post-card (&key boost avatar (actor-name :as string) (actor-username :as string) (domain :as string) (time :as string) content original interactions) (defcomp ~social/post-card (&key boost avatar (actor-name :as string) (actor-username :as string) (domain :as string) (time :as string) content original interactions)
(article :class "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-4" (article :class "bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-4"
boost boost
(div :class "flex items-start gap-3" (div :class "flex items-start gap-3"
@@ -55,36 +55,36 @@
;; --- Interaction buttons --- ;; --- Interaction buttons ---
(defcomp ~federation-reply-link (&key (url :as string)) (defcomp ~social/reply-link (&key (url :as string))
(a :href url :class "hover:text-stone-700" "Reply")) (a :href url :class "hover:text-stone-700" "Reply"))
(defcomp ~federation-like-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) (icon :as string) count) (defcomp ~social/like-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) (icon :as string) count)
(form :sx-post action :sx-target target :sx-swap "innerHTML" (form :sx-post action :sx-target target :sx-swap "innerHTML"
(input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "object_id" :value oid)
(input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "author_inbox" :value ainbox)
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class cls (span icon) " " count))) (button :type "submit" :class cls (span icon) " " count)))
(defcomp ~federation-boost-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) count) (defcomp ~social/boost-form (&key (action :as string) (target :as string) (oid :as string) (ainbox :as string) (csrf :as string) (cls :as string) count)
(form :sx-post action :sx-target target :sx-swap "innerHTML" (form :sx-post action :sx-target target :sx-swap "innerHTML"
(input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "object_id" :value oid)
(input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "author_inbox" :value ainbox)
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class cls (span "\u21bb") " " count))) (button :type "submit" :class cls (span "\u21bb") " " count)))
(defcomp ~federation-interaction-buttons (&key like boost reply) (defcomp ~social/interaction-buttons (&key like boost reply)
(div :class "flex items-center gap-4 mt-3 text-sm text-stone-500" (div :class "flex items-center gap-4 mt-3 text-sm text-stone-500"
like boost reply)) like boost reply))
;; --- Timeline --- ;; --- Timeline ---
(defcomp ~federation-scroll-sentinel (&key (url :as string)) (defcomp ~social/scroll-sentinel (&key (url :as string))
(div :sx-get url :sx-trigger "revealed" :sx-swap "outerHTML")) (div :sx-get url :sx-trigger "revealed" :sx-swap "outerHTML"))
(defcomp ~federation-compose-button (&key (url :as string)) (defcomp ~social/compose-button (&key (url :as string))
(a :href url :class "bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700" "Compose")) (a :href url :class "bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700" "Compose"))
(defcomp ~federation-timeline-page (&key (label :as string) compose timeline) (defcomp ~social/timeline-page (&key (label :as string) compose timeline)
(div :class "flex items-center justify-between mb-6" (div :class "flex items-center justify-between mb-6"
(h1 :class "text-2xl font-bold" label " Timeline") (h1 :class "text-2xl font-bold" label " Timeline")
compose) compose)
@@ -92,24 +92,24 @@
;; --- Data-driven post card (replaces Python _post_card_sx loop) --- ;; --- Data-driven post card (replaces Python _post_card_sx loop) ---
(defcomp ~federation-post-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string) (defcomp ~social/post-card-from-data (&key (d :as dict) (has-actor :as boolean) (csrf :as string)
(like-url :as string) (unlike-url :as string) (like-url :as string) (unlike-url :as string)
(boost-url :as string) (unboost-url :as string)) (boost-url :as string) (unboost-url :as string))
(let* ((boosted-by (get d "boosted_by")) (let* ((boosted-by (get d "boosted_by"))
(actor-icon (get d "actor_icon")) (actor-icon (get d "actor_icon"))
(actor-name (get d "actor_name")) (actor-name (get d "actor_name"))
(initial (or (get d "initial") "?")) (initial (or (get d "initial") "?"))
(avatar (~avatar (avatar (~shared:misc/avatar
:src actor-icon :src actor-icon
:cls (if actor-icon "w-10 h-10 rounded-full" :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") "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))) :initial (when (not actor-icon) initial)))
(boost (when boosted-by (~federation-boost-label :name boosted-by))) (boost (when boosted-by (~social/boost-label :name boosted-by)))
(content-sx (if (get d "summary") (content-sx (if (get d "summary")
(~federation-content :content (get d "content") :summary (get d "summary")) (~social/content :content (get d "content") :summary (get d "summary"))
(~federation-content :content (get d "content")))) (~social/content :content (get d "content"))))
(original (when (get d "original_url") (original (when (get d "original_url")
(~federation-original-link :url (get d "original_url")))) (~social/original-link :url (get d "original_url"))))
(safe-id (get d "safe_id")) (safe-id (get d "safe_id"))
(interactions (when has-actor (interactions (when has-actor
(let* ((oid (get d "object_id")) (let* ((oid (get d "object_id"))
@@ -123,16 +123,16 @@
(b-action (if boosted-me unboost-url boost-url)) (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"))) (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-url (get d "reply_url"))
(reply (when reply-url (~federation-reply-link :url reply-url))) (reply (when reply-url (~social/reply-link :url reply-url)))
(like-form (~federation-like-form (like-form (~social/like-form
:action l-action :target target :oid oid :ainbox ainbox :action l-action :target target :oid oid :ainbox ainbox
:csrf csrf :cls l-cls :icon l-icon :count (get d "like_count"))) :csrf csrf :cls l-cls :icon l-icon :count (get d "like_count")))
(boost-form (~federation-boost-form (boost-form (~social/boost-form
:action b-action :target target :oid oid :ainbox ainbox :action b-action :target target :oid oid :ainbox ainbox
:csrf csrf :cls b-cls :count (get d "boost_count")))) :csrf csrf :cls b-cls :count (get d "boost_count"))))
(div :id (str "interactions-" safe-id) (div :id (str "interactions-" safe-id)
(~federation-interaction-buttons :like like-form :boost boost-form :reply reply)))))) (~social/interaction-buttons :like like-form :boost boost-form :reply reply))))))
(~federation-post-card (~social/post-card
:boost boost :avatar avatar :boost boost :avatar avatar
:actor-name actor-name :actor-username (get d "actor_username") :actor-name actor-name :actor-username (get d "actor_username")
:domain (get d "domain") :time (get d "time") :domain (get d "domain") :time (get d "time")
@@ -140,22 +140,22 @@
:interactions interactions))) :interactions interactions)))
;; Data-driven timeline items (replaces Python _timeline_items_sx loop) ;; Data-driven timeline items (replaces Python _timeline_items_sx loop)
(defcomp ~federation-timeline-items-from-data (&key (items :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string) (defcomp ~social/timeline-items-from-data (&key (items :as list) (next-url :as string?) (has-actor :as boolean) (csrf :as string)
(like-url :as string) (unlike-url :as string) (boost-url :as string) (unboost-url :as string)) (like-url :as string) (unlike-url :as string) (boost-url :as string) (unboost-url :as string))
(<> (<>
(map (lambda (d) (map (lambda (d)
(~federation-post-card-from-data :d d :has-actor has-actor :csrf csrf (~social/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)) :like-url like-url :unlike-url unlike-url :boost-url boost-url :unboost-url unboost-url))
(or items (list))) (or items (list)))
(when next-url (~federation-scroll-sentinel :url next-url)))) (when next-url (~social/scroll-sentinel :url next-url))))
;; --- Compose --- ;; --- Compose ---
(defcomp ~federation-compose-reply (&key (reply-to :as string)) (defcomp ~social/compose-reply (&key (reply-to :as string))
(input :type "hidden" :name "in_reply_to" :value reply-to) (input :type "hidden" :name "in_reply_to" :value reply-to)
(div :class "text-sm text-stone-500" "Replying to " (span :class "font-mono" reply-to))) (div :class "text-sm text-stone-500" "Replying to " (span :class "font-mono" reply-to)))
(defcomp ~federation-compose-form (&key (action :as string) (csrf :as string) reply) (defcomp ~social/compose-form (&key (action :as string) (csrf :as string) reply)
(h1 :class "text-2xl font-bold mb-6" "Compose") (h1 :class "text-2xl font-bold mb-6" "Compose")
(form :method "post" :action action :class "space-y-4" (form :method "post" :action action :class "space-y-4"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
@@ -174,9 +174,9 @@
;; Assembled social nav — replaces Python _social_nav_sx ;; Assembled social nav — replaces Python _social_nav_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-social-nav (&key actor) (defcomp ~social/nav (&key actor)
(if (not actor) (if (not actor)
(~federation-nav-choose-username :url (url-for "identity.choose_username_form")) (~social/nav-choose-username :url (url-for "identity.choose_username_form"))
(let* ((rp (request-path)) (let* ((rp (request-path))
(links (list (links (list
(dict :endpoint "social.defpage_home_timeline" :label "Timeline") (dict :endpoint "social.defpage_home_timeline" :label "Timeline")
@@ -185,7 +185,7 @@
(dict :endpoint "social.defpage_following_list" :label "Following") (dict :endpoint "social.defpage_following_list" :label "Following")
(dict :endpoint "social.defpage_followers_list" :label "Followers") (dict :endpoint "social.defpage_followers_list" :label "Followers")
(dict :endpoint "social.defpage_search" :label "Search")))) (dict :endpoint "social.defpage_search" :label "Search"))))
(~federation-nav-bar (~social/nav-bar
:items (<> :items (<>
(map (lambda (lnk) (map (lambda (lnk)
(let* ((href (url-for (get lnk "endpoint"))) (let* ((href (url-for (get lnk "endpoint")))
@@ -196,7 +196,7 @@
links) links)
(let* ((notif-url (url-for "social.defpage_notifications")) (let* ((notif-url (url-for "social.defpage_notifications"))
(notif-bold (if (= rp notif-url) " font-bold" ""))) (notif-bold (if (= rp notif-url) " font-bold" "")))
(~federation-nav-notification-link (~social/nav-notification-link
:href notif-url :href notif-url
:cls (str "px-2 py-1 rounded hover:bg-stone-200 relative" notif-bold) :cls (str "px-2 py-1 rounded hover:bg-stone-200 relative" notif-bold)
:count-url (url-for "social.notification_count"))) :count-url (url-for "social.notification_count")))
@@ -208,7 +208,7 @@
;; Assembled post card — replaces Python _post_card_sx ;; Assembled post card — replaces Python _post_card_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-post-card-from-data (&key (item :as dict) actor) (defcomp ~social/post-card-from-data (&key (item :as dict) actor)
(let* ((boosted-by (get item "boosted_by")) (let* ((boosted-by (get item "boosted_by"))
(actor-icon (get item "actor_icon")) (actor-icon (get item "actor_icon"))
(actor-name (or (get item "actor_name") "?")) (actor-name (or (get item "actor_name") "?"))
@@ -223,9 +223,9 @@
(safe-id (replace (replace oid "/" "_") ":" "_")) (safe-id (replace (replace oid "/" "_") ":" "_"))
(initial (if (and (not actor-icon) actor-name) (initial (if (and (not actor-icon) actor-name)
(upper (slice actor-name 0 1)) "?"))) (upper (slice actor-name 0 1)) "?")))
(~federation-post-card (~social/post-card
:boost (when boosted-by (~federation-boost-label :name (escape boosted-by))) :boost (when boosted-by (~social/boost-label :name (escape boosted-by)))
:avatar (~avatar :avatar (~shared:misc/avatar
:src actor-icon :src actor-icon
:cls (if actor-icon "w-10 h-10 rounded-full" :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") "w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm")
@@ -235,10 +235,10 @@
:domain (if actor-domain (str "@" (escape actor-domain)) "") :domain (if actor-domain (str "@" (escape actor-domain)) "")
:time published :time published
:content (if summary :content (if summary
(~federation-content :content content :summary (escape summary)) (~social/content :content content :summary (escape summary))
(~federation-content :content content)) (~social/content :content content))
:original (when (and url (= post-type "remote")) :original (when (and url (= post-type "remote"))
(~federation-original-link :url url)) (~social/original-link :url url))
:interactions (when actor :interactions (when actor
(let* ((csrf (csrf-token)) (let* ((csrf (csrf-token))
(liked (get item "liked_by_me")) (liked (get item "liked_by_me"))
@@ -248,50 +248,50 @@
(ainbox (or (get item "author_inbox") "")) (ainbox (or (get item "author_inbox") ""))
(target (str "#interactions-" safe-id))) (target (str "#interactions-" safe-id)))
(div :id (str "interactions-" safe-id) (div :id (str "interactions-" safe-id)
(~federation-interaction-buttons (~social/interaction-buttons
:like (~federation-like-form :like (~social/like-form
:action (url-for (if liked "social.unlike" "social.like")) :action (url-for (if liked "social.unlike" "social.like"))
:target target :oid oid :ainbox ainbox :csrf csrf :target target :oid oid :ainbox ainbox :csrf csrf
:cls (str "flex items-center gap-1 " (if liked "text-red-500 hover:text-red-600" "hover:text-red-500")) :cls (str "flex items-center gap-1 " (if liked "text-red-500 hover:text-red-600" "hover:text-red-500"))
:icon (if liked "\u2665" "\u2661") :count (str lcount)) :icon (if liked "\u2665" "\u2661") :count (str lcount))
:boost (~federation-boost-form :boost (~social/boost-form
:action (url-for (if boosted-me "social.unboost" "social.boost")) :action (url-for (if boosted-me "social.unboost" "social.boost"))
:target target :oid oid :ainbox ainbox :csrf csrf :target target :oid oid :ainbox ainbox :csrf csrf
:cls (str "flex items-center gap-1 " (if boosted-me "text-green-600 hover:text-green-700" "hover:text-green-600")) :cls (str "flex items-center gap-1 " (if boosted-me "text-green-600 hover:text-green-700" "hover:text-green-600"))
:count (str bcount)) :count (str bcount))
:reply (when oid :reply (when oid
(~federation-reply-link (~social/reply-link
:url (url-for "social.defpage_compose_form" :reply-to oid)))))))))) :url (url-for "social.defpage_compose_form" :reply-to oid))))))))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Assembled timeline items — replaces Python _timeline_items_sx ;; Assembled timeline items — replaces Python _timeline_items_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~federation-timeline-items (&key (items :as list) (timeline-type :as string) actor (next-url :as string?)) (defcomp ~social/timeline-items (&key (items :as list) (timeline-type :as string) actor (next-url :as string?))
(<> (<>
(map (lambda (item) (map (lambda (item)
(~federation-post-card-from-data :item item :actor actor)) (~social/post-card-from-data :item item :actor actor))
items) items)
(when next-url (when next-url
(~federation-scroll-sentinel :url next-url)))) (~social/scroll-sentinel :url next-url))))
;; Assembled timeline content — replaces Python _timeline_content_sx ;; Assembled timeline content — replaces Python _timeline_content_sx
(defcomp ~federation-timeline-content (&key (items :as list) (timeline-type :as string) actor) (defcomp ~social/timeline-content (&key (items :as list) (timeline-type :as string) actor)
(let* ((label (if (= timeline-type "home") "Home" "Public"))) (let* ((label (if (= timeline-type "home") "Home" "Public")))
(~federation-timeline-page (~social/timeline-page
:label label :label label
:compose (when actor :compose (when actor
(~federation-compose-button :url (url-for "social.defpage_compose_form"))) (~social/compose-button :url (url-for "social.defpage_compose_form")))
:timeline (~federation-timeline-items :timeline (~social/timeline-items
:items items :timeline-type timeline-type :actor actor :items items :timeline-type timeline-type :actor actor
:next-url (when (not (empty? items)) :next-url (when (not (empty? items))
(url-for (str "social." timeline-type "_timeline_page") (url-for (str "social." timeline-type "_timeline_page")
:before (get (last items) "before_cursor"))))))) :before (get (last items) "before_cursor")))))))
;; Assembled compose content — replaces Python _compose_content_sx ;; Assembled compose content — replaces Python _compose_content_sx
(defcomp ~federation-compose-content (&key (reply-to :as string?)) (defcomp ~social/compose-content (&key (reply-to :as string?))
(~federation-compose-form (~social/compose-form
:action (url-for "social.compose_submit") :action (url-for "social.compose_submit")
:csrf (csrf-token) :csrf (csrf-token)
:reply (when reply-to :reply (when reply-to
(~federation-compose-reply :reply-to (escape reply-to))))) (~social/compose-reply :reply-to (escape reply-to)))))

View File

@@ -6,7 +6,7 @@
:auth :login :auth :login
:layout :social :layout :social
:data (service "federation-page" "home-timeline-data") :data (service "federation-page" "home-timeline-data")
:content (~federation-timeline-content :content (~social/timeline-content
:items items :items items
:timeline-type timeline-type :timeline-type timeline-type
:actor actor)) :actor actor))
@@ -16,7 +16,7 @@
:auth :public :auth :public
:layout :social :layout :social
:data (service "federation-page" "public-timeline-data") :data (service "federation-page" "public-timeline-data")
:content (~federation-timeline-content :content (~social/timeline-content
:items items :items items
:timeline-type timeline-type :timeline-type timeline-type
:actor actor)) :actor actor))
@@ -26,7 +26,7 @@
:auth :login :auth :login
:layout :social :layout :social
:data (service "federation-page" "compose-data") :data (service "federation-page" "compose-data")
:content (~federation-compose-content :content (~social/compose-content
:reply-to reply-to)) :reply-to reply-to))
(defpage search (defpage search
@@ -34,7 +34,7 @@
:auth :public :auth :public
:layout :social :layout :social
:data (service "federation-page" "search-data") :data (service "federation-page" "search-data")
:content (~federation-search-content :content (~search/content
:query query :query query
:actors actors :actors actors
:total total :total total
@@ -46,7 +46,7 @@
:auth :login :auth :login
:layout :social :layout :social
:data (service "federation-page" "following-data") :data (service "federation-page" "following-data")
:content (~federation-following-content :content (~search/following-content
:actors actors :actors actors
:total total :total total
:actor actor)) :actor actor))
@@ -56,7 +56,7 @@
:auth :login :auth :login
:layout :social :layout :social
:data (service "federation-page" "followers-data") :data (service "federation-page" "followers-data")
:content (~federation-followers-content :content (~search/followers-content
:actors actors :actors actors
:total total :total total
:followed-urls followed-urls :followed-urls followed-urls
@@ -67,7 +67,7 @@
:auth :public :auth :public
:layout :social :layout :social
:data (service "federation-page" "actor-timeline-data" :id id) :data (service "federation-page" "actor-timeline-data" :id id)
:content (~federation-actor-timeline-content :content (~profile/actor-timeline-content
:remote-actor remote-actor :remote-actor remote-actor
:items items :items items
:is-following is-following :is-following is-following
@@ -78,5 +78,5 @@
:auth :login :auth :login
:layout :social :layout :social
:data (service "federation-page" "notifications-data") :data (service "federation-page" "notifications-data")
:content (~federation-notifications-content :content (~notifications/content
:notifications notifications)) :notifications notifications))

View File

@@ -1,40 +1,40 @@
;; Market card components — pure data, no raw! HTML injection ;; Market card components — pure data, no raw! HTML injection
(defcomp ~market-label-overlay (&key (src :as string)) (defcomp ~cards/label-overlay (&key (src :as string))
(img :src src :alt "" (img :src src :alt ""
:class "pointer-events-none absolute inset-0 w-full h-full object-contain object-top")) :class "pointer-events-none absolute inset-0 w-full h-full object-contain object-top"))
(defcomp ~market-card-image (&key (image :as string) (labels :as list?) (brand :as string) (brand-highlight :as string?)) (defcomp ~cards/image (&key (image :as string) (labels :as list?) (brand :as string) (brand-highlight :as string?))
(div :class "w-full aspect-square bg-stone-100 relative" (div :class "w-full aspect-square bg-stone-100 relative"
(figure :class "inline-block w-full h-full" (figure :class "inline-block w-full h-full"
(div :class "relative w-full h-full" (div :class "relative w-full h-full"
(img :src image :alt "no image" :class "absolute inset-0 w-full h-full object-contain object-top" :loading "lazy" :decoding "async" :fetchpriority "low") (img :src image :alt "no image" :class "absolute inset-0 w-full h-full object-contain object-top" :loading "lazy" :decoding "async" :fetchpriority "low")
(when labels (map (lambda (src) (~market-label-overlay :src src)) labels))) (when labels (map (lambda (src) (~cards/label-overlay :src src)) labels)))
(figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand)))) (figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand))))
(defcomp ~market-card-no-image (&key (labels :as list?) (brand :as string)) (defcomp ~cards/no-image (&key (labels :as list?) (brand :as string))
(div :class "w-full aspect-square bg-stone-100 relative" (div :class "w-full aspect-square bg-stone-100 relative"
(div :class "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative" (div :class "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative"
(div :class "text-stone-400 text-xs" "No image") (div :class "text-stone-400 text-xs" "No image")
(when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels))) (when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels)))
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand)))) (div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
(defcomp ~market-card-sticker (&key (src :as string) (name :as string) (ring-cls :as string?)) (defcomp ~cards/sticker (&key (src :as string) (name :as string) (ring-cls :as string?))
(img :src src :alt name :class (str "w-6 h-6" ring-cls))) (img :src src :alt name :class (str "w-6 h-6" ring-cls)))
(defcomp ~market-card-stickers (&key (stickers :as list)) (defcomp ~cards/stickers (&key (stickers :as list))
(div :class "flex flex-row justify-center gap-2 p-2" (div :class "flex flex-row justify-center gap-2 p-2"
(map (lambda (s) (~market-card-sticker :src (get s "src") :name (get s "name") :ring-cls (get s "ring-cls"))) stickers))) (map (lambda (s) (~cards/sticker :src (get s "src") :name (get s "name") :ring-cls (get s "ring-cls"))) stickers)))
(defcomp ~market-card-highlight (&key (pre :as string) (mid :as string) (post :as string)) (defcomp ~cards/highlight (&key (pre :as string) (mid :as string) (post :as string))
(<> pre (mark mid) post)) (<> pre (mark mid) post))
;; Price — delegates to shared ~price ;; Price — delegates to shared ~shared:misc/price
(defcomp ~market-card-price (&key (special-price :as string?) (regular-price :as string?)) (defcomp ~cards/price (&key (special-price :as string?) (regular-price :as string?))
(~price :special-price special-price :regular-price regular-price)) (~shared:misc/price :special-price special-price :regular-price regular-price))
;; Main product card — accepts pure data, composes sub-components ;; Main product card — accepts pure data, composes sub-components
(defcomp ~market-product-card (&key (href :as string) (hx-select :as string) (defcomp ~cards/product-card (&key (href :as string) (hx-select :as string)
(has-like :as boolean) (liked :as boolean?) (slug :as string) (csrf :as string) (like-action :as string?) (has-like :as boolean) (liked :as boolean?) (slug :as string) (csrf :as string) (like-action :as string?)
(image :as string?) (labels :as list?) (brand :as string) (brand-highlight :as string?) (image :as string?) (labels :as list?) (brand :as string) (brand-highlight :as string?)
(special-price :as string?) (regular-price :as string?) (special-price :as string?) (regular-price :as string?)
@@ -43,29 +43,29 @@
(title :as string) (has-highlight :as boolean) (search-pre :as string?) (search-mid :as string?) (search-post :as string?)) (title :as string) (has-highlight :as boolean) (search-pre :as string?) (search-mid :as string?) (search-post :as string?))
(div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative" (div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative"
(when has-like (when has-like
(~market-like-button :form-id (str "like-" slug) :action like-action :slug slug :csrf csrf (~cards/like-button :form-id (str "like-" slug) :action like-action :slug slug :csrf csrf
:icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400"))) :icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400")))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
(if image (if image
(~market-card-image :image image :labels labels :brand brand :brand-highlight brand-highlight) (~cards/image :image image :labels labels :brand brand :brand-highlight brand-highlight)
(~market-card-no-image :labels labels :brand brand)) (~cards/no-image :labels labels :brand brand))
(~market-card-price :special-price special-price :regular-price regular-price)) (~cards/price :special-price special-price :regular-price regular-price))
(div :class "flex justify-center" (div :class "flex justify-center"
(if quantity (if quantity
(~market-cart-add-quantity :cart-id (str "cart-" slug) :action cart-action :csrf csrf (~cart/add-quantity :cart-id (str "cart-" slug) :action cart-action :csrf csrf
:minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1)) :minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1))
:quantity (str quantity) :cart-href cart-href) :quantity (str quantity) :cart-href cart-href)
(~market-cart-add-empty :cart-id (str "cart-" slug) :action cart-action :csrf csrf))) (~cart/add-empty :cart-id (str "cart-" slug) :action cart-action :csrf csrf)))
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
(when stickers (~market-card-stickers :stickers stickers)) (when stickers (~cards/stickers :stickers stickers))
(div :class "text-sm font-medium text-stone-800 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" (div :class "text-sm font-medium text-stone-800 text-center line-clamp-3 break-words [overflow-wrap:anywhere]"
(if has-highlight (if has-highlight
(~market-card-highlight :pre search-pre :mid search-mid :post search-post) (~cards/highlight :pre search-pre :mid search-mid :post search-post)
title))))) title)))))
(defcomp ~market-like-button (&key (form-id :as string) (action :as string) (slug :as string) (csrf :as string) (icon-cls :as string)) (defcomp ~cards/like-button (&key (form-id :as string) (action :as string) (slug :as string) (csrf :as string) (icon-cls :as string))
(div :class "absolute top-2 right-2 z-10 text-6xl md:text-xl" (div :class "absolute top-2 right-2 z-10 text-6xl md:text-xl"
(form :id form-id :action action :method "post" (form :id form-id :action action :method "post"
:sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML" :sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML"
@@ -73,22 +73,22 @@
(button :type "submit" :class "cursor-pointer" (button :type "submit" :class "cursor-pointer"
(i :class icon-cls :aria-hidden "true"))))) (i :class icon-cls :aria-hidden "true")))))
(defcomp ~market-market-card-title-link (&key (href :as string) (name :as string)) (defcomp ~cards/market-card-title-link (&key (href :as string) (name :as string))
(a :href href :class "hover:text-emerald-700" (a :href href :class "hover:text-emerald-700"
(h2 :class "text-lg font-semibold text-stone-900" name))) (h2 :class "text-lg font-semibold text-stone-900" name)))
(defcomp ~market-market-card-title (&key (name :as string)) (defcomp ~cards/market-card-title (&key (name :as string))
(h2 :class "text-lg font-semibold text-stone-900" name)) (h2 :class "text-lg font-semibold text-stone-900" name))
(defcomp ~market-market-card-desc (&key (description :as string)) (defcomp ~cards/market-card-desc (&key (description :as string))
(p :class "text-sm text-stone-600 mt-1 line-clamp-2" description)) (p :class "text-sm text-stone-600 mt-1 line-clamp-2" description))
(defcomp ~market-market-card-badge (&key (href :as string) (title :as string)) (defcomp ~cards/market-card-badge (&key (href :as string) (title :as string))
(div :class "flex flex-wrap items-center gap-1.5 mt-3" (div :class "flex flex-wrap items-center gap-1.5 mt-3"
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" (a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200"
title))) title)))
(defcomp ~market-market-card (&key (title-content :as list?) (desc-content :as list?) (badge-content :as list?) (title :as string?) (desc :as string?) (badge :as string?)) (defcomp ~cards/market-card (&key (title-content :as list?) (desc-content :as list?) (badge-content :as list?) (title :as string?) (desc :as string?) (badge :as string?))
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-5 flex flex-col justify-between hover:border-stone-400 transition-colors" (article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-5 flex flex-col justify-between hover:border-stone-400 transition-colors"
(div (div
(if title-content title-content (when title title)) (if title-content title-content (when title title))
@@ -101,11 +101,11 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Product cards grid with infinite scroll sentinels ;; Product cards grid with infinite scroll sentinels
(defcomp ~market-product-cards-content (&key (products :as list) (page :as number) (total-pages :as number) (next-url :as string) (defcomp ~cards/product-cards-content (&key (products :as list) (page :as number) (total-pages :as number) (next-url :as string)
(mobile-sentinel-hs :as string?) (desktop-sentinel-hs :as string?)) (mobile-sentinel-hs :as string?) (desktop-sentinel-hs :as string?))
(<> (<>
(map (lambda (p) (map (lambda (p)
(~market-product-card (~cards/product-card
:href (get p "href") :hx-select (get p "hx-select") :slug (get p "slug") :href (get p "href") :hx-select (get p "hx-select") :slug (get p "slug")
:image (get p "image") :brand (get p "brand") :brand-highlight (get p "brand-highlight") :image (get p "image") :brand (get p "brand") :brand-highlight (get p "brand-highlight")
:special-price (get p "special-price") :regular-price (get p "regular-price") :special-price (get p "special-price") :regular-price (get p "regular-price")
@@ -119,39 +119,39 @@
:search-post (get p "search-post"))) :search-post (get p "search-post")))
products) products)
(if (< page total-pages) (if (< page total-pages)
(<> (~sentinel-mobile :id (str "sentinel-" page "-m") :next-url next-url (<> (~shared:misc/sentinel-mobile :id (str "sentinel-" page "-m") :next-url next-url
:hyperscript mobile-sentinel-hs) :hyperscript mobile-sentinel-hs)
(~sentinel-desktop :id (str "sentinel-" page "-d") :next-url next-url (~shared:misc/sentinel-desktop :id (str "sentinel-" page "-d") :next-url next-url
:hyperscript desktop-sentinel-hs)) :hyperscript desktop-sentinel-hs))
(~end-of-results)))) (~shared:misc/end-of-results))))
;; Single market card from data (handles conditional title/desc/badge) ;; Single market card from data (handles conditional title/desc/badge)
(defcomp ~market-card-from-data (&key (name :as string) (description :as string?) (href :as string?) (show-badge :as boolean) (badge-href :as string?) (badge-title :as string?)) (defcomp ~cards/from-data (&key (name :as string) (description :as string?) (href :as string?) (show-badge :as boolean) (badge-href :as string?) (badge-title :as string?))
(~market-market-card (~cards/market-card
:title-content (if href :title-content (if href
(~market-market-card-title-link :href href :name name) (~cards/market-card-title-link :href href :name name)
(~market-market-card-title :name name)) (~cards/market-card-title :name name))
:desc-content (when description :desc-content (when description
(~market-market-card-desc :description description)) (~cards/market-card-desc :description description))
:badge-content (when (and show-badge badge-title) :badge-content (when (and show-badge badge-title)
(~market-market-card-badge :href badge-href :title badge-title)))) (~cards/market-card-badge :href badge-href :title badge-title))))
;; Market cards list with infinite scroll sentinel ;; Market cards list with infinite scroll sentinel
(defcomp ~market-cards-content (&key (markets :as list) (page :as number) (has-more :as boolean) (next-url :as string)) (defcomp ~cards/content (&key (markets :as list) (page :as number) (has-more :as boolean) (next-url :as string))
(<> (<>
(map (lambda (m) (map (lambda (m)
(~market-card-from-data (~cards/from-data
:name (get m "name") :description (get m "description") :name (get m "name") :description (get m "description")
:href (get m "href") :show-badge (get m "show-badge") :href (get m "href") :show-badge (get m "show-badge")
:badge-href (get m "badge-href") :badge-title (get m "badge-title"))) :badge-href (get m "badge-href") :badge-title (get m "badge-title")))
markets) markets)
(when has-more (when has-more
(~sentinel-simple :id (str "sentinel-" page) :next-url next-url)))) (~shared:misc/sentinel-simple :id (str "sentinel-" page) :next-url next-url))))
;; Market landing page content from data ;; Market landing page content from data
(defcomp ~market-landing-from-data (&key (excerpt :as string?) (feature-image :as string?) (html :as string?)) (defcomp ~cards/landing-from-data (&key (excerpt :as string?) (feature-image :as string?) (html :as string?))
(~market-landing-content :inner (~detail/landing-content :inner
(<> (when excerpt (~market-landing-excerpt :text excerpt)) (<> (when excerpt (~detail/landing-excerpt :text excerpt))
(when feature-image (~market-landing-image :src feature-image)) (when feature-image (~detail/landing-image :src feature-image))
(when html (~market-landing-html :html html))))) (when html (~detail/landing-html :html html)))))

View File

@@ -1,6 +1,6 @@
;; Market cart components ;; Market cart components
(defcomp ~market-cart-add-empty (&key cart-id action csrf) (defcomp ~cart/add-empty (&key cart-id action csrf)
(div :id cart-id (div :id cart-id
(form :action action :method "post" :sx-post action :sx-target "#cart-mini" :sx-swap "outerHTML" :class "rounded flex items-center" (form :action action :method "post" :sx-post action :sx-target "#cart-mini" :sx-swap "outerHTML" :class "rounded flex items-center"
(input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "csrf_token" :value csrf)
@@ -9,7 +9,7 @@
(span :class "relative inline-flex items-center justify-center" (span :class "relative inline-flex items-center justify-center"
(i :class "fa fa-cart-plus text-4xl" :aria-hidden "true")))))) (i :class "fa fa-cart-plus text-4xl" :aria-hidden "true"))))))
(defcomp ~market-cart-add-quantity (&key cart-id action csrf minus-val plus-val quantity cart-href) (defcomp ~cart/add-quantity (&key cart-id action csrf minus-val plus-val quantity cart-href)
(div :id cart-id (div :id cart-id
(div :class "rounded flex items-center gap-2" (div :class "rounded flex items-center gap-2"
(form :action action :method "post" :sx-post action :sx-target "#cart-mini" :sx-swap "outerHTML" (form :action action :method "post" :sx-post action :sx-target "#cart-mini" :sx-swap "outerHTML"
@@ -26,7 +26,7 @@
(input :type "hidden" :name "count" :value plus-val) (input :type "hidden" :name "count" :value plus-val)
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+"))))) (button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+")))))
(defcomp ~market-cart-mini-count (&key href count) (defcomp ~cart/mini-count (&key href count)
(div :id "cart-mini" :sx-swap-oob "outerHTML" (div :id "cart-mini" :sx-swap-oob "outerHTML"
(a :href href :class "relative inline-flex items-center justify-center" (a :href href :class "relative inline-flex items-center justify-center"
(span :class "relative inline-flex items-center justify-center" (span :class "relative inline-flex items-center justify-center"
@@ -35,25 +35,25 @@
(span :class "flex items-center justify-center bg-emerald-500 text-white rounded-full min-w-[1.25rem] h-5 text-xs font-bold px-1" (span :class "flex items-center justify-center bg-emerald-500 text-white rounded-full min-w-[1.25rem] h-5 text-xs font-bold px-1"
count)))))) count))))))
(defcomp ~market-cart-mini-empty (&key href logo) (defcomp ~cart/mini-empty (&key href logo)
(div :id "cart-mini" :sx-swap-oob "outerHTML" (div :id "cart-mini" :sx-swap-oob "outerHTML"
(a :href href :class "relative inline-flex items-center justify-center" (a :href href :class "relative inline-flex items-center justify-center"
(img :src logo :class "h-8 w-8 rounded-full object-cover border border-stone-300" :alt "")))) (img :src logo :class "h-8 w-8 rounded-full object-cover border border-stone-300" :alt ""))))
(defcomp ~market-cart-add-oob (&key id content inner) (defcomp ~cart/add-oob (&key id content inner)
(div :id id :sx-swap-oob "outerHTML" (div :id id :sx-swap-oob "outerHTML"
(if content content (when inner inner)))) (if content content (when inner inner))))
;; Cart added response — composes cart mini + add/remove OOB in sx ;; Cart added response — composes cart mini + add/remove OOB in sx
(defcomp ~market-cart-added-response (&key has-count cart-href blog-href logo (defcomp ~cart/added-response (&key has-count cart-href blog-href logo
slug action csrf quantity minus-val plus-val) slug action csrf quantity minus-val plus-val)
(<> (<>
(if has-count (if has-count
(~market-cart-mini-count :href cart-href :count (str has-count)) (~cart/mini-count :href cart-href :count (str has-count))
(~market-cart-mini-empty :href blog-href :logo logo)) (~cart/mini-empty :href blog-href :logo logo))
(~market-cart-add-oob :id (str "cart-add-" slug) (~cart/add-oob :id (str "cart-add-" slug)
:inner (if (= (or quantity "0") "0") :inner (if (= (or quantity "0") "0")
(~market-cart-add-empty :cart-id (str "cart-" slug) :action action :csrf csrf) (~cart/add-empty :cart-id (str "cart-" slug) :action action :csrf csrf)
(~market-cart-add-quantity :cart-id (str "cart-" slug) :action action :csrf csrf (~cart/add-quantity :cart-id (str "cart-" slug) :action action :csrf csrf
:minus-val minus-val :plus-val plus-val :minus-val minus-val :plus-val plus-val
:quantity quantity :cart-href cart-href))))) :quantity quantity :cart-href cart-href)))))

View File

@@ -1,6 +1,6 @@
;; Market product detail components ;; Market product detail components
(defcomp ~market-detail-gallery-inner (&key (like :as list?) (image :as string) (alt :as string) (labels :as list?) (brand :as string)) (defcomp ~detail/gallery-inner (&key (like :as list?) (image :as string) (alt :as string) (labels :as list?) (brand :as string))
(<> like (<> like
(figure :class "inline-block" (figure :class "inline-block"
(div :class "relative w-full aspect-square" (div :class "relative w-full aspect-square"
@@ -9,7 +9,7 @@
labels) labels)
(figcaption :class "mt-2 text-sm text-stone-600 text-center" brand)))) (figcaption :class "mt-2 text-sm text-stone-600 text-center" brand))))
(defcomp ~market-detail-nav-buttons () (defcomp ~detail/nav-buttons ()
(<> (<>
(button :type "button" :data-prev "" (button :type "button" :data-prev ""
:class "absolute left-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl" :class "absolute left-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl"
@@ -18,79 +18,79 @@
:class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl" :class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl"
:title "Next" "\u203a"))) :title "Next" "\u203a")))
(defcomp ~market-detail-gallery (&key (inner :as list) (nav :as list?)) (defcomp ~detail/gallery (&key (inner :as list) (nav :as list?))
(div :class "relative rounded-xl overflow-hidden bg-stone-100" (div :class "relative rounded-xl overflow-hidden bg-stone-100"
inner nav)) inner nav))
(defcomp ~market-detail-thumb (&key (title :as string) (src :as string) (alt :as string)) (defcomp ~detail/thumb (&key (title :as string) (src :as string) (alt :as string))
(<> (button :type "button" :data-thumb "" (<> (button :type "button" :data-thumb ""
:class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2" :class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2"
:title title :title title
(img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async")) (img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async"))
(span :data-image-src src :class "hidden"))) (span :data-image-src src :class "hidden")))
(defcomp ~market-detail-thumbs (&key (thumbs :as list)) (defcomp ~detail/thumbs (&key (thumbs :as list))
(div :class "flex flex-row justify-center" (div :class "flex flex-row justify-center"
(div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" thumbs))) (div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" thumbs)))
(defcomp ~market-detail-no-image (&key (like :as list?)) (defcomp ~detail/no-image (&key (like :as list?))
(div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400" (div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400"
like "No image")) like "No image"))
(defcomp ~market-detail-sticker (&key (src :as string) (name :as string)) (defcomp ~detail/sticker (&key (src :as string) (name :as string))
(img :src src :alt name :class "w-10 h-10")) (img :src src :alt name :class "w-10 h-10"))
(defcomp ~market-detail-stickers (&key (items :as list)) (defcomp ~detail/stickers (&key (items :as list))
(div :class "p-2 flex flex-row justify-center gap-2" items)) (div :class "p-2 flex flex-row justify-center gap-2" items))
(defcomp ~market-detail-unit-price (&key (price :as string)) (defcomp ~detail/unit-price (&key (price :as string))
(div (str "Unit price: " price))) (div (str "Unit price: " price)))
(defcomp ~market-detail-case-size (&key (size :as string)) (defcomp ~detail/case-size (&key (size :as string))
(div (str "Case size: " size))) (div (str "Case size: " size)))
(defcomp ~market-detail-extras (&key (inner :as list)) (defcomp ~detail/extras (&key (inner :as list))
(div :class "mt-2 space-y-1 text-sm text-stone-600" inner)) (div :class "mt-2 space-y-1 text-sm text-stone-600" inner))
(defcomp ~market-detail-desc-short (&key (text :as string)) (defcomp ~detail/desc-short (&key (text :as string))
(p :class "leading-relaxed text-lg" text)) (p :class "leading-relaxed text-lg" text))
(defcomp ~market-detail-desc-html (&key (html :as string)) (defcomp ~detail/desc-html (&key (html :as string))
(div :class "max-w-none text-sm leading-relaxed" (~rich-text :html html))) (div :class "max-w-none text-sm leading-relaxed" (~rich-text :html html)))
(defcomp ~market-detail-desc-wrapper (&key (inner :as list)) (defcomp ~detail/desc-wrapper (&key (inner :as list))
(div :class "mt-4 text-stone-800 space-y-3" inner)) (div :class "mt-4 text-stone-800 space-y-3" inner))
(defcomp ~market-detail-section (&key (title :as string) (html :as string)) (defcomp ~detail/section (&key (title :as string) (html :as string))
(details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0" (details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0"
(summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between" (summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between"
(span :class "font-medium" title) (span :class "font-medium" title)
(span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304")) (span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304"))
(div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (~rich-text :html html)))) (div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (~rich-text :html html))))
(defcomp ~market-detail-sections (&key (items :as list)) (defcomp ~detail/sections (&key (items :as list))
(div :class "mt-8 space-y-3" items)) (div :class "mt-8 space-y-3" items))
(defcomp ~market-detail-right-col (&key (inner :as list)) (defcomp ~detail/right-col (&key (inner :as list))
(div :class "md:col-span-3" inner)) (div :class "md:col-span-3" inner))
(defcomp ~market-detail-layout (&key (gallery :as list) (stickers :as list?) (details :as list)) (defcomp ~detail/layout (&key (gallery :as list) (stickers :as list?) (details :as list))
(<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root "" (<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root ""
(div :class "md:col-span-2" gallery stickers) (div :class "md:col-span-2" gallery stickers)
details) details)
(div :class "pb-8"))) (div :class "pb-8")))
(defcomp ~market-landing-excerpt (&key (text :as string)) (defcomp ~detail/landing-excerpt (&key (text :as string))
(div :class "w-full text-center italic text-3xl p-2" text)) (div :class "w-full text-center italic text-3xl p-2" text))
(defcomp ~market-landing-image (&key (src :as string)) (defcomp ~detail/landing-image (&key (src :as string))
(div :class "mb-3 flex justify-center" (div :class "mb-3 flex justify-center"
(img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover"))) (img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover")))
(defcomp ~market-landing-html (&key (html :as string)) (defcomp ~detail/landing-html (&key (html :as string))
(div :class "blog-content p-2" (~rich-text :html html))) (div :class "blog-content p-2" (~rich-text :html html)))
(defcomp ~market-landing-content (&key (inner :as list)) (defcomp ~detail/landing-content (&key (inner :as list))
(<> (article :class "relative w-full" inner) (div :class "pb-8"))) (<> (article :class "relative w-full" inner) (div :class "pb-8")))
@@ -99,64 +99,64 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Gallery section from pre-computed data ;; Gallery section from pre-computed data
(defcomp ~market-detail-gallery-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?) (has-nav-buttons :as boolean) (thumbs :as list?)) (defcomp ~detail/gallery-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?) (has-nav-buttons :as boolean) (thumbs :as list?))
(let ((like-sx (when like-data (let ((like-sx (when like-data
(~market-like-button (~cards/like-button
:form-id (get like-data "form-id") :action (get like-data "action") :form-id (get like-data "form-id") :action (get like-data "action")
:slug (get like-data "slug") :csrf (get like-data "csrf") :slug (get like-data "slug") :csrf (get like-data "csrf")
:icon-cls (get like-data "icon-cls"))))) :icon-cls (get like-data "icon-cls")))))
(if images (if images
(<> (<>
(~market-detail-gallery (~detail/gallery
:inner (~market-detail-gallery-inner :inner (~detail/gallery-inner
:like like-sx :like like-sx
:image (get (first images) "src") :alt (get (first images) "alt") :image (get (first images) "src") :alt (get (first images) "alt")
:labels (when labels :labels (when labels
(<> (map (lambda (src) (~market-label-overlay :src src)) labels))) (<> (map (lambda (src) (~cards/label-overlay :src src)) labels)))
:brand brand) :brand brand)
:nav (when has-nav-buttons (~market-detail-nav-buttons))) :nav (when has-nav-buttons (~detail/nav-buttons)))
(when thumbs (when thumbs
(~market-detail-thumbs :thumbs (~detail/thumbs :thumbs
(<> (map (lambda (t) (<> (map (lambda (t)
(~market-detail-thumb (~detail/thumb
:title (get t "title") :src (get t "src") :alt (get t "alt"))) :title (get t "title") :src (get t "src") :alt (get t "alt")))
thumbs))))) thumbs)))))
(~market-detail-no-image :like like-sx)))) (~detail/no-image :like like-sx))))
;; Right column details from data ;; Right column details from data
(defcomp ~market-detail-info-from-data (&key (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?)) (defcomp ~detail/info-from-data (&key (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?))
(~market-detail-right-col :inner (~detail/right-col :inner
(<> (<>
(when extras (when extras
(~market-detail-extras :inner (~detail/extras :inner
(<> (map (lambda (e) (<> (map (lambda (e)
(if (= (get e "type") "unit-price") (if (= (get e "type") "unit-price")
(~market-detail-unit-price :price (get e "value")) (~detail/unit-price :price (get e "value"))
(~market-detail-case-size :size (get e "value")))) (~detail/case-size :size (get e "value"))))
extras)))) extras))))
(when (or desc-short desc-html) (when (or desc-short desc-html)
(~market-detail-desc-wrapper :inner (~detail/desc-wrapper :inner
(<> (when desc-short (~market-detail-desc-short :text desc-short)) (<> (when desc-short (~detail/desc-short :text desc-short))
(when desc-html (~market-detail-desc-html :html desc-html))))) (when desc-html (~detail/desc-html :html desc-html)))))
(when sections (when sections
(~market-detail-sections :items (~detail/sections :items
(<> (map (lambda (s) (<> (map (lambda (s)
(~market-detail-section :title (get s "title") :html (get s "html"))) (~detail/section :title (get s "title") :html (get s "html")))
sections))))))) sections)))))))
;; Full product detail layout from data ;; Full product detail layout from data
(defcomp ~market-product-detail-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?) (defcomp ~detail/product-detail-from-data (&key (images :as list?) (labels :as list?) (brand :as string) (like-data :as dict?)
(has-nav-buttons :as boolean) (thumbs :as list?) (sticker-items :as list?) (has-nav-buttons :as boolean) (thumbs :as list?) (sticker-items :as list?)
(extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?)) (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?))
(~market-detail-layout (~detail/layout
:gallery (~market-detail-gallery-from-data :gallery (~detail/gallery-from-data
:images images :labels labels :brand brand :like-data like-data :images images :labels labels :brand brand :like-data like-data
:has-nav-buttons has-nav-buttons :thumbs thumbs) :has-nav-buttons has-nav-buttons :thumbs thumbs)
:stickers (when sticker-items :stickers (when sticker-items
(~market-detail-stickers :items (~detail/stickers :items
(<> (map (lambda (s) (<> (map (lambda (s)
(~market-detail-sticker :src (get s "src") :name (get s "name"))) (~detail/sticker :src (get s "src") :name (get s "name")))
sticker-items)))) sticker-items))))
:details (~market-detail-info-from-data :details (~detail/info-from-data
:extras extras :desc-short desc-short :desc-html desc-html :extras extras :desc-short desc-short :desc-html desc-html
:sections sections))) :sections sections)))

View File

@@ -1,73 +1,73 @@
;; Market filter components ;; Market filter components
(defcomp ~market-filter-sort-item (&key href hx-select ring-cls src label) (defcomp ~filters/sort-item (&key href hx-select ring-cls src label)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls) :class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
(img :src src :alt label :class "w-10 h-10") (img :src src :alt label :class "w-10 h-10")
(span :class "text-xs" label))) (span :class "text-xs" label)))
(defcomp ~market-filter-sort-row (&key items) (defcomp ~filters/sort-row (&key items)
(div :class "flex flex-row gap-2 justify-center p-1" (div :class "flex flex-row gap-2 justify-center p-1"
items)) items))
(defcomp ~market-filter-like (&key href hx-select icon-cls size-cls) (defcomp ~filters/like (&key href hx-select icon-cls size-cls)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class "flex flex-col items-center gap-1 p-1 cursor-pointer" :class "flex flex-col items-center gap-1 p-1 cursor-pointer"
(i :aria-hidden "true" :class (str icon-cls " " size-cls " leading-none")))) (i :aria-hidden "true" :class (str icon-cls " " size-cls " leading-none"))))
(defcomp ~market-filter-label-item (&key href hx-select ring-cls src name) (defcomp ~filters/label-item (&key href hx-select ring-cls src name)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls) :class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
(img :src src :alt name :class "w-10 h-10"))) (img :src src :alt name :class "w-10 h-10")))
(defcomp ~market-filter-sticker-item (&key href hx-select ring-cls src name count-cls count) (defcomp ~filters/sticker-item (&key href hx-select ring-cls src name count-cls count)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls) :class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
(img :src src :alt name :class "w-6 h-6") (img :src src :alt name :class "w-6 h-6")
(span :class count-cls count))) (span :class count-cls count)))
(defcomp ~market-filter-stickers-row (&key items) (defcomp ~filters/stickers-row (&key items)
(div :class "flex flex-wrap gap-2 justify-center p-1" (div :class "flex flex-wrap gap-2 justify-center p-1"
items)) items))
(defcomp ~market-filter-brand-item (&key href hx-select bg-cls name-cls name count) (defcomp ~filters/brand-item (&key href hx-select bg-cls name-cls name count)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-row items-center gap-2 px-2 py-1 rounded hover:bg-stone-100" bg-cls) :class (str "flex flex-row items-center gap-2 px-2 py-1 rounded hover:bg-stone-100" bg-cls)
(div :class name-cls name) (div :class name-cls count))) (div :class name-cls name) (div :class name-cls count)))
(defcomp ~market-filter-brands-panel (&key items) (defcomp ~filters/brands-panel (&key items)
(div :class "space-y-1 p-2" (div :class "space-y-1 p-2"
items)) items))
(defcomp ~market-filter-category-label (&key label) (defcomp ~filters/category-label (&key label)
(div :class "mb-4" (div :class "text-2xl uppercase tracking-wide text-black-500" label))) (div :class "mb-4" (div :class "text-2xl uppercase tracking-wide text-black-500" label)))
(defcomp ~market-filter-like-labels-nav (&key content inner) (defcomp ~filters/like-labels-nav (&key content inner)
(nav :aria-label "like" :class "flex flex-row justify-center w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-1" (nav :aria-label "like" :class "flex flex-row justify-center w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-1"
(if content content (when inner inner)))) (if content content (when inner inner))))
(defcomp ~market-desktop-category-summary (&key content inner) (defcomp ~filters/desktop-category-summary (&key content inner)
(div :id "category-summary-desktop" :hxx-swap-oob "outerHTML" (div :id "category-summary-desktop" :hxx-swap-oob "outerHTML"
(if content content (when inner inner)))) (if content content (when inner inner))))
(defcomp ~market-desktop-brand-summary (&key inner) (defcomp ~filters/desktop-brand-summary (&key inner)
(div :id "filter-summary-desktop" :hxx-swap-oob "outerHTML" inner)) (div :id "filter-summary-desktop" :hxx-swap-oob "outerHTML" inner))
(defcomp ~market-filter-subcategory-item (&key href hx-select active-cls name) (defcomp ~filters/subcategory-item (&key href hx-select active-cls name)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "block px-2 py-1 rounded hover:bg-stone-100" active-cls) :class (str "block px-2 py-1 rounded hover:bg-stone-100" active-cls)
name)) name))
(defcomp ~market-filter-subcategory-panel (&key items) (defcomp ~filters/subcategory-panel (&key items)
(div :class "mt-4 space-y-1" items)) (div :class "mt-4 space-y-1" items))
(defcomp ~market-mobile-clear-filters (&key href hx-select) (defcomp ~filters/mobile-clear-filters (&key href hx-select)
(div :class "flex flex-row justify-center" (div :class "flex flex-row justify-center"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -75,10 +75,10 @@
:class "flex flex-col items-center justify-start p-1 rounded bg-stone-200 text-black cursor-pointer" :class "flex flex-col items-center justify-start p-1 rounded bg-stone-200 text-black cursor-pointer"
(span :class "mt-1 leading-none tabular-nums" "clear filters")))) (span :class "mt-1 leading-none tabular-nums" "clear filters"))))
(defcomp ~market-mobile-like-labels-row (&key inner) (defcomp ~filters/mobile-like-labels-row (&key inner)
(div :class "flex flex-row gap-2 justify-center items-center" inner)) (div :class "flex flex-row gap-2 justify-center items-center" inner))
(defcomp ~market-mobile-filter-summary (&key search-bar chips filter) (defcomp ~filters/mobile-filter-summary (&key search-bar chips filter)
(details :class "md:hidden group" :id "/filter" (details :class "md:hidden group" :id "/filter"
(summary :class "cursor-pointer select-none" :id "filter-summary-mobile" (summary :class "cursor-pointer select-none" :id "filter-summary-mobile"
search-bar search-bar
@@ -87,40 +87,40 @@
(div :id "filter-details-mobile" :style "display:contents" (div :id "filter-details-mobile" :style "display:contents"
filter))) filter)))
(defcomp ~market-mobile-chips-row (&key inner) (defcomp ~filters/mobile-chips-row (&key inner)
(div :class "flex flex-row items-start gap-2" inner)) (div :class "flex flex-row items-start gap-2" inner))
(defcomp ~market-mobile-chip-sort (&key src label) (defcomp ~filters/mobile-chip-sort (&key src label)
(ul :class "relative inline-flex items-center justify-center gap-2" (ul :class "relative inline-flex items-center justify-center gap-2"
(li :role "listitem" (img :src src :alt label :class "w-10 h-10")))) (li :role "listitem" (img :src src :alt label :class "w-10 h-10"))))
(defcomp ~market-mobile-chip-liked-icon () (defcomp ~filters/mobile-chip-liked-icon ()
(i :aria-hidden "true" :class "fa-solid fa-heart text-red-500 text-[40px] leading-none")) (i :aria-hidden "true" :class "fa-solid fa-heart text-red-500 text-[40px] leading-none"))
(defcomp ~market-mobile-chip-count (&key cls count) (defcomp ~filters/mobile-chip-count (&key cls count)
(div :class (str cls " mt-1 leading-none tabular-nums") count)) (div :class (str cls " mt-1 leading-none tabular-nums") count))
(defcomp ~market-mobile-chip-liked (&key inner) (defcomp ~filters/mobile-chip-liked (&key inner)
(div :class "flex flex-col items-center gap-1 pb-1" inner)) (div :class "flex flex-col items-center gap-1 pb-1" inner))
(defcomp ~market-mobile-chip-image (&key src name) (defcomp ~filters/mobile-chip-image (&key src name)
(img :src src :alt name :class "w-10 h-10")) (img :src src :alt name :class "w-10 h-10"))
(defcomp ~market-mobile-chip-item (&key inner) (defcomp ~filters/mobile-chip-item (&key inner)
(li :role "listitem" :class "flex flex-col items-center gap-1 pb-1" inner)) (li :role "listitem" :class "flex flex-col items-center gap-1 pb-1" inner))
(defcomp ~market-mobile-chip-list (&key items) (defcomp ~filters/mobile-chip-list (&key items)
(ul :class "relative inline-flex items-center justify-center gap-2" items)) (ul :class "relative inline-flex items-center justify-center gap-2" items))
(defcomp ~market-mobile-chip-brand (&key name count) (defcomp ~filters/mobile-chip-brand (&key name count)
(li :role "listitem" :class "flex flex-row items-center gap-2" (li :role "listitem" :class "flex flex-row items-center gap-2"
(div :class "text-md" name) (div :class "text-md" count))) (div :class "text-md" name) (div :class "text-md" count)))
(defcomp ~market-mobile-chip-brand-zero (&key name) (defcomp ~filters/mobile-chip-brand-zero (&key name)
(li :role "listitem" :class "flex flex-row items-center gap-2" (li :role "listitem" :class "flex flex-row items-center gap-2"
(div :class "text-md text-red-500" name) (div :class "text-xl text-red-500" "0"))) (div :class "text-md text-red-500" name) (div :class "text-xl text-red-500" "0")))
(defcomp ~market-mobile-chip-brand-list (&key items) (defcomp ~filters/mobile-chip-brand-list (&key items)
(ul items)) (ul items))
@@ -129,160 +129,160 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Sort option stickers from data ;; Sort option stickers from data
(defcomp ~market-filter-sort-from-data (&key items) (defcomp ~filters/sort-from-data (&key items)
(~market-filter-sort-row :items (~filters/sort-row :items
(<> (map (lambda (s) (<> (map (lambda (s)
(~market-filter-sort-item (~filters/sort-item
:href (get s "href") :hx-select (get s "hx-select") :href (get s "href") :hx-select (get s "hx-select")
:ring-cls (get s "ring-cls") :src (get s "src") :label (get s "label"))) :ring-cls (get s "ring-cls") :src (get s "src") :label (get s "label")))
items)))) items))))
;; Like filter from data ;; Like filter from data
(defcomp ~market-filter-like-from-data (&key href hx-select liked mobile) (defcomp ~filters/like-from-data (&key href hx-select liked mobile)
(~market-filter-like (~filters/like
:href href :hx-select hx-select :href href :hx-select hx-select
:icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400") :icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400")
:size-cls (if mobile "text-[40px]" "text-2xl"))) :size-cls (if mobile "text-[40px]" "text-2xl")))
;; Label filter items from data ;; Label filter items from data
(defcomp ~market-filter-labels-from-data (&key items hx-select) (defcomp ~filters/labels-from-data (&key items hx-select)
(<> (map (lambda (lb) (<> (map (lambda (lb)
(~market-filter-label-item (~filters/label-item
:href (get lb "href") :hx-select hx-select :href (get lb "href") :hx-select hx-select
:ring-cls (get lb "ring-cls") :src (get lb "src") :name (get lb "name"))) :ring-cls (get lb "ring-cls") :src (get lb "src") :name (get lb "name")))
items))) items)))
;; Sticker filter items from data ;; Sticker filter items from data
(defcomp ~market-filter-stickers-from-data (&key items hx-select) (defcomp ~filters/stickers-from-data (&key items hx-select)
(~market-filter-stickers-row :items (~filters/stickers-row :items
(<> (map (lambda (st) (<> (map (lambda (st)
(~market-filter-sticker-item (~filters/sticker-item
:href (get st "href") :hx-select hx-select :href (get st "href") :hx-select hx-select
:ring-cls (get st "ring-cls") :src (get st "src") :name (get st "name") :ring-cls (get st "ring-cls") :src (get st "src") :name (get st "name")
:count-cls (get st "count-cls") :count (get st "count"))) :count-cls (get st "count-cls") :count (get st "count")))
items)))) items))))
;; Brand filter items from data ;; Brand filter items from data
(defcomp ~market-filter-brands-from-data (&key items hx-select) (defcomp ~filters/brands-from-data (&key items hx-select)
(~market-filter-brands-panel :items (~filters/brands-panel :items
(<> (map (lambda (br) (<> (map (lambda (br)
(~market-filter-brand-item (~filters/brand-item
:href (get br "href") :hx-select hx-select :href (get br "href") :hx-select hx-select
:bg-cls (get br "bg-cls") :name-cls (get br "name-cls") :bg-cls (get br "bg-cls") :name-cls (get br "name-cls")
:name (get br "name") :count (get br "count"))) :name (get br "name") :count (get br "count")))
items)))) items))))
;; Subcategory selector from data ;; Subcategory selector from data
(defcomp ~market-filter-subcategories-from-data (&key items hx-select all-href current-sub) (defcomp ~filters/subcategories-from-data (&key items hx-select all-href current-sub)
(~market-filter-subcategory-panel :items (~filters/subcategory-panel :items
(<> (<>
(~market-filter-subcategory-item (~filters/subcategory-item
:href all-href :hx-select hx-select :href all-href :hx-select hx-select
:active-cls (if (not current-sub) " bg-stone-200 font-medium" "") :active-cls (if (not current-sub) " bg-stone-200 font-medium" "")
:name "All") :name "All")
(map (lambda (sub) (map (lambda (sub)
(~market-filter-subcategory-item (~filters/subcategory-item
:href (get sub "href") :hx-select hx-select :href (get sub "href") :hx-select hx-select
:active-cls (get sub "active-cls") :name (get sub "name"))) :active-cls (get sub "active-cls") :name (get sub "name")))
items)))) items))))
;; Desktop filter panel from data ;; Desktop filter panel from data
(defcomp ~market-desktop-filter-from-data (&key search-sx category-label (defcomp ~filters/desktop-filter-from-data (&key search-sx category-label
sort-data like-data label-data sort-data like-data label-data
sticker-data brand-data sub-data hx-select) sticker-data brand-data sub-data hx-select)
(<> (<>
search-sx search-sx
(~market-desktop-category-summary :inner (~filters/desktop-category-summary :inner
(<> (<>
(~market-filter-category-label :label category-label) (~filters/category-label :label category-label)
(when sort-data (~market-filter-sort-from-data :items sort-data)) (when sort-data (~filters/sort-from-data :items sort-data))
(~market-filter-like-labels-nav :inner (~filters/like-labels-nav :inner
(<> (<>
(~market-filter-like-from-data (~filters/like-from-data
:href (get like-data "href") :hx-select hx-select :href (get like-data "href") :hx-select hx-select
:liked (get like-data "liked") :mobile false) :liked (get like-data "liked") :mobile false)
(when label-data (when label-data
(~market-filter-labels-from-data :items label-data :hx-select hx-select)))) (~filters/labels-from-data :items label-data :hx-select hx-select))))
(when sticker-data (when sticker-data
(~market-filter-stickers-from-data :items sticker-data :hx-select hx-select)) (~filters/stickers-from-data :items sticker-data :hx-select hx-select))
(when sub-data (when sub-data
(~market-filter-subcategories-from-data (~filters/subcategories-from-data
:items (get sub-data "items") :hx-select hx-select :items (get sub-data "items") :hx-select hx-select
:all-href (get sub-data "all-href") :all-href (get sub-data "all-href")
:current-sub (get sub-data "current-sub"))))) :current-sub (get sub-data "current-sub")))))
(~market-desktop-brand-summary (~filters/desktop-brand-summary
:inner (when brand-data :inner (when brand-data
(~market-filter-brands-from-data :items brand-data :hx-select hx-select))))) (~filters/brands-from-data :items brand-data :hx-select hx-select)))))
;; Mobile filter chips from active filter data ;; Mobile filter chips from active filter data
(defcomp ~market-mobile-chips-from-data (&key sort-chip liked-chip label-chips sticker-chips brand-chips) (defcomp ~filters/mobile-chips-from-data (&key sort-chip liked-chip label-chips sticker-chips brand-chips)
(~market-mobile-chips-row :inner (~filters/mobile-chips-row :inner
(<> (<>
(when sort-chip (when sort-chip
(~market-mobile-chip-sort :src (get sort-chip "src") :label (get sort-chip "label"))) (~filters/mobile-chip-sort :src (get sort-chip "src") :label (get sort-chip "label")))
(when liked-chip (when liked-chip
(~market-mobile-chip-liked :inner (~filters/mobile-chip-liked :inner
(<> (<>
(~market-mobile-chip-liked-icon) (~filters/mobile-chip-liked-icon)
(when (get liked-chip "count") (when (get liked-chip "count")
(~market-mobile-chip-count (~filters/mobile-chip-count
:cls (get liked-chip "count-cls") :count (get liked-chip "count")))))) :cls (get liked-chip "count-cls") :count (get liked-chip "count"))))))
(when label-chips (when label-chips
(~market-mobile-chip-list :items (~filters/mobile-chip-list :items
(<> (map (lambda (lc) (<> (map (lambda (lc)
(~market-mobile-chip-item :inner (~filters/mobile-chip-item :inner
(<> (<>
(~market-mobile-chip-image :src (get lc "src") :name (get lc "name")) (~filters/mobile-chip-image :src (get lc "src") :name (get lc "name"))
(when (get lc "count") (when (get lc "count")
(~market-mobile-chip-count :cls (get lc "count-cls") :count (get lc "count")))))) (~filters/mobile-chip-count :cls (get lc "count-cls") :count (get lc "count"))))))
label-chips)))) label-chips))))
(when sticker-chips (when sticker-chips
(~market-mobile-chip-list :items (~filters/mobile-chip-list :items
(<> (map (lambda (sc) (<> (map (lambda (sc)
(~market-mobile-chip-item :inner (~filters/mobile-chip-item :inner
(<> (<>
(~market-mobile-chip-image :src (get sc "src") :name (get sc "name")) (~filters/mobile-chip-image :src (get sc "src") :name (get sc "name"))
(when (get sc "count") (when (get sc "count")
(~market-mobile-chip-count :cls (get sc "count-cls") :count (get sc "count")))))) (~filters/mobile-chip-count :cls (get sc "count-cls") :count (get sc "count"))))))
sticker-chips)))) sticker-chips))))
(when brand-chips (when brand-chips
(~market-mobile-chip-brand-list :items (~filters/mobile-chip-brand-list :items
(<> (map (lambda (bc) (<> (map (lambda (bc)
(if (get bc "has-count") (if (get bc "has-count")
(~market-mobile-chip-brand :name (get bc "name") :count (get bc "count")) (~filters/mobile-chip-brand :name (get bc "name") :count (get bc "count"))
(~market-mobile-chip-brand-zero :name (get bc "name")))) (~filters/mobile-chip-brand-zero :name (get bc "name"))))
brand-chips))))))) brand-chips)))))))
;; Mobile filter content (expanded panel) from data ;; Mobile filter content (expanded panel) from data
(defcomp ~market-mobile-filter-content-from-data (&key sort-data like-data label-data (defcomp ~filters/mobile-filter-content-from-data (&key sort-data like-data label-data
sticker-data brand-data clear-href hx-select) sticker-data brand-data clear-href hx-select)
(<> (<>
(when sort-data (~market-filter-sort-from-data :items sort-data)) (when sort-data (~filters/sort-from-data :items sort-data))
(when clear-href (when clear-href
(~market-mobile-clear-filters :href clear-href :hx-select hx-select)) (~filters/mobile-clear-filters :href clear-href :hx-select hx-select))
(~market-mobile-like-labels-row :inner (~filters/mobile-like-labels-row :inner
(<> (<>
(~market-filter-like-from-data (~filters/like-from-data
:href (get like-data "href") :hx-select hx-select :href (get like-data "href") :hx-select hx-select
:liked (get like-data "liked") :mobile true) :liked (get like-data "liked") :mobile true)
(when label-data (when label-data
(~market-filter-labels-from-data :items label-data :hx-select hx-select)))) (~filters/labels-from-data :items label-data :hx-select hx-select))))
(when sticker-data (when sticker-data
(~market-filter-stickers-from-data :items sticker-data :hx-select hx-select)) (~filters/stickers-from-data :items sticker-data :hx-select hx-select))
(when brand-data (when brand-data
(~market-filter-brands-from-data :items brand-data :hx-select hx-select)))) (~filters/brands-from-data :items brand-data :hx-select hx-select))))
;; Composite mobile filter — eliminates SxExpr nesting in Python (M2) ;; Composite mobile filter — eliminates SxExpr nesting in Python (M2)
(defcomp ~market-mobile-filter-from-data (&key search-bar (defcomp ~filters/mobile-filter-from-data (&key search-bar
sort-chip liked-chip label-chips sticker-chips brand-chips sort-chip liked-chip label-chips sticker-chips brand-chips
sort-data like-data label-data sticker-data brand-data sort-data like-data label-data sticker-data brand-data
clear-href hx-select) clear-href hx-select)
(~market-mobile-filter-summary (~filters/mobile-filter-summary
:search-bar search-bar :search-bar search-bar
:chips (~market-mobile-chips-from-data :chips (~filters/mobile-chips-from-data
:sort-chip sort-chip :liked-chip liked-chip :sort-chip sort-chip :liked-chip liked-chip
:label-chips label-chips :sticker-chips sticker-chips :brand-chips brand-chips) :label-chips label-chips :sticker-chips sticker-chips :brand-chips brand-chips)
:filter (~market-mobile-filter-content-from-data :filter (~filters/mobile-filter-content-from-data
:sort-data sort-data :like-data like-data :sort-data sort-data :like-data like-data
:label-data label-data :sticker-data sticker-data :brand-data brand-data :label-data label-data :sticker-data sticker-data :brand-data brand-data
:clear-href clear-href :hx-select hx-select))) :clear-href clear-href :hx-select hx-select)))

View File

@@ -1,15 +1,15 @@
;; Market grid and layout components ;; Market grid and layout components
(defcomp ~market-markets-grid (&key cards) (defcomp ~grids/markets-grid (&key cards)
(<> (div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" cards) (div :class "pb-8"))) (<> (div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" cards) (div :class "pb-8")))
(defcomp ~market-product-grid (&key cards) (defcomp ~grids/product-grid (&key cards)
(<> (div :class "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3" cards) (div :class "pb-8"))) (<> (div :class "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3" cards) (div :class "pb-8")))
(defcomp ~market-admin-content-wrap (&key inner) (defcomp ~grids/admin-content-wrap (&key inner)
(div :id "main-panel" inner)) (div :id "main-panel" inner))
(defcomp ~market-like-toggle-button (&key colour action hx-headers label icon-cls) (defcomp ~grids/like-toggle-button (&key colour action hx-headers label icon-cls)
(button :class (str "flex items-center gap-1 " colour " hover:text-red-600 transition-colors w-[1em] h-[1em]") (button :class (str "flex items-center gap-1 " colour " hover:text-red-600 transition-colors w-[1em] h-[1em]")
:sx-post action :sx-target "this" :sx-swap "outerHTML" :sx-push-url "false" :sx-post action :sx-target "this" :sx-swap "outerHTML" :sx-push-url "false"
:sx-headers hx-headers :sx-headers hx-headers

View File

@@ -15,7 +15,7 @@
(sel-colours (or (jinja-global "select_colours") ""))) (sel-colours (or (jinja-global "select_colours") "")))
(<> (map (fn (m) (<> (map (fn (m)
(let ((href (app-url "market" (str "/" slug "/" (get m "slug") "/")))) (let ((href (app-url "market" (str "/" slug "/" (get m "slug") "/"))))
(~market-link-nav (~shared:navigation/market-link-nav
:href href :href href
:name (get m "name") :name (get m "name")
:nav-class nav-class :nav-class nav-class

View File

@@ -19,7 +19,7 @@
(if (get product "regular_price") (if (get product "regular_price")
(str (get product "regular_price")) (str (get product "regular_price"))
"")))) ""))))
(~link-card (~shared:fragments/link-card
:title (get product "title") :title (get product "title")
:image (get product "image") :image (get product "image")
:subtitle subtitle :subtitle subtitle
@@ -35,7 +35,7 @@
(if (get product "regular_price") (if (get product "regular_price")
(str (get product "regular_price")) (str (get product "regular_price"))
"")))) ""))))
(~link-card (~shared:fragments/link-card
:title (get product "title") :title (get product "title")
:image (get product "image") :image (get product "image")
:subtitle subtitle :subtitle subtitle

View File

@@ -1,15 +1,15 @@
;; Market header components ;; Market header components
(defcomp ~market-shop-label (&key title top-slug sub-div) (defcomp ~headers/shop-label (&key title top-slug sub-div)
(div :class "font-bold text-xl flex-shrink-0 flex gap-2 items-center" (div :class "font-bold text-xl flex-shrink-0 flex gap-2 items-center"
(div (i :class "fa fa-shop") " " title) (div (i :class "fa fa-shop") " " title)
(div :class "flex flex-col md:flex-row md:gap-2 text-xs" (div :class "flex flex-col md:flex-row md:gap-2 text-xs"
(div top-slug) (when sub-div (div sub-div))))) (div top-slug) (when sub-div (div sub-div)))))
(defcomp ~market-product-label (&key title) (defcomp ~headers/product-label (&key title)
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div title))) (<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div title)))
(defcomp ~market-admin-link (&key href hx-select) (defcomp ~headers/admin-link (&key href hx-select)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class "px-2 py-1 text-stone-500 hover:text-stone-700" :class "px-2 py-1 text-stone-500 hover:text-stone-700"
@@ -21,42 +21,42 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Desktop category nav from pre-computed category data ;; Desktop category nav from pre-computed category data
(defcomp ~market-desktop-nav-from-data (&key categories hx-select select-colours (defcomp ~headers/desktop-nav-from-data (&key categories hx-select select-colours
all-href all-active admin-href) all-href all-active admin-href)
(~market-desktop-category-nav (~navigation/desktop-category-nav
:links (<> :links (<>
(~market-category-link :href all-href :hx-select hx-select (~navigation/category-link :href all-href :hx-select hx-select
:active all-active :select-colours select-colours :label "All") :active all-active :select-colours select-colours :label "All")
(map (lambda (cat) (map (lambda (cat)
(~market-category-link (~navigation/category-link
:href (get cat "href") :hx-select hx-select :href (get cat "href") :hx-select hx-select
:active (get cat "active") :select-colours select-colours :active (get cat "active") :select-colours select-colours
:label (get cat "label"))) categories)) :label (get cat "label"))) categories))
:admin (when admin-href :admin (when admin-href
(~market-admin-link :href admin-href :hx-select hx-select)))) (~headers/admin-link :href admin-href :hx-select hx-select))))
;; Market-level header row from data ;; Market-level header row from data
(defcomp ~market-header-from-data (&key market-title top-slug sub-slug link-href (defcomp ~headers/from-data (&key market-title top-slug sub-slug link-href
categories hx-select select-colours categories hx-select select-colours
all-href all-active admin-href oob) all-href all-active admin-href oob)
(~menu-row-sx :id "market-row" :level 2 (~shared:layout/menu-row-sx :id "market-row" :level 2
:link-href link-href :link-href link-href
:link-label-content (~market-shop-label :link-label-content (~headers/shop-label
:title market-title :top-slug (or top-slug "") :sub-div sub-slug) :title market-title :top-slug (or top-slug "") :sub-div sub-slug)
:nav (~market-desktop-nav-from-data :nav (~headers/desktop-nav-from-data
:categories categories :hx-select hx-select :select-colours select-colours :categories categories :hx-select hx-select :select-colours select-colours
:all-href all-href :all-active all-active :admin-href admin-href) :all-href all-href :all-active all-active :admin-href admin-href)
:child-id "market-header-child" :child-id "market-header-child"
:oob oob)) :oob oob))
;; Product-level header row from data ;; Product-level header row from data
(defcomp ~market-product-header-from-data (&key title link-href hx-select (defcomp ~headers/product-header-from-data (&key title link-href hx-select
price-data admin-href oob) price-data admin-href oob)
(~menu-row-sx :id "product-row" :level 3 (~shared:layout/menu-row-sx :id "product-row" :level 3
:link-href link-href :link-href link-href
:link-label-content (~market-product-label :title title) :link-label-content (~headers/product-label :title title)
:nav (<> :nav (<>
(~market-prices-header-from-data (~prices/header-from-data
:cart-id (get price-data "cart-id") :cart-id (get price-data "cart-id")
:cart-action (get price-data "cart-action") :cart-action (get price-data "cart-action")
:csrf (get price-data "csrf") :csrf (get price-data "csrf")
@@ -66,13 +66,13 @@
:rp-val (get price-data "rp-val") :rp-str (get price-data "rp-str") :rp-val (get price-data "rp-val") :rp-str (get price-data "rp-str")
:rrp-str (get price-data "rrp-str")) :rrp-str (get price-data "rrp-str"))
(when admin-href (when admin-href
(~market-admin-link :href admin-href :hx-select hx-select))) (~headers/admin-link :href admin-href :hx-select hx-select)))
:child-id "product-header-child" :child-id "product-header-child"
:oob oob)) :oob oob))
;; Product admin header row from data ;; Product admin header row from data
(defcomp ~market-product-admin-header-from-data (&key link-href oob) (defcomp ~headers/product-admin-header-from-data (&key link-href oob)
(~menu-row-sx :id "product-admin-row" :level 4 (~shared:layout/menu-row-sx :id "product-admin-row" :level 4
:link-href link-href :link-label "admin!!" :icon "fa fa-cog" :link-href link-href :link-label "admin!!" :icon "fa fa-cog"
:child-id "product-admin-header-child" :oob oob)) :child-id "product-admin-header-child" :oob oob))

View File

@@ -9,13 +9,13 @@
"Market header row using (market-header-ctx)." "Market header row using (market-header-ctx)."
(quasiquote (quasiquote
(let ((__mctx (market-header-ctx))) (let ((__mctx (market-header-ctx)))
(~menu-row-sx :id "market-row" :level 2 (~shared:layout/menu-row-sx :id "market-row" :level 2
:link-href (get __mctx "link-href") :link-href (get __mctx "link-href")
:link-label-content (~market-shop-label :link-label-content (~headers/shop-label
:title (get __mctx "market-title") :title (get __mctx "market-title")
:top-slug (get __mctx "top-slug") :top-slug (get __mctx "top-slug")
:sub-div (get __mctx "sub-slug")) :sub-div (get __mctx "sub-slug"))
:nav (~market-desktop-nav-from-data :nav (~headers/desktop-nav-from-data
:categories (get __mctx "categories") :categories (get __mctx "categories")
:hx-select (get __mctx "hx-select") :hx-select (get __mctx "hx-select")
:select-colours (get __mctx "select-colours") :select-colours (get __mctx "select-colours")
@@ -29,44 +29,44 @@
;; OOB clear helpers ;; OOB clear helpers
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-clear-oob () (defcomp ~layouts/clear-oob ()
"Clear OOB divs for browse level." "Clear OOB divs for browse level."
(<> (<>
(~clear-oob-div :id "product-admin-row") (~shared:layout/clear-oob-div :id "product-admin-row")
(~clear-oob-div :id "product-admin-header-child") (~shared:layout/clear-oob-div :id "product-admin-header-child")
(~clear-oob-div :id "product-row") (~shared:layout/clear-oob-div :id "product-row")
(~clear-oob-div :id "product-header-child") (~shared:layout/clear-oob-div :id "product-header-child")
(~clear-oob-div :id "market-admin-row") (~shared:layout/clear-oob-div :id "market-admin-row")
(~clear-oob-div :id "market-admin-header-child") (~shared:layout/clear-oob-div :id "market-admin-header-child")
(~clear-oob-div :id "post-admin-row") (~shared:layout/clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child"))) (~shared:layout/clear-oob-div :id "post-admin-header-child")))
(defcomp ~market-clear-oob-admin () (defcomp ~layouts/clear-oob-admin ()
"Clear OOB divs for admin level." "Clear OOB divs for admin level."
(<> (<>
(~clear-oob-div :id "product-admin-row") (~shared:layout/clear-oob-div :id "product-admin-row")
(~clear-oob-div :id "product-admin-header-child") (~shared:layout/clear-oob-div :id "product-admin-header-child")
(~clear-oob-div :id "product-row") (~shared:layout/clear-oob-div :id "product-row")
(~clear-oob-div :id "product-header-child"))) (~shared:layout/clear-oob-div :id "product-header-child")))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Browse layout: root + post + market (self-contained) ;; Browse layout: root + post + market (self-contained)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-browse-layout-full () (defcomp ~layouts/browse-layout-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~market-header-auto nil))))) (~market-header-auto nil)))))
(defcomp ~market-browse-layout-oob () (defcomp ~layouts/browse-layout-oob ()
(<> (~post-header-auto true) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child" (~shared:layout/oob-header-sx :parent-id "post-header-child"
:row (~market-header-auto nil)) :row (~market-header-auto nil))
(~market-clear-oob) (~layouts/clear-oob)
(~root-header-auto true))) (~root-header-auto true)))
(defcomp ~market-browse-layout-mobile () (defcomp ~layouts/browse-layout-mobile ()
(let ((__mctx (market-header-ctx))) (let ((__mctx (market-header-ctx)))
(get __mctx "mobile-nav"))) (get __mctx "mobile-nav")))
@@ -74,18 +74,18 @@
;; Market admin layout: root + post + market + post-admin (self-contained) ;; Market admin layout: root + post + market + post-admin (self-contained)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-admin-layout-full (&key selected) (defcomp ~layouts/admin-layout-full (&key selected)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~post-header-auto nil) :inner (<> (~post-header-auto nil)
(~market-header-auto nil) (~market-header-auto nil)
(~post-admin-header-auto nil selected))))) (~post-admin-header-auto nil selected)))))
(defcomp ~market-admin-layout-oob (&key selected) (defcomp ~layouts/admin-layout-oob (&key selected)
(<> (~market-header-auto true) (<> (~market-header-auto true)
(~oob-header-sx :parent-id "market-header-child" (~shared:layout/oob-header-sx :parent-id "market-header-child"
:row (~post-admin-header-auto nil selected)) :row (~post-admin-header-auto nil selected))
(~market-clear-oob-admin) (~layouts/clear-oob-admin)
(~root-header-auto true))) (~root-header-auto true)))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -93,46 +93,46 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Product layout: root + post + market + product ;; Product layout: root + post + market + product
(defcomp ~market-product-layout-full (&key post-header market-header product-header) (defcomp ~layouts/product-layout-full (&key post-header market-header product-header)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header product-header)))) (~shared:layout/header-child-sx :inner (<> post-header market-header product-header))))
;; Product admin layout: root + post + market + product + admin ;; Product admin layout: root + post + market + product + admin
(defcomp ~market-product-admin-layout-full (&key post-header market-header product-header admin-header) (defcomp ~layouts/product-admin-layout-full (&key post-header market-header product-header admin-header)
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (<> post-header market-header product-header admin-header)))) (~shared:layout/header-child-sx :inner (<> post-header market-header product-header admin-header))))
;; OOB wrappers — compose headers + clear divs in sx (no Python concatenation) ;; OOB wrappers — compose headers + clear divs in sx (no Python concatenation)
(defcomp ~market-oob-wrap (&key parts) (defcomp ~layouts/oob-wrap (&key parts)
(<> parts)) (<> parts))
(defcomp ~market-clear-product-oob () (defcomp ~layouts/clear-product-oob ()
"Clear admin-level OOB divs when rendering product detail." "Clear admin-level OOB divs when rendering product detail."
(<> (<>
(~clear-oob-div :id "product-admin-row") (~shared:layout/clear-oob-div :id "product-admin-row")
(~clear-oob-div :id "product-admin-header-child") (~shared:layout/clear-oob-div :id "product-admin-header-child")
(~clear-oob-div :id "market-admin-row") (~shared:layout/clear-oob-div :id "market-admin-row")
(~clear-oob-div :id "market-admin-header-child") (~shared:layout/clear-oob-div :id "market-admin-header-child")
(~clear-oob-div :id "post-admin-row") (~shared:layout/clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child"))) (~shared:layout/clear-oob-div :id "post-admin-header-child")))
(defcomp ~market-clear-product-admin-oob () (defcomp ~layouts/clear-product-admin-oob ()
"Clear deeper OOB divs when rendering product admin." "Clear deeper OOB divs when rendering product admin."
(<> (<>
(~clear-oob-div :id "market-admin-row") (~shared:layout/clear-oob-div :id "market-admin-row")
(~clear-oob-div :id "market-admin-header-child") (~shared:layout/clear-oob-div :id "market-admin-header-child")
(~clear-oob-div :id "post-admin-row") (~shared:layout/clear-oob-div :id "post-admin-row")
(~clear-oob-div :id "post-admin-header-child"))) (~shared:layout/clear-oob-div :id "post-admin-header-child")))
(defcomp ~market-product-oob (&key market-header oob-header) (defcomp ~layouts/product-oob (&key market-header oob-header)
"Product detail OOB: market header + product header + clear deeper." "Product detail OOB: market header + product header + clear deeper."
(<> market-header oob-header (~market-clear-product-oob))) (<> market-header oob-header (~layouts/clear-product-oob)))
(defcomp ~market-product-admin-oob (&key product-header oob-header) (defcomp ~layouts/product-admin-oob (&key product-header oob-header)
"Product admin OOB: product header + admin header + clear deeper." "Product admin OOB: product header + admin header + clear deeper."
(<> product-header oob-header (~market-clear-product-admin-oob))) (<> product-header oob-header (~layouts/clear-product-admin-oob)))
;; Content wrappers ;; Content wrappers
(defcomp ~market-content-padded (&key content) (defcomp ~layouts/content-padded (&key content)
(<> content (div :class "pb-8"))) (<> content (div :class "pb-8")))

View File

@@ -1,21 +1,21 @@
;; Market meta/SEO components ;; Market meta/SEO components
(defcomp ~market-meta-title (&key (title :as string)) (defcomp ~meta/title (&key (title :as string))
(title title)) (title title))
(defcomp ~market-meta-description (&key (description :as string)) (defcomp ~meta/description (&key (description :as string))
(meta :name "description" :content description)) (meta :name "description" :content description))
(defcomp ~market-meta-canonical (&key (href :as string)) (defcomp ~meta/canonical (&key (href :as string))
(link :rel "canonical" :href href)) (link :rel "canonical" :href href))
(defcomp ~market-meta-og (&key (property :as string) (content :as string)) (defcomp ~meta/og (&key (property :as string) (content :as string))
(meta :property property :content content)) (meta :property property :content content))
(defcomp ~market-meta-twitter (&key (name :as string) (content :as string)) (defcomp ~meta/twitter (&key (name :as string) (content :as string))
(meta :name name :content content)) (meta :name name :content content))
(defcomp ~market-meta-jsonld (&key (json :as string)) (defcomp ~meta/jsonld (&key (json :as string))
(script :type "application/ld+json" (~rich-text :html json))) (script :type "application/ld+json" (~rich-text :html json)))
@@ -23,30 +23,30 @@
;; Composition: all product meta tags from data ;; Composition: all product meta tags from data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-product-meta-from-data (&key (title :as string) (description :as string) (canonical :as string?) (defcomp ~meta/product-meta-from-data (&key (title :as string) (description :as string) (canonical :as string?)
(image-url :as string?) (image-url :as string?)
(site-title :as string) (brand :as string?) (price :as string?) (price-currency :as string?) (site-title :as string) (brand :as string?) (price :as string?) (price-currency :as string?)
(jsonld-json :as string)) (jsonld-json :as string))
(<> (<>
(~market-meta-title :title title) (~meta/title :title title)
(~market-meta-description :description description) (~meta/description :description description)
(when canonical (~market-meta-canonical :href canonical)) (when canonical (~meta/canonical :href canonical))
;; OpenGraph ;; OpenGraph
(~market-meta-og :property "og:site_name" :content site-title) (~meta/og :property "og:site_name" :content site-title)
(~market-meta-og :property "og:type" :content "product") (~meta/og :property "og:type" :content "product")
(~market-meta-og :property "og:title" :content title) (~meta/og :property "og:title" :content title)
(~market-meta-og :property "og:description" :content description) (~meta/og :property "og:description" :content description)
(when canonical (~market-meta-og :property "og:url" :content canonical)) (when canonical (~meta/og :property "og:url" :content canonical))
(when image-url (~market-meta-og :property "og:image" :content image-url)) (when image-url (~meta/og :property "og:image" :content image-url))
(when (and price price-currency) (when (and price price-currency)
(<> (~market-meta-og :property "product:price:amount" :content price) (<> (~meta/og :property "product:price:amount" :content price)
(~market-meta-og :property "product:price:currency" :content price-currency))) (~meta/og :property "product:price:currency" :content price-currency)))
(when brand (~market-meta-og :property "product:brand" :content brand)) (when brand (~meta/og :property "product:brand" :content brand))
;; Twitter ;; Twitter
(~market-meta-twitter :name "twitter:card" (~meta/twitter :name "twitter:card"
:content (if image-url "summary_large_image" "summary")) :content (if image-url "summary_large_image" "summary"))
(~market-meta-twitter :name "twitter:title" :content title) (~meta/twitter :name "twitter:title" :content title)
(~market-meta-twitter :name "twitter:description" :content description) (~meta/twitter :name "twitter:description" :content description)
(when image-url (~market-meta-twitter :name "twitter:image" :content image-url)) (when image-url (~meta/twitter :name "twitter:image" :content image-url))
;; JSON-LD ;; JSON-LD
(~market-meta-jsonld :json jsonld-json))) (~meta/jsonld :json jsonld-json)))

View File

@@ -1,6 +1,6 @@
;; Market navigation components ;; Market navigation components
(defcomp ~market-category-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string) (label :as string)) (defcomp ~navigation/category-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -8,27 +8,27 @@
:class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours) :class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours)
label))) label)))
(defcomp ~market-desktop-category-nav (&key (links :as list) (admin :as list?)) (defcomp ~navigation/desktop-category-nav (&key (links :as list) (admin :as list?))
(nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center" (nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center"
links admin)) links admin))
(defcomp ~market-mobile-nav-wrapper (&key (items :as list)) (defcomp ~navigation/mobile-nav-wrapper (&key (items :as list))
(div :class "px-4 py-2" (div :class "divide-y" items))) (div :class "px-4 py-2" (div :class "divide-y" items)))
(defcomp ~market-mobile-all-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string)) (defcomp ~navigation/mobile-all-link (&key (href :as string) (hx-select :as string) (active :as boolean) (select-colours :as string))
(a :role "option" :href href :sx-get href :sx-target "#main-panel" (a :role "option" :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:aria-selected (if active "true" "false") :aria-selected (if active "true" "false")
:class (str "block rounded-lg px-3 py-3 text-base hover:bg-stone-50 " select-colours) :class (str "block rounded-lg px-3 py-3 text-base hover:bg-stone-50 " select-colours)
(div :class "prose prose-stone max-w-none" "All"))) (div :class "prose prose-stone max-w-none" "All")))
(defcomp ~market-mobile-chevron () (defcomp ~navigation/mobile-chevron ()
(svg :class "w-4 h-4 shrink-0 transition-transform group-open/cat:rotate-180" (svg :class "w-4 h-4 shrink-0 transition-transform group-open/cat:rotate-180"
:viewBox "0 0 20 20" :fill "currentColor" :viewBox "0 0 20 20" :fill "currentColor"
(path :fill-rule "evenodd" :clip-rule "evenodd" (path :fill-rule "evenodd" :clip-rule "evenodd"
:d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"))) :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z")))
(defcomp ~market-mobile-cat-summary (&key (bg-cls :as string) (href :as string) (hx-select :as string) (select-colours :as string) (cat-name :as string) (count-label :as string) (count-str :as string) (chevron :as list)) (defcomp ~navigation/mobile-cat-summary (&key (bg-cls :as string) (href :as string) (hx-select :as string) (select-colours :as string) (cat-name :as string) (count-label :as string) (count-str :as string) (chevron :as list))
(summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls) (summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls)
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
@@ -37,7 +37,7 @@
(div :aria-label count-label count-str)) (div :aria-label count-label count-str))
chevron)) chevron))
(defcomp ~market-mobile-sub-link (&key (select-colours :as string) (active :as boolean) (href :as string) (hx-select :as string) (label :as string) (count-label :as string) (count-str :as string)) (defcomp ~navigation/mobile-sub-link (&key (select-colours :as string) (active :as boolean) (href :as string) (hx-select :as string) (label :as string) (count-label :as string) (count-str :as string))
(a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2") (a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2")
:aria-selected (if active "true" "false") :aria-selected (if active "true" "false")
:href href :sx-get href :sx-target "#main-panel" :href href :sx-get href :sx-target "#main-panel"
@@ -45,20 +45,20 @@
(div label) (div label)
(div :aria-label count-label count-str))) (div :aria-label count-label count-str)))
(defcomp ~market-mobile-subs-panel (&key (links :as list)) (defcomp ~navigation/mobile-subs-panel (&key (links :as list))
(div :class "pb-3 pl-2" (div :class "pb-3 pl-2"
(div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100" (div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100"
(div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories" (div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories"
links)))) links))))
(defcomp ~market-mobile-view-all (&key (href :as string) (hx-select :as string)) (defcomp ~navigation/mobile-view-all (&key (href :as string) (hx-select :as string))
(div :class "pb-3 pl-2" (div :class "pb-3 pl-2"
(a :class "px-2 py-1 rounded hover:bg-stone-100 block" (a :class "px-2 py-1 rounded hover:bg-stone-100 block"
:href href :sx-get href :sx-target "#main-panel" :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
"View all"))) "View all")))
(defcomp ~market-mobile-cat-details (&key (open :as boolean) (summary :as list) (subs :as list)) (defcomp ~navigation/mobile-cat-details (&key (open :as boolean) (summary :as list) (subs :as list))
(details :class "group/cat py-1" :open open (details :class "group/cat py-1" :open open
summary subs)) summary subs))
@@ -67,25 +67,25 @@
;; Composition: mobile nav panel from pre-computed category data ;; Composition: mobile nav panel from pre-computed category data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-mobile-nav-from-data (&key (categories :as list) (all-href :as string) (all-active :as boolean) (hx-select :as string) (select-colours :as string)) (defcomp ~navigation/mobile-nav-from-data (&key (categories :as list) (all-href :as string) (all-active :as boolean) (hx-select :as string) (select-colours :as string))
(~market-mobile-nav-wrapper :items (~navigation/mobile-nav-wrapper :items
(<> (<>
(~market-mobile-all-link :href all-href :hx-select hx-select (~navigation/mobile-all-link :href all-href :hx-select hx-select
:active all-active :select-colours select-colours) :active all-active :select-colours select-colours)
(map (lambda (cat) (map (lambda (cat)
(~market-mobile-cat-details (~navigation/mobile-cat-details
:open (get cat "active") :open (get cat "active")
:summary (~market-mobile-cat-summary :summary (~navigation/mobile-cat-summary
:bg-cls (if (get cat "active") " bg-stone-900 text-white hover:bg-stone-900" "") :bg-cls (if (get cat "active") " bg-stone-900 text-white hover:bg-stone-900" "")
:href (get cat "href") :hx-select hx-select :href (get cat "href") :hx-select hx-select
:select-colours select-colours :cat-name (get cat "name") :select-colours select-colours :cat-name (get cat "name")
:count-label (str (get cat "count") " products") :count-label (str (get cat "count") " products")
:count-str (str (get cat "count")) :count-str (str (get cat "count"))
:chevron (~market-mobile-chevron)) :chevron (~navigation/mobile-chevron))
:subs (if (get cat "subs") :subs (if (get cat "subs")
(~market-mobile-subs-panel :links (~navigation/mobile-subs-panel :links
(<> (map (lambda (sub) (<> (map (lambda (sub)
(~market-mobile-sub-link (~navigation/mobile-sub-link
:select-colours select-colours :select-colours select-colours
:active (get sub "active") :active (get sub "active")
:href (get sub "href") :hx-select hx-select :href (get sub "href") :hx-select hx-select
@@ -93,5 +93,5 @@
:count-label (str (get sub "count") " products") :count-label (str (get sub "count") " products")
:count-str (str (get sub "count")))) :count-str (str (get sub "count"))))
(get cat "subs")))) (get cat "subs"))))
(~market-mobile-view-all :href (get cat "href") :hx-select hx-select)))) (~navigation/mobile-view-all :href (get cat "href") :hx-select hx-select))))
categories)))) categories))))

View File

@@ -1,36 +1,36 @@
;; Market price display components ;; Market price display components
(defcomp ~market-price-special (&key (price :as string)) (defcomp ~prices/special (&key (price :as string))
(div :class "text-lg font-semibold text-emerald-700" price)) (div :class "text-lg font-semibold text-emerald-700" price))
(defcomp ~market-price-regular-strike (&key (price :as string)) (defcomp ~prices/regular-strike (&key (price :as string))
(div :class "text-sm line-through text-stone-500" price)) (div :class "text-sm line-through text-stone-500" price))
(defcomp ~market-price-regular (&key (price :as string)) (defcomp ~prices/regular (&key (price :as string))
(div :class "mt-1 text-lg font-semibold" price)) (div :class "mt-1 text-lg font-semibold" price))
(defcomp ~market-price-line (&key (inner :as list)) (defcomp ~prices/line (&key (inner :as list))
(div :class "mt-1 flex items-baseline gap-2 justify-center" inner)) (div :class "mt-1 flex items-baseline gap-2 justify-center" inner))
(defcomp ~market-header-price-special-label () (defcomp ~prices/header-price-special-label ()
(div :class "text-md font-bold text-emerald-700" "Special price")) (div :class "text-md font-bold text-emerald-700" "Special price"))
(defcomp ~market-header-price-special (&key (price :as string)) (defcomp ~prices/header-price-special (&key (price :as string))
(div :class "text-xl font-semibold text-emerald-700" price)) (div :class "text-xl font-semibold text-emerald-700" price))
(defcomp ~market-header-price-strike (&key (price :as string)) (defcomp ~prices/header-price-strike (&key (price :as string))
(div :class "text-base text-md line-through text-stone-500" price)) (div :class "text-base text-md line-through text-stone-500" price))
(defcomp ~market-header-price-regular-label () (defcomp ~prices/header-price-regular-label ()
(div :class "hidden md:block text-xl font-bold" "Our price")) (div :class "hidden md:block text-xl font-bold" "Our price"))
(defcomp ~market-header-price-regular (&key (price :as string)) (defcomp ~prices/header-price-regular (&key (price :as string))
(div :class "text-xl font-semibold" price)) (div :class "text-xl font-semibold" price))
(defcomp ~market-header-rrp (&key (rrp :as string)) (defcomp ~prices/header-rrp (&key (rrp :as string))
(div :class "text-base text-stone-400" (span "rrp:") " " (span rrp))) (div :class "text-base text-stone-400" (span "rrp:") " " (span rrp)))
(defcomp ~market-prices-row (&key (inner :as list)) (defcomp ~prices/row (&key (inner :as list))
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner)) (div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner))
@@ -38,31 +38,31 @@
;; Composition: prices header + cart button from data ;; Composition: prices header + cart button from data
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~market-prices-header-from-data (&key (cart-id :as string) (cart-action :as string) (csrf :as string) (quantity :as number?) (defcomp ~prices/header-from-data (&key (cart-id :as string) (cart-action :as string) (csrf :as string) (quantity :as number?)
(cart-href :as string) (cart-href :as string)
(sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?) (rrp-str :as string?)) (sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?) (rrp-str :as string?))
(~market-prices-row :inner (~prices/row :inner
(<> (<>
(if quantity (if quantity
(~market-cart-add-quantity :cart-id cart-id :action cart-action :csrf csrf (~cart/add-quantity :cart-id cart-id :action cart-action :csrf csrf
:minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1)) :minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1))
:quantity (str quantity) :cart-href cart-href) :quantity (str quantity) :cart-href cart-href)
(~market-cart-add-empty :cart-id cart-id :action cart-action :csrf csrf)) (~cart/add-empty :cart-id cart-id :action cart-action :csrf csrf))
(when sp-val (when sp-val
(<> (~market-header-price-special-label) (<> (~prices/header-price-special-label)
(~market-header-price-special :price sp-str) (~prices/header-price-special :price sp-str)
(when rp-val (~market-header-price-strike :price rp-str)))) (when rp-val (~prices/header-price-strike :price rp-str))))
(when (and (not sp-val) rp-val) (when (and (not sp-val) rp-val)
(<> (~market-header-price-regular-label) (<> (~prices/header-price-regular-label)
(~market-header-price-regular :price rp-str))) (~prices/header-price-regular :price rp-str)))
(when rrp-str (~market-header-rrp :rrp rrp-str))))) (when rrp-str (~prices/header-rrp :rrp rrp-str)))))
;; Card price line from data (used in product cards) ;; Card price line from data (used in product cards)
(defcomp ~market-card-price-from-data (&key (sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?)) (defcomp ~prices/card-price-from-data (&key (sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?))
(~market-price-line :inner (~prices/line :inner
(<> (<>
(when sp-val (when sp-val
(<> (~market-price-special :price sp-str) (<> (~prices/special :price sp-str)
(when rp-val (~market-price-regular-strike :price rp-str)))) (when rp-val (~prices/regular-strike :price rp-str))))
(when (and (not sp-val) rp-val) (when (and (not sp-val) rp-val)
(~market-price-regular :price rp-str))))) (~prices/regular :price rp-str)))))

View File

@@ -13,10 +13,10 @@
:layout :root :layout :root
:data (all-markets-data) :data (all-markets-data)
:content (if no-markets :content (if no-markets
(~empty-state :icon "fa fa-store" :message "No markets available" (~shared:misc/empty-state :icon "fa fa-store" :message "No markets available"
:cls "px-3 py-12 text-center text-stone-400") :cls "px-3 py-12 text-center text-stone-400")
(~market-markets-grid (~grids/markets-grid
:cards (~market-cards-content :cards (~cards/content
:markets market-data :page market-page :markets market-data :page market-page
:has-more has-more :next-url next-url)))) :has-more has-more :next-url next-url))))
@@ -26,10 +26,10 @@
:layout :post :layout :post
:data (page-markets-data) :data (page-markets-data)
:content (if no-markets :content (if no-markets
(~empty-state :message "No markets for this page" (~shared:misc/empty-state :message "No markets for this page"
:cls "px-3 py-12 text-center text-stone-400") :cls "px-3 py-12 text-center text-stone-400")
(~market-markets-grid (~grids/markets-grid
:cards (~market-cards-content :cards (~cards/content
:markets market-data :page market-page :markets market-data :page market-page
:has-more has-more :next-url next-url)))) :has-more has-more :next-url next-url))))
@@ -38,24 +38,24 @@
:auth :admin :auth :admin
:layout (:post-admin :selected "markets") :layout (:post-admin :selected "markets")
:data (page-admin-data) :data (page-admin-data)
:content (~market-admin-content-wrap :content (~grids/admin-content-wrap
:inner (~crud-panel :inner (~shared:misc/crud-panel
:list-id "markets-list" :list-id "markets-list"
:form (when can-create :form (when can-create
(~crud-create-form (~shared:misc/crud-create-form
:create-url create-url :csrf csrf :create-url create-url :csrf csrf
:errors-id "market-create-errors" :list-id "markets-list" :errors-id "market-create-errors" :list-id "markets-list"
:placeholder "e.g. Suma, Craft Fair" :btn-label "Add market")) :placeholder "e.g. Suma, Craft Fair" :btn-label "Add market"))
:list (if admin-markets :list (if admin-markets
(<> (map (fn (m) (<> (map (fn (m)
(~crud-item (~shared:misc/crud-item
:href (get m "href") :name (get m "name") :slug (get m "slug") :href (get m "href") :name (get m "name") :slug (get m "slug")
:del-url (get m "del-url") :csrf-hdr (get m "csrf-hdr") :del-url (get m "del-url") :csrf-hdr (get m "csrf-hdr")
:list-id "markets-list" :list-id "markets-list"
:confirm-title "Delete market?" :confirm-title "Delete market?"
:confirm-text "Products will be hidden (soft delete)")) :confirm-text "Products will be hidden (soft delete)"))
admin-markets)) admin-markets))
(~empty-state (~shared:misc/empty-state
:message "No markets yet. Create one above." :message "No markets yet. Create one above."
:cls "text-gray-500 mt-4"))))) :cls "text-gray-500 mt-4")))))
@@ -64,7 +64,7 @@
:auth :public :auth :public
:layout :market :layout :market
:data (market-home-data) :data (market-home-data)
:content (~market-landing-from-data :content (~cards/landing-from-data
:excerpt excerpt :feature-image feature-image :html html)) :excerpt excerpt :feature-image feature-image :html html))
(defpage market-admin (defpage market-admin

View File

@@ -1,6 +1,6 @@
;; Checkout return page components ;; Checkout return page components
(defcomp ~checkout-return-header (&key (status :as string)) (defcomp ~checkout/return-header (&key (status :as string))
(header :class "mb-1 sm:mb-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4" (header :class "mb-1 sm:mb-2 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4"
(div :class "space-y-1" (div :class "space-y-1"
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight"
@@ -16,23 +16,23 @@
((= status "missing") "We couldn\u2019t find that order \u2013 it may have expired or never been created.") ((= status "missing") "We couldn\u2019t find that order \u2013 it may have expired or never been created.")
(t "We\u2019re still waiting for a final confirmation from SumUp.")))))) (t "We\u2019re still waiting for a final confirmation from SumUp."))))))
(defcomp ~checkout-return-missing () (defcomp ~checkout/return-missing ()
(div :class "max-w-full px-1 py-1" (div :class "max-w-full px-1 py-1"
(div :class "rounded-2xl border border-dashed border-rose-300 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-800" (div :class "rounded-2xl border border-dashed border-rose-300 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-800"
"We couldn\u2019t find that order. If you reached this page from an old link, please start a new order."))) "We couldn\u2019t find that order. If you reached this page from an old link, please start a new order.")))
(defcomp ~checkout-return-failed (&key (order-id :as string)) (defcomp ~checkout/return-failed (&key (order-id :as string))
(div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2" (div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2"
(p :class "font-medium" "Your payment was not completed.") (p :class "font-medium" "Your payment was not completed.")
(p "You can go back to your cart and try checkout again. If the problem persists, please contact us and mention order " (p "You can go back to your cart and try checkout again. If the problem persists, please contact us and mention order "
(span :class "font-mono" (str "#" order-id)) "."))) (span :class "font-mono" (str "#" order-id)) ".")))
(defcomp ~checkout-return-paid () (defcomp ~checkout/return-paid ()
(div :class "rounded-2xl border border-emerald-200 bg-emerald-50/80 p-4 sm:p-6 text-sm text-emerald-900 space-y-2" (div :class "rounded-2xl border border-emerald-200 bg-emerald-50/80 p-4 sm:p-6 text-sm text-emerald-900 space-y-2"
(p :class "font-medium" "All done!") (p :class "font-medium" "All done!")
(p "We\u2019ll start processing your order shortly."))) (p "We\u2019ll start processing your order shortly.")))
(defcomp ~checkout-return-ticket (&key (name :as string) (pill :as string) (state :as string) (type-name :as string?) (date-str :as string) (code :as string) (price :as string)) (defcomp ~checkout/return-ticket (&key (name :as string) (pill :as string) (state :as string) (type-name :as string?) (date-str :as string) (code :as string) (price :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div
(div :class "font-medium flex items-center gap-2" (div :class "font-medium flex items-center gap-2"
@@ -42,23 +42,23 @@
(div :class "text-xs text-stone-400 font-mono mt-0.5" code)) (div :class "text-xs text-stone-400 font-mono mt-0.5" code))
(div :class "ml-4 font-medium" price))) (div :class "ml-4 font-medium" price)))
(defcomp ~checkout-return-tickets (&key items) (defcomp ~checkout/return-tickets (&key items)
(section :class "mt-6 space-y-3" (section :class "mt-6 space-y-3"
(h2 :class "text-base sm:text-lg font-semibold" "Event tickets in this order") (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))) (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
;; Data-driven ticket items (replaces Python loop) ;; Data-driven ticket items (replaces Python loop)
(defcomp ~checkout-return-tickets-from-data (&key (tickets :as list)) (defcomp ~checkout/return-tickets-from-data (&key (tickets :as list))
(~checkout-return-tickets (~checkout/return-tickets
:items (<> (map (lambda (tk) :items (<> (map (lambda (tk)
(~checkout-return-ticket (~checkout/return-ticket
:name (get tk "name") :pill (get tk "pill") :name (get tk "name") :pill (get tk "pill")
:state (get tk "state") :type-name (get tk "type_name") :state (get tk "state") :type-name (get tk "type_name")
:date-str (get tk "date_str") :code (get tk "code") :date-str (get tk "date_str") :code (get tk "code")
:price (get tk "price"))) :price (get tk "price")))
(or tickets (list)))))) (or tickets (list))))))
(defcomp ~checkout-return-content (&key summary items calendar tickets status-message) (defcomp ~checkout/return-content (&key summary items calendar tickets status-message)
(div :class "max-w-full px-1 py-1" (div :class "max-w-full px-1 py-1"
(when summary (when summary
(div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2" summary)) (div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2" summary))

View File

@@ -4,6 +4,6 @@
;; Renders the "orders" link for the account dashboard nav. ;; Renders the "orders" link for the account dashboard nav.
(defhandler account-nav-item (&key) (defhandler account-nav-item (&key)
(~account-nav-item (~shared:fragments/account-nav-item
:href (app-url "orders" "/") :href (app-url "orders" "/")
:label "orders")) :label "orders"))

View File

@@ -3,40 +3,40 @@
;; --- orders layout: root + auth + orders rows --- ;; --- orders layout: root + auth + orders rows ---
(defcomp ~orders-layout-full (&key (list-url :as string)) (defcomp ~layouts/full (&key (list-url :as string))
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (<> (~auth-header-row-auto) :inner (<> (~auth-header-row-auto)
(~orders-header-row :list-url (or list-url "/")))))) (~shared:auth/orders-header-row :list-url (or list-url "/"))))))
(defcomp ~orders-layout-oob (&key (list-url :as string)) (defcomp ~layouts/oob (&key (list-url :as string))
(<> (~auth-header-row-auto true) (<> (~auth-header-row-auto true)
(~oob-header-sx (~shared:layout/oob-header-sx
:parent-id "auth-header-child" :parent-id "auth-header-child"
:row (~orders-header-row :list-url (or list-url "/"))) :row (~shared:auth/orders-header-row :list-url (or list-url "/")))
(~root-header-auto true))) (~root-header-auto true)))
(defcomp ~orders-layout-mobile () (defcomp ~layouts/mobile ()
(~root-mobile-auto)) (~root-mobile-auto))
;; --- order-detail layout: root + auth + orders + order rows --- ;; --- order-detail layout: root + auth + orders + order rows ---
(defcomp ~order-detail-layout-full (&key (list-url :as string) (detail-url :as string)) (defcomp ~layouts/order-detail-layout-full (&key (list-url :as string) (detail-url :as string))
(<> (~root-header-auto) (<> (~root-header-auto)
(~order-detail-header-stack (~shared:orders/detail-header-stack
:auth (~auth-header-row-auto) :auth (~auth-header-row-auto)
:orders (~orders-header-row :list-url (or list-url "/")) :orders (~shared:auth/orders-header-row :list-url (or list-url "/"))
:order (~menu-row-sx :id "order-row" :level 3 :colour "sky" :order (~shared:layout/menu-row-sx :id "order-row" :level 3 :colour "sky"
:link-href (or detail-url "/") :link-label "Order" :link-href (or detail-url "/") :link-label "Order"
:icon "fa fa-gbp")))) :icon "fa fa-gbp"))))
(defcomp ~order-detail-layout-oob (&key (detail-url :as string)) (defcomp ~layouts/order-detail-layout-oob (&key (detail-url :as string))
(<> (~oob-header-sx (<> (~shared:layout/oob-header-sx
:parent-id "orders-header-child" :parent-id "orders-header-child"
:row (~menu-row-sx :id "order-row" :level 3 :colour "sky" :row (~shared:layout/menu-row-sx :id "order-row" :level 3 :colour "sky"
:link-href (or detail-url "/") :link-label "Order" :link-href (or detail-url "/") :link-label "Order"
:icon "fa fa-gbp" :oob true)) :icon "fa fa-gbp" :oob true))
(~root-header-auto true))) (~root-header-auto true)))
(defcomp ~order-detail-layout-mobile () (defcomp ~layouts/order-detail-layout-mobile ()
(~root-mobile-auto)) (~root-mobile-auto))

View File

@@ -13,14 +13,14 @@
:page (or (request-arg "page" "1") "1")) :page (or (request-arg "page" "1") "1"))
:layout (:orders :layout (:orders
:list-url (str (route-prefix) (url-for "defpage_orders_list"))) :list-url (str (route-prefix) (url-for "defpage_orders_list")))
:filter (~order-list-header :filter (~shared:orders/list-header
:search-mobile (~search-mobile :search-mobile (~shared:controls/search-mobile
:current-local-href "/" :current-local-href "/"
:search (or search "") :search (or search "")
:search-count (or search-count "") :search-count (or search-count "")
:hx-select "#main-panel" :hx-select "#main-panel"
:search-headers-mobile "{\"X-Origin\":\"search-mobile\",\"X-Search\":\"true\"}")) :search-headers-mobile "{\"X-Origin\":\"search-mobile\",\"X-Search\":\"true\"}"))
:aside (~search-desktop :aside (~shared:controls/search-desktop
:current-local-href "/" :current-local-href "/"
:search (or search "") :search (or search "")
:search-count (or search-count "") :search-count (or search-count "")
@@ -30,7 +30,7 @@
(detail-url-raw (str pfx (url-for "defpage_order_detail" :order-id 0))) (detail-url-raw (str pfx (url-for "defpage_order_detail" :order-id 0)))
(detail-prefix (slice detail-url-raw 0 (- (len detail-url-raw) 2))) (detail-prefix (slice detail-url-raw 0 (- (len detail-url-raw) 2)))
(rows-url (str pfx (url-for "orders.orders_rows")))) (rows-url (str pfx (url-for "orders.orders_rows"))))
(~orders-list-content (~shared:orders/list-content
:orders orders :orders orders
:page page :page page
:total-pages total-pages :total-pages total-pages
@@ -49,12 +49,12 @@
:list-url (str (route-prefix) (url-for "defpage_orders_list")) :list-url (str (route-prefix) (url-for "defpage_orders_list"))
:detail-url (str (route-prefix) (url-for "defpage_order_detail" :order-id order-id))) :detail-url (str (route-prefix) (url-for "defpage_order_detail" :order-id order-id)))
:filter (let* ((pfx (route-prefix))) :filter (let* ((pfx (route-prefix)))
(~order-detail-filter-content (~shared:orders/detail-filter-content
:order order :order order
:list-url (str pfx (url-for "defpage_orders_list")) :list-url (str pfx (url-for "defpage_orders_list"))
:recheck-url (str pfx (url-for "orders.order.order_recheck" :order-id order-id)) :recheck-url (str pfx (url-for "orders.order.order_recheck" :order-id order-id))
:pay-url (str pfx (url-for "orders.order.order_pay" :order-id order-id)) :pay-url (str pfx (url-for "orders.order.order_pay" :order-id order-id))
:csrf (csrf-token))) :csrf (csrf-token)))
:content (~order-detail-content :content (~shared:orders/detail-content
:order order :order order
:calendar-entries calendar-entries)) :calendar-entries calendar-entries))

View File

@@ -39,7 +39,7 @@
(href (if svc-name (href (if svc-name
(app-url svc-name path) (app-url svc-name path)
path))) path)))
(~relation-nav (~shared:navigation/relation-nav
:href href :href href
:name (or (get child "label") "") :name (or (get child "label") "")
:icon (or (get defn "nav_icon") "") :icon (or (get defn "nav_icon") "")

View File

@@ -62,7 +62,7 @@ def _sx_error_page(errnum: str, message: str, image: str | None = None) -> str:
from shared.sx.page import render_page from shared.sx.page import render_page
return render_page( return render_page(
'(~error-page :title title :message message :image image :asset-url "/static")', '(~shared:pages/error-page :title title :message message :image image :asset-url "/static")',
title=f"{errnum} Error", title=f"{errnum} Error",
message=message, message=message,
image=image, image=image,

View File

@@ -170,7 +170,7 @@ def compute_all_deps(env: dict[str, Any]) -> None:
def scan_components_from_sx(source: str) -> set[str]: def scan_components_from_sx(source: str) -> set[str]:
"""Extract component names referenced in SX source text. """Extract component names referenced in SX source text.
Returns names with ~ prefix, e.g. {"~card", "~nav-link"}. Returns names with ~ prefix, e.g. {"~card", "~shared:layout/nav-link"}.
""" """
if _use_ref(): if _use_ref():
from .ref.sx_ref import scan_components_from_source as _ref_sc from .ref.sx_ref import scan_components_from_source as _ref_sc

View File

@@ -90,7 +90,7 @@ def mobile_menu_sx(*sections: str) -> SxExpr:
async def mobile_root_nav_sx(ctx: dict) -> str: async def mobile_root_nav_sx(ctx: dict) -> str:
"""Root-level mobile nav via ~mobile-root-nav component.""" """Root-level mobile nav via ~shared:layout/mobile-root-nav component."""
nav_tree = ctx.get("nav_tree") or "" nav_tree = ctx.get("nav_tree") or ""
auth_menu = ctx.get("auth_menu") or "" auth_menu = ctx.get("auth_menu") or ""
if not nav_tree and not auth_menu: if not nav_tree and not auth_menu:
@@ -263,7 +263,7 @@ async def oob_header_sx(parent_id: str, child_id: str, row_sx: str) -> str:
"""Wrap a header row sx in an OOB swap. """Wrap a header row sx in an OOB swap.
child_id is accepted for call-site compatibility but no longer used — child_id is accepted for call-site compatibility but no longer used —
the child placeholder is created by ~menu-row-sx itself. the child placeholder is created by ~shared:layout/menu-row-sx itself.
""" """
return await _render_to_sx("oob-header-sx", return await _render_to_sx("oob-header-sx",
parent_id=parent_id, parent_id=parent_id,
@@ -636,7 +636,7 @@ def sx_response(source: str, status: int = 200,
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Sx wire-format full page shell # Sx wire-format full page shell
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# The page shell is defined as ~sx-page-shell in shared/sx/templates/shell.sx # The page shell is defined as ~shared:shell/sx-page-shell in shared/sx/templates/shell.sx
# and rendered via render_to_html. No HTML string templates in Python. # and rendered via render_to_html. No HTML string templates in Python.
@@ -780,7 +780,7 @@ async def sx_page(ctx: dict, page_sx: str, *,
renders everything client-side. CSS rules are scanned from the sx renders everything client-side. CSS rules are scanned from the sx
source and component defs, then injected as a <style> block. source and component defs, then injected as a <style> block.
The shell is rendered from the ~sx-page-shell SX component The shell is rendered from the ~shared:shell/sx-page-shell SX component
(shared/sx/templates/shell.sx). (shared/sx/templates/shell.sx).
""" """
from .jinja_bridge import components_for_page, css_classes_for_page from .jinja_bridge import components_for_page, css_classes_for_page

View File

@@ -6,7 +6,7 @@ can coexist during incremental migration:
**Jinja → s-expression** (use s-expression components inside Jinja templates):: **Jinja → s-expression** (use s-expression components inside Jinja templates)::
{{ sx('(~link-card :slug "apple" :title "Apple")') | safe }} {{ sx('(~shared:fragments/link-card :slug "apple" :title "Apple")') | safe }}
**S-expression → Jinja** (embed Jinja output inside s-expressions):: **S-expression → Jinja** (embed Jinja output inside s-expressions)::
@@ -220,7 +220,7 @@ def register_components(sx_source: str) -> None:
Typically called at app startup:: Typically called at app startup::
register_components(''' register_components('''
(defcomp ~link-card (&key link title image icon) (defcomp ~shared:fragments/link-card (&key link title image icon)
(a :href link :class "block rounded ..." (a :href link :class "block rounded ..."
(div :class "flex ..." (div :class "flex ..."
(if image (if image
@@ -269,7 +269,7 @@ def sx(source: str, **kwargs: Any) -> str:
Keyword arguments are merged into the evaluation environment, Keyword arguments are merged into the evaluation environment,
so Jinja context variables can be passed through:: so Jinja context variables can be passed through::
{{ sx('(~link-card :title title :slug slug)', {{ sx('(~shared:fragments/link-card :title title :slug slug)',
title=post.title, slug=post.slug) | safe }} title=post.title, slug=post.slug) | safe }}
This is a synchronous function — suitable for Jinja globals. This is a synchronous function — suitable for Jinja globals.

View File

@@ -15,7 +15,7 @@ Usage::
# Error pages (no context needed) # Error pages (no context needed)
html = render_page( html = render_page(
'(~error-page :title "Not Found" :message "NOT FOUND" :image img :asset-url aurl)', '(~shared:pages/error-page :title "Not Found" :message "NOT FOUND" :image img :asset-url aurl)',
image="/static/errors/404.gif", image="/static/errors/404.gif",
asset_url="/static", asset_url="/static",
) )

View File

@@ -179,9 +179,9 @@ async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
def _replace_suspense_sexp(sx: str, stream_id: str, replacement: str) -> str: def _replace_suspense_sexp(sx: str, stream_id: str, replacement: str) -> str:
"""Replace a rendered ~suspense div in SX source with replacement content. """Replace a rendered ~shared:pages/suspense div in SX source with replacement content.
After _eval_slot, ~suspense expands to: After _eval_slot, ~shared:pages/suspense expands to:
(div :id "sx-suspense-{id}" :data-suspense "{id}" :style "display:contents" ...) (div :id "sx-suspense-{id}" :data-suspense "{id}" :style "display:contents" ...)
This finds the balanced s-expression containing :data-suspense "{id}" and This finds the balanced s-expression containing :data-suspense "{id}" and
replaces it with the given replacement string. replaces it with the given replacement string.
@@ -277,7 +277,7 @@ async def execute_page(
if page_def.shell_expr is not None: if page_def.shell_expr is not None:
shell_sx = await _eval_slot(page_def.shell_expr, env, ctx) shell_sx = await _eval_slot(page_def.shell_expr, env, ctx)
# Replace each rendered suspense div with resolved content. # Replace each rendered suspense div with resolved content.
# _eval_slot expands ~suspense into: # _eval_slot expands ~shared:pages/suspense into:
# (div :id "sx-suspense-X" :data-suspense "X" :style "display:contents" ...) # (div :id "sx-suspense-X" :data-suspense "X" :style "display:contents" ...)
# We find the balanced s-expr containing :data-suspense "X" and replace it. # We find the balanced s-expr containing :data-suspense "X" and replace it.
for stream_id, chunk_sx in chunks: for stream_id, chunk_sx in chunks:
@@ -534,16 +534,16 @@ async def execute_page_streaming(
# Render to HTML so [data-suspense] elements are real DOM immediately. # Render to HTML so [data-suspense] elements are real DOM immediately.
# No dependency on sx-browser.js boot timing for the initial shell. # No dependency on sx-browser.js boot timing for the initial shell.
suspense_header_sx = f'(~suspense :id "stream-headers" :fallback {header_fallback})' suspense_header_sx = f'(~shared:pages/suspense :id "stream-headers" :fallback {header_fallback})'
# When :shell is provided, it renders directly as the content slot # When :shell is provided, it renders directly as the content slot
# (it contains its own ~suspense for the data-dependent part). # (it contains its own ~shared:pages/suspense for the data-dependent part).
# Otherwise, wrap the entire :content in a single suspense. # Otherwise, wrap the entire :content in a single suspense.
if page_def.shell_expr is not None: if page_def.shell_expr is not None:
shell_content_sx = await _eval_slot(page_def.shell_expr, env, ctx) shell_content_sx = await _eval_slot(page_def.shell_expr, env, ctx)
suspense_content_sx = shell_content_sx suspense_content_sx = shell_content_sx
else: else:
suspense_content_sx = f'(~suspense :id "stream-content" :fallback {fallback_sx})' suspense_content_sx = f'(~shared:pages/suspense :id "stream-content" :fallback {fallback_sx})'
initial_page_html = await _helpers_render_to_html("app-body", initial_page_html = await _helpers_render_to_html("app-body",
header_rows=SxExpr(suspense_header_sx), header_rows=SxExpr(suspense_header_sx),
@@ -551,7 +551,7 @@ async def execute_page_streaming(
) )
# Include layout component refs + page content so the scan picks up # Include layout component refs + page content so the scan picks up
# their transitive deps (e.g. ~cart-mini, ~auth-menu in headers). # their transitive deps (e.g. ~shared:fragments/cart-mini, ~auth-menu in headers).
layout_refs = "" layout_refs = ""
if layout is not None and hasattr(layout, "component_names"): if layout is not None and hasattr(layout, "component_names"):
layout_refs = " ".join(f"({n})" for n in layout.component_names) layout_refs = " ".join(f"({n})" for n in layout.component_names)
@@ -561,14 +561,14 @@ async def execute_page_streaming(
shell_ref = "" shell_ref = ""
if page_def.shell_expr is not None: if page_def.shell_expr is not None:
shell_ref = sx_serialize(page_def.shell_expr) shell_ref = sx_serialize(page_def.shell_expr)
page_sx_for_scan = f'(<> {layout_refs} {content_ref} {shell_ref} (~app-body :header-rows {suspense_header_sx} :content {suspense_content_sx}))' page_sx_for_scan = f'(<> {layout_refs} {content_ref} {shell_ref} (~shared:layout/app-body :header-rows {suspense_header_sx} :content {suspense_content_sx}))'
shell, tail = sx_page_streaming_parts( shell, tail = sx_page_streaming_parts(
tctx, initial_page_html, page_sx=page_sx_for_scan, tctx, initial_page_html, page_sx=page_sx_for_scan,
) )
# Capture component env + extras scanner while we still have context. # Capture component env + extras scanner while we still have context.
# Resolved SX may reference components not in the initial scan # Resolved SX may reference components not in the initial scan
# (e.g. ~cart-mini from IO-generated header content). # (e.g. ~shared:fragments/cart-mini from IO-generated header content).
from .jinja_bridge import components_for_page as _comp_scan from .jinja_bridge import components_for_page as _comp_scan
from quart import current_app as _ca from quart import current_app as _ca
_service = _ca.name _service = _ca.name

View File

@@ -6,26 +6,26 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Auth section nav items (newsletters link + account_nav slot) ;; Auth section nav items (newsletters link + account_nav slot)
(defcomp ~auth-nav-items (&key (account-url :as string?) (select-colours :as string?) account-nav) (defcomp ~shared:auth/nav-items (&key (account-url :as string?) (select-colours :as string?) account-nav)
(<> (<>
(~nav-link :href (str (or account-url "") "/newsletters/") (~shared:layout/nav-link :href (str (or account-url "") "/newsletters/")
:label "newsletters" :label "newsletters"
:select-colours (or select-colours "")) :select-colours (or select-colours ""))
(when account-nav account-nav))) (when account-nav account-nav)))
;; Auth header row — wraps ~menu-row-sx for account section ;; Auth header row — wraps ~shared:layout/menu-row-sx for account section
(defcomp ~auth-header-row (&key (account-url :as string?) (select-colours :as string?) account-nav (oob :as boolean?)) (defcomp ~shared:auth/header-row (&key (account-url :as string?) (select-colours :as string?) account-nav (oob :as boolean?))
(~menu-row-sx :id "auth-row" :level 1 :colour "sky" (~shared:layout/menu-row-sx :id "auth-row" :level 1 :colour "sky"
:link-href (str (or account-url "") "/") :link-href (str (or account-url "") "/")
:link-label "account" :icon "fa-solid fa-user" :link-label "account" :icon "fa-solid fa-user"
:nav (~auth-nav-items :account-url account-url :nav (~shared:auth/nav-items :account-url account-url
:select-colours select-colours :select-colours select-colours
:account-nav account-nav) :account-nav account-nav)
:child-id "auth-header-child" :oob oob)) :child-id "auth-header-child" :oob oob))
;; Auth header row without nav (for cart service) ;; Auth header row without nav (for cart service)
(defcomp ~auth-header-row-simple (&key (account-url :as string?) (oob :as boolean?)) (defcomp ~shared:auth/header-row-simple (&key (account-url :as string?) (oob :as boolean?))
(~menu-row-sx :id "auth-row" :level 1 :colour "sky" (~shared:layout/menu-row-sx :id "auth-row" :level 1 :colour "sky"
:link-href (str (or account-url "") "/") :link-href (str (or account-url "") "/")
:link-label "account" :icon "fa-solid fa-user" :link-label "account" :icon "fa-solid fa-user"
:child-id "auth-header-child" :oob oob)) :child-id "auth-header-child" :oob oob))
@@ -34,26 +34,26 @@
;; Expands inline (defmacro) so IO calls resolve in _aser mode. ;; Expands inline (defmacro) so IO calls resolve in _aser mode.
(defmacro ~auth-header-row-auto (oob) (defmacro ~auth-header-row-auto (oob)
(quasiquote (quasiquote
(~auth-header-row :account-url (app-url "account" "") (~shared:auth/header-row :account-url (app-url "account" "")
:select-colours (select-colours) :select-colours (select-colours)
:account-nav (account-nav-ctx) :account-nav (account-nav-ctx)
:oob (unquote oob)))) :oob (unquote oob))))
(defmacro ~auth-header-row-simple-auto (oob) (defmacro ~auth-header-row-simple-auto (oob)
(quasiquote (quasiquote
(~auth-header-row-simple :account-url (app-url "account" "") (~shared:auth/header-row-simple :account-url (app-url "account" "")
:oob (unquote oob)))) :oob (unquote oob))))
;; Auto-fetching auth nav items — for mobile menus ;; Auto-fetching auth nav items — for mobile menus
(defmacro ~auth-nav-items-auto () (defmacro ~auth-nav-items-auto ()
(quasiquote (quasiquote
(~auth-nav-items :account-url (app-url "account" "") (~shared:auth/nav-items :account-url (app-url "account" "")
:select-colours (select-colours) :select-colours (select-colours)
:account-nav (account-nav-ctx)))) :account-nav (account-nav-ctx))))
;; Orders header row ;; Orders header row
(defcomp ~orders-header-row (&key (list-url :as string)) (defcomp ~shared:auth/orders-header-row (&key (list-url :as string))
(~menu-row-sx :id "orders-row" :level 2 :colour "sky" (~shared:layout/menu-row-sx :id "orders-row" :level 2 :colour "sky"
:link-href list-url :link-label "Orders" :icon "fa fa-gbp" :link-href list-url :link-label "Orders" :icon "fa fa-gbp"
:child-id "orders-header-child")) :child-id "orders-header-child"))
@@ -61,12 +61,12 @@
;; Auth forms — login flow, check email ;; Auth forms — login flow, check email
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~auth-error-banner (&key (error :as string?)) (defcomp ~shared:auth/error-banner (&key (error :as string?))
(when error (when error
(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4"
error))) error)))
(defcomp ~auth-login-form (&key error (action :as string) (csrf-token :as string) (email :as string?)) (defcomp ~shared:auth/login-form (&key error (action :as string) (csrf-token :as string) (email :as string?))
(div :class "py-8 max-w-md mx-auto" (div :class "py-8 max-w-md mx-auto"
(h1 :class "text-2xl font-bold mb-6" "Sign in") (h1 :class "text-2xl font-bold mb-6" "Sign in")
error error
@@ -80,12 +80,12 @@
:class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition" :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
"Send magic link")))) "Send magic link"))))
(defcomp ~auth-check-email-error (&key (error :as string?)) (defcomp ~shared:auth/check-email-error (&key (error :as string?))
(when error (when error
(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4" (div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4"
error))) error)))
(defcomp ~auth-check-email (&key (email :as string) error) (defcomp ~shared:auth/check-email (&key (email :as string) error)
(div :class "py-8 max-w-md mx-auto text-center" (div :class "py-8 max-w-md mx-auto text-center"
(h1 :class "text-2xl font-bold mb-4" "Check your email") (h1 :class "text-2xl font-bold mb-4" "Check your email")
(p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong email) ".") (p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong email) ".")

View File

@@ -1,4 +1,4 @@
(defcomp ~post-card (&key (title :as string) (slug :as string) (href :as string) (feature-image :as string?) (defcomp ~shared:cards/post-card (&key (title :as string) (slug :as string) (href :as string) (feature-image :as string?)
(excerpt :as string?) (status :as string?) (published-at :as string?) (updated-at :as string?) (excerpt :as string?) (status :as string?) (published-at :as string?) (updated-at :as string?)
(publish-requested :as boolean?) (hx-select :as string?) like widgets at-bar) (publish-requested :as boolean?) (hx-select :as string?) like widgets at-bar)
(article :class "border-b pb-6 last:border-b-0 relative" (article :class "border-b pb-6 last:border-b-0 relative"
@@ -31,13 +31,13 @@
(when widgets widgets) (when widgets widgets)
(when at-bar at-bar))) (when at-bar at-bar)))
(defcomp ~order-summary-card (&key (order-id :as string) (created-at :as string?) (description :as string?) (defcomp ~shared:cards/order-summary-card (&key (order-id :as string) (created-at :as string?) (description :as string?)
(status :as string?) (currency :as string?) (total-amount :as string?)) (status :as string?) (currency :as string?) (total-amount :as string?))
(div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800" (div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2 text-xs sm:text-sm text-stone-800"
(p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id))) (p (span :class "font-medium" "Order ID:") " " (span :class "font-mono" (str "#" order-id)))
(p (span :class "font-medium" "Created:") " " (or created-at "\u2014")) (p (span :class "font-medium" "Created:") " " (or created-at "\u2014"))
(p (span :class "font-medium" "Description:") " " (or description "\u2013")) (p (span :class "font-medium" "Description:") " " (or description "\u2013"))
(p (span :class "font-medium" "Status:") " " (~status-pill :status (or status "pending") :size "[11px]")) (p (span :class "font-medium" "Status:") " " (~shared:controls/status-pill :status (or status "pending") :size "[11px]"))
(p (span :class "font-medium" "Currency:") " " (or currency "GBP")) (p (span :class "font-medium" "Currency:") " " (or currency "GBP"))
(p (span :class "font-medium" "Total:") " " (p (span :class "font-medium" "Total:") " "
(if total-amount (if total-amount

View File

@@ -1,4 +1,4 @@
(defcomp ~search-mobile (&key (current-local-href :as string) (search :as string?) (search-count :as number?) (defcomp ~shared:controls/search-mobile (&key (current-local-href :as string) (search :as string?) (search-count :as number?)
(hx-select :as string?) (search-headers-mobile :as string?)) (hx-select :as string?) (search-headers-mobile :as string?))
(div :id "search-mobile-wrapper" (div :id "search-mobile-wrapper"
:class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2" :class "flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
@@ -21,7 +21,7 @@
:class (if (not search-count) "text-xl text-red-500" "") :class (if (not search-count) "text-xl text-red-500" "")
(when search (str search-count))))) (when search (str search-count)))))
(defcomp ~search-desktop (&key (current-local-href :as string) (search :as string?) (search-count :as number?) (defcomp ~shared:controls/search-desktop (&key (current-local-href :as string) (search :as string?) (search-count :as number?)
(hx-select :as string?) (search-headers-desktop :as string?)) (hx-select :as string?) (search-headers-desktop :as string?))
(div :id "search-desktop-wrapper" (div :id "search-desktop-wrapper"
:class "flex flex-row gap-2 items-center" :class "flex flex-row gap-2 items-center"
@@ -44,7 +44,7 @@
:class (if (not search-count) "text-xl text-red-500" "") :class (if (not search-count) "text-xl text-red-500" "")
(when search (str search-count))))) (when search (str search-count)))))
(defcomp ~mobile-filter (&key filter-summary action-buttons filter-details) (defcomp ~shared:controls/mobile-filter (&key filter-summary action-buttons filter-details)
(details :class "group/filter p-2 md:hidden" :data-toggle-group "mobile-panels" (details :class "group/filter p-2 md:hidden" :data-toggle-group "mobile-panels"
(summary :class "bg-white/90" (summary :class "bg-white/90"
(div :class "flex flex-row items-start" (div :class "flex flex-row items-start"
@@ -64,7 +64,7 @@
(div :id "filter-details-mobile" :style "display:contents" (div :id "filter-details-mobile" :style "display:contents"
(when filter-details filter-details)))) (when filter-details filter-details))))
(defcomp ~infinite-scroll (&key (url :as string) (page :as number) (total-pages :as number) (defcomp ~shared:controls/infinite-scroll (&key (url :as string) (page :as number) (total-pages :as number)
(id-prefix :as string) (colspan :as number)) (id-prefix :as string) (colspan :as number))
(if (< page total-pages) (if (< page total-pages)
(tr :id (str id-prefix "-sentinel-" page) (tr :id (str id-prefix "-sentinel-" page)
@@ -85,7 +85,7 @@
(tr (td :colspan colspan :class "px-3 py-4 text-center text-xs text-stone-400" (tr (td :colspan colspan :class "px-3 py-4 text-center text-xs text-stone-400"
"End of results")))) "End of results"))))
(defcomp ~status-pill (&key (status :as string?) (size :as string?)) (defcomp ~shared:controls/status-pill (&key (status :as string?) (size :as string?))
(let* ((s (or status "pending")) (let* ((s (or status "pending"))
(lower (lower s)) (lower (lower s))
(sz (or size "xs")) (sz (or size "xs"))

View File

@@ -1,4 +1,4 @@
(defcomp ~link-card (&key (link :as string) (title :as string) (image :as string?) (icon :as string?) (defcomp ~shared:fragments/link-card (&key (link :as string) (title :as string) (image :as string?) (icon :as string?)
(subtitle :as string?) (detail :as string?) (data-app :as string?)) (subtitle :as string?) (detail :as string?) (data-app :as string?))
(a :href link (a :href link
:class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline" :class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline"
@@ -17,7 +17,7 @@
(when detail (when detail
(div :class "text-xs text-stone-400 mt-1" detail)))))) (div :class "text-xs text-stone-400 mt-1" detail))))))
(defcomp ~cart-mini (&key (cart-count :as number) (blog-url :as string) (cart-url :as string) (oob :as string?)) (defcomp ~shared:fragments/cart-mini (&key (cart-count :as number) (blog-url :as string) (cart-url :as string) (oob :as string?))
(div :id "cart-mini" (div :id "cart-mini"
:sx-swap-oob oob :sx-swap-oob oob
(if (= cart-count 0) (if (= cart-count 0)
@@ -34,7 +34,7 @@
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5" (span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
cart-count))))) cart-count)))))
(defcomp ~auth-menu (&key (user-email :as string?) (account-url :as string)) (defcomp ~shared:fragments/auth-menu (&key (user-email :as string?) (account-url :as string))
(<> (<>
(span :id "auth-menu-desktop" :class "hidden md:inline-flex" (span :id "auth-menu-desktop" :class "hidden md:inline-flex"
(if user-email (if user-email
@@ -66,7 +66,7 @@
(i :class "fa-solid fa-key") (i :class "fa-solid fa-key")
(span "sign in or register")))))) (span "sign in or register"))))))
(defcomp ~account-nav-item (&key (href :as string) (label :as string)) (defcomp ~shared:fragments/account-nav-item (&key (href :as string) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3" :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"

View File

@@ -1,4 +1,4 @@
(defcomp ~app-body (&key header-rows filter aside menu content) (defcomp ~shared:layout/app-body (&key header-rows filter aside menu content)
(div :class "max-w-screen-2xl mx-auto py-1 px-1" (div :class "max-w-screen-2xl mx-auto py-1 px-1"
(when header-rows (when header-rows
(div :class "w-full" (div :class "w-full"
@@ -24,7 +24,7 @@
(when content content) (when content content)
(div :class "pb-8"))))))) (div :class "pb-8")))))))
(defcomp ~oob-sx (&key oobs filter aside menu content) (defcomp ~shared:layout/oob-sx (&key oobs filter aside menu content)
(<> (<>
(when oobs oobs) (when oobs oobs)
(div :id "filter" :sx-swap-oob "outerHTML" (div :id "filter" :sx-swap-oob "outerHTML"
@@ -38,7 +38,7 @@
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport" :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
(when content content)))) (when content content))))
(defcomp ~hamburger () (defcomp ~shared:layout/hamburger ()
(div :class "md:hidden bg-stone-200 rounded" (div :class "md:hidden bg-stone-200 rounded"
(svg :class "h-12 w-12 transition-transform group-open/root:hidden block self-start" (svg :class "h-12 w-12 transition-transform group-open/root:hidden block self-start"
:viewBox "0 0 24 24" :fill "none" :stroke "currentColor" :viewBox "0 0 24 24" :fill "none" :stroke "currentColor"
@@ -48,17 +48,17 @@
:class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start" :class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start"
(path :d "M6 9l6 6 6-6" :fill "currentColor")))) (path :d "M6 9l6 6 6-6" :fill "currentColor"))))
(defcomp ~post-label (&key (feature-image :as string?) (title :as string)) (defcomp ~shared:layout/post-label (&key (feature-image :as string?) (title :as string))
(<> (when feature-image (<> (when feature-image
(img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0")) (img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
(span title))) (span title)))
(defcomp ~page-cart-badge (&key (href :as string) (count :as string)) (defcomp ~shared:layout/page-cart-badge (&key (href :as string) (count :as string))
(a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition" (a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
(i :class "fa fa-shopping-cart" :aria-hidden "true") (i :class "fa fa-shopping-cart" :aria-hidden "true")
(span count))) (span count)))
(defcomp ~header-row-sx (&key cart-mini (blog-url :as string?) (site-title :as string?) (defcomp ~shared:layout/header-row-sx (&key cart-mini (blog-url :as string?) (site-title :as string?)
(app-label :as string?) nav-tree auth-menu nav-panel (app-label :as string?) nav-tree auth-menu nav-panel
(settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?)) (settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
(<> (<>
@@ -79,13 +79,13 @@
(when (and is-admin settings-url) (when (and is-admin settings-url)
(a :href settings-url :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3" (a :href settings-url :class "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"
(i :class "fa fa-cog" :aria-hidden "true")))) (i :class "fa fa-cog" :aria-hidden "true"))))
(~hamburger))) (~shared:layout/hamburger)))
(div :class "block md:hidden text-md font-bold" (div :class "block md:hidden text-md font-bold"
(when auth-menu auth-menu)))) (when auth-menu auth-menu))))
; @css bg-sky-400 bg-sky-300 bg-sky-200 bg-sky-100 bg-violet-400 bg-violet-300 bg-violet-200 bg-violet-100 ; @css bg-sky-400 bg-sky-300 bg-sky-200 bg-sky-100 bg-violet-400 bg-violet-300 bg-violet-200 bg-violet-100
; @css aria-selected:bg-violet-200 aria-selected:text-violet-900 aria-selected:bg-stone-500 aria-selected:text-white ; @css aria-selected:bg-violet-200 aria-selected:text-violet-900 aria-selected:bg-stone-500 aria-selected:text-white
(defcomp ~menu-row-sx (&key (id :as string) (level :as number?) (colour :as string?) (defcomp ~shared:layout/menu-row-sx (&key (id :as string) (level :as number?) (colour :as string?)
(link-href :as string) (link-label :as string?) link-label-content (link-href :as string) (link-label :as string?) link-label-content
(icon :as string?) (selected :as string?) (hx-select :as string?) (icon :as string?) (selected :as string?) (hx-select :as string?)
nav (child-id :as string?) child (oob :as boolean?) (external :as boolean?)) nav (child-id :as string?) child (oob :as boolean?) (external :as boolean?))
@@ -117,11 +117,11 @@
(div :id child-id :class "flex flex-col w-full items-center" (div :id child-id :class "flex flex-col w-full items-center"
(when child child)))))) (when child child))))))
(defcomp ~oob-header-sx (&key (parent-id :as string) row) (defcomp ~shared:layout/oob-header-sx (&key (parent-id :as string) row)
(div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center" (div :id parent-id :sx-swap-oob "outerHTML" :class "flex flex-col w-full items-center"
row)) row))
(defcomp ~header-child-sx (&key (id :as string?) inner) (defcomp ~shared:layout/header-child-sx (&key (id :as string?) inner)
(div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner)) (div :id (or id "root-header-child") :class "flex flex-col w-full items-center" inner))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -129,7 +129,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Labelled section: colour bar header + vertical nav items ;; Labelled section: colour bar header + vertical nav items
(defcomp ~mobile-menu-section (&key (label :as string) (href :as string?) (colour :as string?) (defcomp ~shared:layout/mobile-menu-section (&key (label :as string) (href :as string?) (colour :as string?)
(level :as number?) items) (level :as number?) items)
(let* ((c (or colour "sky")) (let* ((c (or colour "sky"))
(lv (or level 1)) (lv (or level 1))
@@ -143,7 +143,7 @@
items)))) items))))
;; Root-level mobile nav: site nav items + auth links ;; Root-level mobile nav: site nav items + auth links
(defcomp ~mobile-root-nav (&key nav-tree auth-menu) (defcomp ~shared:layout/mobile-root-nav (&key nav-tree auth-menu)
(<> (<>
(when nav-tree (when nav-tree
(div :class "flex flex-col gap-2 p-3 text-sm" nav-tree)) (div :class "flex flex-col gap-2 p-3 text-sm" nav-tree))
@@ -156,27 +156,27 @@
;; nested component calls in _aser are serialized without expansion. ;; nested component calls in _aser are serialized without expansion.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~root-header (&key cart-mini (blog-url :as string?) (site-title :as string?) (defcomp ~shared:layout/root-header (&key cart-mini (blog-url :as string?) (site-title :as string?)
(app-label :as string?) nav-tree auth-menu nav-panel (app-label :as string?) nav-tree auth-menu nav-panel
(settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?)) (settings-url :as string?) (is-admin :as boolean?) (oob :as boolean?))
(~header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title (~shared:layout/header-row-sx :cart-mini cart-mini :blog-url blog-url :site-title site-title
:app-label app-label :nav-tree nav-tree :auth-menu auth-menu :app-label app-label :nav-tree nav-tree :auth-menu auth-menu
:nav-panel nav-panel :settings-url settings-url :is-admin is-admin :nav-panel nav-panel :settings-url settings-url :is-admin is-admin
:oob oob)) :oob oob))
(defcomp ~root-mobile (&key nav-tree auth-menu) (defcomp ~shared:layout/root-mobile (&key nav-tree auth-menu)
(~mobile-root-nav :nav-tree nav-tree :auth-menu auth-menu)) (~shared:layout/mobile-root-nav :nav-tree nav-tree :auth-menu auth-menu))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Auto-fetching header/mobile macros — use IO primitives to self-populate. ;; Auto-fetching header/mobile macros — use IO primitives to self-populate.
;; These expand inline so IO calls resolve in _aser mode within layout bodies. ;; These expand inline so IO calls resolve in _aser mode within layout bodies.
;; Replaces the 10-parameter ~root-header boilerplate in layout defcomps. ;; Replaces the 10-parameter ~shared:layout/root-header boilerplate in layout defcomps.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defmacro ~root-header-auto (oob) (defmacro ~root-header-auto (oob)
(quasiquote (quasiquote
(let ((__rhctx (root-header-ctx))) (let ((__rhctx (root-header-ctx)))
(~header-row-sx :cart-mini (get __rhctx "cart-mini") (~shared:layout/header-row-sx :cart-mini (get __rhctx "cart-mini")
:blog-url (get __rhctx "blog-url") :blog-url (get __rhctx "blog-url")
:site-title (get __rhctx "site-title") :site-title (get __rhctx "site-title")
:app-label (get __rhctx "app-label") :app-label (get __rhctx "app-label")
@@ -190,7 +190,7 @@
(defmacro ~root-mobile-auto () (defmacro ~root-mobile-auto ()
(quasiquote (quasiquote
(let ((__rhctx (root-header-ctx))) (let ((__rhctx (root-header-ctx)))
(~mobile-root-nav :nav-tree (get __rhctx "nav-tree") (~shared:layout/mobile-root-nav :nav-tree (get __rhctx "nav-tree")
:auth-menu (get __rhctx "auth-menu"))))) :auth-menu (get __rhctx "auth-menu")))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -198,31 +198,31 @@
;; These use ~root-header-auto / ~root-mobile-auto macros (IO primitives). ;; These use ~root-header-auto / ~root-mobile-auto macros (IO primitives).
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~layout-root-full () (defcomp ~shared:layout/root-full ()
(~root-header-auto)) (~root-header-auto))
(defcomp ~layout-root-oob () (defcomp ~shared:layout/root-oob ()
(~oob-header-sx :parent-id "root-header-child" (~shared:layout/oob-header-sx :parent-id "root-header-child"
:row (~root-header-auto true))) :row (~root-header-auto true)))
(defcomp ~layout-root-mobile () (defcomp ~shared:layout/root-mobile ()
(~root-mobile-auto)) (~root-mobile-auto))
;; Post layout — root + post header ;; Post layout — root + post header
(defcomp ~layout-post-full () (defcomp ~shared:layout/post-full ()
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx :inner (~post-header-auto)))) (~shared:layout/header-child-sx :inner (~post-header-auto))))
(defcomp ~layout-post-oob () (defcomp ~shared:layout/post-oob ()
(<> (~post-header-auto true) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child" :row ""))) (~shared:layout/oob-header-sx :parent-id "post-header-child" :row "")))
(defcomp ~layout-post-mobile () (defcomp ~shared:layout/post-mobile ()
(let ((__phctx (post-header-ctx)) (let ((__phctx (post-header-ctx))
(__rhctx (root-header-ctx))) (__rhctx (root-header-ctx)))
(<> (<>
(when (get __phctx "slug") (when (get __phctx "slug")
(~mobile-menu-section (~shared:layout/mobile-menu-section
:label (slice (get __phctx "title") 0 40) :label (slice (get __phctx "title") 0 40)
:href (get __phctx "link-href") :href (get __phctx "link-href")
:level 1 :level 1
@@ -230,35 +230,35 @@
(~root-mobile-auto)))) (~root-mobile-auto))))
;; Post-admin layout — root + post header with nested admin row ;; Post-admin layout — root + post header with nested admin row
(defcomp ~layout-post-admin-full (&key (selected :as string?)) (defcomp ~shared:layout/post-admin-full (&key (selected :as string?))
(let ((__admin-hdr (~post-admin-header-auto nil selected))) (let ((__admin-hdr (~post-admin-header-auto nil selected)))
(<> (~root-header-auto) (<> (~root-header-auto)
(~header-child-sx (~shared:layout/header-child-sx
:inner (~post-header-auto nil))))) :inner (~post-header-auto nil)))))
(defcomp ~layout-post-admin-oob (&key (selected :as string?)) (defcomp ~shared:layout/post-admin-oob (&key (selected :as string?))
(<> (~post-header-auto true) (<> (~post-header-auto true)
(~oob-header-sx :parent-id "post-header-child" (~shared:layout/oob-header-sx :parent-id "post-header-child"
:row (~post-admin-header-auto nil selected)))) :row (~post-admin-header-auto nil selected))))
(defcomp ~layout-post-admin-mobile (&key (selected :as string?)) (defcomp ~shared:layout/post-admin-mobile (&key (selected :as string?))
(let ((__phctx (post-header-ctx))) (let ((__phctx (post-header-ctx)))
(<> (<>
(when (get __phctx "slug") (when (get __phctx "slug")
(~mobile-menu-section (~shared:layout/mobile-menu-section
:label "admin" :label "admin"
:href (get __phctx "admin-href") :href (get __phctx "admin-href")
:level 2 :level 2
:items (~post-admin-nav-auto selected))) :items (~post-admin-nav-auto selected)))
(when (get __phctx "slug") (when (get __phctx "slug")
(~mobile-menu-section (~shared:layout/mobile-menu-section
:label (slice (get __phctx "title") 0 40) :label (slice (get __phctx "title") 0 40)
:href (get __phctx "link-href") :href (get __phctx "link-href")
:level 1 :level 1
:items (~post-nav-auto))) :items (~post-nav-auto)))
(~root-mobile-auto)))) (~root-mobile-auto))))
(defcomp ~error-content (&key (errnum :as string) (message :as string) (image :as string?)) (defcomp ~shared:layout/error-content (&key (errnum :as string) (message :as string) (image :as string?))
(div :class "text-center p-8 max-w-lg mx-auto" (div :class "text-center p-8 max-w-lg mx-auto"
(div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum) (div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" errnum)
(div :class "text-stone-600 mb-4" message) (div :class "text-stone-600 mb-4" message)
@@ -266,7 +266,7 @@
(div :class "flex justify-center" (div :class "flex justify-center"
(img :src image :width "300" :height "300"))))) (img :src image :width "300" :height "300")))))
(defcomp ~clear-oob-div (&key (id :as string)) (defcomp ~shared:layout/clear-oob-div (&key (id :as string))
(div :id id :sx-swap-oob "outerHTML")) (div :id id :sx-swap-oob "outerHTML"))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
@@ -280,12 +280,12 @@
(when (get __phctx "slug") (when (get __phctx "slug")
(<> (<>
(when (> (get __phctx "page-cart-count") 0) (when (> (get __phctx "page-cart-count") 0)
(~page-cart-badge :href (get __phctx "cart-href") (~shared:layout/page-cart-badge :href (get __phctx "cart-href")
:count (str (get __phctx "page-cart-count")))) :count (str (get __phctx "page-cart-count"))))
(when (get __phctx "container-nav") (when (get __phctx "container-nav")
(~container-nav-wrapper :content (get __phctx "container-nav"))) (~shared:layout/container-nav-wrapper :content (get __phctx "container-nav")))
(when (get __phctx "is-admin") (when (get __phctx "is-admin")
(~admin-cog-button :href (get __phctx "admin-href") (~shared:layout/admin-cog-button :href (get __phctx "admin-href")
:is-admin-page (get __phctx "is-admin-page")))))))) :is-admin-page (get __phctx "is-admin-page"))))))))
(defmacro ~post-header-auto (oob) (defmacro ~post-header-auto (oob)
@@ -293,9 +293,9 @@
(quasiquote (quasiquote
(let ((__phctx (post-header-ctx))) (let ((__phctx (post-header-ctx)))
(when (get __phctx "slug") (when (get __phctx "slug")
(~menu-row-sx :id "post-row" :level 1 (~shared:layout/menu-row-sx :id "post-row" :level 1
:link-href (get __phctx "link-href") :link-href (get __phctx "link-href")
:link-label-content (~post-label :link-label-content (~shared:layout/post-label
:feature-image (get __phctx "feature-image") :feature-image (get __phctx "feature-image")
:title (get __phctx "title")) :title (get __phctx "title"))
:nav (~post-nav-auto) :nav (~post-nav-auto)
@@ -310,28 +310,28 @@
(let ((__slug (get __phctx "slug")) (let ((__slug (get __phctx "slug"))
(__sc (get __phctx "select-colours"))) (__sc (get __phctx "select-colours")))
(<> (<>
(~nav-link :href (app-url "events" (str "/" __slug "/admin/")) (~shared:layout/nav-link :href (app-url "events" (str "/" __slug "/admin/"))
:label "calendars" :select-colours __sc :label "calendars" :select-colours __sc
:is-selected (when (= (unquote selected) "calendars") "true")) :is-selected (when (= (unquote selected) "calendars") "true"))
(~nav-link :href (app-url "market" (str "/" __slug "/admin/")) (~shared:layout/nav-link :href (app-url "market" (str "/" __slug "/admin/"))
:label "markets" :select-colours __sc :label "markets" :select-colours __sc
:is-selected (when (= (unquote selected) "markets") "true")) :is-selected (when (= (unquote selected) "markets") "true"))
(~nav-link :href (app-url "cart" (str "/" __slug "/admin/payments/")) (~shared:layout/nav-link :href (app-url "cart" (str "/" __slug "/admin/payments/"))
:label "payments" :select-colours __sc :label "payments" :select-colours __sc
:is-selected (when (= (unquote selected) "payments") "true")) :is-selected (when (= (unquote selected) "payments") "true"))
(~nav-link :href (app-url "blog" (str "/" __slug "/admin/entries/")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/entries/"))
:label "entries" :select-colours __sc :label "entries" :select-colours __sc
:is-selected (when (= (unquote selected) "entries") "true")) :is-selected (when (= (unquote selected) "entries") "true"))
(~nav-link :href (app-url "blog" (str "/" __slug "/admin/data/")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/data/"))
:label "data" :select-colours __sc :label "data" :select-colours __sc
:is-selected (when (= (unquote selected) "data") "true")) :is-selected (when (= (unquote selected) "data") "true"))
(~nav-link :href (app-url "blog" (str "/" __slug "/admin/preview/")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/preview/"))
:label "preview" :select-colours __sc :label "preview" :select-colours __sc
:is-selected (when (= (unquote selected) "preview") "true")) :is-selected (when (= (unquote selected) "preview") "true"))
(~nav-link :href (app-url "blog" (str "/" __slug "/admin/edit/")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/edit/"))
:label "edit" :select-colours __sc :label "edit" :select-colours __sc
:is-selected (when (= (unquote selected) "edit") "true")) :is-selected (when (= (unquote selected) "edit") "true"))
(~nav-link :href (app-url "blog" (str "/" __slug "/admin/settings/")) (~shared:layout/nav-link :href (app-url "blog" (str "/" __slug "/admin/settings/"))
:label "settings" :select-colours __sc :label "settings" :select-colours __sc
:is-selected (when (= (unquote selected) "settings") "true")))))))) :is-selected (when (= (unquote selected) "settings") "true"))))))))
@@ -340,9 +340,9 @@
(quasiquote (quasiquote
(let ((__phctx (post-header-ctx))) (let ((__phctx (post-header-ctx)))
(when (get __phctx "slug") (when (get __phctx "slug")
(~menu-row-sx :id "post-admin-row" :level 2 (~shared:layout/menu-row-sx :id "post-admin-row" :level 2
:link-href (get __phctx "admin-href") :link-href (get __phctx "admin-href")
:link-label-content (~post-admin-label :link-label-content (~shared:layout/post-admin-label
:selected (unquote selected)) :selected (unquote selected))
:nav (~post-admin-nav-auto (unquote selected)) :nav (~post-admin-nav-auto (unquote selected))
:child-id "post-admin-header-child" :child-id "post-admin-header-child"
@@ -352,27 +352,27 @@
;; Shared nav helpers — used by post_header_sx / post_admin_header_sx ;; Shared nav helpers — used by post_header_sx / post_admin_header_sx
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~container-nav-wrapper (&key content) (defcomp ~shared:layout/container-nav-wrapper (&key content)
(div :id "entries-calendars-nav-wrapper" (div :id "entries-calendars-nav-wrapper"
:class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
content)) content))
; @css justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 !bg-stone-500 !text-white ; @css justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 !bg-stone-500 !text-white
(defcomp ~admin-cog-button (&key (href :as string) (is-admin-page :as boolean?)) (defcomp ~shared:layout/admin-cog-button (&key (href :as string) (is-admin-page :as boolean?))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href
:class (str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 " :class (str "justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3 "
(if is-admin-page "!bg-stone-500 !text-white" "")) (if is-admin-page "!bg-stone-500 !text-white" ""))
(i :class "fa fa-cog" :aria-hidden "true")))) (i :class "fa fa-cog" :aria-hidden "true"))))
(defcomp ~post-admin-label (&key (selected :as string?)) (defcomp ~shared:layout/post-admin-label (&key (selected :as string?))
(<> (<>
(i :class "fa fa-shield-halved" :aria-hidden "true") (i :class "fa fa-shield-halved" :aria-hidden "true")
" admin" " admin"
(when selected (when selected
(span :class "text-white" selected)))) (span :class "text-white" selected))))
(defcomp ~nav-link (&key (href :as string) (hx-select :as string?) (label :as string?) (icon :as string?) (defcomp ~shared:layout/nav-link (&key (href :as string) (hx-select :as string?) (label :as string?) (icon :as string?)
(aclass :as string?) (select-colours :as string?) (is-selected :as string?)) (aclass :as string?) (select-colours :as string?) (is-selected :as string?))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href (a :href href

View File

@@ -2,33 +2,33 @@
;; The single place where raw! lives — for CMS content (Ghost post body, ;; The single place where raw! lives — for CMS content (Ghost post body,
;; product descriptions, etc.) that arrives as pre-rendered HTML. ;; product descriptions, etc.) that arrives as pre-rendered HTML.
(defcomp ~rich-text (&key (html :as string)) (defcomp ~shared:misc/rich-text (&key (html :as string))
(raw! html)) (raw! html))
(defcomp ~error-inline (&key (message :as string)) (defcomp ~shared:misc/error-inline (&key (message :as string))
(div :class "text-red-600 text-sm" message)) (div :class "text-red-600 text-sm" message))
(defcomp ~notification-badge (&key (count :as number)) (defcomp ~shared:misc/notification-badge (&key (count :as number))
(span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count)) (span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count))
(defcomp ~cache-cleared (&key (time-str :as string)) (defcomp ~shared:misc/cache-cleared (&key (time-str :as string))
(span :class "text-green-600 font-bold" "Cache cleared at " time-str)) (span :class "text-green-600 font-bold" "Cache cleared at " time-str))
(defcomp ~error-list (&key (items :as list?)) (defcomp ~shared:misc/error-list (&key (items :as list?))
(ul :class "list-disc pl-5 space-y-1 text-sm text-red-600" (ul :class "list-disc pl-5 space-y-1 text-sm text-red-600"
(when items items))) (when items items)))
(defcomp ~error-list-item (&key (message :as string)) (defcomp ~shared:misc/error-list-item (&key (message :as string))
(li message)) (li message))
(defcomp ~fragment-error (&key (service :as string)) (defcomp ~shared:misc/fragment-error (&key (service :as string))
(p :class "text-sm text-red-600" "Service " (b service) " is unavailable.")) (p :class "text-sm text-red-600" "Service " (b service) " is unavailable."))
(defcomp ~htmx-sentinel (&key (id :as string) (hx-get :as string) (hx-trigger :as string) (defcomp ~shared:misc/htmx-sentinel (&key (id :as string) (hx-get :as string) (hx-trigger :as string)
(hx-swap :as string) (class :as string?) extra-attrs) (hx-swap :as string) (class :as string?) extra-attrs)
(div :id id :sx-get hx-get :sx-trigger hx-trigger :sx-swap hx-swap :class class)) (div :id id :sx-get hx-get :sx-trigger hx-trigger :sx-swap hx-swap :class class))
(defcomp ~nav-group-link (&key (href :as string) (hx-select :as string?) (nav-class :as string?) (label :as string)) (defcomp ~shared:misc/nav-group-link (&key (href :as string) (hx-select :as string?) (nav-class :as string?) (label :as string))
(div :class "relative nav-group" (div :class "relative nav-group"
(a :href href :sx-get href :sx-target "#main-panel" (a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-select hx-select :sx-swap "outerHTML"
@@ -39,7 +39,7 @@
;; Shared sentinel components — infinite scroll triggers ;; Shared sentinel components — infinite scroll triggers
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sentinel-mobile (&key (id :as string) (next-url :as string) (hyperscript :as string?)) (defcomp ~shared:misc/sentinel-mobile (&key (id :as string) (next-url :as string) (hyperscript :as string?))
(div :id id :class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel" (div :id id :class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry" :sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry"
:sx-swap "outerHTML" :_ hyperscript :sx-swap "outerHTML" :_ hyperscript
@@ -50,7 +50,7 @@
(i :class "fa fa-exclamation-triangle text-2xl") (i :class "fa fa-exclamation-triangle text-2xl")
(p :class "mt-2" "Loading failed \u2014 retrying\u2026")))) (p :class "mt-2" "Loading failed \u2014 retrying\u2026"))))
(defcomp ~sentinel-desktop (&key (id :as string) (next-url :as string) (hyperscript :as string?)) (defcomp ~shared:misc/sentinel-desktop (&key (id :as string) (next-url :as string) (hyperscript :as string?))
(div :id id :class "hidden md:block h-4 opacity-0 pointer-events-none" (div :id id :class "hidden md:block h-4 opacity-0 pointer-events-none"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry" :sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry"
:sx-swap "outerHTML" :_ hyperscript :sx-swap "outerHTML" :_ hyperscript
@@ -59,20 +59,20 @@
(div :class "animate-spin h-6 w-6 border-2 border-stone-300 border-t-stone-600 rounded-full")) (div :class "animate-spin h-6 w-6 border-2 border-stone-300 border-t-stone-600 rounded-full"))
(div :class "js-neterr hidden text-center py-2 text-stone-400 text-sm" "Retry\u2026"))) (div :class "js-neterr hidden text-center py-2 text-stone-400 text-sm" "Retry\u2026")))
(defcomp ~sentinel-simple (&key (id :as string) (next-url :as string)) (defcomp ~shared:misc/sentinel-simple (&key (id :as string) (next-url :as string))
(div :id id :class "h-4 opacity-0 pointer-events-none" (div :id id :class "h-4 opacity-0 pointer-events-none"
:sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML" :sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML"
:role "status" :aria-hidden "true" :role "status" :aria-hidden "true"
(div :class "text-center text-xs text-stone-400" "loading..."))) (div :class "text-center text-xs text-stone-400" "loading...")))
(defcomp ~end-of-results (&key (cls :as string?)) (defcomp ~shared:misc/end-of-results (&key (cls :as string?))
(div :class (or cls "col-span-full mt-4 text-center text-xs text-stone-400") "End of results")) (div :class (or cls "col-span-full mt-4 text-center text-xs text-stone-400") "End of results"))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Shared empty state — icon + message + optional action ;; Shared empty state — icon + message + optional action
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~empty-state (&key (icon :as string?) (message :as string) (cls :as string?) &rest children) (defcomp ~shared:misc/empty-state (&key (icon :as string?) (message :as string) (cls :as string?) &rest children)
(div :class (or cls "p-8 text-center text-stone-400") (div :class (or cls "p-8 text-center text-stone-400")
(when icon (div (i :class (str icon " text-4xl mb-2") :aria-hidden "true"))) (when icon (div (i :class (str icon " text-4xl mb-2") :aria-hidden "true")))
(p message) (p message)
@@ -82,7 +82,7 @@
;; Shared badge — inline pill with configurable colours ;; Shared badge — inline pill with configurable colours
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~badge (&key (label :as string) (cls :as string?)) (defcomp ~shared:misc/badge (&key (label :as string) (cls :as string?))
(span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " (or cls "bg-stone-100 text-stone-700")) (span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " (or cls "bg-stone-100 text-stone-700"))
label)) label))
@@ -90,7 +90,7 @@
;; Shared delete button with confirm dialog ;; Shared delete button with confirm dialog
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~delete-btn (&key (url :as string) (trigger-target :as string) (title :as string?) (defcomp ~shared:misc/delete-btn (&key (url :as string) (trigger-target :as string) (title :as string?)
(text :as string?) (confirm-text :as string?) (cancel-text :as string?) (text :as string?) (confirm-text :as string?) (cancel-text :as string?)
(sx-headers :as string?) (cls :as string?)) (sx-headers :as string?) (cls :as string?))
(button :type "button" (button :type "button"
@@ -110,7 +110,7 @@
;; Shared price display — special + regular with strikethrough ;; Shared price display — special + regular with strikethrough
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~price (&key (special-price :as string?) (regular-price :as string?)) (defcomp ~shared:misc/price (&key (special-price :as string?) (regular-price :as string?))
(div :class "mt-1 flex items-baseline gap-2 justify-center" (div :class "mt-1 flex items-baseline gap-2 justify-center"
(when special-price (div :class "text-lg font-semibold text-emerald-700" special-price)) (when special-price (div :class "text-lg font-semibold text-emerald-700" special-price))
(when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price)) (when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price))
@@ -120,7 +120,7 @@
;; Shared image-or-placeholder ;; Shared image-or-placeholder
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~img-or-placeholder (&key (src :as string?) (alt :as string?) (size-cls :as string?) (defcomp ~shared:misc/img-or-placeholder (&key (src :as string?) (alt :as string?) (size-cls :as string?)
(placeholder-icon :as string?) (placeholder-text :as string?)) (placeholder-icon :as string?) (placeholder-text :as string?))
(if src (if src
(img :src src :alt (or alt "") :class (or size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0")) (img :src src :alt (or alt "") :class (or size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0"))
@@ -133,35 +133,35 @@
;; Shared view toggle — list/tile view switcher ;; Shared view toggle — list/tile view switcher
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~list-svg () (defcomp ~shared:misc/list-svg ()
(svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none" :viewBox "0 0 24 24" (svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none" :viewBox "0 0 24 24"
:stroke "currentColor" :stroke-width "2" :stroke "currentColor" :stroke-width "2"
(path :stroke-linecap "round" :stroke-linejoin "round" :d "M4 6h16M4 12h16M4 18h16"))) (path :stroke-linecap "round" :stroke-linejoin "round" :d "M4 6h16M4 12h16M4 18h16")))
(defcomp ~tile-svg () (defcomp ~shared:misc/tile-svg ()
(svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none" :viewBox "0 0 24 24" (svg :xmlns "http://www.w3.org/2000/svg" :class "h-5 w-5" :fill "none" :viewBox "0 0 24 24"
:stroke "currentColor" :stroke-width "2" :stroke "currentColor" :stroke-width "2"
(path :stroke-linecap "round" :stroke-linejoin "round" (path :stroke-linecap "round" :stroke-linejoin "round"
:d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"))) :d "M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z")))
(defcomp ~view-toggle (&key (list-href :as string) (tile-href :as string) (hx-select :as string?) (defcomp ~shared:misc/view-toggle (&key (list-href :as string) (tile-href :as string) (hx-select :as string?)
(list-cls :as string?) (tile-cls :as string?) (storage-key :as string?) (list-cls :as string?) (tile-cls :as string?) (storage-key :as string?)
list-svg tile-svg) list-svg tile-svg)
(div :class "hidden md:flex justify-end px-3 pt-3 gap-1" (div :class "hidden md:flex justify-end px-3 pt-3 gap-1"
(a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select (a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " list-cls) :title "List view" :sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " list-cls) :title "List view"
:_ (str "on click js localStorage.removeItem('" (or storage-key "view") "') end") :_ (str "on click js localStorage.removeItem('" (or storage-key "view") "') end")
(or list-svg (~list-svg))) (or list-svg (~shared:misc/list-svg)))
(a :href tile-href :sx-get tile-href :sx-target "#main-panel" :sx-select hx-select (a :href tile-href :sx-get tile-href :sx-target "#main-panel" :sx-select hx-select
:sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " tile-cls) :title "Tile view" :sx-swap "outerHTML" :sx-push-url "true" :class (str "p-1.5 rounded " tile-cls) :title "Tile view"
:_ (str "on click js localStorage.setItem('" (or storage-key "view") "','tile') end") :_ (str "on click js localStorage.setItem('" (or storage-key "view") "','tile') end")
(or tile-svg (~tile-svg))))) (or tile-svg (~shared:misc/tile-svg)))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Shared CRUD admin panel — for calendars, markets, etc. ;; Shared CRUD admin panel — for calendars, markets, etc.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~crud-create-form (&key (create-url :as string) (csrf :as string) (errors-id :as string?) (defcomp ~shared:misc/crud-create-form (&key (create-url :as string) (csrf :as string) (errors-id :as string?)
(list-id :as string?) (placeholder :as string?) (label :as string?) (list-id :as string?) (placeholder :as string?) (label :as string?)
(btn-label :as string?)) (btn-label :as string?))
(<> (<>
@@ -177,12 +177,12 @@
:placeholder (or placeholder "Name"))) :placeholder (or placeholder "Name")))
(button :type "submit" :class "border rounded px-3 py-2" (or btn-label "Add"))))) (button :type "submit" :class "border rounded px-3 py-2" (or btn-label "Add")))))
(defcomp ~crud-panel (&key form list (list-id :as string?)) (defcomp ~shared:misc/crud-panel (&key form list (list-id :as string?))
(section :class "p-4" (section :class "p-4"
form form
(div :id (or list-id "crud-list") :class "mt-6" list))) (div :id (or list-id "crud-list") :class "mt-6" list)))
(defcomp ~crud-item (&key (href :as string) (name :as string) (slug :as string) (del-url :as string) (defcomp ~shared:misc/crud-item (&key (href :as string) (name :as string) (slug :as string) (del-url :as string)
(csrf-hdr :as string) (list-id :as string?) (confirm-title :as string?) (csrf-hdr :as string) (list-id :as string?) (confirm-title :as string?)
(confirm-text :as string?)) (confirm-text :as string?))
(div :class "mt-6 border rounded-lg p-4" (div :class "mt-6 border rounded-lg p-4"
@@ -206,7 +206,7 @@
;; checkout prefix) used by blog, events, and cart admin panels. ;; checkout prefix) used by blog, events, and cart admin panels.
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sumup-settings-form (&key (update-url :as string) (csrf :as string?) (merchant-code :as string?) (defcomp ~shared:misc/sumup-settings-form (&key (update-url :as string) (csrf :as string?) (merchant-code :as string?)
(placeholder :as string?) (input-cls :as string?) (placeholder :as string?) (input-cls :as string?)
(sumup-configured :as boolean?) (checkout-prefix :as string?) (sumup-configured :as boolean?) (checkout-prefix :as string?)
(panel-id :as string?) (sx-select :as string?)) (panel-id :as string?) (sx-select :as string?))
@@ -241,7 +241,7 @@
;; Shared avatar — image or initial-letter placeholder ;; Shared avatar — image or initial-letter placeholder
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~avatar (&key (src :as string?) (cls :as string?) (initial :as string?)) (defcomp ~shared:misc/avatar (&key (src :as string?) (cls :as string?) (initial :as string?))
(if src (if src
(img :src src :alt "" :class cls) (img :src src :alt "" :class cls)
(div :class cls initial))) (div :class cls initial)))
@@ -250,7 +250,7 @@
;; Shared scroll-nav wrapper — horizontal scrollable nav with arrows ;; Shared scroll-nav wrapper — horizontal scrollable nav with arrows
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~scroll-nav-wrapper (&key (wrapper-id :as string) (container-id :as string) (arrow-cls :as string?) (defcomp ~shared:misc/scroll-nav-wrapper (&key (wrapper-id :as string) (container-id :as string) (arrow-cls :as string?)
(left-hs :as string?) (scroll-hs :as string?) (right-hs :as string?) (left-hs :as string?) (scroll-hs :as string?) (right-hs :as string?)
items (oob :as boolean?)) items (oob :as boolean?))
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl" (div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"

View File

@@ -1,24 +1,24 @@
;; Blog navigation components ;; Blog navigation components
(defcomp ~blog-nav-empty (&key wrapper-id) (defcomp ~shared:nav/blog-nav-empty (&key wrapper-id)
(div :id wrapper-id :sx-swap-oob "outerHTML")) (div :id wrapper-id :sx-swap-oob "outerHTML"))
(defcomp ~blog-nav-item-link (&key href hx-get selected nav-cls img label) (defcomp ~shared:nav/blog-nav-item-link (&key href hx-get selected nav-cls img label)
(div (a :href href :sx-get hx-get :sx-target "#main-panel" (div (a :href href :sx-get hx-get :sx-target "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true" :sx-swap "outerHTML" :sx-push-url "true"
:aria-selected selected :class nav-cls :aria-selected selected :class nav-cls
img (span label)))) img (span label))))
(defcomp ~blog-nav-item-plain (&key href selected nav-cls img label) (defcomp ~shared:nav/blog-nav-item-plain (&key href selected nav-cls img label)
(div (a :href href :aria-selected selected :class nav-cls (div (a :href href :aria-selected selected :class nav-cls
img (span label)))) img (span label))))
;; Nav entries ;; Nav entries
(defcomp ~blog-nav-entries-empty () (defcomp ~shared:nav/blog-nav-entries-empty ()
(div :id "entries-calendars-nav-wrapper" :sx-swap-oob "true")) (div :id "entries-calendars-nav-wrapper" :sx-swap-oob "true"))
(defcomp ~blog-nav-calendar-item (&key href nav-cls name) (defcomp ~shared:nav/blog-nav-calendar-item (&key href nav-cls name)
(a :href href :class nav-cls (a :href href :class nav-cls
(i :class "fa fa-calendar" :aria-hidden "true") (i :class "fa fa-calendar" :aria-hidden "true")
(div name))) (div name)))

View File

@@ -1,11 +1,11 @@
(defcomp ~calendar-entry-nav (&key (href :as string) (name :as string) (date-str :as string) (nav-class :as string?)) (defcomp ~shared:navigation/calendar-entry-nav (&key (href :as string) (name :as string) (date-str :as string) (nav-class :as string?))
(a :href href :class nav-class (a :href href :class nav-class
(div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0") (div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0")
(div :class "flex-1 min-w-0" (div :class "flex-1 min-w-0"
(div :class "font-medium truncate" name) (div :class "font-medium truncate" name)
(div :class "text-xs text-stone-600 truncate" date-str)))) (div :class "text-xs text-stone-600 truncate" date-str))))
(defcomp ~calendar-link-nav (&key (href :as string) (name :as string) (nav-class :as string?) (defcomp ~shared:navigation/calendar-link-nav (&key (href :as string) (name :as string) (nav-class :as string?)
(is-selected :as string?) (select-colours :as string?)) (is-selected :as string?) (select-colours :as string?))
(a :href href (a :href href
:sx-get href :sx-get href
@@ -18,13 +18,13 @@
(i :class "fa fa-calendar" :aria-hidden "true") (i :class "fa fa-calendar" :aria-hidden "true")
(span name))) (span name)))
(defcomp ~market-link-nav (&key (href :as string) (name :as string) (nav-class :as string?) (defcomp ~shared:navigation/market-link-nav (&key (href :as string) (name :as string) (nav-class :as string?)
(select-colours :as string?)) (select-colours :as string?))
(a :href href :class (str (or nav-class "") " " (or select-colours "")) (a :href href :class (str (or nav-class "") " " (or select-colours ""))
(i :class "fa fa-shopping-bag" :aria-hidden "true") (i :class "fa fa-shopping-bag" :aria-hidden "true")
(span name))) (span name)))
(defcomp ~relation-nav (&key (href :as string) (name :as string) (icon :as string?) (defcomp ~shared:navigation/relation-nav (&key (href :as string) (name :as string) (icon :as string?)
(nav-class :as string?) (relation-type :as string?)) (nav-class :as string?) (relation-type :as string?))
(a :href href :class (or nav-class "flex items-center gap-3 rounded-lg py-2 px-3 text-sm text-stone-700 hover:bg-stone-100 transition-colors") (a :href href :class (or nav-class "flex items-center gap-3 rounded-lg py-2 px-3 text-sm text-stone-700 hover:bg-stone-100 transition-colors")
(when icon (when icon

View File

@@ -6,7 +6,7 @@
;; Order table rows ;; Order table rows
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-row-desktop (&key (oid :as string) (created :as string) (desc :as string) (total :as string) (defcomp ~shared:orders/row-desktop (&key (oid :as string) (created :as string) (desc :as string) (total :as string)
(pill :as string) (status :as string) (url :as string)) (pill :as string) (status :as string) (url :as string))
(tr :class "hidden sm:table-row border-t border-stone-100 hover:bg-stone-50/60" (tr :class "hidden sm:table-row border-t border-stone-100 hover:bg-stone-50/60"
(td :class "px-3 py-2 align-top" (span :class "font-mono text-[11px] sm:text-xs" oid)) (td :class "px-3 py-2 align-top" (span :class "font-mono text-[11px] sm:text-xs" oid))
@@ -17,7 +17,7 @@
(td :class "px-3 py-0.5 align-top text-right" (td :class "px-3 py-0.5 align-top text-right"
(a :href url :class "inline-flex items-center px-3 py-1.5 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition" "View")))) (a :href url :class "inline-flex items-center px-3 py-1.5 text-xs sm:text-sm rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition" "View"))))
(defcomp ~order-row-mobile (&key (oid :as string) (created :as string) (total :as string) (defcomp ~shared:orders/row-mobile (&key (oid :as string) (created :as string) (total :as string)
(pill :as string) (status :as string) (url :as string)) (pill :as string) (status :as string) (url :as string))
(tr :class "sm:hidden border-t border-stone-100" (tr :class "sm:hidden border-t border-stone-100"
(td :colspan "5" :class "px-3 py-3" (td :colspan "5" :class "px-3 py-3"
@@ -30,16 +30,16 @@
(div :class "font-medium text-stone-800" total) (div :class "font-medium text-stone-800" total)
(a :href url :class "inline-flex items-center px-2 py-1 text-[11px] rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition shrink-0" "View")))))) (a :href url :class "inline-flex items-center px-2 py-1 text-[11px] rounded-full border border-stone-300 bg-white hover:bg-stone-50 transition shrink-0" "View"))))))
(defcomp ~order-end-row () (defcomp ~shared:orders/end-row ()
(tr (td :colspan "5" :class "px-3 py-4 text-center text-xs text-stone-400" (tr (td :colspan "5" :class "px-3 py-4 text-center text-xs text-stone-400"
(~end-of-results :cls "text-center text-xs text-stone-400")))) (~shared:misc/end-of-results :cls "text-center text-xs text-stone-400"))))
(defcomp ~order-empty-state () (defcomp ~shared:orders/empty-state ()
(div :class "max-w-full px-3 py-3 space-y-3" (div :class "max-w-full px-3 py-3 space-y-3"
(div :class "rounded-2xl border border-dashed border-stone-300 bg-white/80 p-4 sm:p-6 text-sm text-stone-700" (div :class "rounded-2xl border border-dashed border-stone-300 bg-white/80 p-4 sm:p-6 text-sm text-stone-700"
"No orders yet."))) "No orders yet.")))
(defcomp ~order-table (&key rows) (defcomp ~shared:orders/table (&key rows)
(div :class "max-w-full px-3 py-3 space-y-3" (div :class "max-w-full px-3 py-3 space-y-3"
(div :class "overflow-x-auto rounded-2xl border border-stone-200 bg-white/80" (div :class "overflow-x-auto rounded-2xl border border-stone-200 bg-white/80"
(table :class "min-w-full text-xs sm:text-sm" (table :class "min-w-full text-xs sm:text-sm"
@@ -53,7 +53,7 @@
(th :class "px-3 py-2 text-left font-medium" ""))) (th :class "px-3 py-2 text-left font-medium" "")))
(tbody rows))))) (tbody rows)))))
(defcomp ~order-list-header (&key search-mobile) (defcomp ~shared:orders/list-header (&key search-mobile)
(header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4" (header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4"
(div :class "space-y-1" (div :class "space-y-1"
(p :class "text-xs sm:text-sm text-stone-600" "Recent orders placed via the checkout.")) (p :class "text-xs sm:text-sm text-stone-600" "Recent orders placed via the checkout."))
@@ -63,13 +63,13 @@
;; Order detail panels ;; Order detail panels
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-item-image (&key (src :as string) (alt :as string)) (defcomp ~shared:orders/item-image (&key (src :as string) (alt :as string))
(img :src src :alt alt :class "w-full h-full object-contain object-center" :loading "lazy" :decoding "async")) (img :src src :alt alt :class "w-full h-full object-contain object-center" :loading "lazy" :decoding "async"))
(defcomp ~order-item-no-image () (defcomp ~shared:orders/item-no-image ()
(div :class "w-full h-full flex items-center justify-center text-[9px] text-stone-400" "No image")) (div :class "w-full h-full flex items-center justify-center text-[9px] text-stone-400" "No image"))
(defcomp ~order-item-row (&key (href :as string) img (title :as string) (pid :as string) (defcomp ~shared:orders/item-row (&key (href :as string) img (title :as string) (pid :as string)
(qty :as string) (price :as string)) (qty :as string) (price :as string))
(li (a :class "w-full py-2 flex gap-3" :href href (li (a :class "w-full py-2 flex gap-3" :href href
(div :class "w-12 h-12 sm:w-14 sm:h-14 rounded-md bg-stone-100 flex-shrink-0 overflow-hidden" img) (div :class "w-12 h-12 sm:w-14 sm:h-14 rounded-md bg-stone-100 flex-shrink-0 overflow-hidden" img)
@@ -81,12 +81,12 @@
(p qty) (p qty)
(p price)))))) (p price))))))
(defcomp ~order-items-panel (&key items) (defcomp ~shared:orders/items-panel (&key items)
(div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6" (div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6"
(h2 :class "text-sm sm:text-base font-semibold mb-3" "Items") (h2 :class "text-sm sm:text-base font-semibold mb-3" "Items")
(ul :class "divide-y divide-stone-100 text-xs sm:text-sm" items))) (ul :class "divide-y divide-stone-100 text-xs sm:text-sm" items)))
(defcomp ~order-calendar-entry (&key (name :as string) (pill :as string) (status :as string) (defcomp ~shared:orders/calendar-entry (&key (name :as string) (pill :as string) (status :as string)
(date-str :as string) (cost :as string)) (date-str :as string) (cost :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
(div (div :class "font-medium flex items-center gap-2" (div (div :class "font-medium flex items-center gap-2"
@@ -94,19 +94,19 @@
(div :class "text-xs text-stone-500" date-str)) (div :class "text-xs text-stone-500" date-str))
(div :class "ml-4 font-medium" cost))) (div :class "ml-4 font-medium" cost)))
(defcomp ~order-calendar-section (&key items) (defcomp ~shared:orders/calendar-section (&key items)
(section :class "mt-6 space-y-3" (section :class "mt-6 space-y-3"
(h2 :class "text-base sm:text-lg font-semibold" "Calendar bookings in this order") (h2 :class "text-base sm:text-lg font-semibold" "Calendar bookings in this order")
(ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items))) (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
(defcomp ~order-detail-panel (&key summary items calendar) (defcomp ~shared:orders/detail-panel (&key summary items calendar)
(div :class "max-w-full px-3 py-3 space-y-4" summary items calendar)) (div :class "max-w-full px-3 py-3 space-y-4" summary items calendar))
(defcomp ~order-pay-btn (&key (url :as string)) (defcomp ~shared:orders/pay-btn (&key (url :as string))
(a :href url :class "inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition" (a :href url :class "inline-flex items-center px-3 py-2 text-xs sm:text-sm rounded-full border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 transition"
(i :class "fa fa-credit-card mr-2" :aria-hidden "true") "Open payment page")) (i :class "fa fa-credit-card mr-2" :aria-hidden "true") "Open payment page"))
(defcomp ~order-detail-filter (&key (info :as string) (list-url :as string) (recheck-url :as string) (defcomp ~shared:orders/detail-filter (&key (info :as string) (list-url :as string) (recheck-url :as string)
(csrf :as string) pay) (csrf :as string) pay)
(header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4" (header :class "mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4"
(div :class "space-y-1" (div :class "space-y-1"
@@ -120,43 +120,43 @@
(i :class "fa-solid fa-rotate mr-2" :aria-hidden "true") "Re-check status")) (i :class "fa-solid fa-rotate mr-2" :aria-hidden "true") "Re-check status"))
pay))) pay)))
(defcomp ~order-detail-header-stack (&key auth orders order) (defcomp ~shared:orders/detail-header-stack (&key auth orders order)
(~header-child-sx :inner (~shared:layout/header-child-sx :inner
(<> auth (~header-child-sx :id "auth-header-child" :inner (<> auth (~shared:layout/header-child-sx :id "auth-header-child" :inner
(<> orders (~header-child-sx :id "orders-header-child" :inner order)))))) (<> orders (~shared:layout/header-child-sx :id "orders-header-child" :inner order))))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Data-driven order rows (replaces Python loop) ;; Data-driven order rows (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-rows-from-data (&key (orders :as list?) (page :as number) (total-pages :as number) (defcomp ~shared:orders/rows-from-data (&key (orders :as list?) (page :as number) (total-pages :as number)
(next-url :as string?)) (next-url :as string?))
(<> (<>
(map (lambda (o) (map (lambda (o)
(<> (<>
(~order-row-desktop :oid (get o "oid") :created (get o "created") (~shared:orders/row-desktop :oid (get o "oid") :created (get o "created")
:desc (get o "desc") :total (get o "total") :desc (get o "desc") :total (get o "total")
:pill (get o "pill_desktop") :status (get o "status") :url (get o "url")) :pill (get o "pill_desktop") :status (get o "status") :url (get o "url"))
(~order-row-mobile :oid (get o "oid") :created (get o "created") (~shared:orders/row-mobile :oid (get o "oid") :created (get o "created")
:total (get o "total") :pill (get o "pill_mobile") :total (get o "total") :pill (get o "pill_mobile")
:status (get o "status") :url (get o "url")))) :status (get o "status") :url (get o "url"))))
(or orders (list))) (or orders (list)))
(if next-url (if next-url
(~infinite-scroll :url next-url :page page :total-pages total-pages (~shared:controls/infinite-scroll :url next-url :page page :total-pages total-pages
:id-prefix "orders" :colspan 5) :id-prefix "orders" :colspan 5)
(~order-end-row)))) (~shared:orders/end-row))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Data-driven order items (replaces Python loop) ;; Data-driven order items (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-items-from-data (&key (items :as list?)) (defcomp ~shared:orders/items-from-data (&key (items :as list?))
(~order-items-panel (~shared:orders/items-panel
:items (<> (map (lambda (item) :items (<> (map (lambda (item)
(let* ((img (if (get item "product_image") (let* ((img (if (get item "product_image")
(~order-item-image :src (get item "product_image") :alt (or (get item "product_title") "Product image")) (~shared:orders/item-image :src (get item "product_image") :alt (or (get item "product_title") "Product image"))
(~order-item-no-image)))) (~shared:orders/item-no-image))))
(~order-item-row (~shared:orders/item-row
:href (get item "href") :img img :href (get item "href") :img img
:title (or (get item "product_title") "Unknown product") :title (or (get item "product_title") "Unknown product")
:pid (str "Product ID: " (get item "product_id")) :pid (str "Product ID: " (get item "product_id"))
@@ -168,10 +168,10 @@
;; Data-driven calendar entries (replaces Python loop) ;; Data-driven calendar entries (replaces Python loop)
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~order-calendar-from-data (&key (entries :as list?)) (defcomp ~shared:orders/calendar-from-data (&key (entries :as list?))
(~order-calendar-section (~shared:orders/calendar-section
:items (<> (map (lambda (e) :items (<> (map (lambda (e)
(~order-calendar-entry (~shared:orders/calendar-entry
:name (get e "name") :pill (get e "pill") :name (get e "name") :pill (get e "pill")
:status (get e "status") :date-str (get e "date_str") :status (get e "status") :date-str (get e "date_str")
:cost (get e "cost"))) :cost (get e "cost")))
@@ -186,7 +186,7 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Status pill class mapping ;; Status pill class mapping
(defcomp ~order-status-pill-cls (&key (status :as string?)) (defcomp ~shared:orders/status-pill-cls (&key (status :as string?))
(let* ((sl (lower (or status "")))) (let* ((sl (lower (or status ""))))
(cond (cond
((= sl "paid") "border-emerald-300 bg-emerald-50 text-emerald-700") ((= sl "paid") "border-emerald-300 bg-emerald-50 text-emerald-700")
@@ -194,46 +194,46 @@
(true "border-stone-300 bg-stone-50 text-stone-700")))) (true "border-stone-300 bg-stone-50 text-stone-700"))))
;; Single order row pair (desktop + mobile) — takes serialized order data dict ;; Single order row pair (desktop + mobile) — takes serialized order data dict
(defcomp ~order-row-pair (&key (order :as dict) (detail-url-prefix :as string)) (defcomp ~shared:orders/row-pair (&key (order :as dict) (detail-url-prefix :as string))
(let* ((status (or (get order "status") "pending")) (let* ((status (or (get order "status") "pending"))
(pill-base (~order-status-pill-cls :status status)) (pill-base (~shared:orders/status-pill-cls :status status))
(oid (str "#" (get order "id"))) (oid (str "#" (get order "id")))
(created (or (get order "created_at_formatted") "\u2014")) (created (or (get order "created_at_formatted") "\u2014"))
(desc (or (get order "description") "")) (desc (or (get order "description") ""))
(total (str (or (get order "currency") "GBP") " " (or (get order "total_formatted") "0.00"))) (total (str (or (get order "currency") "GBP") " " (or (get order "total_formatted") "0.00")))
(url (str detail-url-prefix (get order "id") "/"))) (url (str detail-url-prefix (get order "id") "/")))
(<> (<>
(~order-row-desktop (~shared:orders/row-desktop
:oid oid :created created :desc desc :total total :oid oid :created created :desc desc :total total
:pill (str "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] sm:text-xs " pill-base) :pill (str "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] sm:text-xs " pill-base)
:status status :url url) :status status :url url)
(~order-row-mobile (~shared:orders/row-mobile
:oid oid :created created :total total :oid oid :created created :total total
:pill (str "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] " pill-base) :pill (str "inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] " pill-base)
:status status :url url)))) :status status :url url))))
;; Assembled orders list content ;; Assembled orders list content
(defcomp ~orders-list-content (&key (orders :as list) (page :as number) (total-pages :as number) (defcomp ~shared:orders/list-content (&key (orders :as list) (page :as number) (total-pages :as number)
(rows-url :as string) (detail-url-prefix :as string)) (rows-url :as string) (detail-url-prefix :as string))
(if (empty? orders) (if (empty? orders)
(~order-empty-state) (~shared:orders/empty-state)
(~order-table (~shared:orders/table
:rows (<> :rows (<>
(map (lambda (order) (map (lambda (order)
(~order-row-pair :order order :detail-url-prefix detail-url-prefix)) (~shared:orders/row-pair :order order :detail-url-prefix detail-url-prefix))
orders) orders)
(if (< page total-pages) (if (< page total-pages)
(~infinite-scroll (~shared:controls/infinite-scroll
:url (str rows-url "?page=" (inc page)) :url (str rows-url "?page=" (inc page))
:page page :total-pages total-pages :page page :total-pages total-pages
:id-prefix "orders" :colspan 5) :id-prefix "orders" :colspan 5)
(~order-end-row)))))) (~shared:orders/end-row))))))
;; Assembled order detail content — replaces Python _order_main_sx ;; Assembled order detail content — replaces Python _order_main_sx
(defcomp ~order-detail-content (&key (order :as dict) (calendar-entries :as list?)) (defcomp ~shared:orders/detail-content (&key (order :as dict) (calendar-entries :as list?))
(let* ((items (get order "items"))) (let* ((items (get order "items")))
(~order-detail-panel (~shared:orders/detail-panel
:summary (~order-summary-card :summary (~shared:cards/order-summary-card
:order-id (get order "id") :order-id (get order "id")
:created-at (get order "created_at_formatted") :created-at (get order "created_at_formatted")
:description (get order "description") :description (get order "description")
@@ -241,21 +241,21 @@
:currency (get order "currency") :currency (get order "currency")
:total-amount (get order "total_formatted")) :total-amount (get order "total_formatted"))
:items (when (not (empty? (or items (list)))) :items (when (not (empty? (or items (list))))
(~order-items-panel (~shared:orders/items-panel
:items (map (lambda (item) :items (map (lambda (item)
(~order-item-row (~shared:orders/item-row
:href (get item "product_url") :href (get item "product_url")
:img (if (get item "product_image") :img (if (get item "product_image")
(~order-item-image :src (get item "product_image") (~shared:orders/item-image :src (get item "product_image")
:alt (or (get item "product_title") "Product image")) :alt (or (get item "product_title") "Product image"))
(~order-item-no-image)) (~shared:orders/item-no-image))
:title (or (get item "product_title") "Unknown product") :title (or (get item "product_title") "Unknown product")
:pid (str "Product ID: " (get item "product_id")) :pid (str "Product ID: " (get item "product_id"))
:qty (str "Qty: " (get item "quantity")) :qty (str "Qty: " (get item "quantity"))
:price (str (or (get item "currency") (get order "currency") "GBP") " " (or (get item "unit_price_formatted") "0.00")))) :price (str (or (get item "currency") (get order "currency") "GBP") " " (or (get item "unit_price_formatted") "0.00"))))
items))) items)))
:calendar (when (not (empty? (or calendar-entries (list)))) :calendar (when (not (empty? (or calendar-entries (list))))
(~order-calendar-section (~shared:orders/calendar-section
:items (map (lambda (e) :items (map (lambda (e)
(let* ((st (or (get e "state") "")) (let* ((st (or (get e "state") ""))
(pill (cond (pill (cond
@@ -263,7 +263,7 @@
((= st "provisional") "bg-amber-100 text-amber-800") ((= st "provisional") "bg-amber-100 text-amber-800")
((= st "ordered") "bg-blue-100 text-blue-800") ((= st "ordered") "bg-blue-100 text-blue-800")
(true "bg-stone-100 text-stone-700")))) (true "bg-stone-100 text-stone-700"))))
(~order-calendar-entry (~shared:orders/calendar-entry
:name (get e "name") :name (get e "name")
:pill (str "inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium " pill) :pill (str "inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium " pill)
:status (upper (slice st 0 1)) :status (upper (slice st 0 1))
@@ -272,33 +272,33 @@
calendar-entries)))))) calendar-entries))))))
;; Assembled order detail filter — replaces Python _order_filter_sx ;; Assembled order detail filter — replaces Python _order_filter_sx
(defcomp ~order-detail-filter-content (&key (order :as dict) (list-url :as string) (recheck-url :as string) (defcomp ~shared:orders/detail-filter-content (&key (order :as dict) (list-url :as string) (recheck-url :as string)
(pay-url :as string) (csrf :as string)) (pay-url :as string) (csrf :as string))
(let* ((status (or (get order "status") "pending")) (let* ((status (or (get order "status") "pending"))
(created (or (get order "created_at_formatted") "\u2014"))) (created (or (get order "created_at_formatted") "\u2014")))
(~order-detail-filter (~shared:orders/detail-filter
:info (str "Placed " created " \u00b7 Status: " status) :info (str "Placed " created " \u00b7 Status: " status)
:list-url list-url :list-url list-url
:recheck-url recheck-url :recheck-url recheck-url
:csrf csrf :csrf csrf
:pay (when (!= status "paid") :pay (when (!= status "paid")
(~order-pay-btn :url pay-url))))) (~shared:orders/pay-btn :url pay-url)))))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; Checkout return components ;; Checkout return components
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~checkout-return-header (&key (status :as string)) (defcomp ~shared:orders/checkout-return-header (&key (status :as string))
(header :class "mb-6 sm:mb-8" (header :class "mb-6 sm:mb-8"
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Payment complete") (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Payment complete")
(p :class "text-xs sm:text-sm text-stone-600" (p :class "text-xs sm:text-sm text-stone-600"
(str "Your checkout session is " status ".")))) (str "Your checkout session is " status "."))))
(defcomp ~checkout-return-missing () (defcomp ~shared:orders/checkout-return-missing ()
(div :class "max-w-full px-3 py-3 space-y-4" (div :class "max-w-full px-3 py-3 space-y-4"
(p :class "text-sm text-stone-600" "Order not found."))) (p :class "text-sm text-stone-600" "Order not found.")))
(defcomp ~checkout-return-ticket (&key (name :as string) (pill :as string) (state :as string) (defcomp ~shared:orders/checkout-return-ticket (&key (name :as string) (pill :as string) (state :as string)
(type-name :as string?) (date-str :as string) (code :as string?) (type-name :as string?) (date-str :as string) (code :as string?)
(price :as string)) (price :as string))
(li :class "px-4 py-3 flex items-start justify-between text-sm" (li :class "px-4 py-3 flex items-start justify-between text-sm"
@@ -310,23 +310,23 @@
(when code (div :class "font-mono text-xs text-stone-400" code))) (when code (div :class "font-mono text-xs text-stone-400" code)))
(div :class "ml-4 font-medium" price))) (div :class "ml-4 font-medium" price)))
(defcomp ~checkout-return-tickets (&key items) (defcomp ~shared:orders/checkout-return-tickets (&key items)
(section :class "mt-6 space-y-3" (section :class "mt-6 space-y-3"
(h2 :class "text-base sm:text-lg font-semibold" "Tickets") (h2 :class "text-base sm:text-lg font-semibold" "Tickets")
(ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items))) (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items)))
(defcomp ~checkout-return-failed (&key (order-id :as string?)) (defcomp ~shared:orders/checkout-return-failed (&key (order-id :as string?))
(div :class "rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900" (div :class "rounded-lg border border-rose-200 bg-rose-50 p-4 text-sm text-rose-900"
(p :class "font-medium" "Payment failed") (p :class "font-medium" "Payment failed")
(p "Please try again or contact support." (p "Please try again or contact support."
(when order-id (span " Order #" (str order-id)))))) (when order-id (span " Order #" (str order-id))))))
(defcomp ~checkout-return-paid () (defcomp ~shared:orders/checkout-return-paid ()
(div :class "rounded-lg border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900" (div :class "rounded-lg border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900"
(p :class "font-medium" "Payment successful!") (p :class "font-medium" "Payment successful!")
(p "Your order has been confirmed."))) (p "Your order has been confirmed.")))
(defcomp ~checkout-return-content (&key summary items calendar tickets status-message) (defcomp ~shared:orders/checkout-return-content (&key summary items calendar tickets status-message)
(div :class "max-w-full px-3 py-3 space-y-4" (div :class "max-w-full px-3 py-3 space-y-4"
status-message summary items calendar tickets)) status-message summary items calendar tickets))
@@ -334,15 +334,15 @@
;; Checkout error screens ;; Checkout error screens
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~checkout-error-header () (defcomp ~shared:orders/checkout-error-header ()
(header :class "mb-6 sm:mb-8" (header :class "mb-6 sm:mb-8"
(h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Checkout error") (h1 :class "text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight" "Checkout error")
(p :class "text-xs sm:text-sm text-stone-600" "We tried to start your payment with SumUp but hit a problem."))) (p :class "text-xs sm:text-sm text-stone-600" "We tried to start your payment with SumUp but hit a problem.")))
(defcomp ~checkout-error-order-id (&key (oid :as string)) (defcomp ~shared:orders/checkout-error-order-id (&key (oid :as string))
(p :class "text-xs text-rose-800/80" "Order ID: " (span :class "font-mono" oid))) (p :class "text-xs text-rose-800/80" "Order ID: " (span :class "font-mono" oid)))
(defcomp ~checkout-error-content (&key (msg :as string) order (back-url :as string)) (defcomp ~shared:orders/checkout-error-content (&key (msg :as string) order (back-url :as string))
(div :class "max-w-full px-3 py-3 space-y-4" (div :class "max-w-full px-3 py-3 space-y-4"
(div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2" (div :class "rounded-2xl border border-rose-200 bg-rose-50/80 p-4 sm:p-6 text-sm text-rose-900 space-y-2"
(p :class "font-medium" "Something went wrong.") (p :class "font-medium" "Something went wrong.")

View File

@@ -1,4 +1,4 @@
(defcomp ~base-shell (&key (title :as string) (asset-url :as string) &rest children) (defcomp ~shared:pages/base-shell (&key (title :as string) (asset-url :as string) &rest children)
(<> (<>
(raw! "<!doctype html>") (raw! "<!doctype html>")
(html :lang "en" (html :lang "en"
@@ -23,14 +23,14 @@
;; <script>__sxResolve("id", "(resolved sx ...)")</script> ;; <script>__sxResolve("id", "(resolved sx ...)")</script>
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~suspense (&key (id :as string) fallback &rest children) (defcomp ~shared:pages/suspense (&key (id :as string) fallback &rest children)
(div :id (str "sx-suspense-" id) (div :id (str "sx-suspense-" id)
:data-suspense id :data-suspense id
:style "display:contents" :style "display:contents"
(if (not (empty? children)) children fallback))) (if (not (empty? children)) children fallback)))
(defcomp ~error-page (&key (title :as string) (message :as string) (image :as string?) (asset-url :as string)) (defcomp ~shared:pages/error-page (&key (title :as string) (message :as string) (image :as string?) (asset-url :as string))
(~base-shell :title title :asset-url asset-url (~shared:pages/base-shell :title title :asset-url asset-url
(div :class "text-center p-8 max-w-lg mx-auto" (div :class "text-center p-8 max-w-lg mx-auto"
(div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4" (div :class "font-bold text-2xl md:text-4xl text-red-500 mb-4"
(div message)) (div message))

View File

@@ -1,4 +1,4 @@
(defcomp ~relation-attach (&key (create-url :as string) (label :as string?) (icon :as string?)) (defcomp ~shared:relations/attach (&key (create-url :as string) (label :as string?) (icon :as string?))
(a :href create-url (a :href create-url
:sx-get create-url :sx-get create-url
:sx-target "#main-panel" :sx-target "#main-panel"
@@ -8,7 +8,7 @@
(when icon (i :class icon)) (when icon (i :class icon))
(span (or label "Add")))) (span (or label "Add"))))
(defcomp ~relation-detach (&key (detach-url :as string) (name :as string?)) (defcomp ~shared:relations/detach (&key (detach-url :as string) (name :as string?))
(button :sx-delete detach-url (button :sx-delete detach-url
:sx-confirm (str "Remove " (or name "this item") "?") :sx-confirm (str "Remove " (or name "this item") "?")
:class "text-red-500 hover:text-red-700 text-sm p-1 rounded hover:bg-red-50 transition-colors" :class "text-red-500 hover:text-red-700 text-sm p-1 rounded hover:bg-red-50 transition-colors"

View File

@@ -11,7 +11,7 @@
;; - Pre-rendered meta HTML from callers ;; - Pre-rendered meta HTML from callers
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
(defcomp ~sx-page-shell (&key (title :as string) (meta-html :as string?) (csrf :as string) (defcomp ~shared:shell/sx-page-shell (&key (title :as string) (meta-html :as string?) (csrf :as string)
(sx-css :as string?) (sx-css-classes :as string?) (sx-css :as string?) (sx-css-classes :as string?)
(component-hash :as string?) (component-defs :as string?) (component-hash :as string?) (component-defs :as string?)
(pages-sx :as string?) (page-sx :as string?) (pages-sx :as string?) (page-sx :as string?)

View File

@@ -14,13 +14,13 @@ def _load_components():
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~cart-mini # ~shared:fragments/cart-mini
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCartMini: class TestCartMini:
def test_empty_cart_shows_logo(self): def test_empty_cart_shows_logo(self):
html = sx( html = sx(
'(~cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)', '(~shared:fragments/cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)',
**{"cart-count": 0, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"}, **{"cart-count": 0, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"},
) )
assert 'id="cart-mini"' in html assert 'id="cart-mini"' in html
@@ -30,7 +30,7 @@ class TestCartMini:
def test_nonempty_cart_shows_badge(self): def test_nonempty_cart_shows_badge(self):
html = sx( html = sx(
'(~cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)', '(~shared:fragments/cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)',
**{"cart-count": 3, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"}, **{"cart-count": 3, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"},
) )
assert 'id="cart-mini"' in html assert 'id="cart-mini"' in html
@@ -41,13 +41,13 @@ class TestCartMini:
def test_oob_attribute(self): def test_oob_attribute(self):
html = sx( html = sx(
'(~cart-mini :cart-count 0 :blog-url "" :cart-url "" :oob "true")', '(~shared:fragments/cart-mini :cart-count 0 :blog-url "" :cart-url "" :oob "true")',
) )
assert 'sx-swap-oob="true"' in html assert 'sx-swap-oob="true"' in html
def test_no_oob_when_nil(self): def test_no_oob_when_nil(self):
html = sx( html = sx(
'(~cart-mini :cart-count 0 :blog-url "" :cart-url "")', '(~shared:fragments/cart-mini :cart-count 0 :blog-url "" :cart-url "")',
) )
assert "sx-swap-oob" not in html assert "sx-swap-oob" not in html
@@ -94,13 +94,13 @@ class TestAuthMenu:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~account-nav-item # ~shared:fragments/account-nav-item
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestAccountNavItem: class TestAccountNavItem:
def test_renders_link(self): def test_renders_link(self):
html = sx( html = sx(
'(~account-nav-item :href "/orders/" :label "orders")', '(~shared:fragments/account-nav-item :href "/orders/" :label "orders")',
) )
assert 'href="/orders/"' in html assert 'href="/orders/"' in html
assert ">orders<" in html assert ">orders<" in html
@@ -109,19 +109,19 @@ class TestAccountNavItem:
def test_custom_label(self): def test_custom_label(self):
html = sx( html = sx(
'(~account-nav-item :href "/cart/orders/" :label "my orders")', '(~shared:fragments/account-nav-item :href "/cart/orders/" :label "my orders")',
) )
assert ">my orders<" in html assert ">my orders<" in html
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~calendar-entry-nav # ~shared:navigation/calendar-entry-nav
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCalendarEntryNav: class TestCalendarEntryNav:
def test_renders_entry(self): def test_renders_entry(self):
html = sx( html = sx(
'(~calendar-entry-nav :href "/events/entry/1/" :name "Workshop" :date-str "Jan 15, 2026 at 14:00" :nav-class "btn")', '(~shared:navigation/calendar-entry-nav :href "/events/entry/1/" :name "Workshop" :date-str "Jan 15, 2026 at 14:00" :nav-class "btn")',
**{"date-str": "Jan 15, 2026 at 14:00", "nav-class": "btn"}, **{"date-str": "Jan 15, 2026 at 14:00", "nav-class": "btn"},
) )
assert 'href="/events/entry/1/"' in html assert 'href="/events/entry/1/"' in html
@@ -130,13 +130,13 @@ class TestCalendarEntryNav:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~calendar-link-nav # ~shared:navigation/calendar-link-nav
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCalendarLinkNav: class TestCalendarLinkNav:
def test_renders_calendar_link(self): def test_renders_calendar_link(self):
html = sx( html = sx(
'(~calendar-link-nav :href "/events/cal/" :name "Art Events" :nav-class "btn")', '(~shared:navigation/calendar-link-nav :href "/events/cal/" :name "Art Events" :nav-class "btn")',
**{"nav-class": "btn"}, **{"nav-class": "btn"},
) )
assert 'href="/events/cal/"' in html assert 'href="/events/cal/"' in html
@@ -145,13 +145,13 @@ class TestCalendarLinkNav:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~market-link-nav # ~shared:navigation/market-link-nav
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestMarketLinkNav: class TestMarketLinkNav:
def test_renders_market_link(self): def test_renders_market_link(self):
html = sx( html = sx(
'(~market-link-nav :href "/market/farm/" :name "Farm Shop" :nav-class "btn")', '(~shared:navigation/market-link-nav :href "/market/farm/" :name "Farm Shop" :nav-class "btn")',
**{"nav-class": "btn"}, **{"nav-class": "btn"},
) )
assert 'href="/market/farm/"' in html assert 'href="/market/farm/"' in html
@@ -160,13 +160,13 @@ class TestMarketLinkNav:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~post-card # ~shared:cards/post-card
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestPostCard: class TestPostCard:
def test_basic_card(self): def test_basic_card(self):
html = sx( html = sx(
'(~post-card :title "Hello World" :slug "hello" :href "/hello/"' '(~shared:cards/post-card :title "Hello World" :slug "hello" :href "/hello/"'
' :feature-image "/img/hello.jpg" :excerpt "A test post"' ' :feature-image "/img/hello.jpg" :excerpt "A test post"'
' :status "published" :published-at "15 Jan 2026"' ' :status "published" :published-at "15 Jan 2026"'
' :hx-select "#main-panel")', ' :hx-select "#main-panel")',
@@ -184,7 +184,7 @@ class TestPostCard:
def test_draft_status(self): def test_draft_status(self):
html = sx( html = sx(
'(~post-card :title "Draft" :slug "draft" :href "/draft/"' '(~shared:cards/post-card :title "Draft" :slug "draft" :href "/draft/"'
' :status "draft" :updated-at "15 Jan 2026"' ' :status "draft" :updated-at "15 Jan 2026"'
' :hx-select "#main-panel")', ' :hx-select "#main-panel")',
**{"hx-select": "#main-panel", "updated-at": "15 Jan 2026"}, **{"hx-select": "#main-panel", "updated-at": "15 Jan 2026"},
@@ -195,7 +195,7 @@ class TestPostCard:
def test_draft_with_publish_requested(self): def test_draft_with_publish_requested(self):
html = sx( html = sx(
'(~post-card :title "Pending" :slug "pending" :href "/pending/"' '(~shared:cards/post-card :title "Pending" :slug "pending" :href "/pending/"'
' :status "draft" :publish-requested true' ' :status "draft" :publish-requested true'
' :hx-select "#main-panel")', ' :hx-select "#main-panel")',
**{"hx-select": "#main-panel", "publish-requested": True}, **{"hx-select": "#main-panel", "publish-requested": True},
@@ -205,7 +205,7 @@ class TestPostCard:
def test_no_image(self): def test_no_image(self):
html = sx( html = sx(
'(~post-card :title "No Img" :slug "no-img" :href "/no-img/"' '(~shared:cards/post-card :title "No Img" :slug "no-img" :href "/no-img/"'
' :status "published" :hx-select "#main-panel")', ' :status "published" :hx-select "#main-panel")',
**{"hx-select": "#main-panel"}, **{"hx-select": "#main-panel"},
) )
@@ -214,7 +214,7 @@ class TestPostCard:
def test_widgets_and_at_bar(self): def test_widgets_and_at_bar(self):
"""Widgets and at-bar are sx kwarg slots rendered by the client.""" """Widgets and at-bar are sx kwarg slots rendered by the client."""
html = sx( html = sx(
'(~post-card :title "T" :slug "s" :href "/"' '(~shared:cards/post-card :title "T" :slug "s" :href "/"'
' :status "published" :hx-select "#mp")', ' :status "published" :hx-select "#mp")',
**{"hx-select": "#mp"}, **{"hx-select": "#mp"},
) )
@@ -224,13 +224,13 @@ class TestPostCard:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~base-shell and ~error-page # ~shared:pages/base-shell and ~shared:pages/error-page
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestBaseShell: class TestBaseShell:
def test_renders_full_page(self): def test_renders_full_page(self):
html = sx( html = sx(
'(~base-shell :title "Test" :asset-url "/static" (p "Hello"))', '(~shared:pages/base-shell :title "Test" :asset-url "/static" (p "Hello"))',
**{"asset-url": "/static"}, **{"asset-url": "/static"},
) )
assert "<!doctype html>" in html assert "<!doctype html>" in html
@@ -243,7 +243,7 @@ class TestBaseShell:
class TestErrorPage: class TestErrorPage:
def test_404_page(self): def test_404_page(self):
html = sx( html = sx(
'(~error-page :title "404 Error" :message "NOT FOUND" :image "/static/errors/404.gif" :asset-url "/static")', '(~shared:pages/error-page :title "404 Error" :message "NOT FOUND" :image "/static/errors/404.gif" :asset-url "/static")',
**{"asset-url": "/static"}, **{"asset-url": "/static"},
) )
assert "<!doctype html>" in html assert "<!doctype html>" in html
@@ -253,7 +253,7 @@ class TestErrorPage:
def test_error_page_no_image(self): def test_error_page_no_image(self):
html = sx( html = sx(
'(~error-page :title "500 Error" :message "SERVER ERROR" :asset-url "/static")', '(~shared:pages/error-page :title "500 Error" :message "SERVER ERROR" :asset-url "/static")',
**{"asset-url": "/static"}, **{"asset-url": "/static"},
) )
assert "SERVER ERROR" in html assert "SERVER ERROR" in html
@@ -261,13 +261,13 @@ class TestErrorPage:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~relation-nav # ~shared:navigation/relation-nav
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestRelationNav: class TestRelationNav:
def test_renders_link(self): def test_renders_link(self):
html = sx( html = sx(
'(~relation-nav :href "/market/farm/" :name "Farm Shop" :icon "fa fa-shopping-bag")', '(~shared:navigation/relation-nav :href "/market/farm/" :name "Farm Shop" :icon "fa fa-shopping-bag")',
) )
assert 'href="/market/farm/"' in html assert 'href="/market/farm/"' in html
assert "Farm Shop" in html assert "Farm Shop" in html
@@ -275,7 +275,7 @@ class TestRelationNav:
def test_no_icon(self): def test_no_icon(self):
html = sx( html = sx(
'(~relation-nav :href "/cal/" :name "Events")', '(~shared:navigation/relation-nav :href "/cal/" :name "Events")',
) )
assert 'href="/cal/"' in html assert 'href="/cal/"' in html
assert "Events" in html assert "Events" in html
@@ -283,20 +283,20 @@ class TestRelationNav:
def test_custom_nav_class(self): def test_custom_nav_class(self):
html = sx( html = sx(
'(~relation-nav :href "/" :name "X" :nav-class "custom-class")', '(~shared:navigation/relation-nav :href "/" :name "X" :nav-class "custom-class")',
**{"nav-class": "custom-class"}, **{"nav-class": "custom-class"},
) )
assert 'class="custom-class"' in html assert 'class="custom-class"' in html
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~relation-attach # ~shared:relations/attach
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestRelationAttach: class TestRelationAttach:
def test_renders_button(self): def test_renders_button(self):
html = sx( html = sx(
'(~relation-attach :create-url "/market/create/" :label "Add Market" :icon "fa fa-plus")', '(~shared:relations/attach :create-url "/market/create/" :label "Add Market" :icon "fa fa-plus")',
**{"create-url": "/market/create/"}, **{"create-url": "/market/create/"},
) )
assert 'href="/market/create/"' in html assert 'href="/market/create/"' in html
@@ -306,20 +306,20 @@ class TestRelationAttach:
def test_default_label(self): def test_default_label(self):
html = sx( html = sx(
'(~relation-attach :create-url "/create/")', '(~shared:relations/attach :create-url "/create/")',
**{"create-url": "/create/"}, **{"create-url": "/create/"},
) )
assert "Add" in html assert "Add" in html
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# ~relation-detach # ~shared:relations/detach
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestRelationDetach: class TestRelationDetach:
def test_renders_button(self): def test_renders_button(self):
html = sx( html = sx(
'(~relation-detach :detach-url "/api/unrelate" :name "Farm Shop")', '(~shared:relations/detach :detach-url "/api/unrelate" :name "Farm Shop")',
**{"detach-url": "/api/unrelate"}, **{"detach-url": "/api/unrelate"},
) )
assert 'sx-delete="/api/unrelate"' in html assert 'sx-delete="/api/unrelate"' in html
@@ -328,7 +328,7 @@ class TestRelationDetach:
def test_default_name(self): def test_default_name(self):
html = sx( html = sx(
'(~relation-detach :detach-url "/api/unrelate")', '(~shared:relations/detach :detach-url "/api/unrelate")',
**{"detach-url": "/api/unrelate"}, **{"detach-url": "/api/unrelate"},
) )
assert "this item" in html assert "this item" in html
@@ -343,7 +343,7 @@ class TestRenderPage:
from shared.sx.page import render_page from shared.sx.page import render_page
html = render_page( html = render_page(
'(~error-page :title "Test" :message "MSG" :asset-url "/s")', '(~shared:pages/error-page :title "Test" :message "MSG" :asset-url "/s")',
**{"asset-url": "/s"}, **{"asset-url": "/s"},
) )
assert "<!doctype html>" in html assert "<!doctype html>" in html

View File

@@ -33,10 +33,10 @@ def make_env(*sx_sources: str) -> dict:
class TestScanAst: class TestScanAst:
def test_simple_component_ref(self): def test_simple_component_ref(self):
env = make_env('(defcomp ~card (&key title) (div (~badge :label title)))') env = make_env('(defcomp ~card (&key title) (div (~shared:misc/badge :label title)))')
comp = env["~card"] comp = env["~card"]
refs = _scan_ast(comp.body) refs = _scan_ast(comp.body)
assert refs == {"~badge"} assert refs == {"~shared:misc/badge"}
def test_no_refs(self): def test_no_refs(self):
env = make_env('(defcomp ~plain (&key text) (div :class "p-4" text))') env = make_env('(defcomp ~plain (&key text) (div :class "p-4" text))')
@@ -77,11 +77,11 @@ class TestScanAst:
class TestTransitiveDeps: class TestTransitiveDeps:
def test_direct_dep(self): def test_direct_dep(self):
env = make_env( env = make_env(
'(defcomp ~card (&key) (div (~badge)))', '(defcomp ~card (&key) (div (~shared:misc/badge)))',
'(defcomp ~badge (&key) (span ""))', '(defcomp ~shared:misc/badge (&key) (span ""))',
) )
deps = transitive_deps("~card", env) deps = transitive_deps("~card", env)
assert deps == {"~badge"} assert deps == {"~shared:misc/badge"}
def test_transitive(self): def test_transitive(self):
env = make_env( env = make_env(
@@ -115,11 +115,11 @@ class TestTransitiveDeps:
def test_without_tilde_prefix(self): def test_without_tilde_prefix(self):
env = make_env( env = make_env(
'(defcomp ~card (&key) (div (~badge)))', '(defcomp ~card (&key) (div (~shared:misc/badge)))',
'(defcomp ~badge (&key) (span ""))', '(defcomp ~shared:misc/badge (&key) (span ""))',
) )
deps = transitive_deps("card", env) deps = transitive_deps("card", env)
assert deps == {"~badge"} assert deps == {"~shared:misc/badge"}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -130,13 +130,13 @@ class TestComputeAllDeps:
def test_sets_deps_on_components(self): def test_sets_deps_on_components(self):
env = make_env( env = make_env(
'(defcomp ~page (&key) (div (~card)))', '(defcomp ~page (&key) (div (~card)))',
'(defcomp ~card (&key) (div (~badge)))', '(defcomp ~card (&key) (div (~shared:misc/badge)))',
'(defcomp ~badge (&key) (span ""))', '(defcomp ~shared:misc/badge (&key) (span ""))',
) )
compute_all_deps(env) compute_all_deps(env)
assert env["~page"].deps == {"~card", "~badge"} assert env["~page"].deps == {"~card", "~shared:misc/badge"}
assert env["~card"].deps == {"~badge"} assert env["~card"].deps == {"~shared:misc/badge"}
assert env["~badge"].deps == set() assert env["~shared:misc/badge"].deps == set()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -145,9 +145,9 @@ class TestComputeAllDeps:
class TestScanComponentsFromSx: class TestScanComponentsFromSx:
def test_basic(self): def test_basic(self):
source = '(~card :title "hi" (~badge :label "new"))' source = '(~card :title "hi" (~shared:misc/badge :label "new"))'
refs = scan_components_from_sx(source) refs = scan_components_from_sx(source)
assert refs == {"~card", "~badge"} assert refs == {"~card", "~shared:misc/badge"}
def test_no_components(self): def test_no_components(self):
source = '(div :class "p-4" (p "hello"))' source = '(div :class "p-4" (p "hello"))'
@@ -162,8 +162,8 @@ class TestScanComponentsFromSx:
class TestComponentsNeeded: class TestComponentsNeeded:
def test_page_with_deps(self): def test_page_with_deps(self):
env = make_env( env = make_env(
'(defcomp ~page-layout (&key) (div (~nav) (~footer)))', '(defcomp ~page-layout (&key) (div (~plans/environment-images/nav) (~footer)))',
'(defcomp ~nav (&key) (nav "nav"))', '(defcomp ~plans/environment-images/nav (&key) (nav "nav"))',
'(defcomp ~footer (&key) (footer "footer"))', '(defcomp ~footer (&key) (footer "footer"))',
'(defcomp ~unused (&key) (div "not needed"))', '(defcomp ~unused (&key) (div "not needed"))',
) )
@@ -171,6 +171,6 @@ class TestComponentsNeeded:
page_sx = '(~page-layout)' page_sx = '(~page-layout)'
needed = components_needed(page_sx, env) needed = components_needed(page_sx, env)
assert "~page-layout" in needed assert "~page-layout" in needed
assert "~nav" in needed assert "~plans/environment-images/nav" in needed
assert "~footer" in needed assert "~footer" in needed
assert "~unused" not in needed assert "~unused" not in needed

View File

@@ -228,8 +228,8 @@ class TestRawHtml:
class TestComponents: class TestComponents:
def test_basic_component(self): def test_basic_component(self):
env = {} env = {}
evaluate(parse('(defcomp ~badge (&key label) (span :class "badge" label))'), env) evaluate(parse('(defcomp ~shared:misc/badge (&key label) (span :class "badge" label))'), env)
html = render(parse('(~badge :label "New")'), env) html = render(parse('(~shared:misc/badge :label "New")'), env)
assert html == '<span class="badge">New</span>' assert html == '<span class="badge">New</span>'
def test_component_with_children(self): def test_component_with_children(self):

View File

@@ -87,7 +87,7 @@ class TestScanIoRefs:
assert refs == set() assert refs == set()
def test_component_ref_not_io(self): def test_component_ref_not_io(self):
"""Component references (~name) should not appear as IO refs.""" """Component references (~plans/content-addressed-components/name) should not appear as IO refs."""
env = make_env( env = make_env(
'(defcomp ~page (&key) (div (~card :title "hi")))', '(defcomp ~page (&key) (div (~card :title "hi")))',
'(defcomp ~card (&key title) (div title))', '(defcomp ~card (&key title) (div title))',
@@ -119,8 +119,8 @@ class TestTransitiveIoRefs:
def test_transitive_io_through_dep(self): def test_transitive_io_through_dep(self):
"""IO ref in a dependency should propagate to the parent.""" """IO ref in a dependency should propagate to the parent."""
env = make_env( env = make_env(
'(defcomp ~page (&key) (div (~nav)))', '(defcomp ~page (&key) (div (~plans/environment-images/nav)))',
'(defcomp ~nav (&key) (nav (app-url "/home")))', '(defcomp ~plans/environment-images/nav (&key) (nav (app-url "/home")))',
) )
refs = _transitive_io_refs_fallback("~page", env, IO_NAMES) refs = _transitive_io_refs_fallback("~page", env, IO_NAMES)
assert refs == {"app-url"} assert refs == {"app-url"}
@@ -157,7 +157,7 @@ class TestTransitiveIoRefs:
def test_without_tilde_prefix(self): def test_without_tilde_prefix(self):
"""Should auto-add ~ prefix when not provided.""" """Should auto-add ~ prefix when not provided."""
env = make_env( env = make_env(
'(defcomp ~nav (&key) (nav (app-url "/")))', '(defcomp ~plans/environment-images/nav (&key) (nav (app-url "/")))',
) )
refs = _transitive_io_refs_fallback("nav", env, IO_NAMES) refs = _transitive_io_refs_fallback("nav", env, IO_NAMES)
assert refs == {"app-url"} assert refs == {"app-url"}
@@ -187,13 +187,13 @@ class TestTransitiveIoRefs:
class TestComputeAllIoRefs: class TestComputeAllIoRefs:
def test_sets_io_refs_on_components(self): def test_sets_io_refs_on_components(self):
env = make_env( env = make_env(
'(defcomp ~page (&key) (div (~nav) (fetch-data "x")))', '(defcomp ~page (&key) (div (~plans/environment-images/nav) (fetch-data "x")))',
'(defcomp ~nav (&key) (nav (app-url "/")))', '(defcomp ~plans/environment-images/nav (&key) (nav (app-url "/")))',
'(defcomp ~card (&key title) (div title))', '(defcomp ~card (&key title) (div title))',
) )
_compute_all_io_refs_fallback(env, IO_NAMES) _compute_all_io_refs_fallback(env, IO_NAMES)
assert env["~page"].io_refs == {"fetch-data", "app-url"} assert env["~page"].io_refs == {"fetch-data", "app-url"}
assert env["~nav"].io_refs == {"app-url"} assert env["~plans/environment-images/nav"].io_refs == {"app-url"}
assert env["~card"].io_refs == set() assert env["~card"].io_refs == set()
def test_pure_components_get_empty_set(self): def test_pure_components_get_empty_set(self):
@@ -284,8 +284,8 @@ class TestSxRefIoFunctions:
def test_transitive_io_refs(self): def test_transitive_io_refs(self):
from shared.sx.ref.sx_ref import transitive_io_refs from shared.sx.ref.sx_ref import transitive_io_refs
env = make_env( env = make_env(
'(defcomp ~page (&key) (div (~nav)))', '(defcomp ~page (&key) (div (~plans/environment-images/nav)))',
'(defcomp ~nav (&key) (nav (app-url "/")))', '(defcomp ~plans/environment-images/nav (&key) (nav (app-url "/")))',
) )
refs = transitive_io_refs("~page", env, list(IO_NAMES)) refs = transitive_io_refs("~page", env, list(IO_NAMES))
assert set(refs) == {"app-url"} assert set(refs) == {"app-url"}
@@ -299,13 +299,13 @@ class TestSxRefIoFunctions:
def test_compute_all_io_refs(self): def test_compute_all_io_refs(self):
from shared.sx.ref.sx_ref import compute_all_io_refs as ref_compute from shared.sx.ref.sx_ref import compute_all_io_refs as ref_compute
env = make_env( env = make_env(
'(defcomp ~page (&key) (div (~nav) (fetch-data "x")))', '(defcomp ~page (&key) (div (~plans/environment-images/nav) (fetch-data "x")))',
'(defcomp ~nav (&key) (nav (app-url "/")))', '(defcomp ~plans/environment-images/nav (&key) (nav (app-url "/")))',
'(defcomp ~card (&key) (div "pure"))', '(defcomp ~card (&key) (div "pure"))',
) )
ref_compute(env, list(IO_NAMES)) ref_compute(env, list(IO_NAMES))
page_refs = env["~page"].io_refs page_refs = env["~page"].io_refs
nav_refs = env["~nav"].io_refs nav_refs = env["~plans/environment-images/nav"].io_refs
card_refs = env["~card"].io_refs card_refs = env["~card"].io_refs
assert "fetch-data" in page_refs assert "fetch-data" in page_refs
assert "app-url" in page_refs assert "app-url" in page_refs
@@ -385,8 +385,8 @@ class TestFallbackVsRefParity:
def test_parity_mixed(self): def test_parity_mixed(self):
self._check_parity( self._check_parity(
'(defcomp ~layout (&key) (div (~nav) (~content) (~footer)))', '(defcomp ~layout (&key) (div (~plans/environment-images/nav) (~content) (~footer)))',
'(defcomp ~nav (&key) (nav (app-url "/")))', '(defcomp ~plans/environment-images/nav (&key) (nav (app-url "/")))',
'(defcomp ~content (&key) (main "pure content"))', '(defcomp ~content (&key) (main "pure content"))',
'(defcomp ~footer (&key) (footer (config "name")))', '(defcomp ~footer (&key) (footer (config "name")))',
) )

Some files were not shown because too many files have changed in this diff Show More