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:
@@ -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)))))
|
||||||
|
|
||||||
|
|||||||
@@ -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")))))))
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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})"
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
234
blog/sx/admin.sx
234
blog/sx/admin.sx
@@ -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))
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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)))))))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
106
blog/sx/index.sx
106
blog/sx/index.sx
@@ -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))))
|
||||||
|
|||||||
@@ -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-->"))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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" "")
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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"))))
|
||||||
|
|||||||
@@ -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")))
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))))))))))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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"))))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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: "
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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)))))))
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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")))
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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))))
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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") "")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) ".")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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?)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user