diff --git a/account/sx/auth.sx b/account/sx/auth.sx index 9146e9b..7f133f9 100644 --- a/account/sx/auth.sx +++ b/account/sx/auth.sx @@ -1,12 +1,12 @@ ;; Auth page components (device auth — account-specific) ;; 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 (div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" 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" (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.") @@ -22,30 +22,30 @@ :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition" "Authorize")))) -(defcomp ~account-device-approved () +(defcomp ~auth/device-approved () (div :class "py-8 max-w-md mx-auto text-center" (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."))) ;; Assembled auth page content — replaces Python _login_page_content etc. -(defcomp ~account-login-content (&key (error :as string?) (email :as string?)) - (~auth-login-form - :error (when error (~auth-error-banner :error error)) +(defcomp ~auth/login-content (&key (error :as string?) (email :as string?)) + (~shared:auth/login-form + :error (when error (~shared:auth/error-banner :error error)) :action (url-for "auth.start_login") :csrf-token (csrf-token) :email (or email ""))) -(defcomp ~account-device-content (&key (error :as string?) (code :as string?)) - (~account-device-form - :error (when error (~account-device-error :error error)) +(defcomp ~auth/device-content (&key (error :as string?) (code :as string?)) + (~auth/device-form + :error (when error (~auth/device-error :error error)) :action (url-for "auth.device_submit") :csrf-token (csrf-token) :code (or code ""))) -(defcomp ~account-check-email-content (&key (email :as string?) (email-error :as string?)) - (~auth-check-email +(defcomp ~auth/check-email-content (&key (email :as string?) (email-error :as string?)) + (~shared:auth/check-email :email (escape (or email "")) :error (when email-error - (~auth-check-email-error :error (escape email-error))))) + (~shared:auth/check-email-error :error (escape email-error))))) diff --git a/account/sx/dashboard.sx b/account/sx/dashboard.sx index 2731a74..e17092e 100644 --- a/account/sx/dashboard.sx +++ b/account/sx/dashboard.sx @@ -1,36 +1,36 @@ ;; Account dashboard components -(defcomp ~account-error-banner (&key (error :as string)) +(defcomp ~dashboard/error-banner (&key (error :as string)) (when error (div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm" error))) -(defcomp ~account-user-email (&key (email :as string)) +(defcomp ~dashboard/user-email (&key (email :as string)) (when 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 (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" (input :type "hidden" :name "csrf_token" :value csrf-token) (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" (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" name)) -(defcomp ~account-labels-section (&key items) +(defcomp ~dashboard/labels-section (&key items) (when items (div (h2 :class "text-base font-semibold tracking-tight mb-3" "Labels") (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 "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-8" error @@ -43,18 +43,18 @@ labels))) ;; 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)) (csrf (csrf-token))) - (~account-main-panel - :error (when error (~account-error-banner :error error)) + (~dashboard/main-panel + :error (when error (~dashboard/error-banner :error error)) :email (when (get user "email") - (~account-user-email :email (get user "email"))) + (~dashboard/user-email :email (get user "email"))) :name (when (get user "name") - (~account-user-name :name (get user "name"))) - :logout (~account-logout-form :csrf-token csrf) + (~dashboard/user-name :name (get user "name"))) + :logout (~dashboard/logout-form :csrf-token csrf) :labels (when (not (empty? (or (get user "labels") (list)))) - (~account-labels-section + (~dashboard/labels-section :items (map (lambda (label) - (~account-label-item :name (get label "name"))) + (~dashboard/label-item :name (get label "name"))) (get user "labels"))))))) diff --git a/account/sx/layouts.sx b/account/sx/layouts.sx index 10052e5..f0d82fe 100644 --- a/account/sx/layouts.sx +++ b/account/sx/layouts.sx @@ -2,19 +2,19 @@ ;; Registered via register_sx_layout("account", ...) in __init__.py. ;; Full page: root header + auth header row in header-child -(defcomp ~account-layout-full () +(defcomp ~layouts/full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (~auth-header-row-auto)))) ;; OOB (HTMX): auth row + root header, both with oob=true -(defcomp ~account-layout-oob () +(defcomp ~layouts/oob () (<> (~auth-header-row-auto true) (~root-header-auto true))) ;; Mobile menu: auth section + root nav -(defcomp ~account-layout-mobile () - (<> (~mobile-menu-section +(defcomp ~layouts/mobile () + (<> (~shared:layout/mobile-menu-section :label "account" :href "/" :level 1 :colour "sky" :items (~auth-nav-items-auto)) (~root-mobile-auto))) diff --git a/account/sx/newsletters.sx b/account/sx/newsletters.sx index ec10974..caf548e 100644 --- a/account/sx/newsletters.sx +++ b/account/sx/newsletters.sx @@ -1,30 +1,30 @@ ;; Newsletter management components -(defcomp ~account-newsletter-desc (&key (description :as string)) +(defcomp ~newsletters/desc (&key (description :as string)) (when 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" (button :sx-post url :sx-headers hdrs :sx-target target :sx-swap "outerHTML" :class cls :role "switch" :aria-checked checked (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 "min-w-0 flex-1" (p :class "text-sm font-medium text-stone-800" name) desc) (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)) -(defcomp ~account-newsletter-empty () +(defcomp ~newsletters/empty () (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 "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") @@ -32,12 +32,12 @@ ;; Assembled newsletters content — replaces Python _newsletters_panel_sx ;; 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))) (if (empty? newsletter-list) - (~account-newsletter-empty) - (~account-newsletters-panel - :list (~account-newsletter-list + (~newsletters/empty) + (~newsletters/panel + :list (~newsletters/list :items (map (lambda (item) (let* ((nl (get item "newsletter")) (un (get item "un")) @@ -47,11 +47,11 @@ (bg (if subscribed "bg-emerald-500" "bg-stone-300")) (translate (if subscribed "translate-x-6" "translate-x-1")) (checked (if subscribed "true" "false"))) - (~account-newsletter-item + (~newsletters/item :name (get nl "name") :desc (when (get nl "description") - (~account-newsletter-desc :description (get nl "description"))) - :toggle (~account-newsletter-toggle + (~newsletters/desc :description (get nl "description"))) + :toggle (~newsletters/toggle :id (str "nl-" nid) :url toggle-url :hdrs {:X-CSRFToken csrf} diff --git a/account/sxc/pages/account.sx b/account/sxc/pages/account.sx index 751e299..e8d477d 100644 --- a/account/sxc/pages/account.sx +++ b/account/sxc/pages/account.sx @@ -8,7 +8,7 @@ :path "/" :auth :login :layout :account - :content (~account-dashboard-content)) + :content (~dashboard/content)) ;; --------------------------------------------------------------------------- ;; Newsletters @@ -19,7 +19,7 @@ :auth :login :layout :account :data (service "account-page" "newsletters-data") - :content (~account-newsletters-content + :content (~newsletters/content :newsletter-list newsletter-list :account-url account-url)) diff --git a/blog/bp/blog/ghost/lexical_to_sx.py b/blog/bp/blog/ghost/lexical_to_sx.py index d13c317..04609f6 100644 --- a/blog/bp/blog/ghost/lexical_to_sx.py +++ b/blog/bp/blog/ghost/lexical_to_sx.py @@ -256,7 +256,7 @@ def _image(node: dict) -> str: parts.append(f':width "{_esc(width)}"') if href: parts.append(f':href "{_esc(href)}"') - return "(~kg-image " + " ".join(parts) + ")" + return "(~kg_cards/kg-image " + " ".join(parts) + ")" @_converter("gallery") @@ -282,14 +282,14 @@ def _gallery(node: dict) -> str: images_sx = "(list " + " ".join(rows) + ")" caption = node.get("caption", "") 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") def _html_card(node: dict) -> str: raw = node.get("html", "") inner = html_to_sx(raw) - return f"(~kg-html {inner})" + return f"(~kg_cards/kg-html {inner})" @_converter("embed") @@ -299,7 +299,7 @@ def _embed(node: dict) -> str: parts = [f':html "{_esc(embed_html)}"'] if caption: parts.append(f":caption {html_to_sx(caption)}") - return "(~kg-embed " + " ".join(parts) + ")" + return "(~kg_cards/kg-embed " + " ".join(parts) + ")" @_converter("bookmark") @@ -330,7 +330,7 @@ def _bookmark(node: dict) -> str: if caption: parts.append(f":caption {html_to_sx(caption)}") - return "(~kg-bookmark " + " ".join(parts) + ")" + return "(~kg_cards/kg-bookmark " + " ".join(parts) + ")" @_converter("callout") @@ -344,7 +344,7 @@ def _callout(node: dict) -> str: parts.append(f':emoji "{_esc(emoji)}"') if inner: parts.append(f':content {inner}') - return "(~kg-callout " + " ".join(parts) + ")" + return "(~kg_cards/kg-callout " + " ".join(parts) + ")" @_converter("button") @@ -352,7 +352,7 @@ def _button(node: dict) -> str: text = node.get("buttonText", "") url = node.get("buttonUrl", "") 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") @@ -360,7 +360,7 @@ def _toggle(node: dict) -> str: heading = node.get("heading", "") inner = _convert_children(node.get("children", [])) 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") @@ -380,7 +380,7 @@ def _audio(node: dict) -> str: parts.append(f':duration "{duration_str}"') if thumbnail: parts.append(f':thumbnail "{_esc(thumbnail)}"') - return "(~kg-audio " + " ".join(parts) + ")" + return "(~kg_cards/kg-audio " + " ".join(parts) + ")" @_converter("video") @@ -400,7 +400,7 @@ def _video(node: dict) -> str: parts.append(f':thumbnail "{_esc(thumbnail)}"') if loop: parts.append(":loop true") - return "(~kg-video " + " ".join(parts) + ")" + return "(~kg_cards/kg-video " + " ".join(parts) + ")" @_converter("file") @@ -429,12 +429,12 @@ def _file(node: dict) -> str: parts.append(f':filesize "{size_str}"') if caption: parts.append(f":caption {html_to_sx(caption)}") - return "(~kg-file " + " ".join(parts) + ")" + return "(~kg_cards/kg-file " + " ".join(parts) + ")" @_converter("paywall") def _paywall(_node: dict) -> str: - return "(~kg-paywall)" + return "(~kg_cards/kg-paywall)" @_converter("markdown") @@ -442,4 +442,4 @@ def _markdown(node: dict) -> str: md_text = node.get("markdown", "") rendered = mistune.html(md_text) inner = html_to_sx(rendered) - return f"(~kg-md {inner})" + return f"(~kg_cards/kg-md {inner})" diff --git a/blog/scripts/migrate_sx_html.py b/blog/scripts/migrate_sx_html.py index 68aeb1a..2ab2890 100644 --- a/blog/scripts/migrate_sx_html.py +++ b/blog/scripts/migrate_sx_html.py @@ -1,10 +1,10 @@ #!/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. 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 posts that already have sx_content, overwriting the old output. @@ -50,11 +50,11 @@ async def migrate(dry_run: bool = False) -> int: continue 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 "") markers = [] if old_has_kg: - markers.append("~kg-html") + markers.append("~kg_cards/kg-html") if old_has_raw: markers.append("raw-caption") tag = f" [{', '.join(markers)}]" if markers else "" @@ -76,7 +76,7 @@ async def migrate(dry_run: bool = False) -> int: def main(): 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", help="Preview changes without writing to database") diff --git a/blog/services/blog_page.py b/blog/services/blog_page.py index 0a42fe5..3f4e31b 100644 --- a/blog/services/blog_page.py +++ b/blog/services/blog_page.py @@ -398,7 +398,7 @@ class BlogPageService: } 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", "") is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) user_id = getattr(user, "id", None) if user else None diff --git a/blog/sx/admin.sx b/blog/sx/admin.sx index 2092750..58fb520 100644 --- a/blog/sx/admin.sx +++ b/blog/sx/admin.sx @@ -1,6 +1,6 @@ ;; 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 "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" @@ -8,21 +8,21 @@ (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")))) -(defcomp ~blog-snippets-panel (&key list) +(defcomp ~admin/snippets-panel (&key list) (div :class "max-w-4xl mx-auto p-6" (div :class "mb-6 flex justify-between items-center" (h1 :class "text-3xl font-bold" "Snippets")) (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" :sx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1" 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)) -(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-1 min-w-0" (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) 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))) -(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 "mb-6 flex justify-end items-center" (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-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 "text-stone-400 cursor-move" (i :class "fa fa-grip-vertical")) img @@ -54,16 +54,16 @@ (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" (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 :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))) ;; 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" (input :type "hidden" :name "csrf_token" :value csrf) (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") (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")) -(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" :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" icon (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" (str "order: " sort-order)))) -(defcomp ~blog-tag-groups-list (&key items) +(defcomp ~admin/tag-groups-list (&key 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)) -(defcomp ~blog-unassigned-tags (&key heading spans) +(defcomp ~admin/unassigned-tags (&key heading spans) (div :class "border-t pt-4" (h3 :class "text-sm font-semibold text-stone-700 mb-2" heading) (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" form groups unassigned)) ;; 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" (input :type "checkbox" :name "tag_ids" :value tag-id :checked checked :class "rounded border-stone-300") 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")) -(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" (input :type "hidden" :name "csrf_token" :value csrf) (div :class "space-y-3" @@ -133,19 +133,19 @@ (div :class "flex gap-3" (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" :onsubmit "return confirm('Delete this tag group? Tags will not be deleted.')" (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"))) -(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" edit-form delete-form)) ;; Data-driven snippets list (replaces Python _snippets_sx loop) -(defcomp ~blog-snippets-from-data (&key snippets user-id is-admin csrf badge-colours) - (~blog-snippets-list +(defcomp ~admin/snippets-from-data (&key snippets user-id is-admin csrf badge-colours) + (~admin/snippets-list :rows (<> (map (lambda (s) (let* ((s-id (get s "id")) (s-name (get s "name")) @@ -155,31 +155,31 @@ (badge-cls (or (get badge-colours s-vis) "bg-stone-200 text-stone-700")) (extra (<> (when is-admin - (~blog-snippet-visibility-select + (~admin/snippet-visibility-select :patch-url (get s "patch_url") :hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}") :options (<> - (~blog-snippet-option :value "private" :selected (= s-vis "private") :label "private") - (~blog-snippet-option :value "shared" :selected (= s-vis "shared") :label "shared") - (~blog-snippet-option :value "admin" :selected (= s-vis "admin") :label "admin")) + (~admin/snippet-option :value "private" :selected (= s-vis "private") :label "private") + (~admin/snippet-option :value "shared" :selected (= s-vis "shared") :label "shared") + (~admin/snippet-option :value "admin" :selected (= s-vis "admin") :label "admin")) :cls "text-sm border border-stone-300 rounded px-2 py-1")) (when (or (= s-uid user-id) is-admin) - (~delete-btn :url (get s "delete_url") :trigger-target "#snippets-list" + (~shared:misc/delete-btn :url (get s "delete_url") :trigger-target "#snippets-list" :title "Delete snippet?" :text (str "Delete \u201c" s-name "\u201d?") :sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}") :cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0"))))) - (~blog-snippet-row :name s-name :owner owner :badge-cls badge-cls + (~admin/snippet-row :name s-name :owner owner :badge-cls badge-cls :visibility s-vis :extra extra))) (or snippets (list)))))) ;; Data-driven menu items list (replaces Python _menu_items_list_sx loop) -(defcomp ~blog-menu-items-from-data (&key items csrf) - (~blog-menu-items-list +(defcomp ~admin/menu-items-from-data (&key items csrf) + (~admin/menu-items-list :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"))) - (~blog-menu-item-row + (~admin/menu-item-row :img img :label (get item "label") :slug (get item "slug") :sort-order (get item "sort_order") :edit-url (get item "edit_url") :delete-url (get item "delete_url") @@ -188,38 +188,38 @@ (or items (list)))))) ;; Data-driven tag groups main (replaces Python _tag_groups_main_panel_sx loops) -(defcomp ~blog-tag-groups-from-data (&key groups unassigned-tags csrf create-url) - (~blog-tag-groups-main - :form (~blog-tag-groups-create-form :create-url create-url :csrf csrf) +(defcomp ~admin/tag-groups-from-data (&key groups unassigned-tags csrf create-url) + (~admin/tag-groups-main + :form (~admin/tag-groups-create-form :create-url create-url :csrf csrf) :groups (if (empty? (or groups (list))) - (~empty-state :message "No tag groups yet." :cls "text-stone-500 text-sm") - (~blog-tag-groups-list + (~shared:misc/empty-state :message "No tag groups yet." :cls "text-stone-500 text-sm") + (~admin/tag-groups-list :items (<> (map (lambda (g) (let* ((icon (if (get g "feature_image") - (~blog-tag-group-icon-image :src (get g "feature_image") :name (get g "name")) - (~blog-tag-group-icon-color :style (get g "style") :initial (get g "initial"))))) - (~blog-tag-group-li :icon icon :edit-href (get g "edit_href") + (~admin/tag-group-icon-image :src (get g "feature_image") :name (get g "name")) + (~admin/tag-group-icon-color :style (get g "style") :initial (get g "initial"))))) + (~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")))) groups)))) :unassigned (when (not (empty? (or unassigned-tags (list)))) - (~blog-unassigned-tags + (~admin/unassigned-tags :heading (str "Unassigned Tags (" (len unassigned-tags) ")") :spans (<> (map (lambda (t) - (~blog-unassigned-tag :name (get t "name"))) + (~admin/unassigned-tag :name (get t "name"))) unassigned-tags)))))) ;; Data-driven tag group edit (replaces Python _tag_groups_edit_main_panel_sx loop) -(defcomp ~blog-tag-checkboxes-from-data (&key tags) +(defcomp ~admin/tag-checkboxes-from-data (&key tags) (<> (map (lambda (t) - (~blog-tag-checkbox + (~admin/tag-checkbox :tag-id (get t "tag_id") :checked (get t "checked") - :img (when (get t "feature_image") (~blog-tag-checkbox-image :src (get t "feature_image"))) + :img (when (get t "feature_image") (~admin/tag-checkbox-image :src (get t "feature_image"))) :name (get t "name"))) (or tags (list))))) ;; 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" (style " .sx-pretty, .json-pretty { font-family: monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; } @@ -239,18 +239,18 @@ ") sections)) -(defcomp ~blog-preview-section (&key title content) +(defcomp ~admin/preview-section (&key title content) (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) (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))) -(defcomp ~blog-preview-empty () +(defcomp ~admin/preview-empty () (div :class "p-8 text-stone-500" "No content to preview.")) -(defcomp ~blog-admin-placeholder () +(defcomp ~admin/placeholder () (div :class "pb-8")) ;; --------------------------------------------------------------------------- @@ -258,12 +258,12 @@ ;; --------------------------------------------------------------------------- ;; Snippets — receives serialized snippet dicts from service -(defcomp ~blog-snippets-content (&key snippets is-admin csrf) - (~blog-snippets-panel +(defcomp ~admin/snippets-content (&key snippets is-admin csrf) + (~admin/snippets-panel :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.") - (~blog-snippets-list + (~admin/snippets-list :rows (map (lambda (s) (let* ((badge-colours (dict "private" "bg-stone-200 text-stone-700" @@ -274,19 +274,19 @@ (name (get s "name")) (owner (get s "owner")) (can-delete (get s "can_delete"))) - (~blog-snippet-row + (~admin/snippet-row :name name :owner owner :badge-cls badge-cls :visibility vis :extra (<> (when is-admin - (~blog-snippet-visibility-select + (~admin/snippet-visibility-select :patch-url (get s "patch_url") :hx-headers {:X-CSRFToken csrf} :options (<> - (~blog-snippet-option :value "private" :selected (= vis "private") :label "private") - (~blog-snippet-option :value "shared" :selected (= vis "shared") :label "shared") - (~blog-snippet-option :value "admin" :selected (= vis "admin") :label "admin")))) + (~admin/snippet-option :value "private" :selected (= vis "private") :label "private") + (~admin/snippet-option :value "shared" :selected (= vis "shared") :label "shared") + (~admin/snippet-option :value "admin" :selected (= vis "admin") :label "admin")))) (when can-delete - (~delete-btn + (~shared:misc/delete-btn :url (get s "delete_url") :trigger-target "#snippets-list" :title "Delete snippet?" @@ -296,16 +296,16 @@ (or snippets (list))))))) ;; Menu Items — receives serialized menu item dicts from service -(defcomp ~blog-menu-items-content (&key menu-items new-url csrf) - (~blog-menu-items-panel +(defcomp ~admin/menu-items-content (&key menu-items new-url csrf) + (~admin/menu-items-panel :new-url new-url :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!") - (~blog-menu-items-list + (~admin/menu-items-list :rows (map (lambda (mi) - (~blog-menu-item-row - :img (~img-or-placeholder + (~admin/menu-item-row + :img (~shared:misc/img-or-placeholder :src (get mi "feature_image") :alt (get mi "label") :size-cls "w-12 h-12 rounded-full object-cover flex-shrink-0") :label (get mi "label") @@ -318,23 +318,23 @@ (or menu-items (list))))))) ;; Tag Groups — receives serialized tag group data from service -(defcomp ~blog-tag-groups-content (&key groups unassigned-tags create-url csrf) - (~blog-tag-groups-main - :form (~blog-tag-groups-create-form :create-url create-url :csrf csrf) +(defcomp ~admin/tag-groups-content (&key groups unassigned-tags create-url csrf) + (~admin/tag-groups-main + :form (~admin/tag-groups-create-form :create-url create-url :csrf csrf) :groups (if (empty? (or groups (list))) - (~empty-state :icon "fa fa-tags" :message "No tag groups yet.") - (~blog-tag-groups-list + (~shared:misc/empty-state :icon "fa fa-tags" :message "No tag groups yet.") + (~admin/tag-groups-list :items (map (lambda (g) (let* ((fi (get g "feature_image")) (colour (get g "colour")) (name (get g "name")) (initial (slice (or name "?") 0 1)) (icon (if fi - (~blog-tag-group-icon-image :src fi :name name) - (~blog-tag-group-icon-color + (~admin/tag-group-icon-image :src fi :name name) + (~admin/tag-group-icon-color :style (if colour (str "background:" colour) "background:#e7e5e4") :initial initial)))) - (~blog-tag-group-li + (~admin/tag-group-li :icon icon :edit-href (get g "edit_href") :name name @@ -342,57 +342,57 @@ :sort-order (or (get g "sort_order") 0)))) (or groups (list))))) :unassigned (when (not (empty? (or unassigned-tags (list)))) - (~blog-unassigned-tags + (~admin/unassigned-tags :heading (str (len (or unassigned-tags (list))) " Unassigned Tags") :spans (map (lambda (t) - (~blog-unassigned-tag :name (get t "name"))) + (~admin/unassigned-tag :name (get t "name"))) (or unassigned-tags (list))))))) ;; 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) - (~blog-tag-group-edit-main - :edit-form (~blog-tag-group-edit-form +(defcomp ~admin/tag-group-edit-content (&key group all-tags save-url delete-url csrf) + (~admin/tag-group-edit-main + :edit-form (~admin/tag-group-edit-form :save-url save-url :csrf csrf :name (get group "name") :colour (get group "colour") :sort-order (get group "sort_order") :feature-image (get group "feature_image") :tags (map (lambda (t) - (~blog-tag-checkbox + (~admin/tag-checkbox :tag-id (get t "id") :checked (get t "checked") :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"))) (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 ;; --------------------------------------------------------------------------- -(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))) (if (and (not sx-pretty) (not json-pretty) (not sx-rendered) (not lex-rendered)) - (~blog-preview-empty) - (~blog-preview-panel :sections + (~admin/preview-empty) + (~admin/preview-panel :sections (<> (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 - (~blog-preview-section :title "Lexical JSON" :content json-pretty)) + (~admin/preview-section :title "Lexical JSON" :content json-pretty)) (when sx-rendered - (~blog-preview-section :title "SX Rendered" - :content (~blog-preview-rendered :html sx-rendered))) + (~admin/preview-section :title "SX Rendered" + :content (~admin/preview-rendered :html sx-rendered))) (when lex-rendered - (~blog-preview-section :title "Lexical Rendered" - :content (~blog-preview-rendered :html lex-rendered)))))))) + (~admin/preview-section :title "Lexical Rendered" + :content (~admin/preview-rendered :html lex-rendered)))))))) ;; --------------------------------------------------------------------------- ;; 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") (span :class "text-neutral-400" "\u2014") (pre :class "whitespace-pre-wrap break-words break-all text-xs" @@ -400,7 +400,7 @@ (code 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" (table :class "w-full table-fixed text-sm border border-neutral-200 rounded-xl overflow-hidden" (thead :class "bg-neutral-50/70" @@ -411,10 +411,10 @@ (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 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))))))) -(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" (td :class "px-2 py-1 whitespace-nowrap align-top" (str index)) (td :class "px-2 py-1 align-top" @@ -422,11 +422,11 @@ (code summary)) (when children (div :class "mt-2 pl-3 border-l border-neutral-200" - (~blog-data-model-content + (~admin/data-model-content :columns (get children "columns") :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 "px-3 py-2 bg-neutral-50/70 text-sm font-medium" "Relationship: " (span :class "font-semibold" name) @@ -448,7 +448,7 @@ (th :class "px-2 py-1 text-left" "Summary"))) (tbody (map (lambda (item) - (~blog-data-relationship-item + (~admin/data-relationship-item :index (get item "index") :summary (get item "summary") :children (get item "children"))) @@ -459,17 +459,17 @@ (code (get value "summary"))) (when (get value "children") (div :class "pl-3 border-l border-neutral-200" - (~blog-data-model-content + (~admin/data-model-content :columns (get (get value "children") "columns") :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" - (~blog-data-scalar-table :columns columns) + (~admin/data-scalar-table :columns columns) (when (not (empty? (or relationships (list)))) (div :class "space-y-3" (map (lambda (rel) - (~blog-data-relationship + (~admin/data-relationship :name (get rel "name") :cardinality (get rel "cardinality") :class-name (get rel "class_name") @@ -477,13 +477,13 @@ :value (get rel "value"))) relationships))))) -(defcomp ~blog-data-table-content (&key tablename model-data) +(defcomp ~admin/data-table-content (&key tablename 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" (div :class "mb-6 text-sm text-neutral-500" "Model: " (code "Post") " \u2022 Table: " (code tablename)) - (~blog-data-model-content + (~admin/data-model-content :columns (get model-data "columns") :relationships (get model-data "relationships"))))) @@ -491,7 +491,7 @@ ;; 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" (span :class "truncate flex-1" name) (button :type "button" :class "flex-shrink-0 hover:text-red-600" @@ -505,7 +505,7 @@ :sx-on:afterSwap "document.body.dispatchEvent(new CustomEvent('entryToggled'))" (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" :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?" @@ -518,7 +518,7 @@ :sx-on:afterSwap "document.body.dispatchEvent(new CustomEvent('entryToggled'))" (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 next-month-url next-year-url weekday-names days csrf) @@ -553,9 +553,9 @@ (div :class "space-y-0.5" (map (lambda (e) (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) - (~blog-cal-entry-unassociated + (~admin/cal-entry-unassociated :name (get e "name") :toggle-url (get e "toggle_url") :csrf csrf))) entries)))))) (or days (list)))))))) @@ -564,15 +564,15 @@ ;; 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))) (cal-list (or calendars (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") (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) - (~blog-nav-entries-empty) - (~scroll-nav-wrapper + (~shared:nav/blog-nav-entries-empty) + (~shared:misc/scroll-nav-wrapper :wrapper-id "entries-calendars-nav-wrapper" :container-id "associated-items-container" :arrow-cls "entries-nav-arrow" @@ -581,12 +581,12 @@ :right-hs "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft + 200" :items (<> (map (lambda (e) - (~calendar-entry-nav + (~shared:navigation/calendar-entry-nav :href (get e "href") :nav-class nav-cls :name (get e "name") :date-str (get e "date_str"))) entry-list) (map (lambda (c) - (~blog-nav-calendar-item + (~shared:nav/blog-nav-calendar-item :href (get c "href") :nav-cls nav-cls :name (get c "name"))) cal-list)) diff --git a/blog/sx/cards.sx b/blog/sx/cards.sx index 008120f..769ff7c 100644 --- a/blog/sx/cards.sx +++ b/blog/sx/cards.sx @@ -1,51 +1,51 @@ ;; 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" - (~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" (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 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))) ;; 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 (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))) -(defcomp ~blog-tag-item (&key src name initial) +(defcomp ~cards/tag-item (&key src name initial) (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)))) ;; 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) (div :class "flex flex-row justify-center gap-3" (when tags (div :class "mt-4 flex items-center gap-2" (div "in") (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) (when authors (div :class "mt-4 flex items-center gap-2" (div "by") (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 -(defcomp ~blog-author-item (&key image name) +(defcomp ~cards/author-item (&key image name) (li :class "flex items-center gap-1" (when image (img :src image :alt name :class "h-5 w-5 rounded-full object-cover")) (span :class "text-stone-700" name))) ;; 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?) status (is-draft :as boolean) (publish-requested :as boolean) (status-timestamp :as string?) (liked :as boolean) (like-url :as string?) (csrf-token :as string?) @@ -53,7 +53,7 @@ (tags :as list?) (authors :as list?) widget) (article :class "border-b pb-6 last:border-b-0 relative" (when has-like - (~blog-like-button + (~cards/like-button :like-url like-url :hx-headers {:X-CSRFToken csrf-token} :heart (if liked "❤️" "🤍"))) @@ -63,8 +63,8 @@ (header :class "mb-2 text-center" (h2 :class "text-4xl font-bold text-stone-900" title) (if is-draft - (~blog-draft-status :publish-requested publish-requested :timestamp status-timestamp) - (when status-timestamp (~blog-published-status :timestamp status-timestamp)))) + (~cards/draft-status :publish-requested publish-requested :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 excerpt (p :class "text-stone-700 text-lg leading-relaxed text-center" excerpt))) widget @@ -73,14 +73,14 @@ (when tags (div :class "mt-4 flex items-center gap-2" (div "in") (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) (when authors (div :class "mt-4 flex items-center gap-2" (div "by") (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?) (excerpt :as string?) (tags :as list?) (authors :as list?)) (article :class "relative" @@ -91,33 +91,33 @@ (div :class "p-3 text-center" (h2 :class "text-lg font-bold text-stone-900" title) (if is-draft - (~blog-draft-status :publish-requested publish-requested :timestamp status-timestamp) - (when status-timestamp (~blog-published-status :timestamp status-timestamp))) + (~cards/draft-status :publish-requested publish-requested :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 (or tags authors) (div :class "flex flex-row justify-center gap-3" (when tags (div :class "mt-4 flex items-center gap-2" (div "in") (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) (when authors (div :class "mt-4 flex items-center gap-2" (div "by") (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) -(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) (if (= view "tile") - (~blog-card-tile + (~cards/tile :href (get p "href") :hx-select (get p "hx_select") :feature-image (get p "feature_image") :title (get p "title") :is-draft (get p "is_draft") :publish-requested (get p "publish_requested") :status-timestamp (get p "status_timestamp") :excerpt (get p "excerpt") :tags (get p "tags") :authors (get p "authors")) - (~blog-card + (~cards/index :slug (get p "slug") :href (get p "href") :hx-select (get p "hx_select") :title (get p "title") :feature-image (get p "feature_image") :excerpt (get p "excerpt") :is-draft (get p "is_draft") @@ -131,10 +131,10 @@ sentinel)) ;; 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) - (~blog-page-card + (~cards/page-card :href (get pg "href") :hx-select (get pg "hx_select") :title (get pg "title") :has-calendar (get pg "has_calendar") :has-market (get pg "has_market") @@ -143,21 +143,21 @@ (or pages (list))) 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" (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")) (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")))) -(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" (a :href href :sx-get href :sx-target "#main-panel" :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" (header :class "mb-2 text-center" (h2 :class "text-4xl font-bold text-stone-900" title) - (~blog-page-badges :has-calendar has-calendar :has-market has-market) - (when pub-timestamp (~blog-published-status :timestamp pub-timestamp))) + (~cards/page-badges :has-calendar has-calendar :has-market has-market) + (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 excerpt (p :class "text-stone-700 text-lg leading-relaxed text-center" excerpt))))) diff --git a/blog/sx/detail.sx b/blog/sx/detail.sx index fcaa9cb..12f27b1 100644 --- a/blog/sx/detail.sx +++ b/blog/sx/detail.sx @@ -1,34 +1,34 @@ ;; 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" :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" (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" (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")) 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" :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" - (~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)) -(defcomp ~blog-detail-chrome (&key like excerpt at-bar) +(defcomp ~detail/chrome (&key like excerpt at-bar) (<> like excerpt (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" draft chrome @@ -43,34 +43,34 @@ ;; 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?) (custom-excerpt :as string?) (tags :as list?) (authors :as list?) (feature-image :as string?) (html-content :as string?) (sx-content :as string?)) (let* ((hx-select "#main-panel") (draft-sx (when is-draft - (~blog-detail-draft + (~detail/draft :publish-requested publish-requested :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) - (~blog-detail-chrome + (~detail/chrome :like (when has-user - (~blog-detail-like + (~detail/like :like-url like-url :hx-headers {:X-CSRFToken csrf} :heart (if liked "❤️" "🤍"))) :excerpt (when (not (= custom-excerpt "")) - (~blog-detail-excerpt :excerpt custom-excerpt)) - :at-bar (~blog-at-bar :tags tags :authors authors))))) - (~blog-detail-main + (~detail/excerpt :excerpt custom-excerpt)) + :at-bar (~cards/at-bar :tags tags :authors authors))))) + (~detail/main :draft draft-sx :chrome chrome-sx :feature-image feature-image :html-content html-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) (title page-title) @@ -86,7 +86,7 @@ (meta :name "twitter:description" :content desc) (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" (if sx-content (div :class "blog-content p-2" sx-content) diff --git a/blog/sx/editor.sx b/blog/sx/editor.sx index f5c0d59..c275af0 100644 --- a/blog/sx/editor.sx +++ b/blog/sx/editor.sx @@ -1,10 +1,10 @@ ;; 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" (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]" (input :type "hidden" :name "csrf_token" :value csrf) (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)))) ;; Edit form — pre-populated version for //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?) (sx-content-val :as string?) (lexical-json :as string?) (has-sx :as boolean) (title-placeholder :as string) @@ -135,7 +135,7 @@ (when footer-extra footer-extra))))) ;; Publish-mode show/hide script for edit form -(defcomp ~blog-editor-publish-js (&key already-emailed) +(defcomp ~editor/publish-js (&key already-emailed) (script "(function() {" " var statusSel = document.getElementById('status-select');" @@ -153,20 +153,20 @@ " sync();" "})();")) -(defcomp ~blog-editor-styles (&key (css-href :as string)) +(defcomp ~editor/styles (&key (css-href :as string)) (<> (link :rel "stylesheet" :href css-href) (style "#lexical-editor { display: flow-root; }" "#lexical-editor [data-kg-card=\"html\"] * { float: none !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) (when sx-editor-js-src (script :src sx-editor-js-src)) (script init-js))) ;; SX editor styles — comprehensive CSS for the Koenig-style block editor -(defcomp ~sx-editor-styles () +(defcomp ~editor/sx-editor-styles () (style ;; Editor container ".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) ;; --------------------------------------------------------------------------- -(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 save-error) - (~blog-editor-panel :parts + (~layouts/editor-panel :parts (<> - (when save-error (~blog-editor-error :error save-error)) - (~blog-editor-form :csrf csrf :title-placeholder title-placeholder + (when save-error (~editor/error :error save-error)) + (~editor/form :csrf csrf :title-placeholder title-placeholder :create-label create-label) - (~blog-editor-styles :css-href css-href) - (~sx-editor-styles) - (~blog-editor-scripts :js-src js-src :sx-editor-js-src sx-editor-js-src + (~editor/styles :css-href css-href) + (~editor/sx-editor-styles) + (~editor/scripts :js-src js-src :sx-editor-js-src sx-editor-js-src :init-js init-js)))) ;; --------------------------------------------------------------------------- ;; 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 sx-content-val lexical-json has-sx title-placeholder status already-emailed newsletter-options footer-extra css-href js-src sx-editor-js-src init-js save-error) - (~blog-editor-panel :parts + (~layouts/editor-panel :parts (<> - (when save-error (~blog-editor-error :error save-error)) - (~blog-editor-edit-form + (when save-error (~editor/error :error save-error)) + (~editor/edit-form :csrf csrf :updated-at updated-at :title-val title-val :excerpt-val excerpt-val :feature-image feature-image :feature-image-caption feature-image-caption @@ -343,8 +343,8 @@ :has-sx has-sx :title-placeholder title-placeholder :status status :already-emailed already-emailed :newsletter-options newsletter-options :footer-extra footer-extra) - (~blog-editor-publish-js :already-emailed already-emailed) - (~blog-editor-styles :css-href css-href) - (~sx-editor-styles) - (~blog-editor-scripts :js-src js-src :sx-editor-js-src sx-editor-js-src + (~editor/publish-js :already-emailed already-emailed) + (~editor/styles :css-href css-href) + (~editor/sx-editor-styles) + (~editor/scripts :js-src js-src :sx-editor-js-src sx-editor-js-src :init-js init-js)))) diff --git a/blog/sx/filters.sx b/blog/sx/filters.sx index 505bef0..a64c58d 100644 --- a/blog/sx/filters.sx +++ b/blog/sx/filters.sx @@ -1,37 +1,37 @@ ;; 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" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :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" :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 " (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" :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 " (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)) -(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) :sx-get "?page=1" :sx-target "#main-panel" :sx-select hx-select :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")) -(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)) -(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) :sx-get hx-get :sx-target "#main-panel" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" @@ -40,19 +40,19 @@ (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)))) -(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" (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) :sx-get "?page=1" :sx-target "#main-panel" :sx-select hx-select :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")) -(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) :sx-get hx-get :sx-target "#main-panel" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" @@ -61,41 +61,41 @@ (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)))) -(defcomp ~blog-filter-summary (&key (text :as string)) +(defcomp ~filters/summary (&key (text :as string)) (span :class "text-sm text-stone-600" text)) ;; Data-driven tag groups filter (replaces Python _tag_groups_filter_sx loop) -(defcomp ~blog-tag-groups-filter-from-data (&key groups selected-groups hx-select) +(defcomp ~filters/tag-groups-filter-from-data (&key groups selected-groups hx-select) (let* ((is-any (empty? (or selected-groups (list)))) (any-cls (if is-any "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))) - (~blog-filter-nav + (~filters/nav :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) (let* ((slug (get g "slug")) (name (get g "name")) (is-on (contains? selected-groups slug)) (cls (if is-on "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")) (icon (if (get g "feature_image") - (~blog-filter-group-icon-image :src (get g "feature_image") :name name) - (~blog-filter-group-icon-color :style (get g "style") :initial (get g "initial"))))) - (~blog-filter-group-li :cls cls :hx-get (str "?group=" slug "&page=1") :hx-select hx-select + (~filters/group-icon-image :src (get g "feature_image") :name name) + (~filters/group-icon-color :style (get g "style") :initial (get g "initial"))))) + (~filters/group-li :cls cls :hx-get (str "?group=" slug "&page=1") :hx-select hx-select :icon icon :name name :count (get g "count")))) (or groups (list))))))) ;; Data-driven authors filter (replaces Python _authors_filter_sx loop) -(defcomp ~blog-authors-filter-from-data (&key authors selected-authors hx-select) +(defcomp ~filters/authors-filter-from-data (&key authors selected-authors hx-select) (let* ((is-any (empty? (or selected-authors (list)))) (any-cls (if is-any "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))) - (~blog-filter-nav + (~filters/nav :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) (let* ((slug (get a "slug")) (is-on (contains? selected-authors slug)) (cls (if is-on "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50")) (icon (when (get a "profile_image") - (~blog-filter-author-icon :src (get a "profile_image") :name (get a "name"))))) - (~blog-filter-author-li :cls cls :hx-get (str "?author=" slug "&page=1") :hx-select hx-select + (~filters/author-icon :src (get a "profile_image") :name (get a "name"))))) + (~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")))) (or authors (list))))))) diff --git a/blog/sx/handlers/link-card.sx b/blog/sx/handlers/link-card.sx index bdc26ae..7c39621 100644 --- a/blog/sx/handlers/link-card.sx +++ b/blog/sx/handlers/link-card.sx @@ -11,7 +11,7 @@ (let ((post (query "blog" "post-by-slug" :slug (trim s)))) (when post (<> (str "") - (~link-card + (~shared:fragments/link-card :link (app-url "blog" (str "/" (get post "slug") "/")) :title (get post "title") :image (get post "feature_image") @@ -22,7 +22,7 @@ (when slug (let ((post (query "blog" "post-by-slug" :slug slug))) (when post - (~link-card + (~shared:fragments/link-card :link (app-url "blog" (str "/" (get post "slug") "/")) :title (get post "title") :image (get post "feature_image") diff --git a/blog/sx/handlers/nav-tree.sx b/blog/sx/handlers/nav-tree.sx index 72a54b4..e84c122 100644 --- a/blog/sx/handlers/nav-tree.sx +++ b/blog/sx/handlers/nav-tree.sx @@ -30,25 +30,25 @@ (app-url "blog" (str "/" item-slug "/")))) (selected (or (= item-slug (or first-seg "")) (= item-slug app)))) - (~blog-nav-item-link + (~shared:nav/blog-nav-item-link :href href :hx-get href :selected (if selected "true" "false") :nav-cls nav-cls - :img (~img-or-placeholder + :img (~shared:misc/img-or-placeholder :src (get item "feature_image") :alt (or (get item "label") item-slug) :size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0") :label (or (get item "label") item-slug)))) items) ;; Hardcoded artdag link - (~blog-nav-item-link + (~shared:nav/blog-nav-item-link :href (app-url "artdag" "/") :hx-get (app-url "artdag" "/") :selected (if (or (= "artdag" (or first-seg "")) (= "artdag" app)) "true" "false") :nav-cls nav-cls - :img (~img-or-placeholder + :img (~shared:misc/img-or-placeholder :src nil :alt "art-dag" :size-cls "w-8 h-8 rounded-full object-cover flex-shrink-0") :label "art-dag"))) @@ -69,8 +69,8 @@ (right-hs (str "on click set #" cid ".scrollLeft to #" cid ".scrollLeft + 200"))) (if (empty? items) - (~blog-nav-empty :wrapper-id "menu-items-nav-wrapper") - (~scroll-nav-wrapper + (~shared:nav/blog-nav-empty :wrapper-id "menu-items-nav-wrapper") + (~shared:misc/scroll-nav-wrapper :wrapper-id "menu-items-nav-wrapper" :container-id cid :arrow-cls arrow-cls diff --git a/blog/sx/header.sx b/blog/sx/header.sx index b214207..7e4d1cb 100644 --- a/blog/sx/header.sx +++ b/blog/sx/header.sx @@ -1,21 +1,21 @@ ;; 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" :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")) -(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" (a :href href :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 "")) label))) -(defcomp ~blog-sub-settings-label (&key icon label) +(defcomp ~header/sub-settings-label (&key icon 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))) diff --git a/blog/sx/index.sx b/blog/sx/index.sx index 978c2ae..e027cf0 100644 --- a/blog/sx/index.sx +++ b/blog/sx/index.sx @@ -1,9 +1,9 @@ ;; 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.")) -(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" (a :href posts-href :sx-get posts-href :sx-target "#main-panel" :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" :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 (div :class "max-w-full px-3 py-3 space-y-3" cards) (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 toggle (div :class grid-cls cards) (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 action-buttons (div :id "category-summary-desktop" :hxx-swap-oob "outerHTML" @@ -36,12 +36,12 @@ ;; --------------------------------------------------------------------------- ;; 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 nil) ;; 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) (let* ((posts-href (str blog-url-base "/index")) (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"))) (if (= content-type "pages") ;; Pages listing - (~blog-main-panel-pages - :tabs (~blog-content-type-tabs + (~index/main-panel-pages + :tabs (~index/content-type-tabs :posts-href posts-href :pages-href pages-href :hx-select hx-select :posts-cls posts-cls :pages-cls pages-cls) :cards (<> (map (lambda (card) - (~blog-page-card + (~cards/page-card :href (get card "href") :hx-select hx-select :title (get card "title") :has-calendar (get card "has_calendar") @@ -67,14 +67,14 @@ :excerpt (get card "excerpt"))) (or cards (list))) (if (< page total-pages) - (~sentinel-simple + (~shared:misc/sentinel-simple :id (str "sentinel-" page "-d") :next-url (str current-local-href (if (contains? current-local-href "?") "&" "?") "page=" (+ page 1))) (if (not (empty? (or cards (list)))) - (~end-of-results) - (~blog-no-pages))))) + (~shared:misc/end-of-results) + (~index/no-pages))))) ;; Posts listing (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" @@ -88,19 +88,19 @@ (tile-cls (if (= view "tile") "bg-stone-200 text-stone-800" "text-stone-400 hover:text-stone-600"))) - (~blog-main-panel-posts - :tabs (~blog-content-type-tabs + (~index/main-panel-posts + :tabs (~index/content-type-tabs :posts-href posts-href :pages-href pages-href :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-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 :cards (<> (map (lambda (card) (if (= view "tile") - (~blog-card-tile + (~cards/tile :href (get card "href") :hx-select hx-select :feature-image (get card "feature_image") :title (get card "title") :is-draft (get card "is_draft") @@ -108,7 +108,7 @@ :status-timestamp (get card "status_timestamp") :excerpt (get card "excerpt") :tags (get card "tags") :authors (get card "authors")) - (~blog-card + (~cards/index :slug (get card "slug") :href (get card "href") :hx-select hx-select :title (get card "title") :feature-image (get card "feature_image") :excerpt (get card "excerpt") :is-draft (get card "is_draft") @@ -119,52 +119,52 @@ :tags (get card "tags") :authors (get card "authors") :widget (get card "widget")))) (or cards (list))) - (~blog-index-sentinel + (~index/sentinel :page page :total-pages total-pages :current-local-href current-local-href))))))) ;; 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) (let* ((next-url (str current-local-href "?page=" (+ page 1)))) - (~sentinel-desktop + (~shared:misc/sentinel-desktop :id (str "sentinel-" page "-d") :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()")))) ;; 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) - (~blog-action-buttons-wrapper + (~filters/action-buttons-wrapper :inner (<> (when is-admin (<> - (~blog-action-button + (~filters/action-button :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" :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 :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"))) (when (and has-user (or draft-count drafts)) (if drafts - (~blog-drafts-button + (~filters/drafts-button :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" :title "Hide Drafts" :label " Drafts " :draft-count (str draft-count)) (let* ((on-href (str current-local-href (if (contains? current-local-href "?") "&" "?") "drafts=1"))) - (~blog-drafts-button-amber + (~filters/drafts-button-amber :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" :title "Show Drafts" :label " Drafts " :draft-count (str draft-count)))))))) ;; Tag groups filter — replaces _tag_groups_filter_sx -(defcomp ~blog-index-tag-groups-filter (&key tag-groups is-any-group hx-select) - (~blog-filter-nav +(defcomp ~index/tag-groups-filter (&key tag-groups is-any-group hx-select) + (~filters/nav :items (<> - (~blog-filter-any-topic + (~filters/any-topic :cls (if is-any-group "bg-stone-900 text-white border-stone-900" "bg-white text-stone-600 border-stone-300 hover:bg-stone-50") @@ -178,23 +178,23 @@ (colour (get grp "colour")) (name (get grp "name")) (icon (if fi - (~blog-filter-group-icon-image :src fi :name name) - (~blog-filter-group-icon-color + (~filters/group-icon-image :src fi :name name) + (~filters/group-icon-color :style (if colour (str "background-color: " colour "; color: white;") "background-color: #e7e5e4; color: #57534e;") :initial (slice (or name "?") 0 1))))) - (~blog-filter-group-li + (~filters/group-li :cls cls :hx-get (str "?group=" (get grp "slug") "&page=1") :hx-select hx-select :icon icon :name name :count (str (get grp "post_count"))))) (or tag-groups (list)))))) ;; Authors filter — replaces _authors_filter_sx -(defcomp ~blog-index-authors-filter (&key authors is-any-author hx-select) - (~blog-filter-nav +(defcomp ~index/authors-filter (&key authors is-any-author hx-select) + (~filters/nav :items (<> - (~blog-filter-any-author + (~filters/any-author :cls (if is-any-author "bg-stone-900 text-white border-stone-900" "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-white text-stone-600 border-stone-300 hover:bg-stone-50")) (img (get a "profile_image"))) - (~blog-filter-author-li + (~filters/author-li :cls cls :hx-get (str "?author=" (get a "slug") "&page=1") :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") :count (str (get a "published_post_count"))))) (or authors (list)))))) ;; 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 tag-groups authors is-any-group is-any-author) - (~blog-aside - :search (~search-desktop) - :action-buttons (~blog-index-actions + (~index/aside + :search (~shared:controls/search-desktop) + :action-buttons (~index/actions :is-admin is-admin :has-user has-user :hx-select hx-select :draft-count draft-count :drafts drafts :new-post-href new-post-href :new-page-href new-page-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) - :authors-filter (~blog-index-authors-filter + :authors-filter (~index/authors-filter :authors authors :is-any-author is-any-author :hx-select hx-select))) ;; 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 tag-groups authors is-any-group is-any-author tg-summary au-summary) - (~mobile-filter + (~shared:controls/mobile-filter :filter-summary (<> - (~search-mobile) + (~shared:controls/search-mobile) (when (not (= tg-summary "")) - (~blog-filter-summary :text tg-summary)) + (~filters/summary :text tg-summary)) (when (not (= au-summary "")) - (~blog-filter-summary :text au-summary))) - :action-buttons (~blog-index-actions + (~filters/summary :text au-summary))) + :action-buttons (~index/actions :is-admin is-admin :has-user has-user :hx-select hx-select :draft-count draft-count :drafts drafts :new-post-href new-post-href :new-page-href new-page-href :current-local-href current-local-href) :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) - (~blog-index-authors-filter + (~index/authors-filter :authors authors :is-any-author is-any-author :hx-select hx-select)))) diff --git a/blog/sx/kg_cards.sx b/blog/sx/kg_cards.sx index 3d6daca..4de3bf9 100644 --- a/blog/sx/kg_cards.sx +++ b/blog/sx/kg_cards.sx @@ -7,7 +7,7 @@ ;; --------------------------------------------------------------------------- ;; 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" (if (= width "wide") " kg-width-wide" (if (= width "full") " kg-width-full" ""))) @@ -19,7 +19,7 @@ ;; --------------------------------------------------------------------------- ;; 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" (div :class "kg-gallery-container" (map (lambda (row) @@ -36,19 +36,19 @@ ;; HTML card — wraps user-pasted HTML so the editor can identify the block. ;; 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)) ;; --------------------------------------------------------------------------- ;; 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)) ;; --------------------------------------------------------------------------- ;; 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" (~rich-text :html html) (when caption (figcaption caption)))) @@ -56,7 +56,7 @@ ;; --------------------------------------------------------------------------- ;; 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" (a :class "kg-bookmark-container" :href url (div :class "kg-bookmark-content" @@ -75,7 +75,7 @@ ;; --------------------------------------------------------------------------- ;; 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")) (when emoji (div :class "kg-callout-emoji" emoji)) (div :class "kg-callout-text" (or content "")))) @@ -83,14 +83,14 @@ ;; --------------------------------------------------------------------------- ;; 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")) (a :href url :class "kg-btn kg-btn-accent" (or text "")))) ;; --------------------------------------------------------------------------- ;; 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-toggle-heading" (h4 :class "kg-toggle-heading-text" (or heading "")) @@ -101,7 +101,7 @@ ;; --------------------------------------------------------------------------- ;; 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" (if thumbnail (img :src thumbnail :alt "audio-thumbnail" :class "kg-audio-thumbnail") @@ -124,7 +124,7 @@ ;; --------------------------------------------------------------------------- ;; 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" (if (= width "wide") " kg-width-wide" (if (= width "full") " kg-width-full" ""))) @@ -136,7 +136,7 @@ ;; --------------------------------------------------------------------------- ;; 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" (a :class "kg-file-card-container" :href src :download (or filename "") (div :class "kg-file-card-contents" @@ -149,5 +149,5 @@ ;; --------------------------------------------------------------------------- ;; Paywall marker ;; --------------------------------------------------------------------------- -(defcomp ~kg-paywall () +(defcomp ~kg_cards/kg-paywall () (~rich-text :html "")) diff --git a/blog/sx/layouts.sx b/blog/sx/layouts.sx index 2521386..f3a1855 100644 --- a/blog/sx/layouts.sx +++ b/blog/sx/layouts.sx @@ -3,8 +3,8 @@ ;; --- Blog header (invisible row for blog-header-child swap target) --- -(defcomp ~blog-header (&key oob) - (~menu-row-sx :id "blog-row" :level 1 +(defcomp ~layouts/header (&key oob) + (~shared:layout/menu-row-sx :id "blog-row" :level 1 :link-label-content (div) :child-id "blog-header-child" :oob oob)) @@ -12,10 +12,10 @@ (defmacro ~blog-settings-header-auto (oob) (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-label-content (~blog-admin-label) - :nav (~blog-settings-nav) + :link-label-content (~header/admin-label) + :nav (~layouts/settings-nav) :child-id "root-settings-header-child" :oob (unquote oob)))) @@ -23,9 +23,9 @@ (defmacro ~blog-sub-settings-header-auto (row-id child-id endpoint icon label oob) (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-label-content (~blog-sub-settings-label + :link-label-content (~header/sub-settings-label :icon (str "fa fa-" (unquote icon)) :label (unquote label)) :child-id (unquote child-id) @@ -35,47 +35,47 @@ ;; Blog layout (root + blog header) ;; --------------------------------------------------------------------------- -(defcomp ~blog-layout-full () +(defcomp ~layouts/full () (<> (~root-header-auto) - (~blog-header))) + (~layouts/header))) -(defcomp ~blog-layout-oob () - (<> (~blog-header :oob true) - (~clear-oob-div :id "blog-header-child") +(defcomp ~layouts/oob () + (<> (~layouts/header :oob true) + (~shared:layout/clear-oob-div :id "blog-header-child") (~root-header-auto true))) ;; --------------------------------------------------------------------------- ;; Settings layout (root + settings header) ;; --------------------------------------------------------------------------- -(defcomp ~blog-settings-layout-full () +(defcomp ~layouts/settings-layout-full () (<> (~root-header-auto) (~blog-settings-header-auto))) -(defcomp ~blog-settings-layout-oob () +(defcomp ~layouts/settings-layout-oob () (<> (~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))) -(defcomp ~blog-settings-layout-mobile () - (~blog-settings-nav)) +(defcomp ~layouts/settings-layout-mobile () + (~layouts/settings-nav)) ;; --------------------------------------------------------------------------- ;; Cache layout (root + settings + cache sub-header) ;; --------------------------------------------------------------------------- -(defcomp ~blog-cache-layout-full () +(defcomp ~layouts/cache-layout-full () (<> (~root-header-auto) (~blog-settings-header-auto) (~blog-sub-settings-header-auto "cache-row" "cache-header-child" "settings.defpage_cache_page" "refresh" "Cache"))) -(defcomp ~blog-cache-layout-oob () +(defcomp ~layouts/cache-layout-oob () (<> (~blog-sub-settings-header-auto "cache-row" "cache-header-child" "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) (~root-header-auto true))) @@ -83,18 +83,18 @@ ;; Snippets layout (root + settings + snippets sub-header) ;; --------------------------------------------------------------------------- -(defcomp ~blog-snippets-layout-full () +(defcomp ~layouts/snippets-layout-full () (<> (~root-header-auto) (~blog-settings-header-auto) (~blog-sub-settings-header-auto "snippets-row" "snippets-header-child" "snippets.defpage_snippets_page" "puzzle-piece" "Snippets"))) -(defcomp ~blog-snippets-layout-oob () +(defcomp ~layouts/snippets-layout-oob () (<> (~blog-sub-settings-header-auto "snippets-row" "snippets-header-child" "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) (~root-header-auto true))) @@ -102,18 +102,18 @@ ;; Menu Items layout (root + settings + menu-items sub-header) ;; --------------------------------------------------------------------------- -(defcomp ~blog-menu-items-layout-full () +(defcomp ~layouts/menu-items-layout-full () (<> (~root-header-auto) (~blog-settings-header-auto) (~blog-sub-settings-header-auto "menu_items-row" "menu_items-header-child" "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 "menu_items-row" "menu_items-header-child" "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) (~root-header-auto true))) @@ -121,18 +121,18 @@ ;; Tag Groups layout (root + settings + tag-groups sub-header) ;; --------------------------------------------------------------------------- -(defcomp ~blog-tag-groups-layout-full () +(defcomp ~layouts/tag-groups-layout-full () (<> (~root-header-auto) (~blog-settings-header-auto) (~blog-sub-settings-header-auto "tag-groups-row" "tag-groups-header-child" "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 "tag-groups-row" "tag-groups-header-child" "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) (~root-header-auto true))) @@ -140,31 +140,31 @@ ;; 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) (~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" :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") :child-id "tag-groups-header-child"))) -(defcomp ~blog-tag-group-edit-layout-oob () - (<> (~menu-row-sx :id "tag-groups-row" :level 2 +(defcomp ~layouts/tag-group-edit-layout-oob () + (<> (~shared:layout/menu-row-sx :id "tag-groups-row" :level 2 :link-href (url-for "blog.tag_groups_admin.defpage_tag_group_edit" :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") :child-id "tag-groups-header-child" :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) (~root-header-auto true))) ;; --- Settings nav links — uses IO primitives --- -(defcomp ~blog-settings-nav () +(defcomp ~layouts/settings-nav () (let* ((sc (select-colours)) (links (list (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 "settings.defpage_cache_page" :icon "fa fa-refresh" :label "Cache")))) (<> (map (lambda (lnk) - (~nav-link + (~shared:layout/nav-link :href (url-for (get lnk "endpoint")) :icon (get lnk "icon") :label (get lnk "label") @@ -181,5 +181,5 @@ ;; --- Editor panel wrapper --- -(defcomp ~blog-editor-panel (&key parts) +(defcomp ~layouts/editor-panel (&key parts) (<> parts)) diff --git a/blog/sx/menu_items.sx b/blog/sx/menu_items.sx index 2cbc6f8..bd9d018 100644 --- a/blog/sx/menu_items.sx +++ b/blog/sx/menu_items.sx @@ -1,6 +1,6 @@ ;; 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" :data-page-id id :data-page-title title :data-page-slug slug :data-page-image (or feature-image "") @@ -11,50 +11,50 @@ (div :class "font-medium truncate" title) (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" 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" :sx-vals (str "{\"q\": \"" query "\", \"page\": " next-page "}") :class "p-3 text-center text-sm text-stone-400" (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" (str "No pages found matching \"" query "\""))) ;; Data-driven page search results (replaces Python render_page_search_results loop) -(defcomp ~page-search-results-from-data (&key pages query has-more search-url next-page) +(defcomp ~menu_items/page-search-results-from-data (&key pages query has-more search-url next-page) (if (and (not pages) query) - (~page-search-empty :query query) + (~menu_items/page-search-empty :query query) (when pages - (~page-search-results + (~menu_items/page-search-results :items (<> (map (lambda (p) - (~page-search-item + (~menu_items/page-search-item :id (get p "id") :title (get p "title") :slug (get p "slug") :feature-image (get p "feature_image"))) pages)) :sentinel (when has-more - (~page-search-sentinel :url search-url :query query :next-page next-page)))))) + (~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) -(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) - (~blog-nav-empty :wrapper-id "menu-items-nav-wrapper") - (~scroll-nav-wrapper :wrapper-id "menu-items-nav-wrapper" :container-id container-id + (~shared:nav/blog-nav-empty :wrapper-id "menu-items-nav-wrapper") + (~shared:misc/scroll-nav-wrapper :wrapper-id "menu-items-nav-wrapper" :container-id container-id :arrow-cls arrow-cls :left-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft - 200") :scroll-hs scroll-hs :right-hs (str "on click set #" container-id ".scrollLeft to #" container-id ".scrollLeft + 200") :items (<> (map (lambda (item) - (let* ((img (~img-or-placeholder :src (get item "feature_image") :alt (get item "label") + (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"))) (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")) - (~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"))))) items)) :oob true))) diff --git a/blog/sx/settings.sx b/blog/sx/settings.sx index f6e70cb..7b60b86 100644 --- a/blog/sx/settings.sx +++ b/blog/sx/settings.sx @@ -1,6 +1,6 @@ ;; 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" :sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3" (label :class "flex items-center gap-3 cursor-pointer" @@ -18,33 +18,33 @@ (i :class "fa fa-shopping-bag text-green-600 mr-1") " 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" - (~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 :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" (h3 :class "text-lg font-semibold text-stone-800" "Page Features") form sumup)) ;; 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" (div (span :class "font-medium" name) (span :class "text-stone-400 text-sm ml-2" (str "/" slug "/"))) (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"))) -(defcomp ~blog-markets-list (&key items) +(defcomp ~settings/markets-list (&key 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.")) -(defcomp ~blog-markets-panel (&key list create-url) +(defcomp ~settings/markets-panel (&key list create-url) (div :id "markets-panel" (h3 :class "text-lg font-semibold mb-3" "Markets") list @@ -59,17 +59,17 @@ ;; --------------------------------------------------------------------------- ;; 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 sumup-configured checkout-prefix) - (~blog-features-panel - :form (~blog-features-form + (~settings/features-panel + :form (~settings/features-form :features-url features-url :calendar-checked calendar-checked :market-checked market-checked :hs-trigger "on change trigger submit on closest
") :sumup (when show-sumup - (~blog-sumup-form + (~settings/sumup-form :sumup-url sumup-url :merchant-code merchant-code :placeholder placeholder @@ -77,13 +77,13 @@ :checkout-prefix checkout-prefix)))) ;; Markets panel composition — replaces render_markets_panel -(defcomp ~blog-markets-panel-content (&key markets create-url) - (~blog-markets-panel +(defcomp ~settings/markets-panel-content (&key markets create-url) + (~settings/markets-panel :list (if (empty? (or markets (list))) - (~blog-markets-empty) - (~blog-markets-list + (~settings/markets-empty) + (~settings/markets-list :items (map (lambda (m) - (~blog-market-item + (~settings/market-item :name (get m "name") :slug (get m "slug") :delete-url (get m "delete_url") @@ -93,11 +93,11 @@ ;; 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") (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" :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?" @@ -115,14 +115,14 @@ (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")))) -(defcomp ~blog-associated-entries-content (&key items) +(defcomp ~settings/associated-entries-content (&key items) (div :class "space-y-1" items)) -(defcomp ~blog-associated-entries-empty () +(defcomp ~settings/associated-entries-empty () (div :class "text-sm text-stone-400" "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" (h3 :class "text-lg font-semibold mb-4" "Associated Entries") content)) @@ -131,17 +131,17 @@ ;; Associated entries composition — replaces _render_associated_entries ;; --------------------------------------------------------------------------- -(defcomp ~blog-associated-entries-from-data (&key entries csrf) - (~blog-associated-entries-panel +(defcomp ~settings/associated-entries-from-data (&key entries csrf) + (~settings/associated-entries-panel :content (if (empty? (or entries (list))) - (~blog-associated-entries-empty) - (~blog-associated-entries-content + (~settings/associated-entries-empty) + (~settings/associated-entries-content :items (map (lambda (e) - (~blog-associated-entry + (~settings/associated-entry :confirm-text (get e "confirm_text") :toggle-url (get e "toggle_url") :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") :date-str (get e "date_str"))) (or entries (list))))))) @@ -150,7 +150,7 @@ ;; 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" (summary :class "p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3" (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 "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" entries-panel (div :class "space-y-3" @@ -171,7 +171,7 @@ (if (empty? (or calendars (list))) (div :class "text-sm text-stone-400" "No calendars found.") (map (lambda (cal) - (~blog-calendar-browser-item + (~settings/calendar-browser-item :name (get cal "name") :title (get cal "title") :image (get cal "image") @@ -182,17 +182,17 @@ ;; 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 :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 (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) (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 tags feature-image-alt meta-title meta-description canonical-url @@ -209,19 +209,19 @@ (input :type "hidden" :name "updated_at" :value (or updated-at "")) (div :class "space-y-[12px] mt-[16px]" ;; 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 "") :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" :value (or published-at "") :class input-cls)) (div (label :class "inline-flex items-center gap-[8px] cursor-pointer" (input :type "checkbox" :name "featured" :id "settings-featured" :checked featured :class "rounded border-stone-300 text-stone-600 focus:ring-stone-300") (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 (option :value "public" :selected (= visibility "public") "Public") (option :value "members" :selected (= visibility "members") "Members") @@ -231,57 +231,57 @@ :class "rounded border-stone-300 text-stone-600 focus:ring-stone-300") (span :class "text-[14px] text-stone-600" "Email only"))))) ;; Tags - (~blog-settings-section :title "Tags" :content - (div (~blog-settings-field-label :text "Tags (comma-separated)" :field-for "settings-tags") + (~settings/section :title "Tags" :content + (div (~settings/field-label :text "Tags (comma-separated)" :field-for "settings-tags") (input :type "text" :name "tags" :id "settings-tags" :value (or tags "") :placeholder "news, updates, featured" :class input-cls) (p :class "text-[12px] text-stone-400 mt-[4px]" "Unknown tags will be created automatically."))) ;; Feature Image - (~blog-settings-section :title "Feature Image" :content - (div (~blog-settings-field-label :text "Alt text" :field-for "settings-feature_image_alt") + (~settings/section :title "Feature Image" :content + (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" :value (or feature-image-alt "") :placeholder "Describe the feature image" :class input-cls))) ;; 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 "") :placeholder "SEO title" :maxlength "300" :class input-cls) (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" :placeholder "SEO description" :maxlength "500" :class textarea-cls (or meta-description "")) (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" :value (or canonical-url "") :placeholder "https://example.com/original-post" :class input-cls)))) ;; 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)) - (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 (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 "") :placeholder "https://..." :class input-cls)))) ;; 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" :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 (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" :value (or twitter-image "") :placeholder "https://..." :class input-cls)))) ;; Advanced - (~blog-settings-section :title "Advanced" :content - (div (~blog-settings-field-label :text "Custom template" :field-for "settings-custom_template") + (~settings/section :title "Advanced" :content + (div (~settings/field-label :text "Custom template" :field-for "settings-custom_template") (input :type "text" :name "custom_template" :id "settings-custom_template" :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" diff --git a/blog/sxc/pages/blog.sx b/blog/sxc/pages/blog.sx index 2c01e8c..e3383bb 100644 --- a/blog/sxc/pages/blog.sx +++ b/blog/sxc/pages/blog.sx @@ -9,7 +9,7 @@ :auth :admin :layout :blog :data (editor-data) - :content (~blog-editor-content + :content (~editor/content :csrf csrf :title-placeholder title-placeholder :create-label create-label :css-href css-href :js-src js-src :sx-editor-js-src sx-editor-js-src @@ -20,7 +20,7 @@ :auth :admin :layout :blog :data (editor-page-data) - :content (~blog-editor-content + :content (~editor/content :csrf csrf :title-placeholder title-placeholder :create-label create-label :css-href css-href :js-src js-src :sx-editor-js-src sx-editor-js-src @@ -33,21 +33,21 @@ :auth :admin :layout (:post-admin :selected "admin") :data (post-admin-data slug) - :content (~blog-admin-placeholder)) + :content (~admin/placeholder)) (defpage post-data :path "//admin/data/" :auth :admin :layout (:post-admin :selected "data") :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 :path "//admin/preview/" :auth :admin :layout (:post-admin :selected "preview") :data (post-preview-data slug) - :content (~blog-preview-content + :content (~admin/preview-content :sx-pretty sx-pretty :json-pretty json-pretty :sx-rendered sx-rendered :lex-rendered lex-rendered)) @@ -56,8 +56,8 @@ :auth :admin :layout (:post-admin :selected "entries") :data (post-entries-data slug) - :content (~blog-entries-browser-content - :entries-panel (~blog-associated-entries-from-data :entries entries :csrf csrf) + :content (~settings/entries-browser-content + :entries-panel (~settings/associated-entries-from-data :entries entries :csrf csrf) :calendars calendars)) (defpage post-settings @@ -65,7 +65,7 @@ :auth :post_author :layout (:post-admin :selected "settings") :data (post-settings-data slug) - :content (~blog-settings-form-content + :content (~settings/form-content :csrf csrf :updated-at updated-at :is-page is-page :save-success save-success :slug settings-slug :published-at published-at :featured featured @@ -82,7 +82,7 @@ :auth :post_author :layout (:post-admin :selected "edit") :data (post-edit-data slug) - :content (~blog-edit-content + :content (~editor/edit-content :csrf csrf :updated-at updated-at :title-val title-val :excerpt-val excerpt-val :feature-image feature-image :feature-image-caption feature-image-caption @@ -111,7 +111,7 @@ :auth :admin :layout :blog-cache :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 --- @@ -120,7 +120,7 @@ :auth :login :layout :blog-snippets :data (service "blog-page" "snippets-data") - :content (~blog-snippets-content + :content (~admin/snippets-content :snippets snippets :is-admin is-admin :csrf csrf)) ; --- Menu Items --- @@ -130,7 +130,7 @@ :auth :admin :layout :blog-menu-items :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)) ; --- Tag Groups --- @@ -140,7 +140,7 @@ :auth :admin :layout :blog-tag-groups :data (service "blog-page" "tag-groups-data") - :content (~blog-tag-groups-content + :content (~admin/tag-groups-content :groups groups :unassigned-tags unassigned-tags :create-url create-url :csrf csrf)) @@ -149,6 +149,6 @@ :auth :admin :layout :blog-tag-group-edit :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 :save-url save-url :delete-url delete-url :csrf csrf)) diff --git a/blog/tests/test_lexical_to_sx.py b/blog/tests/test_lexical_to_sx.py index ce3f2c6..fbaf630 100644 --- a/blog/tests/test_lexical_to_sx.py +++ b/blog/tests/test_lexical_to_sx.py @@ -167,7 +167,7 @@ class TestCards: result = lexical_to_sx(_doc({ "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): result = lexical_to_sx(_doc({ @@ -189,7 +189,7 @@ class TestCards: "type": "bookmark", "url": "https://example.com", "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 ':title "Example"' in result @@ -199,7 +199,7 @@ class TestCards: "calloutEmoji": "💡", "children": [_text("Note")] })) - assert "(~kg-callout " in result + assert "(~kg_cards/kg-callout " in result assert ':color "blue"' in result def test_button(self): @@ -207,7 +207,7 @@ class TestCards: "type": "button", "buttonText": "Click", "buttonUrl": "https://example.com" })) - assert "(~kg-button " in result + assert "(~kg_cards/kg-button " in result assert ':text "Click"' in result def test_toggle(self): @@ -215,28 +215,28 @@ class TestCards: "type": "toggle", "heading": "FAQ", "children": [_text("Answer")] })) - assert "(~kg-toggle " in result + assert "(~kg_cards/kg-toggle " in result assert ':heading "FAQ"' in result def test_html(self): result = lexical_to_sx(_doc({ "type": "html", "html": "
custom
" })) - assert result == '(~kg-html (div "custom"))' + assert result == '(~kg_cards/kg-html (div "custom"))' def test_embed(self): result = lexical_to_sx(_doc({ "type": "embed", "html": "", "caption": "Video" })) - assert "(~kg-embed " in result + assert "(~kg_cards/kg-embed " in result assert ':caption "Video"' in result def test_markdown(self): result = lexical_to_sx(_doc({ "type": "markdown", "markdown": "**bold** text" })) - assert result.startswith("(~kg-md ") + assert result.startswith("(~kg_cards/kg-md ") assert "(p " in result assert "(strong " in result @@ -244,14 +244,14 @@ class TestCards: result = lexical_to_sx(_doc({ "type": "video", "src": "v.mp4", "cardWidth": "wide" })) - assert "(~kg-video " in result + assert "(~kg_cards/kg-video " in result assert ':width "wide"' in result def test_audio(self): result = lexical_to_sx(_doc({ "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 def test_file(self): @@ -259,13 +259,13 @@ class TestCards: "type": "file", "src": "f.pdf", "fileName": "doc.pdf", "fileSize": 2100000 })) - assert "(~kg-file " in result + assert "(~kg_cards/kg-file " in result assert ':filename "doc.pdf"' in result assert "MB" in result def test_paywall(self): result = lexical_to_sx(_doc({"type": "paywall"})) - assert result == "(~kg-paywall)" + assert result == "(~kg_cards/kg-paywall)" # --------------------------------------------------------------------------- diff --git a/cart/sx/calendar.sx b/cart/sx/calendar.sx index a1751df..20897fa 100644 --- a/cart/sx/calendar.sx +++ b/cart/sx/calendar.sx @@ -1,12 +1,12 @@ ;; 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" (div (div :class "font-medium" name) (div :class "text-xs text-stone-500" date-str)) (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" (h2 :class "text-base font-semibold mb-2" "Calendar bookings") (ul :class "space-y-2" items))) diff --git a/cart/sx/handlers/account-nav-item.sx b/cart/sx/handlers/account-nav-item.sx index c939a69..121dce0 100644 --- a/cart/sx/handlers/account-nav-item.sx +++ b/cart/sx/handlers/account-nav-item.sx @@ -4,6 +4,6 @@ ;; Renders the "orders" link for the account dashboard nav. (defhandler account-nav-item (&key) - (~account-nav-item + (~shared:fragments/account-nav-item :href (app-url "cart" "/orders/") :label "orders")) diff --git a/cart/sx/handlers/cart-mini.sx b/cart/sx/handlers/cart-mini.sx index 87e9a75..2e9aa76 100644 --- a/cart/sx/handlers/cart-mini.sx +++ b/cart/sx/handlers/cart-mini.sx @@ -10,7 +10,7 @@ (count (+ (or (get summary "count") 0) (or (get summary "calendar_count") 0) (or (get summary "ticket_count") 0)))) - (~cart-mini + (~shared:fragments/cart-mini :cart-count count :blog-url (app-url "blog" "") :cart-url (app-url "cart" "") diff --git a/cart/sx/header.sx b/cart/sx/header.sx index 1fc17bf..b9d94cc 100644 --- a/cart/sx/header.sx +++ b/cart/sx/header.sx @@ -1,14 +1,14 @@ ;; 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")) -(defcomp ~cart-page-label (&key feature-image title) +(defcomp ~header/page-label (&key feature-image title) (<> (when feature-image - (~cart-page-label-img :src feature-image)) + (~header/page-label-img :src feature-image)) (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" (i :class "fa fa-arrow-left text-xs" :aria-hidden "true") "All carts")) diff --git a/cart/sx/items.sx b/cart/sx/items.sx index 488f7b5..d0b3e42 100644 --- a/cart/sx/items.sx +++ b/cart/sx/items.sx @@ -1,29 +1,29 @@ ;; 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")) -(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)) -(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)) -(defcomp ~cart-item-no-price () +(defcomp ~items/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" (i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true") " 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)) -(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)) -(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" (div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img)) (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" "+"))) (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 :id "cart" (div (section :class "space-y-3 sm:space-y-4" items cal tickets) summary)))) ;; 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") "")) (title (or (get item "title") "")) (image (get item "image")) @@ -71,48 +71,48 @@ (qty-url (or (get item "qty_url") "")) (csrf (csrf-token)) (line-total (when unit-price (* unit-price quantity)))) - (~cart-item + (~items/index :id (str "cart-item-" slug) :img (if image - (~cart-item-img :src image :alt title) - (~img-or-placeholder :src nil + (~items/img :src image :alt title) + (~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" :placeholder-text "No image")) :prod-url prod-url :title title - :brand (when brand (~cart-item-brand :brand brand)) - :deleted (when is-deleted (~cart-item-deleted)) + :brand (when brand (~items/brand :brand brand)) + :deleted (when is-deleted (~items/deleted)) :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)) - (~cart-item-price-was :text (str symbol (format-decimal regular-price 2))))) - (~cart-item-no-price)) + (~items/price-was :text (str symbol (format-decimal regular-price 2))))) + (~items/no-price)) :qty-url qty-url :csrf csrf :minus (str (- quantity 1)) :qty (str quantity) :plus (str (+ quantity 1)) :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 -(defcomp ~cart-cal-section-from-data (&key (entries :as list)) +(defcomp ~items/cal-section-from-data (&key (entries :as list)) (when (not (empty? entries)) - (~cart-cal-section + (~calendar/cal-section :items (map (lambda (e) (let* ((name (or (get e "name") "")) (date-str (or (get e "date_str") ""))) - (~cart-cal-entry + (~calendar/cal-entry :name name :date-str date-str :cost (str "\u00a3" (format-decimal (or (get e "cost") 0) 2))))) entries)))) ;; 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)) (let* ((csrf (csrf-token)) (qty-url (url-for "cart_global.update_ticket_quantity"))) - (~cart-tickets-section + (~tickets/section :items (map (lambda (tg) (let* ((name (or (get tg "entry_name") "")) (tt-name (get tg "ticket_type_name")) @@ -122,14 +122,14 @@ (entry-id (str (or (get tg "entry_id") ""))) (tt-id (get tg "ticket_type_id")) (date-str (or (get tg "date_str") ""))) - (~cart-ticket-article + (~tickets/article :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 :price (str "\u00a3" (format-decimal price 2)) :qty-url qty-url :csrf csrf :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)) :qty (str quantity) :plus (str (+ quantity 1)) @@ -137,29 +137,29 @@ ticket-groups))))) ;; 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?)) - (~cart-summary-panel +(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?)) + (~summary/panel :item-count (str item-count) :subtotal (str symbol (format-decimal grand-total 2)) :checkout (if is-logged-in - (~cart-checkout-form + (~summary/checkout-form :action checkout-action :csrf (csrf-token) :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 -(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))) (empty? (or cal-entries (list))) (empty? (or ticket-groups (list)))) (div :class "max-w-full px-3 py-3 space-y-3" (div :id "cart" (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")))) - (~cart-page-panel - :items (map (lambda (item) (~cart-item-from-data :item item)) (or cart-items (list))) + (~shared:misc/empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center")))) + (~items/page-panel + :items (map (lambda (item) (~items/from-data :item item)) (or cart-items (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)))) - (~cart-tickets-section-from-data :ticket-groups ticket-groups)) + (~items/tickets-section-from-data :ticket-groups ticket-groups)) :summary summary))) diff --git a/cart/sx/layouts.sx b/cart/sx/layouts.sx index 30822fd..4d6b42c 100644 --- a/cart/sx/layouts.sx +++ b/cart/sx/layouts.sx @@ -10,17 +10,17 @@ (quasiquote (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-label "cart" :icon "fa fa-shopping-cart" :child-id "cart-header-child") - (~header-child-sx :id "cart-header-child" - :inner (~menu-row-sx :id "page-cart-row" :level 2 :colour "sky" + (~shared:layout/header-child-sx :id "cart-header-child" + :inner (~shared:layout/menu-row-sx :id "page-cart-row" :level 2 :colour "sky" :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") :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))))))) (defmacro ~cart-page-header-oob () @@ -28,14 +28,14 @@ (quasiquote (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-label-content (~cart-page-label + :link-label-content (~header/page-label :feature-image (get __cpctx "feature-image") :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) - (~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-label "cart" :icon "fa fa-shopping-cart" :child-id "cart-header-child" @@ -45,12 +45,12 @@ ;; cart-page layout: root + cart row + page-cart row ;; --------------------------------------------------------------------------- -(defcomp ~cart-page-layout-full () +(defcomp ~layouts/page-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (~cart-page-header-auto)))) -(defcomp ~cart-page-layout-oob () +(defcomp ~layouts/page-layout-oob () (<> (~cart-page-header-oob) (~root-header-auto true))) @@ -59,14 +59,14 @@ ;; 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) - (~header-child-sx + (~shared:layout/header-child-sx :inner (~post-header-auto nil)))) -(defcomp ~cart-admin-layout-oob (&key selected) +(defcomp ~layouts/admin-layout-oob (&key selected) (<> (~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)) (~root-header-auto true))) @@ -74,63 +74,63 @@ ;; 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) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~auth-header-row-simple-auto) - (~header-child-sx :id "auth-header-child" - :inner (~orders-header-row :list-url list-url)))))) + (~shared:layout/header-child-sx :id "auth-header-child" + :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) - (~oob-header-sx + (~shared:layout/oob-header-sx :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))) ;; --------------------------------------------------------------------------- ;; 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) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~auth-header-row-simple-auto) - (~header-child-sx :id "auth-header-child" - :inner (<> (~orders-header-row :list-url list-url) - (~header-child-sx :id "orders-header-child" - :inner (~menu-row-sx :id "order-row" :level 3 :colour "sky" + (~shared:layout/header-child-sx :id "auth-header-child" + :inner (<> (~shared:auth/orders-header-row :list-url list-url) + (~shared:layout/header-child-sx :id "orders-header-child" + :inner (~shared:layout/menu-row-sx :id "order-row" :level 3 :colour "sky" :link-href detail-url :link-label order-label :icon "fa fa-gbp")))))))) -(defcomp ~cart-order-detail-layout-oob (&key detail-url order-label) - (<> (~oob-header-sx +(defcomp ~layouts/order-detail-layout-oob (&key detail-url order-label) + (<> (~shared:layout/oob-header-sx :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 :icon "fa fa-gbp" :oob true)) (~root-header-auto true))) ;; --- 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)) ;; Composition defcomp — replaces Python loop in render_orders_rows -(defcomp ~cart-orders-rows-content (&key orders detail-url-prefix page total-pages next-url) - (~cart-orders-rows +(defcomp ~layouts/orders-rows-content (&key orders detail-url-prefix page total-pages next-url) + (~layouts/orders-rows :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))) :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) - (~order-end-row)))) + (~shared:orders/end-row)))) ;; Composition defcomp — replaces conditional composition in render_checkout_error_page -(defcomp ~cart-checkout-error-from-data (&key msg order-id back-url) - (~checkout-error-content +(defcomp ~layouts/checkout-error-from-data (&key msg order-id back-url) + (~shared:orders/checkout-error-content :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)) diff --git a/cart/sx/overview.sx b/cart/sx/overview.sx index 32b7936..9cea22b 100644 --- a/cart/sx/overview.sx +++ b/cart/sx/overview.sx @@ -1,20 +1,20 @@ ;; 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" (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" 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")) -(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)) -(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" (div :class "flex items-start gap-4" img @@ -25,7 +25,7 @@ (div :class "text-lg font-bold text-stone-900" total) (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 "flex items-start gap-4" (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-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 "space-y-4" cards))) -(defcomp ~cart-empty () +(defcomp ~overview/empty () (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" - (~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 -(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")) (product-count (or (get grp "product_count") 0)) (calendar-count (or (get grp "calendar_count") 0)) @@ -55,13 +55,13 @@ (market-place (get grp "market_place")) (badges (<> (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)))) (when (> calendar-count 0) - (~cart-badge :icon "fa fa-calendar" + (~overview/badge :icon "fa fa-calendar" :text (str calendar-count " booking" (pluralize calendar-count)))) (when (> ticket-count 0) - (~cart-badge :icon "fa fa-ticket" + (~overview/badge :icon "fa fa-ticket" :text (str ticket-count " ticket" (pluralize ticket-count))))))) (if post (let* ((slug (or (get post "slug") "")) @@ -69,26 +69,26 @@ (feature-image (get post "feature_image")) (mp-name (if market-place (or (get market-place "name") "") "")) (display-title (if (!= mp-name "") mp-name title))) - (~cart-group-card + (~overview/group-card :href (str cart-url-base "/" slug "/") :img (if feature-image - (~cart-group-card-img :src feature-image :alt title) - (~img-or-placeholder :src nil :size-cls "h-16 w-16 rounded-xl" + (~overview/group-card-img :src feature-image :alt title) + (~shared:misc/img-or-placeholder :src nil :size-cls "h-16 w-16 rounded-xl" :placeholder-icon "fa fa-store text-xl")) :display-title display-title :subtitle (when (!= mp-name "") - (~cart-mp-subtitle :title title)) - :badges (~cart-badges-wrap :badges badges) + (~overview/mp-subtitle :title title)) + :badges (~overview/badges-wrap :badges badges) :total (str "\u00a3" (format-decimal total 2)))) - (~cart-orphan-card - :badges (~cart-badges-wrap :badges badges) + (~overview/orphan-card + :badges (~overview/badges-wrap :badges badges) :total (str "\u00a3" (format-decimal total 2)))))) ;; 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) - (~cart-empty) - (~cart-overview-panel + (~overview/empty) + (~overview/panel :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)))) diff --git a/cart/sx/payments.sx b/cart/sx/payments.sx index 36f6d35..f5daa62 100644 --- a/cart/sx/payments.sx +++ b/cart/sx/payments.sx @@ -1,13 +1,13 @@ ;; 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" - (~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 :checkout-prefix checkout-prefix :sx-select "#payments-panel"))) ;; Assembled cart admin overview content -(defcomp ~cart-admin-content () +(defcomp ~payments/admin-content () (let* ((payments-href (url-for "defpage_cart_payments"))) (div :id "main-panel" (div :class "flex items-center justify-between p-3 border-b" @@ -15,13 +15,13 @@ (a :href payments-href :class "text-sm underline" "configure"))))) ;; 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"))) (merchant-code (or (get page-config "sumup_merchant_code") "")) (checkout-prefix (or (get page-config "sumup_checkout_prefix") "")) (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")) - (~cart-payments-panel + (~payments/panel :update-url (url-for "page_admin.update_sumup") :csrf (csrf-token) :merchant-code merchant-code diff --git a/cart/sx/summary.sx b/cart/sx/summary.sx index 4058119..8d278b0 100644 --- a/cart/sx/summary.sx +++ b/cart/sx/summary.sx @@ -1,17 +1,17 @@ ;; 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" (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" (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" (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")))) -(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" (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") diff --git a/cart/sx/tickets.sx b/cart/sx/tickets.sx index fff0a12..2f107b2 100644 --- a/cart/sx/tickets.sx +++ b/cart/sx/tickets.sx @@ -1,12 +1,12 @@ ;; 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)) -(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)) -(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" (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" @@ -35,7 +35,7 @@ (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)))))) -(defcomp ~cart-tickets-section (&key items) +(defcomp ~tickets/section (&key items) (div :class "mt-6 border-t border-stone-200 pt-4" (h2 :class "text-base font-semibold mb-2" (i :class "fa fa-ticket mr-1" :aria-hidden "true") " Event tickets") diff --git a/cart/sxc/pages/cart.sx b/cart/sxc/pages/cart.sx index 6b3ffd0..07fef36 100644 --- a/cart/sxc/pages/cart.sx +++ b/cart/sxc/pages/cart.sx @@ -6,7 +6,7 @@ :auth :public :layout :root :data (service "cart-page" "overview-data") - :content (~cart-overview-content + :content (~overview/content :page-groups page-groups :cart-url-base cart-url-base)) @@ -15,11 +15,11 @@ :auth :public :layout :cart-page :data (service "cart-page" "page-cart-data") - :content (~cart-page-cart-content + :content (~items/page-cart-content :cart-items cart-items :cal-entries cal-entries :ticket-groups ticket-groups - :summary (~cart-summary-from-data + :summary (~items/summary-from-data :item-count (get summary "item_count") :grand-total (get summary "grand_total") :symbol (get summary "symbol") @@ -33,12 +33,12 @@ :auth :admin :layout :cart-admin :data (service "cart-page" "admin-data") - :content (~cart-admin-content)) + :content (~payments/admin-content)) (defpage cart-payments :path "//admin/payments/" :auth :admin :layout (:cart-admin :selected "payments") :data (service "cart-page" "payments-admin-data") - :content (~cart-payments-content + :content (~payments/content :page-config page-config)) diff --git a/events/sx/admin.sx b/events/sx/admin.sx index df2c286..bb43079 100644 --- a/events/sx/admin.sx +++ b/events/sx/admin.sx @@ -1,6 +1,6 @@ ;; 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" (div (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")))) (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" (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 "text-xs font-semibold uppercase tracking-wide text-stone-500" label) content)) -(defcomp ~events-entry-name-field (&key name) +(defcomp ~admin/entry-name-field (&key 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" (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))) -(defcomp ~events-entry-slot-none () +(defcomp ~admin/entry-slot-none () (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)) -(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))) -(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))) -(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)) -(defcomp ~events-entry-date-field (&key date-str) +(defcomp ~admin/entry-date-field (&key 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)) -(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) (section :id (str "entry-" entry-id) :class list-container name slot time state cost @@ -68,21 +68,21 @@ :sx-get edit-url :sx-target (str "#entry-" entry-id) :sx-swap "outerHTML" "Edit")))) -(defcomp ~events-entry-title (&key name badge) +(defcomp ~admin/entry-title (&key 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)) -(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-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" 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) (form :sx-post url :sx-select target :sx-target target :sx-swap "outerHTML" :sx-trigger (if is-btn "confirmed" nil) diff --git a/events/sx/calendar.sx b/events/sx/calendar.sx index 47808df..eac26ae 100644 --- a/events/sx/calendar.sx +++ b/events/sx/calendar.sx @@ -1,34 +1,34 @@ ;; 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 :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))) -(defcomp ~events-calendar-weekday (&key (name :as string)) +(defcomp ~calendar/weekday (&key (name :as string)) (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)) -(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" :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) (span :class "truncate" name) (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 "flex justify-between items-center" (div :class "flex flex-col" day-short day-num)) (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" (header :class "flex items-center justify-center mt-2" (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)))) ;; 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) (next-month-href :as string) (next-year-href :as string) (weekday-names :as list) (cells :as list)) - (~events-calendar-grid + (~calendar/grid :arrows (<> - (~events-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") - (~events-calendar-month-label :month-name month-name :year year) - (~events-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")) - :weekdays (<> (map (lambda (wd) (~events-calendar-weekday :name wd)) + (~calendar/nav-arrow :pill-cls pill-cls :href prev-year-href :label "\u00ab") + (~calendar/nav-arrow :pill-cls pill-cls :href prev-month-href :label "\u2039") + (~calendar/month-label :month-name month-name :year year) + (~calendar/nav-arrow :pill-cls pill-cls :href next-month-href :label "\u203a") + (~calendar/nav-arrow :pill-cls pill-cls :href next-year-href :label "\u00bb")) + :weekdays (<> (map (lambda (wd) (~calendar/weekday :name wd)) (or weekday-names (list)))) :cells (<> (map (lambda (cell) - (~events-calendar-cell + (~calendar/cell :cell-cls (get cell "cell-cls") :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") - (~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"))) :badges (when (get cell "badges") (<> (map (lambda (b) - (~events-calendar-entry-badge + (~calendar/entry-badge :bg-cls (get b "bg-cls") :name (get b "name") :state-label (get b "state-label"))) (get cell "badges")))))) (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" (if 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" (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" :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block" 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" (form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML" (input :type "hidden" :name "csrf_token" :value csrf) diff --git a/events/sx/day.sx b/events/sx/day.sx index dfd49d7..36f5c20 100644 --- a/events/sx/day.sx +++ b/events/sx/day.sx @@ -1,18 +1,18 @@ ;; 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" (div :class "flex-1 min-w-0" (div :class "font-medium truncate" name) (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" :id "day-entries-nav-wrapper" (div :class "flex overflow-x-auto gap-1 scrollbar-thin" 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 (table :class "w-full text-sm border table-fixed" (thead :class "bg-stone-100" @@ -29,95 +29,95 @@ :sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML" "+ Add entry")))) -(defcomp ~events-day-empty-row () +(defcomp ~day/empty-row () (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" (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)))) -(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" (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) (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)))) -(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))) -(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))) -(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" (div :class "font-medium text-green-600" price-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"))) -(defcomp ~events-day-row-actions () +(defcomp ~day/row-actions () (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)) -(defcomp ~events-day-admin-panel () +(defcomp ~day/admin-panel () (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")) -(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" :id "day-entries-nav-wrapper" :sx-swap-oob "true" (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 (div :class "flex-1 min-w-0" (div :class "font-medium truncate" name) (div :class "text-xs text-stone-600 truncate" time-str)))) ;; 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?)) - (~events-day-table +(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?)) + (~day/table :list-container list-container :rows (if (empty? (or rows (list))) - (~events-day-empty-row) + (~day/empty-row) (<> (map (lambda (r) - (~events-day-row + (~day/row :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")) :slot (if (get r "slot-name") - (~events-day-row-slot + (~day/row-slot :href (get r "slot-href") :pill-cls pill-cls :slot-name (get r "slot-name") :time-str (get r "slot-time")) - (~events-day-row-time :start (get r "start") :end (get r "end"))) - :state (~events-day-row-state + (~day/row-time :start (get r "start") :end (get r "end"))) + :state (~day/row-state :state-id (get r "state-id") - :badge (~entry-state-badge :state (get r "state"))) - :cost (~events-day-row-cost :cost-str (get r "cost-str")) + :badge (~entries/entry-state-badge :state (get r "state"))) + :cost (~day/row-cost :cost-str (get r "cost-str")) :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")) - (~events-day-row-no-tickets)) - :actions (~events-day-row-actions))) + (~day/row-no-tickets)) + :actions (~day/row-actions))) (or rows (list))))) :pre-action pre-action :add-url add-url)) ;; 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))) - (~events-day-entries-nav-oob-empty) - (~events-day-entries-nav-oob + (~day/entries-nav-oob-empty) + (~day/entries-nav-oob :items (<> (map (lambda (e) - (~events-day-nav-entry + (~day/nav-entry :href (get e "href") :nav-btn nav-btn :name (get e "name") :time-str (get e "time-str"))) entries))))) diff --git a/events/sx/entries.sx b/events/sx/entries.sx index 4c2d1e5..d85046f 100644 --- a/events/sx/entries.sx +++ b/events/sx/entries.sx @@ -4,8 +4,8 @@ ;; State badges — cond maps state string to class + label ;; --------------------------------------------------------------------------- -(defcomp ~entry-state-badge (&key state) - (~badge +(defcomp ~entries/entry-state-badge (&key state) + (~shared:misc/badge :cls (cond ((= state "confirmed") "bg-emerald-100 text-emerald-800") ((= state "provisional") "bg-amber-100 text-amber-800") @@ -21,7 +21,7 @@ ((= state "declined") "Declined") (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 " (cond ((= state "confirmed") "bg-emerald-100 text-emerald-800") @@ -38,8 +38,8 @@ ((= state "declined") "Declined") (true (or state "Unknown"))))) -(defcomp ~ticket-state-badge (&key state) - (~badge +(defcomp ~entries/ticket-state-badge (&key state) + (~shared:misc/badge :cls (cond ((= state "confirmed") "bg-emerald-100 text-emerald-800") ((= state "checked_in") "bg-blue-100 text-blue-800") @@ -53,7 +53,7 @@ ((= state "cancelled") "Cancelled") (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 " (cond ((= state "confirmed") "bg-emerald-100 text-emerald-800") @@ -73,36 +73,36 @@ ;; 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" (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)) -(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" (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)) -(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)) -(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)) -(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) " · ")) -(defcomp ~events-entry-time-plain (&key date-str) +(defcomp ~entries/entry-time-plain (&key 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)) -(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" (div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-3" (div :class "flex-1 min-w-0" @@ -112,7 +112,7 @@ cost) 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" (div :class "p-3" title @@ -121,20 +121,20 @@ cost) 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)) -(defcomp ~events-entry-widget-wrapper (&key widget) +(defcomp ~entries/entry-widget-wrapper (&key 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" (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)) -(defcomp ~events-main-panel-body (&key toggle body) +(defcomp ~entries/main-panel-body (&key toggle body) (<> toggle body (div :class "pb-8"))) @@ -143,46 +143,46 @@ ;; --------------------------------------------------------------------------- ;; Ticket widget from data — replaces _ticket_widget_html Python composition -(defcomp ~events-tw-widget-from-data (&key entry-id price qty ticket-url csrf) - (~events-tw-widget :entry-id (str entry-id) :price price +(defcomp ~entries/tw-widget-from-data (&key entry-id price qty ticket-url csrf) + (~page/tw-widget :entry-id (str entry-id) :price price :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" - :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)) - :btn (~events-tw-minus)) - (~events-tw-cart-icon :qty (str qty)) - (~events-tw-form :ticket-url ticket-url :target (str "#page-ticket-" entry-id) + :btn (~page/tw-minus)) + (~page/tw-cart-icon :qty (str qty)) + (~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)) - :btn (~events-tw-plus)))))) + :btn (~page/tw-plus)))))) ;; 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 date-str start-time end-time is-page-scoped cost has-ticket ticket-data) - (~events-entry-card + (~entries/entry-card :title (if entry-href - (~events-entry-title-linked :href entry-href :name name) - (~events-entry-title-plain :name name)) + (~entries/entry-title-linked :href entry-href :name name) + (~entries/entry-title-plain :name name)) :badges (<> (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 - (~events-entry-cal-badge :name cal-name))) + (~entries/entry-cal-badge :name cal-name))) :time-parts (<> (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) - (~events-entry-time-plain :date-str date-str)) + (~entries/entry-time-plain :date-str date-str)) start-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 - (~events-entry-widget-wrapper - :widget (~events-tw-widget-from-data + (~entries/entry-widget-wrapper + :widget (~entries/tw-widget-from-data :entry-id (get ticket-data "entry-id") :price (get ticket-data "price") :qty (get ticket-data "qty") @@ -190,24 +190,24 @@ :csrf (get ticket-data "csrf")))))) ;; 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 date-str time-str cost has-ticket ticket-data) - (~events-entry-card-tile + (~entries/entry-card-tile :title (if entry-href - (~events-entry-title-tile-linked :href entry-href :name name) - (~events-entry-title-tile-plain :name name)) + (~entries/entry-title-tile-linked :href entry-href :name name) + (~entries/entry-title-tile-plain :name name)) :badges (<> (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 - (~events-entry-cal-badge :name cal-name))) + (~entries/entry-cal-badge :name cal-name))) :time time-str - :cost (when cost (~events-entry-cost :cost cost)) + :cost (when cost (~entries/entry-cost :cost cost)) :widget (when has-ticket - (~events-entry-tile-widget-wrapper - :widget (~events-tw-widget-from-data + (~entries/entry-tile-widget-wrapper + :widget (~entries/tw-widget-from-data :entry-id (get ticket-data "entry-id") :price (get ticket-data "price") :qty (get ticket-data "qty") @@ -215,13 +215,13 @@ :csrf (get ticket-data "csrf")))))) ;; 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) (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") - (~events-entry-card-tile-from-data + (~entries/entry-card-tile-from-data :entry-href (get item "entry-href") :name (get item "name") :day-href (get item "day-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") :cost (get item "cost") :has-ticket (get item "has-ticket") :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") :day-href (get item "day-href") :page-badge-href (get item "page-badge-href") @@ -243,20 +243,20 @@ :ticket-data (get item "ticket-data"))))) (or items (list))) (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 -(defcomp ~events-main-panel-from-data (&key toggle items view page has-more next-url) - (~events-main-panel-body +(defcomp ~entries/main-panel-from-data (&key toggle items view page has-more next-url) + (~entries/main-panel-body :toggle toggle :body (if items - (~events-grid + (~entries/grid :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 space-y-3") - :cards (~events-entry-cards-from-data + :cards (~entries/entry-cards-from-data :items items :view view :page page :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" :cls "px-3 py-12 text-center text-stone-400")))) diff --git a/events/sx/forms.sx b/events/sx/forms.sx index 4e79700..c3cab5a 100644 --- a/events/sx/forms.sx +++ b/events/sx/forms.sx @@ -5,25 +5,25 @@ ;; 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 :data-flexible data-flexible :data-cost data-cost :selected selected 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" :data-slot-picker "" :required "required" options)) -(defcomp ~events-no-slots () +(defcomp ~forms/no-slots () (div :class "text-sm text-stone-500" "No slots defined for this day.")) ;; --------------------------------------------------------------------------- ;; 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 start-val end-val cost-display ticket-price-val ticket-count-val @@ -115,7 +115,7 @@ ;; 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) (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" @@ -129,7 +129,7 @@ :data-confirm-cancel-text "Cancel" 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) :sx-get next-url :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-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")) @@ -180,17 +180,17 @@ ;; 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" (input :type "checkbox" :name name :value "1" :data-day name :checked checked) (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" (input :type "checkbox" :data-day-all "" :checked checked) (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 days flexible-checked action-btn cancel-btn) @@ -271,7 +271,7 @@ ;; 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" :sx-disinherit "sx-select" :sx-swap "outerHTML" :sx-headers csrf :class "space-y-3" @@ -312,7 +312,7 @@ :data-confirm-cancel-text "Cancel" (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 :sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML" "+ Add slot")) @@ -323,20 +323,20 @@ ;; --------------------------------------------------------------------------- ;; 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) - (~events-day-checkbox + (~forms/day-checkbox :name (get d "name") :label (get d "label") :checked (when (get d "checked") "checked"))) (or days-data (list))))) ;; 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) - (~events-slot-option + (~forms/slot-option :value (get s "value") :data-start (get s "data-start") :data-end (get s "data-end") @@ -347,32 +347,32 @@ (or slots (list))))) ;; 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))) - (~events-no-slots) - (~events-slot-picker + (~forms/no-slots) + (~forms/slot-picker :id id - :options (~events-slot-options-from-data :slots slots)))) + :options (~forms/slot-options-from-data :slots slots)))) ;; 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 days-data all-checked flexible-checked action-btn cancel-btn) - (~events-slot-edit-form + (~forms/slot-edit-form :slot-id slot-id :list-container list-container :put-url put-url :cancel-url cancel-url :csrf csrf :name-val name-val :cost-val cost-val :start-val start-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 :action-btn action-btn :cancel-btn cancel-btn)) ;; Slot add form from data -(defcomp ~events-slot-add-form-from-data (&key post-url csrf days-data action-btn cancel-btn cancel-url) - (~events-slot-add-form +(defcomp ~forms/slot-add-form-from-data (&key post-url csrf days-data action-btn cancel-btn cancel-url) + (~forms/slot-add-form :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)) @@ -380,7 +380,7 @@ ;; 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) (<> (div :id "entry-errors" :class "mt-2 text-sm text-red-600") @@ -446,7 +446,7 @@ :data-confirm-cancel-text "Cancel" (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 :sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML" "+ Add entry")) @@ -456,7 +456,7 @@ ;; 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 action-btn cancel-btn) (section :id (str "ticket-" ticket-id) :class list-container @@ -509,7 +509,7 @@ ;; 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" :sx-disinherit "sx-select" :sx-swap "outerHTML" :sx-headers csrf :class "space-y-3" @@ -540,7 +540,7 @@ :data-confirm-cancel-text "Cancel" (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 :sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML" (i :class "fa fa-plus") " Add ticket type")) @@ -550,6 +550,6 @@ ;; Entry admin nav — placeholder ;; --------------------------------------------------------------------------- -(defcomp ~events-admin-placeholder-nav () +(defcomp ~forms/admin-placeholder-nav () (div :class "relative nav-group" (span :class "block px-3 py-2 text-stone-400 text-sm italic" "Admin options"))) \ No newline at end of file diff --git a/events/sx/fragments.sx b/events/sx/fragments.sx index 1de6cd6..b7bc9a3 100644 --- a/events/sx/fragments.sx +++ b/events/sx/fragments.sx @@ -5,14 +5,14 @@ ;; 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 :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 "text-xs text-stone-600" date-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" (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;" @@ -23,7 +23,7 @@ ;; 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 "flex items-start justify-between gap-4" (div :class "min-w-0 flex-1" @@ -35,13 +35,13 @@ type-name)) (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 "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") items))) -(defcomp ~events-frag-tickets-list (&key items) +(defcomp ~fragments/frag-tickets-list (&key items) (div :class "divide-y divide-stone-100" items)) @@ -49,7 +49,7 @@ ;; 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 "flex items-start justify-between gap-4" (div :class "min-w-0 flex-1" @@ -60,13 +60,13 @@ cost-str)) (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 "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") items))) -(defcomp ~events-frag-bookings-list (&key items) +(defcomp ~fragments/frag-bookings-list (&key items) (div :class "divide-y divide-stone-100" items)) @@ -75,12 +75,12 @@ ;; --------------------------------------------------------------------------- ;; 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) (if (get w "entries") - (~events-frag-entries-widget + (~fragments/frag-entries-widget :cards (<> (map (lambda (e) - (~events-frag-entry-card + (~fragments/frag-entry-card :href (get e "href") :name (get e "name") :date-str (get e "date-str") :time-str (get e "time-str"))) (get w "entries")))) @@ -88,43 +88,43 @@ (or widgets (list))))) ;; 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) - (~events-frag-ticket-item +(defcomp ~fragments/frag-ticket-item-from-data (&key href entry-name date-str calendar-name type-name state) + (~fragments/frag-ticket-item :href href :entry-name entry-name :date-str date-str :calendar-name (when calendar-name (span "\u00b7 " calendar-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 -(defcomp ~events-frag-tickets-panel-from-data (&key tickets) - (~events-frag-tickets-panel +(defcomp ~fragments/frag-tickets-panel-from-data (&key tickets) + (~fragments/frag-tickets-panel :items (if (empty? (or tickets (list))) - (~empty-state :message "No tickets yet." :cls "text-sm text-stone-500") - (~events-frag-tickets-list + (~shared:misc/empty-state :message "No tickets yet." :cls "text-sm text-stone-500") + (~fragments/frag-tickets-list :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") :date-str (get t "date-str") :calendar-name (get t "calendar-name") :type-name (get t "type-name") :state (get t "state"))) tickets)))))) ;; 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) - (~events-frag-booking-item +(defcomp ~fragments/frag-booking-item-from-data (&key name date-str end-time calendar-name cost-str state) + (~fragments/frag-booking-item :name name :date-str (<> date-str (when end-time (span "\u2013 " end-time))) :calendar-name (when calendar-name (span "\u00b7 " calendar-name)) :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 -(defcomp ~events-frag-bookings-panel-from-data (&key bookings) - (~events-frag-bookings-panel +(defcomp ~fragments/frag-bookings-panel-from-data (&key bookings) + (~fragments/frag-bookings-panel :items (if (empty? (or bookings (list))) - (~empty-state :message "No bookings yet." :cls "text-sm text-stone-500") - (~events-frag-bookings-list + (~shared:misc/empty-state :message "No bookings yet." :cls "text-sm text-stone-500") + (~fragments/frag-bookings-list :items (<> (map (lambda (b) - (~events-frag-booking-item-from-data + (~fragments/frag-booking-item-from-data :href (get b "href") :name (get b "name") :date-str (get b "date-str") :end-time (get b "end-time") :calendar-name (get b "calendar-name") :cost-str (get b "cost-str") diff --git a/events/sx/handlers/account-nav-item.sx b/events/sx/handlers/account-nav-item.sx index e3c8881..9f0b5d8 100644 --- a/events/sx/handlers/account-nav-item.sx +++ b/events/sx/handlers/account-nav-item.sx @@ -8,12 +8,12 @@ (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")) (<> - (~nav-group-link + (~shared:misc/nav-group-link :href (app-url "account" "/tickets/") :hx-select hx-select :nav-class nav-class :label "tickets") - (~nav-group-link + (~shared:misc/nav-group-link :href (app-url "account" "/bookings/") :hx-select hx-select :nav-class nav-class diff --git a/events/sx/handlers/account-page.sx b/events/sx/handlers/account-page.sx index b0b5893..b545667 100644 --- a/events/sx/handlers/account-page.sx +++ b/events/sx/handlers/account-page.sx @@ -10,13 +10,13 @@ (cond (= slug "tickets") (let ((tickets (service "calendar" "user-tickets" :user-id uid))) - (~events-frag-tickets-panel + (~fragments/frag-tickets-panel :items (if (empty? tickets) - (~empty-state :message "No tickets yet." + (~shared:misc/empty-state :message "No tickets yet." :cls "text-sm text-stone-500") - (~events-frag-tickets-list + (~fragments/frag-tickets-list :items (<> (map (fn (t) - (~events-frag-ticket-item + (~fragments/frag-ticket-item :href (app-url "events" (str "/tickets/" (get t "code") "/")) :entry-name (get t "entry_name") @@ -25,18 +25,18 @@ (span (str "\u00b7 " (get t "calendar_name")))) :type-name (when (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)))))) (= slug "bookings") (let ((bookings (service "calendar" "user-bookings" :user-id uid))) - (~events-frag-bookings-panel + (~fragments/frag-bookings-panel :items (if (empty? bookings) - (~empty-state :message "No bookings yet." + (~shared:misc/empty-state :message "No bookings yet." :cls "text-sm text-stone-500") - (~events-frag-bookings-list + (~fragments/frag-bookings-list :items (<> (map (fn (b) - (~events-frag-booking-item + (~fragments/frag-booking-item :name (get b "name") :date-str (str (format-date (get b "start_at") "%d %b %Y, %H:%M") (if (get b "end_at") @@ -46,5 +46,5 @@ (span (str "\u00b7 " (get b "calendar_name")))) :cost-str (when (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)))))))))) diff --git a/events/sx/handlers/container-cards.sx b/events/sx/handlers/container-cards.sx index 7025723..09591e2 100644 --- a/events/sx/handlers/container-cards.sx +++ b/events/sx/handlers/container-cards.sx @@ -19,13 +19,13 @@ (post-slug (or (nth slugs i) ""))) (<> (str "") (when (not (empty? entries)) - (~events-frag-entries-widget + (~fragments/frag-entries-widget :cards (<> (map (fn (e) (let ((time-str (str (format-date (get e "start_at") "%H:%M") (if (get e "end_at") (str " \u2013 " (format-date (get e "end_at") "%H:%M")) "")))) - (~events-frag-entry-card + (~fragments/frag-entry-card :href (app-url "events" (str "/" post-slug "/" (get e "calendar_slug") diff --git a/events/sx/handlers/container-nav.sx b/events/sx/handlers/container-nav.sx index 7bb41e9..f057c31 100644 --- a/events/sx/handlers/container-nav.sx +++ b/events/sx/handlers/container-nav.sx @@ -53,7 +53,7 @@ (if (get entry "end_at") (str " – " (format-date (get entry "end_at") "%H:%M")) "")))) - (~calendar-entry-nav + (~shared:navigation/calendar-entry-nav :href (app-url "events" entry-path) :name (get entry "name") :date-str date-str @@ -61,7 +61,7 @@ ;; Infinite scroll sentinel (when (and has-more (not (empty? purl))) - (~htmx-sentinel + (~shared:misc/htmx-sentinel :id (str "entries-load-sentinel-" pg) :hx-get (str purl "?page=" (+ pg 1)) :hx-trigger "intersect once" @@ -74,7 +74,7 @@ (is-selected (if (not (empty? cur-cal)) (= (get cal "slug") cur-cal) false))) - (~calendar-link-nav + (~shared:navigation/calendar-link-nav :href href :name (get cal "name") :nav-class nav-class diff --git a/events/sx/handlers/link-card.sx b/events/sx/handlers/link-card.sx index 630eead..43cfca6 100644 --- a/events/sx/handlers/link-card.sx +++ b/events/sx/handlers/link-card.sx @@ -16,7 +16,7 @@ :container-type "page" :container-id (get post "id"))) (cal-names (join ", " (map (fn (c) (get c "name")) calendars)))) - (~link-card + (~shared:fragments/link-card :title (get post "title") :image (get post "feature_image") :subtitle cal-names @@ -28,7 +28,7 @@ :container-type "page" :container-id (get post "id"))) (cal-names (join ", " (map (fn (c) (get c "name")) calendars)))) - (~link-card + (~shared:fragments/link-card :title (get post "title") :image (get post "feature_image") :subtitle cal-names diff --git a/events/sx/header.sx b/events/sx/header.sx index 0f62026..fdec1c1 100644 --- a/events/sx/header.sx +++ b/events/sx/header.sx @@ -1,12 +1,12 @@ ;; Events header components -(defcomp ~events-calendars-label () +(defcomp ~header/calendars-label () (<> (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"))) -(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-row items-center gap-2" (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" description))) -(defcomp ~events-day-label (&key date-str) +(defcomp ~header/day-label (&key date-str) (div :class "flex gap-1 items-center" (i :class "fa fa-calendar-day") (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" 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-row items-center gap-2" (i :class "fa fa-clock") diff --git a/events/sx/layouts.sx b/events/sx/layouts.sx index 646e429..2fde1a1 100644 --- a/events/sx/layouts.sx +++ b/events/sx/layouts.sx @@ -11,20 +11,20 @@ (let ((__cal (events-calendar-ctx)) (__sc (select-colours))) (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" :calendar-slug (get __cal "slug")) - :link-label-content (~events-calendar-label + :link-label-content (~header/calendar-label :name (get __cal "name") :description (get __cal "description")) :nav (<> - (~nav-link :href (url-for "defpage_slots_listing" + (~shared:layout/nav-link :href (url-for "defpage_slots_listing" :calendar-slug (get __cal "slug")) :icon "fa fa-clock" :label "Slots" :select-colours __sc) (let ((__rights (app-rights))) (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")) :icon "fa fa-cog" :select-colours __sc)))) @@ -37,13 +37,13 @@ (let ((__cal (events-calendar-ctx)) (__sc (select-colours))) (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" :nav (<> - (~nav-link :href (url-for "defpage_slots_listing" + (~shared:layout/nav-link :href (url-for "defpage_slots_listing" :calendar-slug (get __cal "slug")) :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")) :label "description" :select-colours __sc)) :child-id "calendar-admin-header-child" @@ -55,13 +55,13 @@ (let ((__day (events-day-ctx)) (__cal (events-calendar-ctx))) (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" :calendar-slug (get __cal "slug") :year (get __day "year") :month (get __day "month") :day (get __day "day")) - :link-label-content (~events-day-label + :link-label-content (~header/day-label :date-str (get __day "date-str")) :nav (get __day "nav") :child-id "day-header-child" @@ -73,7 +73,7 @@ (let ((__day (events-day-ctx)) (__cal (events-calendar-ctx))) (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" :calendar-slug (get __cal "slug") :year (get __day "year") @@ -88,12 +88,12 @@ (quasiquote (let ((__ectx (events-entry-ctx))) (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-label-content (~events-entry-label + :link-label-content (~header/entry-label :entry-id (get __ectx "id") - :title (~events-entry-title :name (get __ectx "name")) - :times (~events-entry-times :time-str (get __ectx "time-str"))) + :title (~admin/entry-title :name (get __ectx "name")) + :times (~admin/entry-times :time-str (get __ectx "time-str"))) :nav (get __ectx "nav") :child-id "entry-header-child" :oob (unquote oob)))))) @@ -103,11 +103,11 @@ (quasiquote (let ((__ectx (events-entry-ctx))) (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-label "admin" :icon "fa fa-cog" :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" :select-colours (get __ectx "select-colours"))) :child-id "entry-admin-header-child" @@ -118,8 +118,8 @@ (quasiquote (let ((__slot (events-slot-ctx))) (when (get __slot "name") - (~menu-row-sx :id "slot-row" :level 5 - :link-label-content (~events-slot-label + (~shared:layout/menu-row-sx :id "slot-row" :level 5 + :link-label-content (~header/slot-label :name (get __slot "name") :description (get __slot "description")) :child-id "slot-header-child" @@ -131,12 +131,12 @@ (let ((__ectx (events-entry-ctx)) (__cal (events-calendar-ctx))) (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-label-content (<> (i :class "fa fa-ticket") (div :class "shrink-0" "ticket types")) - :nav (~events-admin-placeholder-nav) + :nav (~forms/admin-placeholder-nav) :child-id "ticket_type-header-child" :oob (unquote oob)))))) @@ -145,22 +145,22 @@ (quasiquote (let ((__tt (events-ticket-type-ctx))) (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-label-content (div :class "flex flex-col md:flex-row md:gap-2 items-center" (div :class "flex flex-row items-center gap-2" (i :class "fa fa-ticket") (div :class "shrink-0" (get __tt "name")))) - :nav (~events-admin-placeholder-nav) + :nav (~forms/admin-placeholder-nav) :child-id "ticket_type-header-child-inner" :oob (unquote oob)))))) (defmacro ~events-markets-header-auto (oob) "Markets section header row." (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-label-content (~events-markets-label) + :link-label-content (~header/markets-label) :child-id "markets-header-child" :oob (unquote oob)))) @@ -168,218 +168,218 @@ ;; 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-div :id "entry-admin-row") - (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "entry-row") - (~clear-oob-div :id "entry-header-child") - (~clear-oob-div :id "day-admin-row") - (~clear-oob-div :id "day-admin-header-child") - (~clear-oob-div :id "day-row") - (~clear-oob-div :id "day-header-child") - (~clear-oob-div :id "calendars-row") - (~clear-oob-div :id "calendars-header-child"))) + (~shared:layout/clear-oob-div :id "entry-admin-row") + (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "entry-row") + (~shared:layout/clear-oob-div :id "entry-header-child") + (~shared:layout/clear-oob-div :id "day-admin-row") + (~shared:layout/clear-oob-div :id "day-admin-header-child") + (~shared:layout/clear-oob-div :id "day-row") + (~shared:layout/clear-oob-div :id "day-header-child") + (~shared:layout/clear-oob-div :id "calendars-row") + (~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-div :id "entry-admin-row") - (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "entry-row") - (~clear-oob-div :id "entry-header-child") - (~clear-oob-div :id "day-admin-row") - (~clear-oob-div :id "day-admin-header-child") - (~clear-oob-div :id "day-row") - (~clear-oob-div :id "day-header-child") - (~clear-oob-div :id "calendars-row") - (~clear-oob-div :id "calendars-header-child"))) + (~shared:layout/clear-oob-div :id "entry-admin-row") + (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "entry-row") + (~shared:layout/clear-oob-div :id "entry-header-child") + (~shared:layout/clear-oob-div :id "day-admin-row") + (~shared:layout/clear-oob-div :id "day-admin-header-child") + (~shared:layout/clear-oob-div :id "day-row") + (~shared:layout/clear-oob-div :id "day-header-child") + (~shared:layout/clear-oob-div :id "calendars-row") + (~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-div :id "entry-admin-row") - (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "entry-row") - (~clear-oob-div :id "entry-header-child") - (~clear-oob-div :id "calendars-row") - (~clear-oob-div :id "calendars-header-child"))) + (~shared:layout/clear-oob-div :id "entry-admin-row") + (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "entry-row") + (~shared:layout/clear-oob-div :id "entry-header-child") + (~shared:layout/clear-oob-div :id "calendars-row") + (~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-div :id "entry-admin-row") - (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "day-admin-row") - (~clear-oob-div :id "day-admin-header-child") - (~clear-oob-div :id "calendar-admin-row") - (~clear-oob-div :id "calendar-admin-header-child") - (~clear-oob-div :id "calendars-row") - (~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 "entry-admin-row") + (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "day-admin-row") + (~shared:layout/clear-oob-div :id "day-admin-header-child") + (~shared:layout/clear-oob-div :id "calendar-admin-row") + (~shared:layout/clear-oob-div :id "calendar-admin-header-child") + (~shared:layout/clear-oob-div :id "calendars-row") + (~shared:layout/clear-oob-div :id "calendars-header-child") + (~shared:layout/clear-oob-div :id "post-admin-row") + (~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-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"))) ;; --------------------------------------------------------------------------- ;; 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-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") - (~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") - (~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child") - (~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") - (~clear-oob-div :id "calendar-row") (~clear-oob-div :id "calendar-header-child") - (~clear-oob-div :id "calendars-row") (~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 "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child") + (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child") + (~shared:layout/clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-header-child") + (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child") + (~shared:layout/clear-oob-div :id "calendar-row") (~shared:layout/clear-oob-div :id "calendar-header-child") + (~shared:layout/clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-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-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") - (~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") - (~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child") - (~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") - (~clear-oob-div :id "calendar-row") (~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 "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child") + (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child") + (~shared:layout/clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-header-child") + (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child") + (~shared:layout/clear-oob-div :id "calendar-row") (~shared:layout/clear-oob-div :id "calendar-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-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") - (~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") - (~clear-oob-div :id "day-row") (~clear-oob-div :id "day-header-child") - (~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") - (~clear-oob-div :id "calendars-row") (~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 "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child") + (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child") + (~shared:layout/clear-oob-div :id "day-row") (~shared:layout/clear-oob-div :id "day-header-child") + (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child") + (~shared:layout/clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-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-oob-div :id "entry-admin-row") (~clear-oob-div :id "entry-admin-header-child") - (~clear-oob-div :id "entry-row") (~clear-oob-div :id "entry-header-child") - (~clear-oob-div :id "day-admin-row") (~clear-oob-div :id "day-admin-header-child") - (~clear-oob-div :id "calendar-admin-row") (~clear-oob-div :id "calendar-admin-header-child") - (~clear-oob-div :id "calendars-row") (~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 "entry-admin-row") (~shared:layout/clear-oob-div :id "entry-admin-header-child") + (~shared:layout/clear-oob-div :id "entry-row") (~shared:layout/clear-oob-div :id "entry-header-child") + (~shared:layout/clear-oob-div :id "day-admin-row") (~shared:layout/clear-oob-div :id "day-admin-header-child") + (~shared:layout/clear-oob-div :id "calendar-admin-row") (~shared:layout/clear-oob-div :id "calendar-admin-header-child") + (~shared:layout/clear-oob-div :id "calendars-row") (~shared:layout/clear-oob-div :id "calendars-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) ;; --------------------------------------------------------------------------- -(defcomp ~events-cal-admin-layout-full () +(defcomp ~layouts/cal-admin-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~post-admin-header-auto nil "calendars") (~events-calendar-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") (~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)) - (~events-clear-oob-cal-admin) + (~layouts/clear-oob-cal-admin) (~root-header-auto true))) ;; --------------------------------------------------------------------------- ;; Slots layout: same full as cal-admin ;; --------------------------------------------------------------------------- -(defcomp ~events-slots-layout-full () +(defcomp ~layouts/slots-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~post-admin-header-auto nil "calendars") (~events-calendar-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") (~events-calendar-admin-header-auto true) - (~events-clear-oob-cal-admin) + (~layouts/clear-oob-cal-admin) (~root-header-auto true))) ;; --------------------------------------------------------------------------- ;; Slot detail layout: root + post + child(admin + cal + cal-admin + slot) ;; --------------------------------------------------------------------------- -(defcomp ~events-slot-layout-full () +(defcomp ~layouts/slot-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~post-admin-header-auto nil "calendars") (~events-calendar-header-auto nil) (~events-calendar-admin-header-auto nil) (~events-slot-header-auto nil))))) -(defcomp ~events-slot-layout-oob () +(defcomp ~layouts/slot-layout-oob () (<> (~post-admin-header-auto true "calendars") (~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)) - (~events-clear-oob-slot) + (~layouts/clear-oob-slot) (~root-header-auto true))) ;; --------------------------------------------------------------------------- ;; 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) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~post-admin-header-auto nil "calendars") (~events-calendar-header-auto nil) (~events-day-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") (~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)) - (~events-clear-oob-day-admin) + (~layouts/clear-oob-day-admin) (~root-header-auto true))) ;; --------------------------------------------------------------------------- ;; Entry layout: root + child(post + cal + day + entry) — public, no admin ;; --------------------------------------------------------------------------- -(defcomp ~events-entry-layout-full () +(defcomp ~layouts/entry-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~events-calendar-header-auto nil) (~events-day-header-auto nil) (~events-entry-header-auto nil))))) -(defcomp ~events-entry-layout-oob () +(defcomp ~layouts/entry-layout-oob () (<> (~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)) - (~events-clear-oob-entry) + (~layouts/clear-oob-entry) (~root-header-auto true))) ;; --------------------------------------------------------------------------- ;; 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) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~post-admin-header-auto nil "calendars") (~events-calendar-header-auto nil) @@ -387,21 +387,21 @@ (~events-entry-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") (~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)) - (~events-clear-oob-entry-admin) + (~layouts/clear-oob-entry-admin) (~root-header-auto true))) ;; --------------------------------------------------------------------------- ;; 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) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~events-calendar-header-auto nil) (~events-day-header-auto nil) @@ -409,9 +409,9 @@ (~events-entry-admin-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) - (~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)) (~root-header-auto true))) @@ -419,9 +419,9 @@ ;; Ticket type layout: all headers down to ticket-type ;; --------------------------------------------------------------------------- -(defcomp ~events-ticket-type-layout-full () +(defcomp ~layouts/ticket-type-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~events-calendar-header-auto nil) (~events-day-header-auto nil) @@ -430,9 +430,9 @@ (~events-ticket-types-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) - (~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)) (~root-header-auto true))) @@ -440,14 +440,14 @@ ;; Markets layout: root + child(post + markets) ;; --------------------------------------------------------------------------- -(defcomp ~events-markets-layout-full () +(defcomp ~layouts/markets-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~events-markets-header-auto nil))))) -(defcomp ~events-markets-layout-oob () +(defcomp ~layouts/markets-layout-oob () (<> (~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)) (~root-header-auto true))) diff --git a/events/sx/page.sx b/events/sx/page.sx index b163c01..f0c3fe1 100644 --- a/events/sx/page.sx +++ b/events/sx/page.sx @@ -1,15 +1,15 @@ ;; 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)) -(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)) -(defcomp ~events-slot-no-days () +(defcomp ~page/slot-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 (div :class "flex flex-col" (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 :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" :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block" description)) -(defcomp ~events-slots-empty-row () +(defcomp ~page/slots-empty-row () (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) (tr :class tr-cls (td :class "p-2 align-top w-1/6" @@ -57,7 +57,7 @@ :sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed" (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 (table :class "w-full text-sm border table-fixed" (thead :class "bg-stone-100" @@ -78,61 +78,61 @@ ;; --------------------------------------------------------------------------- ;; 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))) - (~events-slot-no-days) - (~events-slot-days-pills - :days-inner (<> (map (lambda (d) (~events-slot-day-pill :day d)) days))))) + (~page/slot-no-days) + (~page/slot-days-pills + :days-inner (<> (map (lambda (d) (~page/slot-day-pill :day d)) days))))) ;; 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 pre-action edit-url description oob) (<> - (~events-slot-panel + (~page/slot-panel :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 :pre-action pre-action :edit-url edit-url) (when oob - (~events-slot-description-oob :description (or description ""))))) + (~page/slot-description-oob :description (or description ""))))) ;; 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) - (~events-slots-table + (~page/slots-table :list-container list-container :rows (if (empty? (or slots (list))) - (~events-slots-empty-row) + (~page/slots-empty-row) (<> (map (lambda (s) - (~events-slots-row + (~page/slots-row :tr-cls tr-cls :slot-href (get s "slot-href") :pill-cls pill-cls :hx-select hx-select :slot-name (get s "slot-name") :description (get s "description") :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") :cost-str (get s "cost-str") :action-btn action-btn :del-url (get s "del-url") :csrf-hdr csrf-hdr)) (or slots (list))))) :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 "text-xs font-semibold uppercase tracking-wide text-stone-500" label) (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 (div :class "grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm" c1 c2 c3) (button :type "button" :class pre-action :sx-get edit-url :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."))) -(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) (tr :class tr-cls (td :class "p-2 align-top w-1/3" @@ -151,7 +151,7 @@ :sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed" (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 (table :class "w-full text-sm border table-fixed" (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" (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 "flex items-center gap-2" (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" :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" (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" :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") :sx-post update-url :sx-target (str "#entry-tickets-" entry-id) :sx-swap "innerHTML" (input :type "hidden" :name "csrf_token" :value csrf) @@ -203,12 +203,12 @@ :onclick hide-js "Cancel")))) ;; 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 user-ticket-count price-str adjust-url csrf state my-tickets-href) (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)) (target (str "#ticket-buy-" entry-id))) (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 (div :class "font-medium text-sm" (get tt "name")) (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 :my-tickets-href my-tickets-href)))) ticket-types)) (<> (div :class "flex items-center justify-between mb-4" (div (span :class "font-medium text-green-600" price-str) (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) :ticket-type-id nil :my-tickets-href my-tickets-href))))))) ;; 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) (form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" :class "flex items-center" (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" "+"))))) -(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" (i :class "fa fa-ticket mr-1" :aria-hidden "true") "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)) (suffix (if (= count 1) "" "s"))) (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"))))) ;; 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) (<> - (~events-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/cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo) + (~page/buy-result :entry-id entry-id :tickets tickets :remaining remaining :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 ticket-types user-ticket-counts-by-type user-ticket-count price-str adjust-url csrf state my-tickets-href) (<> - (~events-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/cart-icon :cart-count cart-count :blog-href blog-href :cart-href cart-href :logo logo) + (~page/buy-form :entry-id entry-id :info-sold info-sold :info-remaining info-remaining :info-basket info-basket :ticket-types ticket-types :user-ticket-counts-by-type user-ticket-counts-by-type :user-ticket-count user-ticket-count :price-str price-str @@ -330,18 +330,18 @@ :my-tickets-href my-tickets-href))) ;; 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) - (~events-cart-icon-logo :blog-href blog-href :logo logo) - (~events-cart-icon-badge :cart-href cart-href :count (str cart-count)))) + (~page/cart-icon-logo :blog-href blog-href :logo logo) + (~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 :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" (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" (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") @@ -349,37 +349,37 @@ count)))) ;; 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" (input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "entry_id" :value entry-id) (input :type "hidden" :name "count" :value count-val) 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" (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" "-")) -(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" "+")) -(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" (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 "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" (span :class "text-green-600 font-medium text-sm" price) inner)) ;; 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" posts (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") (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)) -(defcomp ~events-entry-posts-none () +(defcomp ~page/entry-posts-none () (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" img (span :class "text-sm flex-1" title) (button :type "button" :class "text-xs text-red-600 hover:text-red-800 flex-shrink-0" @@ -409,41 +409,41 @@ :sx-headers csrf-hdr (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")) -(defcomp ~events-post-img-placeholder () +(defcomp ~page/post-img-placeholder () (div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0")) ;; 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")) -(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" :id "entry-posts-nav-wrapper" :sx-swap-oob "true" (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)))) ;; 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")) -(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 (div :class "w-8 h-8 rounded bg-stone-200 flex-shrink-0") (div :class "flex-1 min-w-0" (div :class "font-medium truncate" name) (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 (i :class "fa fa-calendar" :aria-hidden "true") (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" :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" @@ -461,7 +461,7 @@ (i :class "fa fa-chevron-right")))) ;; 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" img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title)))) @@ -471,60 +471,60 @@ ;; --------------------------------------------------------------------------- ;; 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 - (~events-post-img :src src :alt alt) - (~events-post-img-placeholder))) + (~page/post-img :src src :alt alt) + (~page/post-img-placeholder))) ;; 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))) - (~events-entry-posts-nav-oob-empty) - (~events-entry-posts-nav-oob + (~page/entry-posts-nav-oob-empty) + (~page/entry-posts-nav-oob :items (<> (map (lambda (p) - (~events-entry-nav-post + (~page/entry-nav-post :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"))) posts))))) ;; 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)))) - (~events-entry-posts-nav-oob + (~page/entry-posts-nav-oob :items (<> (map (lambda (p) - (~events-entry-nav-post-link + (~page/entry-nav-post-link :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"))) posts))))) ;; 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)))) - (~events-post-nav-oob-empty) - (~events-post-nav-wrapper + (~page/post-nav-oob-empty) + (~page/post-nav-wrapper :items (<> (map (lambda (e) - (~events-post-nav-entry + (~page/post-nav-entry :href (get e "href") :nav-btn nav-btn :name (get e "name") :time-str (get e "time-str"))) (or entries (list))) (map (lambda (c) - (~events-post-nav-calendar + (~page/post-nav-calendar :href (get c "href") :nav-btn nav-btn :name (get c "name"))) (or calendars (list)))) :hyperscript hyperscript))) ;; Entry posts panel from data -(defcomp ~events-entry-posts-panel-from-data (&key entry-id posts search-url) - (~events-entry-posts-panel +(defcomp ~page/entry-posts-panel-from-data (&key entry-id posts search-url) + (~page/entry-posts-panel :posts (if (empty? (or posts (list))) - (~events-entry-posts-none) - (~events-entry-posts-list + (~page/entry-posts-none) + (~page/entry-posts-list :items (<> (map (lambda (p) - (~events-entry-post-item - :img (~events-post-img-from-data :src (get p "img") :alt (get p "title")) + (~page/entry-post-item + :img (~page/post-img-from-data :src (get p "img") :alt (get p "title")) :title (get p "title") :del-url (get p "del-url") :entry-id entry-id :csrf-hdr (get p "csrf-hdr"))) @@ -532,11 +532,11 @@ :search-url search-url :entry-id entry-id)) ;; 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))) - (~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) - (~crud-item + (~shared:misc/crud-item :href (get item "href") :name (get item "name") :slug (get item "slug") :del-url (get item "del-url") :csrf-hdr (get item "csrf-hdr") :list-id list-id @@ -544,84 +544,84 @@ :confirm-text (get item "confirm-text"))) 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) - (~crud-panel + (~shared:misc/crud-panel :form (when can-create - (~crud-create-form + (~shared:misc/crud-create-form :create-url create-url :csrf csrf :errors-id errors-id :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)) ;; 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" (a :href href :class aclass (i :class "fa fa-cog" :aria-hidden "true")))) ;; 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) (<> (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 :is-selected (get c "is-selected"))) (or calendars (list))) (when container-nav container-nav) (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 -(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) (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)))) ;; 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) - (~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)) (or links (list))))) ;; 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)))) - (~events-day-entries-nav + (~day/entries-nav :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"))) entries)))) (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 -(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) - (~events-post-search-item + (~forms/post-search-item :post-url (get item "post-url") :entry-id (get item "entry-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"))) (or items (list))) (cond - (has-more (~events-post-search-sentinel :page page :next-url next-url)) - ((not (empty? (or items (list)))) (~events-post-search-end)) + (has-more (~forms/post-search-sentinel :page page :next-url next-url)) + ((not (empty? (or items (list)))) (~forms/post-search-end)) (true "")))) ;; Entry options from data — state-driven button composition -(defcomp ~events-entry-options-from-data (&key entry-id state buttons) - (~events-entry-options +(defcomp ~page/entry-options-from-data (&key entry-id state buttons) + (~admin/entry-options :entry-id entry-id :buttons (<> (map (lambda (b) - (~events-entry-option-button + (~admin/entry-option-button :url (get b "url") :target (str "#calendar_entry_options_" entry-id) :csrf (get b "csrf") :btn-type (get b "btn-type") :action-btn (get b "action-btn") diff --git a/events/sx/payments.sx b/events/sx/payments.sx index 641a1b4..9eacc16 100644 --- a/events/sx/payments.sx +++ b/events/sx/payments.sx @@ -1,12 +1,12 @@ ;; 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" - (~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 :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") (form :class "mt-4 flex gap-2 items-end" :sx-post create-url @@ -20,15 +20,15 @@ :placeholder "e.g. Farm Shop, Bakery")) (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" form (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.")) -(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 "flex items-center justify-between gap-3" (a :class "flex items-baseline gap-3" :href href diff --git a/events/sx/tickets.sx b/events/sx/tickets.sx index 2029ea8..72cfcbd 100644 --- a/events/sx/tickets.sx +++ b/events/sx/tickets.sx @@ -1,6 +1,6 @@ ;; 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" (div :class "flex items-start justify-between gap-4" (div :class "flex-1 min-w-0" @@ -12,7 +12,7 @@ badge (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 (h1 :class "text-2xl font-bold mb-6" "My Tickets") (if has-tickets @@ -22,7 +22,7 @@ (p :class "text-lg" "No tickets yet") (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-desc :as string?) (checkin-str :as string?) (qr-script :as string)) (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 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 "text-2xl font-bold " text-cls) value) (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)) -(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" (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" (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" (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) (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) @@ -80,7 +80,7 @@ (td :class "px-4 py-3" badge) (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 (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) @@ -113,11 +113,11 @@ (tbody :class "divide-y divide-stone-100" rows)) (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" (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) (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) @@ -127,65 +127,65 @@ (span :class "text-xs text-blue-600" (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" (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)) -(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)) -(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)) -(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)) -(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))) -(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))) -(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" (input :type "hidden" :name "csrf_token" :value csrf) (button :type "submit" :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"))) -(defcomp ~events-lookup-checked-in () +(defcomp ~tickets/lookup-checked-in () (div :class "text-blue-600 text-center" (i :class "fa fa-check-circle text-3xl" :aria-hidden "true") (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" (i :class "fa fa-times-circle text-3xl" :aria-hidden "true") (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 "flex items-start justify-between gap-4" (div :class "flex-1" info) (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) (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" badge) (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" (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" "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" (table :class "w-full text-sm" (thead :class "bg-stone-50" @@ -195,10 +195,10 @@ (th :class "px-4 py-2 text-left font-medium text-stone-600" "Actions"))) (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")) -(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 "flex items-center justify-between" (h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name)) @@ -211,72 +211,72 @@ ;; --------------------------------------------------------------------------- ;; My tickets panel from data -(defcomp ~events-tickets-panel-from-data (&key (list-container :as string) (tickets :as list?)) - (~events-tickets-panel +(defcomp ~tickets/panel-from-data (&key (list-container :as string) (tickets :as list?)) + (~tickets/panel :list-container list-container :has-tickets (not (empty? (or tickets (list)))) :cards (<> (map (lambda (t) - (~events-ticket-card + (~tickets/card :href (get t "href") :entry-name (get t "entry-name") :type-name (get t "type-name") :time-str (get t "time-str") :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"))) (or tickets (list)))))) ;; 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?) (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 :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 :time-date time-date :time-range time-range :cal-name cal-name :type-desc type-desc :checkin-str checkin-str :qr-script qr-script)) ;; 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) (checked-in-time :as string?)) - (~events-ticket-admin-row + (~tickets/admin-row :code code :code-short code-short :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 - :badge (~ticket-state-badge :state state) + :badge (~entries/ticket-state-badge :state state) :action (cond ((or (= state "confirmed") (= state "reserved")) - (~events-ticket-admin-checkin-form + (~tickets/admin-checkin-form :checkin-url checkin-url :code code :csrf csrf)) ((= 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)))) ;; 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?)) - (~events-ticket-admin-panel + (~tickets/admin-panel :list-container list-container :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" :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" :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" :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" :value (str (or reserved 0)) :label "Reserved")) :lookup-url lookup-url :has-tickets (not (empty? (or tickets (list)))) :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") :entry-name (get t "entry-name") :date-str (get t "date-str") :type-name (get t "type-name") :state (get t "state") @@ -285,45 +285,45 @@ (or tickets (list)))))) ;; 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)) - (~events-entry-tickets-admin-panel +(defcomp ~tickets/entry-tickets-admin-from-data (&key (entry-name :as string) (count-label :as string) (tickets :as list?) (csrf :as string)) + (~tickets/entry-tickets-admin-panel :entry-name entry-name :count-label count-label :body (if (empty? (or tickets (list))) - (~events-entry-tickets-admin-empty) - (~events-entry-tickets-admin-table + (~tickets/entry-tickets-admin-empty) + (~tickets/entry-tickets-admin-table :rows (<> (map (lambda (t) - (~events-entry-tickets-admin-row + (~tickets/entry-tickets-admin-row :code (get t "code") :code-short (get t "code-short") :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 ((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)) ((= (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)))) (or tickets (list)))))))) ;; 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)) - (~events-checkin-success-row +(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)) + (~tickets/checkin-success-row :code code :code-short code-short :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 - :badge (~ticket-state-badge :state "checked_in") + :badge (~entries/ticket-state-badge :state "checked_in") :time-str time-str)) ;; 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)) - (~events-ticket-types-table + (~page/ticket-types-table :list-container list-container :rows (if (empty? (or ticket-types (list))) - (~events-ticket-types-empty-row) + (~page/ticket-types-empty-row) (<> (map (lambda (tt) - (~events-ticket-types-row + (~page/ticket-types-row :tr-cls tr-cls :tt-href (get tt "tt-href") :pill-cls pill-cls :hx-select hx-select :tt-name (get tt "tt-name") :cost-str (get tt "cost-str") @@ -333,23 +333,23 @@ :action-btn action-btn :add-url add-url)) ;; 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?) (checkin-url :as string) (csrf :as string)) - (~events-lookup-card + (~tickets/lookup-card :info (<> - (~events-lookup-info :entry-name entry-name) - (when type-name (~events-lookup-type :type-name type-name)) - (when date-str (~events-lookup-date :date-str date-str)) - (when cal-name (~events-lookup-cal :cal-name cal-name)) - (~events-lookup-status - :badge (~ticket-state-badge :state state) :code code) + (~tickets/lookup-info :entry-name entry-name) + (when type-name (~tickets/lookup-type :type-name type-name)) + (when date-str (~tickets/lookup-date :date-str date-str)) + (when cal-name (~tickets/lookup-cal :cal-name cal-name)) + (~tickets/lookup-status + :badge (~entries/ticket-state-badge :state state) :code code) (when checked-in-str - (~events-lookup-checkin-time :date-str checked-in-str))) + (~tickets/lookup-checkin-time :date-str checked-in-str))) :code code :action (cond ((or (= state "confirmed") (= state "reserved")) - (~events-lookup-checkin-btn :checkin-url checkin-url :code code :csrf csrf)) - ((= state "checked_in") (~events-lookup-checked-in)) - ((= state "cancelled") (~events-lookup-cancelled)) + (~tickets/lookup-checkin-btn :checkin-url checkin-url :code code :csrf csrf)) + ((= state "checked_in") (~tickets/lookup-checked-in)) + ((= state "cancelled") (~tickets/lookup-cancelled)) (true nil)))) diff --git a/events/sxc/pages/events.sx b/events/sxc/pages/events.sx index 130e693..c21c7fb 100644 --- a/events/sxc/pages/events.sx +++ b/events/sxc/pages/events.sx @@ -7,8 +7,8 @@ :auth :admin :layout :events-calendar-admin :data (calendar-admin-data calendar-slug) - :content (~events-calendar-admin-panel - :description-content (~events-calendar-description-display + :content (~admin/calendar-admin-panel + :description-content (~calendar/description-display :description cal-description :edit-url desc-edit-url) :csrf csrf :description cal-description)) @@ -18,7 +18,7 @@ :auth :admin :layout :events-day-admin :data (day-admin-data calendar-slug year month day) - :content (~events-day-admin-panel)) + :content (~day/admin-panel)) ;; Slots listing (defpage slots-listing @@ -26,25 +26,25 @@ :auth :public :layout :events-slots :data (slots-data calendar-slug) - :content (~events-slots-table + :content (~page/slots-table :list-container list-container :rows (if has-slots (<> (map (fn (s) - (~events-slots-row + (~page/slots-row :tr-cls tr-cls :slot-href (get s "slot-href") :pill-cls pill-cls :hx-select hx-select :slot-name (get s "name") :description (get s "description") :flexible (get s "flexible") :days (if (get s "has-days") - (~events-slot-days-pills :days-inner - (<> (map (fn (d) (~events-slot-day-pill :day d)) (get s "day-list")))) - (~events-slot-no-days)) + (~page/slot-days-pills :days-inner + (<> (map (fn (d) (~page/slot-day-pill :day d)) (get s "day-list")))) + (~page/slot-no-days)) :time-str (get s "time-str") :cost-str (get s "cost-str") :action-btn action-btn :del-url (get s "del-url") :csrf-hdr csrf-hdr)) slots-list)) - (~events-slots-empty-row)) + (~page/slots-empty-row)) :pre-action pre-action :add-url add-url)) ;; Slot detail @@ -53,13 +53,13 @@ :auth :admin :layout :events-slot :data (slot-data calendar-slug slot-id) - :content (~events-slot-panel + :content (~page/slot-panel :slot-id slot-id-str :list-container list-container :days (if has-days - (~events-slot-days-pills :days-inner - (<> (map (fn (d) (~events-slot-day-pill :day d)) day-list))) - (~events-slot-no-days)) + (~page/slot-days-pills :days-inner + (<> (map (fn (d) (~page/slot-day-pill :day d)) day-list))) + (~page/slot-no-days)) :flexible flexible :time-str time-str :cost-str cost-str :pre-action pre-action :edit-url edit-url)) @@ -70,29 +70,29 @@ :auth :admin :layout :events-entry :data (entry-data calendar-slug entry-id) - :content (~events-entry-panel + :content (~admin/entry-panel :entry-id entry-id-str :list-container list-container - :name (~events-entry-field :label "Name" - :content (~events-entry-name-field :name entry-name)) - :slot (~events-entry-field :label "Slot" + :name (~admin/entry-field :label "Name" + :content (~admin/entry-name-field :name entry-name)) + :slot (~admin/entry-field :label "Slot" :content (if has-slot - (~events-entry-slot-assigned :slot-name slot-name :flex-label flex-label) - (~events-entry-slot-none))) - :time (~events-entry-field :label "Time Period" - :content (~events-entry-time-field :time-str time-str)) - :state (~events-entry-field :label "State" - :content (~events-entry-state-field :entry-id entry-id-str - :badge (~badge :cls state-badge-cls :label state-badge-label))) - :cost (~events-entry-field :label "Cost" - :content (~events-entry-cost-field :cost cost-str)) - :tickets (~events-entry-field :label "Tickets" - :content (~events-entry-tickets-field :entry-id entry-id-str + (~admin/entry-slot-assigned :slot-name slot-name :flex-label flex-label) + (~admin/entry-slot-none))) + :time (~admin/entry-field :label "Time Period" + :content (~admin/entry-time-field :time-str time-str)) + :state (~admin/entry-field :label "State" + :content (~admin/entry-state-field :entry-id entry-id-str + :badge (~shared:misc/badge :cls state-badge-cls :label state-badge-label))) + :cost (~admin/entry-field :label "Cost" + :content (~admin/entry-cost-field :cost cost-str)) + :tickets (~admin/entry-field :label "Tickets" + :content (~admin/entry-tickets-field :entry-id entry-id-str :tickets-config tickets-config)) :buy buy-form - :date (~events-entry-field :label "Date" - :content (~events-entry-date-field :date-str date-str)) - :posts (~events-entry-field :label "Associated Posts" - :content (~events-entry-posts-field :entry-id entry-id-str + :date (~admin/entry-field :label "Date" + :content (~admin/entry-date-field :date-str date-str)) + :posts (~admin/entry-field :label "Associated Posts" + :content (~admin/entry-posts-field :entry-id entry-id-str :posts-panel posts-panel)) :options options-html :pre-action pre-action :edit-url edit-url) @@ -104,9 +104,9 @@ :auth :admin :layout :events-entry-admin :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) - :menu (~events-admin-placeholder-nav)) + :menu (~forms/admin-placeholder-nav)) ;; Ticket types listing (defpage ticket-types-listing @@ -114,11 +114,11 @@ :auth :public :layout :events-ticket-types :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 :rows (if has-types (<> (map (fn (tt) - (~events-ticket-types-row + (~page/ticket-types-row :tr-cls tr-cls :tt-href (get tt "tt-href") :pill-cls pill-cls :hx-select hx-select :tt-name (get tt "tt-name") :cost-str (get tt "cost-str") @@ -126,9 +126,9 @@ :del-url (get tt "del-url") :csrf-hdr csrf-hdr)) types-list)) - (~events-ticket-types-empty-row)) + (~page/ticket-types-empty-row)) :action-btn action-btn :add-url add-url) - :menu (~events-admin-placeholder-nav)) + :menu (~forms/admin-placeholder-nav)) ;; Ticket type detail (defpage ticket-type-detail @@ -136,13 +136,13 @@ :auth :admin :layout :events-ticket-type :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 - :c1 (~events-ticket-type-col :label "Name" :value tt-name) - :c2 (~events-ticket-type-col :label "Cost" :value cost-str) - :c3 (~events-ticket-type-col :label "Count" :value count-str) + :c1 (~page/ticket-type-col :label "Name" :value tt-name) + :c2 (~page/ticket-type-col :label "Cost" :value cost-str) + :c3 (~page/ticket-type-col :label "Count" :value count-str) :pre-action pre-action :edit-url edit-url) - :menu (~events-admin-placeholder-nav)) + :menu (~forms/admin-placeholder-nav)) ;; My tickets (defpage my-tickets @@ -150,16 +150,16 @@ :auth :public :layout :root :data (tickets-data) - :content (~events-tickets-panel + :content (~tickets/panel :list-container list-container :has-tickets has-tickets :cards (when has-tickets (<> (map (fn (t) - (~events-ticket-card + (~tickets/card :href (get t "href") :entry-name (get t "entry-name") :type-name (get t "type-name") :time-str (get t "time-str") :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"))) tickets-list))))) @@ -169,7 +169,7 @@ :auth :public :layout :root :data (ticket-detail-data code) - :content (~events-ticket-detail + :content (~tickets/detail :list-container list-container :back-href back-href :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) @@ -185,10 +185,10 @@ :auth :admin :layout :root :data (ticket-admin-data) - :content (~events-ticket-admin-panel + :content (~tickets/admin-panel :list-container list-container :stats (<> (map (fn (s) - (~events-ticket-admin-stat + (~tickets/admin-stat :border (get s "border") :bg (get s "bg") :text-cls (get s "text-cls") :label-cls (get s "label-cls") :value (get s "value") :label (get s "label"))) @@ -196,18 +196,18 @@ :lookup-url lookup-url :has-tickets has-tickets :rows (when has-tickets (<> (map (fn (t) - (~events-ticket-admin-row + (~tickets/admin-row :code (get t "code") :code-short (get t "code-short") :entry-name (get t "entry-name") :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") - :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") - (~events-ticket-admin-checkin-form + (~tickets/admin-checkin-form :checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf) (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))))) ;; Markets @@ -216,20 +216,20 @@ :auth :public :layout :events-markets :data (markets-data) - :content (~crud-panel + :content (~shared:misc/crud-panel :list-id "markets-list" :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" :placeholder "e.g. Farm Shop, Bakery" :btn-label "Add market")) :list (if markets-list (<> (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") :csrf-hdr (get m "csrf-hdr") :list-id "markets-list" :confirm-title "Delete market?" :confirm-text "Products will be hidden (soft delete)")) 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")))) diff --git a/events/sxc/pages/utils.py b/events/sxc/pages/utils.py index 5a95df6..1724560 100644 --- a/events/sxc/pages/utils.py +++ b/events/sxc/pages/utils.py @@ -117,7 +117,7 @@ def _cart_icon_oob(count: int) -> str: 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 blog_url_fn = getattr(g, "blog_url", None) diff --git a/federation/sx/auth.sx b/federation/sx/auth.sx index bdb2158..7e93a5d 100644 --- a/federation/sx/auth.sx +++ b/federation/sx/auth.sx @@ -1,7 +1,7 @@ ;; Auth components (choose username — federation-specific) ;; 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" (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: " diff --git a/federation/sx/handlers/link-card.sx b/federation/sx/handlers/link-card.sx index cf30791..e14ab4c 100644 --- a/federation/sx/handlers/link-card.sx +++ b/federation/sx/handlers/link-card.sx @@ -12,7 +12,7 @@ (let ((actor (service "federation" "get-actor-by-username" :username u))) (<> (str "") (when (not (nil? actor)) - (~link-card + (~shared:fragments/link-card :link (app-url "federation" (str "/users/" (get actor "preferred_username"))) :title (or (get actor "display_name") @@ -28,7 +28,7 @@ (let ((actor (service "federation" "get-actor-by-username" :username lookup))) (when (not (nil? actor)) - (~link-card + (~shared:fragments/link-card :link (app-url "federation" (str "/users/" (get actor "preferred_username"))) :title (or (get actor "display_name") diff --git a/federation/sx/layouts.sx b/federation/sx/layouts.sx index 1549e03..9d02437 100644 --- a/federation/sx/layouts.sx +++ b/federation/sx/layouts.sx @@ -2,16 +2,16 @@ ;; Registered via register_sx_layout("social", ...) in __init__.py. ;; Full page: root header + social header in header-child -(defcomp ~social-layout-full () +(defcomp ~layouts/social-layout-full () (<> (~root-header-auto) - (~header-child-sx - :inner (~federation-social-header - :nav (~federation-social-nav :actor (federation-actor-ctx)))))) + (~shared:layout/header-child-sx + :inner (~social/header + :nav (~social/nav :actor (federation-actor-ctx)))))) ;; OOB (HTMX): social header oob + root header oob -(defcomp ~social-layout-oob () - (<> (~oob-header-sx +(defcomp ~layouts/social-layout-oob () + (<> (~shared:layout/oob-header-sx :parent-id "root-header-child" - :row (~federation-social-header - :nav (~federation-social-nav :actor (federation-actor-ctx)))) + :row (~social/header + :nav (~social/nav :actor (federation-actor-ctx)))) (~root-header-auto true))) diff --git a/federation/sx/notifications.sx b/federation/sx/notifications.sx index ffad831..4b448fb 100644 --- a/federation/sx/notifications.sx +++ b/federation/sx/notifications.sx @@ -1,9 +1,9 @@ ;; 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)) -(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 "flex items-start gap-3" avatar @@ -15,14 +15,14 @@ preview (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)) -(defcomp ~federation-notifications-page (&key notifs) +(defcomp ~notifications/page (&key notifs) (h1 :class "text-2xl font-bold mb-6" "Notifications") notifs) ;; 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") "?")) (from-username (or (get notif "from_actor_username") "")) (from-domain (or (get notif "from_actor_domain") "")) @@ -44,9 +44,9 @@ ((= ntype "mention") "mentioned you") ((= ntype "reply") "replied to your post") (true "")))) - (~federation-notification-card + (~notifications/card :cls (str "bg-white rounded-lg shadow-sm border border-stone-200 p-4" border) - :avatar (~avatar + :avatar (~shared:misc/avatar :src from-icon :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") @@ -55,15 +55,15 @@ :from-username (escape from-username) :from-domain (if from-domain (str "@" (escape from-domain)) "") :action-text action-text - :preview (when preview (~federation-notification-preview :preview (escape preview))) + :preview (when preview (~notifications/preview :preview (escape preview))) :time created))) ;; Assembled notifications content — replaces Python _notifications_content_sx -(defcomp ~federation-notifications-content (&key (notifications :as list)) - (~federation-notifications-page +(defcomp ~notifications/content (&key (notifications :as list)) + (~notifications/page :notifs (if (empty? notifications) - (~empty-state :message "No notifications yet." :cls "text-stone-500") - (~federation-notifications-list + (~shared:misc/empty-state :message "No notifications yet." :cls "text-stone-500") + (~notifications/list :items (map (lambda (n) - (~federation-notification-from-data :notif n)) + (~notifications/from-data :notif n)) notifications))))) diff --git a/federation/sx/profile.sx b/federation/sx/profile.sx index 461647a..1c60a52 100644 --- a/federation/sx/profile.sx +++ b/federation/sx/profile.sx @@ -1,6 +1,6 @@ ;; 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 "flex items-center gap-4" avatar @@ -10,39 +10,39 @@ summary) follow))) -(defcomp ~federation-actor-timeline-layout (&key header timeline) +(defcomp ~profile/actor-timeline-layout (&key header timeline) header (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" (form :method "post" :action action (input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "actor_url" :value actor-url) (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))) ;; 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)) -(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 "flex justify-between items-start" (span :class "font-medium" activity-type) (span :class "text-sm text-stone-400" published)) obj-type)) -(defcomp ~federation-activities-list (&key (items :as list)) +(defcomp ~profile/activities-list (&key (items :as list)) (div :class "space-y-4" items)) -(defcomp ~federation-activities-empty () +(defcomp ~profile/activities-empty () (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 "bg-white rounded-lg shadow p-6 mb-6" (h1 :class "text-2xl font-bold" display-name) @@ -51,11 +51,11 @@ (h2 :class "text-xl font-bold mb-4" activities-heading) activities)) -(defcomp ~federation-profile-summary-text (&key (text :as string)) +(defcomp ~profile/summary-text (&key (text :as string)) (p :class "mt-2" text)) ;; 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") "")) (icon-url (get remote-actor "icon_url")) (summary (get remote-actor "summary")) @@ -63,9 +63,9 @@ (csrf (csrf-token)) (initial (if (and (not icon-url) display-name) (upper (slice display-name 0 1)) "?"))) - (~federation-actor-timeline-layout - :header (~federation-actor-profile-header - :avatar (~avatar + (~profile/actor-timeline-layout + :header (~profile/actor-profile-header + :avatar (~shared:misc/avatar :src icon-url :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") @@ -73,18 +73,18 @@ :display-name (escape display-name) :username (escape (or (get remote-actor "preferred_username") "")) :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 (if is-following - (~federation-follow-form + (~profile/follow-form :action (url-for "social.unfollow") :csrf csrf :actor-url actor-url :label "Unfollow" :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 :label "Follow" :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 :next-url (when (not (empty? items)) (url-for "social.actor_timeline_page" @@ -92,14 +92,14 @@ :before (get (last items) "before_cursor"))))))) ;; 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))) - (~federation-activities-empty) - (~federation-activities-list + (~profile/activities-empty) + (~profile/activities-list :items (<> (map (lambda (a) - (~federation-activity-card + (~profile/activity-card :activity-type (get a "activity_type") :published (get a "published") :obj-type (when (get a "object_type") - (~federation-activity-obj-type :obj-type (get a "object_type"))))) + (~profile/activity-obj-type :obj-type (get a "object_type"))))) activities))))) diff --git a/federation/sx/search.sx b/federation/sx/search.sx index 52d1c72..5d3ad00 100644 --- a/federation/sx/search.sx +++ b/federation/sx/search.sx @@ -1,37 +1,37 @@ ;; Search and actor card components -;; Aliases — delegate to shared ~avatar -(defcomp ~federation-actor-avatar-img (&key (src :as string) (cls :as string)) - (~avatar :src src :cls cls)) +;; Aliases — delegate to shared ~shared:misc/avatar +(defcomp ~search/actor-avatar-img (&key (src :as string) (cls :as string)) + (~shared:misc/avatar :src src :cls cls)) -(defcomp ~federation-actor-avatar-placeholder (&key (cls :as string) (initial :as string)) - (~avatar :cls cls :initial initial)) +(defcomp ~search/actor-avatar-placeholder (&key (cls :as string) (initial :as string)) + (~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)) -(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" :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))) -(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" (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 "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")))) -(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" (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 "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)))) -(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 avatar (div :class "flex-1 min-w-0" @@ -41,7 +41,7 @@ button)) ;; 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")) (display-name (get d "display_name")) (username (get d "username")) @@ -49,42 +49,42 @@ (actor-url (get d "actor_url")) (safe-id (get d "safe_id")) (initial (or (get d "initial") "?")) - (avatar (~avatar + (avatar (~shared:misc/avatar :src icon-url :cls (if icon-url "w-12 h-12 rounded-full" "w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold") :initial (when (not icon-url) initial))) (name-sx (if (get d "external_link") - (~federation-actor-name-link-external :href (get d "name_href") :name display-name) - (~federation-actor-name-link :href (get d "name_href") :name display-name))) + (~search/actor-name-link-external :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") - (~federation-actor-summary :summary (get d "summary")))) + (~search/actor-summary :summary (get d "summary")))) (is-followed (get d "is_followed")) (button (when has-actor (if (or (= list-type "following") is-followed) - (~federation-unfollow-button :action unfollow-url :csrf csrf :actor-url actor-url) - (~federation-follow-button :action follow-url :csrf csrf :actor-url actor-url + (~search/unfollow-button :action unfollow-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")))))) - (~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" :id (str "actor-" safe-id) :avatar avatar :name name-sx :username username :domain domain :summary summary-sx :button button))) ;; Data-driven actor list (replaces Python _search_results_sx / _actor_list_items_sx loops) -(defcomp ~federation-actor-list-from-data (&key (actors :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)) (<> (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)) (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)) -(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") (form :method "get" :action search-url :class "mb-6" :sx-get search-page-url :sx-target "#search-results" :sx-push-url search-url @@ -97,7 +97,7 @@ (div :id "search-results" results)) ;; 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 " " (span :class "text-stone-400 font-normal" count-str)) (div :id "actor-list" items)) @@ -106,7 +106,7 @@ ;; 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") "")) (username (or (get a "preferred_username") "")) (domain (or (get a "domain") "")) @@ -119,81 +119,81 @@ (upper (slice (or display-name username) 0 1)) "?")) (csrf (csrf-token)) (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" :id (str "actor-" safe-id) - :avatar (~avatar + :avatar (~shared:misc/avatar :src icon-url :cls (if icon-url "w-12 h-12 rounded-full" "w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold") :initial (when (not icon-url) initial)) :name (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) :name (escape display-name)) - (~federation-actor-name-link-external + (~search/actor-name-link-external :href (str "https://" domain "/@" username) :name (escape display-name))) :username (escape username) :domain (escape domain) - :summary (when summary (~federation-actor-summary :summary summary)) + :summary (when summary (~search/actor-summary :summary summary)) :button (when actor (if (or (= list-type "following") is-followed) - (~federation-unfollow-button + (~search/unfollow-button :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 :label (if (= list-type "followers") "Follow Back" "Follow"))))))) ;; 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) - (~federation-search-page +(defcomp ~search/content (&key (query :as string?) (actors :as list) (total :as number) (followed-urls :as list) actor) + (~search/page :search-url (url-for "social.defpage_search") :search-page-url (url-for "social.search_page") :query (escape (or query "")) :info (cond ((and query (> total 0)) - (~federation-search-info + (~search/info :cls "text-sm text-stone-500 mb-4" :text (str total " result" (pluralize total) " for " (escape query)))) (query - (~federation-search-info + (~search/info :cls "text-stone-500 mb-4" :text (str "No results found for " (escape query)))) (true nil)) :results (when (not (empty? actors)) (<> (map (lambda (a) - (~federation-actor-card-from-data + (~search/actor-card-from-data :a a :actor actor :followed-urls followed-urls :list-type "search")) actors) (when (>= (len actors) 20) - (~federation-scroll-sentinel + (~social/scroll-sentinel :url (url-for "social.search_page" :q query :page 2))))))) ;; Assembled following/followers content — replaces Python _following_content_sx etc. -(defcomp ~federation-following-content (&key (actors :as list) (total :as number) actor) - (~federation-actor-list-page +(defcomp ~search/following-content (&key (actors :as list) (total :as number) actor) + (~search/actor-list-page :title "Following" :count-str (str "(" total ")") :items (when (not (empty? actors)) (<> (map (lambda (a) - (~federation-actor-card-from-data + (~search/actor-card-from-data :a a :actor actor :followed-urls (list) :list-type "following")) actors) (when (>= (len actors) 20) - (~federation-scroll-sentinel + (~social/scroll-sentinel :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) - (~federation-actor-list-page +(defcomp ~search/followers-content (&key (actors :as list) (total :as number) (followed-urls :as list) actor) + (~search/actor-list-page :title "Followers" :count-str (str "(" total ")") :items (when (not (empty? actors)) (<> (map (lambda (a) - (~federation-actor-card-from-data + (~search/actor-card-from-data :a a :actor actor :followed-urls followed-urls :list-type "followers")) actors) (when (>= (len actors) 20) - (~federation-scroll-sentinel + (~social/scroll-sentinel :url (url-for "social.followers_list_page" :page 2))))))) diff --git a/federation/sx/social.sx b/federation/sx/social.sx index 4027ebd..f89c0d1 100644 --- a/federation/sx/social.sx +++ b/federation/sx/social.sx @@ -2,46 +2,46 @@ ;; --- 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" (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" (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"))) -(defcomp ~federation-nav-bar (&key items) +(defcomp ~social/nav-bar (&key 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 :class "w-full flex flex-row items-center gap-2 flex-wrap" nav))) ;; --- 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)) -;; Aliases — delegate to shared ~avatar -(defcomp ~federation-avatar-img (&key (src :as string) (cls :as string)) - (~avatar :src src :cls cls)) +;; Aliases — delegate to shared ~shared:misc/avatar +(defcomp ~social/avatar-img (&key (src :as string) (cls :as string)) + (~shared:misc/avatar :src src :cls cls)) -(defcomp ~federation-avatar-placeholder (&key (cls :as string) (initial :as string)) - (~avatar :cls cls :initial initial)) +(defcomp ~social/avatar-placeholder (&key (cls :as string) (initial :as string)) + (~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 (details :class "mt-2" (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)))) -(defcomp ~federation-original-link (&key (url :as string)) +(defcomp ~social/original-link (&key (url :as string)) (a :href url :target "_blank" :rel "noopener" :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" boost (div :class "flex items-start gap-3" @@ -55,36 +55,36 @@ ;; --- 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")) -(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" (input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "csrf_token" :value csrf) (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" (input :type "hidden" :name "object_id" :value oid) (input :type "hidden" :name "author_inbox" :value ainbox) (input :type "hidden" :name "csrf_token" :value csrf) (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" like boost reply)) ;; --- 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")) -(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")) -(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" (h1 :class "text-2xl font-bold" label " Timeline") compose) @@ -92,24 +92,24 @@ ;; --- 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) (boost-url :as string) (unboost-url :as string)) (let* ((boosted-by (get d "boosted_by")) (actor-icon (get d "actor_icon")) (actor-name (get d "actor_name")) (initial (or (get d "initial") "?")) - (avatar (~avatar + (avatar (~shared:misc/avatar :src actor-icon :cls (if actor-icon "w-10 h-10 rounded-full" "w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm") :initial (when (not actor-icon) initial))) - (boost (when boosted-by (~federation-boost-label :name boosted-by))) + (boost (when boosted-by (~social/boost-label :name boosted-by))) (content-sx (if (get d "summary") - (~federation-content :content (get d "content") :summary (get d "summary")) - (~federation-content :content (get d "content")))) + (~social/content :content (get d "content") :summary (get d "summary")) + (~social/content :content (get d "content")))) (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")) (interactions (when has-actor (let* ((oid (get d "object_id")) @@ -123,16 +123,16 @@ (b-action (if boosted-me unboost-url boost-url)) (b-cls (str "flex items-center gap-1 " (if boosted-me "text-green-600 hover:text-green-700" "hover:text-green-600"))) (reply-url (get d "reply_url")) - (reply (when reply-url (~federation-reply-link :url reply-url))) - (like-form (~federation-like-form + (reply (when reply-url (~social/reply-link :url reply-url))) + (like-form (~social/like-form :action l-action :target target :oid oid :ainbox ainbox :csrf csrf :cls l-cls :icon l-icon :count (get d "like_count"))) - (boost-form (~federation-boost-form + (boost-form (~social/boost-form :action b-action :target target :oid oid :ainbox ainbox :csrf csrf :cls b-cls :count (get d "boost_count")))) (div :id (str "interactions-" safe-id) - (~federation-interaction-buttons :like like-form :boost boost-form :reply reply)))))) - (~federation-post-card + (~social/interaction-buttons :like like-form :boost boost-form :reply reply)))))) + (~social/post-card :boost boost :avatar avatar :actor-name actor-name :actor-username (get d "actor_username") :domain (get d "domain") :time (get d "time") @@ -140,22 +140,22 @@ :interactions interactions))) ;; 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)) (<> (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)) (or items (list))) - (when next-url (~federation-scroll-sentinel :url next-url)))) + (when next-url (~social/scroll-sentinel :url next-url)))) ;; --- 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) (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") (form :method "post" :action action :class "space-y-4" (input :type "hidden" :name "csrf_token" :value csrf) @@ -174,9 +174,9 @@ ;; Assembled social nav — replaces Python _social_nav_sx ;; --------------------------------------------------------------------------- -(defcomp ~federation-social-nav (&key actor) +(defcomp ~social/nav (&key 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)) (links (list (dict :endpoint "social.defpage_home_timeline" :label "Timeline") @@ -185,7 +185,7 @@ (dict :endpoint "social.defpage_following_list" :label "Following") (dict :endpoint "social.defpage_followers_list" :label "Followers") (dict :endpoint "social.defpage_search" :label "Search")))) - (~federation-nav-bar + (~social/nav-bar :items (<> (map (lambda (lnk) (let* ((href (url-for (get lnk "endpoint"))) @@ -196,7 +196,7 @@ links) (let* ((notif-url (url-for "social.defpage_notifications")) (notif-bold (if (= rp notif-url) " font-bold" ""))) - (~federation-nav-notification-link + (~social/nav-notification-link :href notif-url :cls (str "px-2 py-1 rounded hover:bg-stone-200 relative" notif-bold) :count-url (url-for "social.notification_count"))) @@ -208,7 +208,7 @@ ;; 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")) (actor-icon (get item "actor_icon")) (actor-name (or (get item "actor_name") "?")) @@ -223,9 +223,9 @@ (safe-id (replace (replace oid "/" "_") ":" "_")) (initial (if (and (not actor-icon) actor-name) (upper (slice actor-name 0 1)) "?"))) - (~federation-post-card - :boost (when boosted-by (~federation-boost-label :name (escape boosted-by))) - :avatar (~avatar + (~social/post-card + :boost (when boosted-by (~social/boost-label :name (escape boosted-by))) + :avatar (~shared:misc/avatar :src actor-icon :cls (if actor-icon "w-10 h-10 rounded-full" "w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm") @@ -235,10 +235,10 @@ :domain (if actor-domain (str "@" (escape actor-domain)) "") :time published :content (if summary - (~federation-content :content content :summary (escape summary)) - (~federation-content :content content)) + (~social/content :content content :summary (escape summary)) + (~social/content :content content)) :original (when (and url (= post-type "remote")) - (~federation-original-link :url url)) + (~social/original-link :url url)) :interactions (when actor (let* ((csrf (csrf-token)) (liked (get item "liked_by_me")) @@ -248,50 +248,50 @@ (ainbox (or (get item "author_inbox") "")) (target (str "#interactions-" safe-id))) (div :id (str "interactions-" safe-id) - (~federation-interaction-buttons - :like (~federation-like-form + (~social/interaction-buttons + :like (~social/like-form :action (url-for (if liked "social.unlike" "social.like")) :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")) :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")) :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")) :count (str bcount)) :reply (when oid - (~federation-reply-link + (~social/reply-link :url (url-for "social.defpage_compose_form" :reply-to oid)))))))))) ;; --------------------------------------------------------------------------- ;; 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) - (~federation-post-card-from-data :item item :actor actor)) + (~social/post-card-from-data :item item :actor actor)) items) (when next-url - (~federation-scroll-sentinel :url next-url)))) + (~social/scroll-sentinel :url next-url)))) ;; 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"))) - (~federation-timeline-page + (~social/timeline-page :label label :compose (when actor - (~federation-compose-button :url (url-for "social.defpage_compose_form"))) - :timeline (~federation-timeline-items + (~social/compose-button :url (url-for "social.defpage_compose_form"))) + :timeline (~social/timeline-items :items items :timeline-type timeline-type :actor actor :next-url (when (not (empty? items)) (url-for (str "social." timeline-type "_timeline_page") :before (get (last items) "before_cursor"))))))) ;; Assembled compose content — replaces Python _compose_content_sx -(defcomp ~federation-compose-content (&key (reply-to :as string?)) - (~federation-compose-form +(defcomp ~social/compose-content (&key (reply-to :as string?)) + (~social/compose-form :action (url-for "social.compose_submit") :csrf (csrf-token) :reply (when reply-to - (~federation-compose-reply :reply-to (escape reply-to))))) + (~social/compose-reply :reply-to (escape reply-to))))) diff --git a/federation/sxc/pages/social.sx b/federation/sxc/pages/social.sx index 84e94b5..157b1c7 100644 --- a/federation/sxc/pages/social.sx +++ b/federation/sxc/pages/social.sx @@ -6,7 +6,7 @@ :auth :login :layout :social :data (service "federation-page" "home-timeline-data") - :content (~federation-timeline-content + :content (~social/timeline-content :items items :timeline-type timeline-type :actor actor)) @@ -16,7 +16,7 @@ :auth :public :layout :social :data (service "federation-page" "public-timeline-data") - :content (~federation-timeline-content + :content (~social/timeline-content :items items :timeline-type timeline-type :actor actor)) @@ -26,7 +26,7 @@ :auth :login :layout :social :data (service "federation-page" "compose-data") - :content (~federation-compose-content + :content (~social/compose-content :reply-to reply-to)) (defpage search @@ -34,7 +34,7 @@ :auth :public :layout :social :data (service "federation-page" "search-data") - :content (~federation-search-content + :content (~search/content :query query :actors actors :total total @@ -46,7 +46,7 @@ :auth :login :layout :social :data (service "federation-page" "following-data") - :content (~federation-following-content + :content (~search/following-content :actors actors :total total :actor actor)) @@ -56,7 +56,7 @@ :auth :login :layout :social :data (service "federation-page" "followers-data") - :content (~federation-followers-content + :content (~search/followers-content :actors actors :total total :followed-urls followed-urls @@ -67,7 +67,7 @@ :auth :public :layout :social :data (service "federation-page" "actor-timeline-data" :id id) - :content (~federation-actor-timeline-content + :content (~profile/actor-timeline-content :remote-actor remote-actor :items items :is-following is-following @@ -78,5 +78,5 @@ :auth :login :layout :social :data (service "federation-page" "notifications-data") - :content (~federation-notifications-content + :content (~notifications/content :notifications notifications)) diff --git a/market/sx/cards.sx b/market/sx/cards.sx index cbdaf48..e443d47 100644 --- a/market/sx/cards.sx +++ b/market/sx/cards.sx @@ -1,40 +1,40 @@ ;; 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 "" :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" (figure :class "inline-block 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") - (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)))) -(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 "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") (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)))) -(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))) -(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" - (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)) -;; Price — delegates to shared ~price -(defcomp ~market-card-price (&key (special-price :as string?) (regular-price :as string?)) - (~price :special-price special-price :regular-price regular-price)) +;; Price — delegates to shared ~shared:misc/price +(defcomp ~cards/price (&key (special-price :as string?) (regular-price :as string?)) + (~shared:misc/price :special-price special-price :regular-price regular-price)) ;; 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?) (image :as string?) (labels :as list?) (brand :as string) (brand-highlight :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?)) (div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative" (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"))) (a :href href :sx-get href :sx-target "#main-panel" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" (if image - (~market-card-image :image image :labels labels :brand brand :brand-highlight brand-highlight) - (~market-card-no-image :labels labels :brand brand)) - (~market-card-price :special-price special-price :regular-price regular-price)) + (~cards/image :image image :labels labels :brand brand :brand-highlight brand-highlight) + (~cards/no-image :labels labels :brand brand)) + (~cards/price :special-price special-price :regular-price regular-price)) (div :class "flex justify-center" (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)) :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" :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]" (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))))) -(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" (form :id form-id :action action :method "post" :sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML" @@ -73,22 +73,22 @@ (button :type "submit" :class "cursor-pointer" (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" (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)) -(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)) -(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" (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 ~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" (div (if title-content title-content (when title title)) @@ -101,11 +101,11 @@ ;; --------------------------------------------------------------------------- ;; 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?)) (<> (map (lambda (p) - (~market-product-card + (~cards/product-card :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") :special-price (get p "special-price") :regular-price (get p "regular-price") @@ -119,39 +119,39 @@ :search-post (get p "search-post"))) products) (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) - (~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)) - (~end-of-results)))) + (~shared:misc/end-of-results)))) ;; 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?)) - (~market-market-card +(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?)) + (~cards/market-card :title-content (if href - (~market-market-card-title-link :href href :name name) - (~market-market-card-title :name name)) + (~cards/market-card-title-link :href href :name name) + (~cards/market-card-title :name name)) :desc-content (when description - (~market-market-card-desc :description description)) + (~cards/market-card-desc :description description)) :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 -(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) - (~market-card-from-data + (~cards/from-data :name (get m "name") :description (get m "description") :href (get m "href") :show-badge (get m "show-badge") :badge-href (get m "badge-href") :badge-title (get m "badge-title"))) markets) (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 -(defcomp ~market-landing-from-data (&key (excerpt :as string?) (feature-image :as string?) (html :as string?)) - (~market-landing-content :inner - (<> (when excerpt (~market-landing-excerpt :text excerpt)) - (when feature-image (~market-landing-image :src feature-image)) - (when html (~market-landing-html :html html))))) +(defcomp ~cards/landing-from-data (&key (excerpt :as string?) (feature-image :as string?) (html :as string?)) + (~detail/landing-content :inner + (<> (when excerpt (~detail/landing-excerpt :text excerpt)) + (when feature-image (~detail/landing-image :src feature-image)) + (when html (~detail/landing-html :html html))))) diff --git a/market/sx/cart.sx b/market/sx/cart.sx index 7695d2a..dc1052d 100644 --- a/market/sx/cart.sx +++ b/market/sx/cart.sx @@ -1,6 +1,6 @@ ;; 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 (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) @@ -9,7 +9,7 @@ (span :class "relative inline-flex items-center justify-center" (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 :class "rounded flex items-center gap-2" (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) (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" (a :href href :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" count)))))) -(defcomp ~market-cart-mini-empty (&key href logo) +(defcomp ~cart/mini-empty (&key href logo) (div :id "cart-mini" :sx-swap-oob "outerHTML" (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 "")))) -(defcomp ~market-cart-add-oob (&key id content inner) +(defcomp ~cart/add-oob (&key id content inner) (div :id id :sx-swap-oob "outerHTML" (if content content (when inner inner)))) ;; 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) (<> (if has-count - (~market-cart-mini-count :href cart-href :count (str has-count)) - (~market-cart-mini-empty :href blog-href :logo logo)) - (~market-cart-add-oob :id (str "cart-add-" slug) + (~cart/mini-count :href cart-href :count (str has-count)) + (~cart/mini-empty :href blog-href :logo logo)) + (~cart/add-oob :id (str "cart-add-" slug) :inner (if (= (or quantity "0") "0") - (~market-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-empty :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 :quantity quantity :cart-href cart-href))))) diff --git a/market/sx/detail.sx b/market/sx/detail.sx index 2640a89..df34f51 100644 --- a/market/sx/detail.sx +++ b/market/sx/detail.sx @@ -1,6 +1,6 @@ ;; 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 (figure :class "inline-block" (div :class "relative w-full aspect-square" @@ -9,7 +9,7 @@ labels) (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 "" :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" :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" 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 "" :class "shrink-0 rounded-lg overflow-hidden bg-stone-100 hover:opacity-90 ring-offset-2" :title title (img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async")) (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 "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" 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")) -(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)) -(defcomp ~market-detail-unit-price (&key (price :as string)) +(defcomp ~detail/unit-price (&key (price :as string)) (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))) -(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)) -(defcomp ~market-detail-desc-short (&key (text :as string)) +(defcomp ~detail/desc-short (&key (text :as string)) (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))) -(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)) -(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" (summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between" (span :class "font-medium" title) (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)))) -(defcomp ~market-detail-sections (&key (items :as list)) +(defcomp ~detail/sections (&key (items :as list)) (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)) -(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 "md:col-span-2" gallery stickers) details) (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)) -(defcomp ~market-landing-image (&key (src :as string)) +(defcomp ~detail/landing-image (&key (src :as string)) (div :class "mb-3 flex justify-center" (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))) -(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"))) @@ -99,64 +99,64 @@ ;; --------------------------------------------------------------------------- ;; 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 - (~market-like-button + (~cards/like-button :form-id (get like-data "form-id") :action (get like-data "action") :slug (get like-data "slug") :csrf (get like-data "csrf") :icon-cls (get like-data "icon-cls"))))) (if images (<> - (~market-detail-gallery - :inner (~market-detail-gallery-inner + (~detail/gallery + :inner (~detail/gallery-inner :like like-sx :image (get (first images) "src") :alt (get (first images) "alt") :labels (when labels - (<> (map (lambda (src) (~market-label-overlay :src src)) labels))) + (<> (map (lambda (src) (~cards/label-overlay :src src)) labels))) :brand brand) - :nav (when has-nav-buttons (~market-detail-nav-buttons))) + :nav (when has-nav-buttons (~detail/nav-buttons))) (when thumbs - (~market-detail-thumbs :thumbs + (~detail/thumbs :thumbs (<> (map (lambda (t) - (~market-detail-thumb + (~detail/thumb :title (get t "title") :src (get t "src") :alt (get t "alt"))) thumbs))))) - (~market-detail-no-image :like like-sx)))) + (~detail/no-image :like like-sx)))) ;; 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?)) - (~market-detail-right-col :inner +(defcomp ~detail/info-from-data (&key (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?)) + (~detail/right-col :inner (<> (when extras - (~market-detail-extras :inner + (~detail/extras :inner (<> (map (lambda (e) (if (= (get e "type") "unit-price") - (~market-detail-unit-price :price (get e "value")) - (~market-detail-case-size :size (get e "value")))) + (~detail/unit-price :price (get e "value")) + (~detail/case-size :size (get e "value")))) extras)))) (when (or desc-short desc-html) - (~market-detail-desc-wrapper :inner - (<> (when desc-short (~market-detail-desc-short :text desc-short)) - (when desc-html (~market-detail-desc-html :html desc-html))))) + (~detail/desc-wrapper :inner + (<> (when desc-short (~detail/desc-short :text desc-short)) + (when desc-html (~detail/desc-html :html desc-html))))) (when sections - (~market-detail-sections :items + (~detail/sections :items (<> (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))))))) ;; 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?) (extras :as list?) (desc-short :as string?) (desc-html :as string?) (sections :as list?)) - (~market-detail-layout - :gallery (~market-detail-gallery-from-data + (~detail/layout + :gallery (~detail/gallery-from-data :images images :labels labels :brand brand :like-data like-data :has-nav-buttons has-nav-buttons :thumbs thumbs) :stickers (when sticker-items - (~market-detail-stickers :items + (~detail/stickers :items (<> (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)))) - :details (~market-detail-info-from-data + :details (~detail/info-from-data :extras extras :desc-short desc-short :desc-html desc-html :sections sections))) diff --git a/market/sx/filters.sx b/market/sx/filters.sx index d994dda..fce9314 100644 --- a/market/sx/filters.sx +++ b/market/sx/filters.sx @@ -1,73 +1,73 @@ ;; 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" :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) (img :src src :alt label :class "w-10 h-10") (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" 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" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :class "flex flex-col items-center gap-1 p-1 cursor-pointer" (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" :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) (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" :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) (img :src src :alt name :class "w-6 h-6") (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" 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" :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) (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" 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))) -(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" (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" (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)) -(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" :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) name)) -(defcomp ~market-filter-subcategory-panel (&key items) +(defcomp ~filters/subcategory-panel (&key 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" (a :href href :sx-get href :sx-target "#main-panel" :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" (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)) -(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" (summary :class "cursor-pointer select-none" :id "filter-summary-mobile" search-bar @@ -87,40 +87,40 @@ (div :id "filter-details-mobile" :style "display:contents" 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)) -(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" (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")) -(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)) -(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)) -(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")) -(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)) -(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)) -(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" (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" (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)) @@ -129,160 +129,160 @@ ;; --------------------------------------------------------------------------- ;; Sort option stickers from data -(defcomp ~market-filter-sort-from-data (&key items) - (~market-filter-sort-row :items +(defcomp ~filters/sort-from-data (&key items) + (~filters/sort-row :items (<> (map (lambda (s) - (~market-filter-sort-item + (~filters/sort-item :href (get s "href") :hx-select (get s "hx-select") :ring-cls (get s "ring-cls") :src (get s "src") :label (get s "label"))) items)))) ;; Like filter from data -(defcomp ~market-filter-like-from-data (&key href hx-select liked mobile) - (~market-filter-like +(defcomp ~filters/like-from-data (&key href hx-select liked mobile) + (~filters/like :href href :hx-select hx-select :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"))) ;; 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) - (~market-filter-label-item + (~filters/label-item :href (get lb "href") :hx-select hx-select :ring-cls (get lb "ring-cls") :src (get lb "src") :name (get lb "name"))) items))) ;; Sticker filter items from data -(defcomp ~market-filter-stickers-from-data (&key items hx-select) - (~market-filter-stickers-row :items +(defcomp ~filters/stickers-from-data (&key items hx-select) + (~filters/stickers-row :items (<> (map (lambda (st) - (~market-filter-sticker-item + (~filters/sticker-item :href (get st "href") :hx-select hx-select :ring-cls (get st "ring-cls") :src (get st "src") :name (get st "name") :count-cls (get st "count-cls") :count (get st "count"))) items)))) ;; Brand filter items from data -(defcomp ~market-filter-brands-from-data (&key items hx-select) - (~market-filter-brands-panel :items +(defcomp ~filters/brands-from-data (&key items hx-select) + (~filters/brands-panel :items (<> (map (lambda (br) - (~market-filter-brand-item + (~filters/brand-item :href (get br "href") :hx-select hx-select :bg-cls (get br "bg-cls") :name-cls (get br "name-cls") :name (get br "name") :count (get br "count"))) items)))) ;; Subcategory selector from data -(defcomp ~market-filter-subcategories-from-data (&key items hx-select all-href current-sub) - (~market-filter-subcategory-panel :items +(defcomp ~filters/subcategories-from-data (&key items hx-select all-href current-sub) + (~filters/subcategory-panel :items (<> - (~market-filter-subcategory-item + (~filters/subcategory-item :href all-href :hx-select hx-select :active-cls (if (not current-sub) " bg-stone-200 font-medium" "") :name "All") (map (lambda (sub) - (~market-filter-subcategory-item + (~filters/subcategory-item :href (get sub "href") :hx-select hx-select :active-cls (get sub "active-cls") :name (get sub "name"))) items)))) ;; 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 sticker-data brand-data sub-data hx-select) (<> search-sx - (~market-desktop-category-summary :inner + (~filters/desktop-category-summary :inner (<> - (~market-filter-category-label :label category-label) - (when sort-data (~market-filter-sort-from-data :items sort-data)) - (~market-filter-like-labels-nav :inner + (~filters/category-label :label category-label) + (when sort-data (~filters/sort-from-data :items sort-data)) + (~filters/like-labels-nav :inner (<> - (~market-filter-like-from-data + (~filters/like-from-data :href (get like-data "href") :hx-select hx-select :liked (get like-data "liked") :mobile false) (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 - (~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 - (~market-filter-subcategories-from-data + (~filters/subcategories-from-data :items (get sub-data "items") :hx-select hx-select :all-href (get sub-data "all-href") :current-sub (get sub-data "current-sub"))))) - (~market-desktop-brand-summary + (~filters/desktop-brand-summary :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 -(defcomp ~market-mobile-chips-from-data (&key sort-chip liked-chip label-chips sticker-chips brand-chips) - (~market-mobile-chips-row :inner +(defcomp ~filters/mobile-chips-from-data (&key sort-chip liked-chip label-chips sticker-chips brand-chips) + (~filters/mobile-chips-row :inner (<> (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 - (~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") - (~market-mobile-chip-count + (~filters/mobile-chip-count :cls (get liked-chip "count-cls") :count (get liked-chip "count")))))) (when label-chips - (~market-mobile-chip-list :items + (~filters/mobile-chip-list :items (<> (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") - (~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)))) (when sticker-chips - (~market-mobile-chip-list :items + (~filters/mobile-chip-list :items (<> (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") - (~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)))) (when brand-chips - (~market-mobile-chip-brand-list :items + (~filters/mobile-chip-brand-list :items (<> (map (lambda (bc) (if (get bc "has-count") - (~market-mobile-chip-brand :name (get bc "name") :count (get bc "count")) - (~market-mobile-chip-brand-zero :name (get bc "name")))) + (~filters/mobile-chip-brand :name (get bc "name") :count (get bc "count")) + (~filters/mobile-chip-brand-zero :name (get bc "name")))) brand-chips))))))) ;; 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) (<> - (when sort-data (~market-filter-sort-from-data :items sort-data)) + (when sort-data (~filters/sort-from-data :items sort-data)) (when clear-href - (~market-mobile-clear-filters :href clear-href :hx-select hx-select)) - (~market-mobile-like-labels-row :inner + (~filters/mobile-clear-filters :href clear-href :hx-select hx-select)) + (~filters/mobile-like-labels-row :inner (<> - (~market-filter-like-from-data + (~filters/like-from-data :href (get like-data "href") :hx-select hx-select :liked (get like-data "liked") :mobile true) (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 - (~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 - (~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) -(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-data like-data label-data sticker-data brand-data clear-href hx-select) - (~market-mobile-filter-summary + (~filters/mobile-filter-summary :search-bar search-bar - :chips (~market-mobile-chips-from-data + :chips (~filters/mobile-chips-from-data :sort-chip sort-chip :liked-chip liked-chip :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 :label-data label-data :sticker-data sticker-data :brand-data brand-data :clear-href clear-href :hx-select hx-select))) diff --git a/market/sx/grids.sx b/market/sx/grids.sx index 1546c18..98f379b 100644 --- a/market/sx/grids.sx +++ b/market/sx/grids.sx @@ -1,15 +1,15 @@ ;; 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"))) -(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"))) -(defcomp ~market-admin-content-wrap (&key inner) +(defcomp ~grids/admin-content-wrap (&key 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]") :sx-post action :sx-target "this" :sx-swap "outerHTML" :sx-push-url "false" :sx-headers hx-headers diff --git a/market/sx/handlers/container-nav.sx b/market/sx/handlers/container-nav.sx index 03019ee..98bfb00 100644 --- a/market/sx/handlers/container-nav.sx +++ b/market/sx/handlers/container-nav.sx @@ -15,7 +15,7 @@ (sel-colours (or (jinja-global "select_colours") ""))) (<> (map (fn (m) (let ((href (app-url "market" (str "/" slug "/" (get m "slug") "/")))) - (~market-link-nav + (~shared:navigation/market-link-nav :href href :name (get m "name") :nav-class nav-class diff --git a/market/sx/handlers/link-card.sx b/market/sx/handlers/link-card.sx index cc517f4..806d161 100644 --- a/market/sx/handlers/link-card.sx +++ b/market/sx/handlers/link-card.sx @@ -19,7 +19,7 @@ (if (get product "regular_price") (str (get product "regular_price")) "")))) - (~link-card + (~shared:fragments/link-card :title (get product "title") :image (get product "image") :subtitle subtitle @@ -35,7 +35,7 @@ (if (get product "regular_price") (str (get product "regular_price")) "")))) - (~link-card + (~shared:fragments/link-card :title (get product "title") :image (get product "image") :subtitle subtitle diff --git a/market/sx/headers.sx b/market/sx/headers.sx index 0f16ac0..c667fcf 100644 --- a/market/sx/headers.sx +++ b/market/sx/headers.sx @@ -1,15 +1,15 @@ ;; 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 (i :class "fa fa-shop") " " title) (div :class "flex flex-col md:flex-row md:gap-2 text-xs" (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))) -(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" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :class "px-2 py-1 text-stone-500 hover:text-stone-700" @@ -21,42 +21,42 @@ ;; --------------------------------------------------------------------------- ;; 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) - (~market-desktop-category-nav + (~navigation/desktop-category-nav :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") (map (lambda (cat) - (~market-category-link + (~navigation/category-link :href (get cat "href") :hx-select hx-select :active (get cat "active") :select-colours select-colours :label (get cat "label"))) categories)) :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 -(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 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-label-content (~market-shop-label + :link-label-content (~headers/shop-label :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 :all-href all-href :all-active all-active :admin-href admin-href) :child-id "market-header-child" :oob oob)) ;; 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) - (~menu-row-sx :id "product-row" :level 3 + (~shared:layout/menu-row-sx :id "product-row" :level 3 :link-href link-href - :link-label-content (~market-product-label :title title) + :link-label-content (~headers/product-label :title title) :nav (<> - (~market-prices-header-from-data + (~prices/header-from-data :cart-id (get price-data "cart-id") :cart-action (get price-data "cart-action") :csrf (get price-data "csrf") @@ -66,13 +66,13 @@ :rp-val (get price-data "rp-val") :rp-str (get price-data "rp-str") :rrp-str (get price-data "rrp-str")) (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" :oob oob)) ;; Product admin header row from data -(defcomp ~market-product-admin-header-from-data (&key link-href oob) - (~menu-row-sx :id "product-admin-row" :level 4 +(defcomp ~headers/product-admin-header-from-data (&key link-href oob) + (~shared:layout/menu-row-sx :id "product-admin-row" :level 4 :link-href link-href :link-label "admin!!" :icon "fa fa-cog" :child-id "product-admin-header-child" :oob oob)) diff --git a/market/sx/layouts.sx b/market/sx/layouts.sx index 946632e..708b7d8 100644 --- a/market/sx/layouts.sx +++ b/market/sx/layouts.sx @@ -9,13 +9,13 @@ "Market header row using (market-header-ctx)." (quasiquote (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-label-content (~market-shop-label + :link-label-content (~headers/shop-label :title (get __mctx "market-title") :top-slug (get __mctx "top-slug") :sub-div (get __mctx "sub-slug")) - :nav (~market-desktop-nav-from-data + :nav (~headers/desktop-nav-from-data :categories (get __mctx "categories") :hx-select (get __mctx "hx-select") :select-colours (get __mctx "select-colours") @@ -29,44 +29,44 @@ ;; OOB clear helpers ;; --------------------------------------------------------------------------- -(defcomp ~market-clear-oob () +(defcomp ~layouts/clear-oob () "Clear OOB divs for browse level." (<> - (~clear-oob-div :id "product-admin-row") - (~clear-oob-div :id "product-admin-header-child") - (~clear-oob-div :id "product-row") - (~clear-oob-div :id "product-header-child") - (~clear-oob-div :id "market-admin-row") - (~clear-oob-div :id "market-admin-header-child") - (~clear-oob-div :id "post-admin-row") - (~clear-oob-div :id "post-admin-header-child"))) + (~shared:layout/clear-oob-div :id "product-admin-row") + (~shared:layout/clear-oob-div :id "product-admin-header-child") + (~shared:layout/clear-oob-div :id "product-row") + (~shared:layout/clear-oob-div :id "product-header-child") + (~shared:layout/clear-oob-div :id "market-admin-row") + (~shared:layout/clear-oob-div :id "market-admin-header-child") + (~shared:layout/clear-oob-div :id "post-admin-row") + (~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-div :id "product-admin-row") - (~clear-oob-div :id "product-admin-header-child") - (~clear-oob-div :id "product-row") - (~clear-oob-div :id "product-header-child"))) + (~shared:layout/clear-oob-div :id "product-admin-row") + (~shared:layout/clear-oob-div :id "product-admin-header-child") + (~shared:layout/clear-oob-div :id "product-row") + (~shared:layout/clear-oob-div :id "product-header-child"))) ;; --------------------------------------------------------------------------- ;; Browse layout: root + post + market (self-contained) ;; --------------------------------------------------------------------------- -(defcomp ~market-browse-layout-full () +(defcomp ~layouts/browse-layout-full () (<> (~root-header-auto) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~market-header-auto nil))))) -(defcomp ~market-browse-layout-oob () +(defcomp ~layouts/browse-layout-oob () (<> (~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)) - (~market-clear-oob) + (~layouts/clear-oob) (~root-header-auto true))) -(defcomp ~market-browse-layout-mobile () +(defcomp ~layouts/browse-layout-mobile () (let ((__mctx (market-header-ctx))) (get __mctx "mobile-nav"))) @@ -74,18 +74,18 @@ ;; 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) - (~header-child-sx + (~shared:layout/header-child-sx :inner (<> (~post-header-auto nil) (~market-header-auto nil) (~post-admin-header-auto nil selected))))) -(defcomp ~market-admin-layout-oob (&key selected) +(defcomp ~layouts/admin-layout-oob (&key selected) (<> (~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)) - (~market-clear-oob-admin) + (~layouts/clear-oob-admin) (~root-header-auto true))) ;; --------------------------------------------------------------------------- @@ -93,46 +93,46 @@ ;; --------------------------------------------------------------------------- ;; 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) - (~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 -(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) - (~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) -(defcomp ~market-oob-wrap (&key parts) +(defcomp ~layouts/oob-wrap (&key parts) (<> parts)) -(defcomp ~market-clear-product-oob () +(defcomp ~layouts/clear-product-oob () "Clear admin-level OOB divs when rendering product detail." (<> - (~clear-oob-div :id "product-admin-row") - (~clear-oob-div :id "product-admin-header-child") - (~clear-oob-div :id "market-admin-row") - (~clear-oob-div :id "market-admin-header-child") - (~clear-oob-div :id "post-admin-row") - (~clear-oob-div :id "post-admin-header-child"))) + (~shared:layout/clear-oob-div :id "product-admin-row") + (~shared:layout/clear-oob-div :id "product-admin-header-child") + (~shared:layout/clear-oob-div :id "market-admin-row") + (~shared:layout/clear-oob-div :id "market-admin-header-child") + (~shared:layout/clear-oob-div :id "post-admin-row") + (~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-oob-div :id "market-admin-row") - (~clear-oob-div :id "market-admin-header-child") - (~clear-oob-div :id "post-admin-row") - (~clear-oob-div :id "post-admin-header-child"))) + (~shared:layout/clear-oob-div :id "market-admin-row") + (~shared:layout/clear-oob-div :id "market-admin-header-child") + (~shared:layout/clear-oob-div :id "post-admin-row") + (~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." - (<> 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-header oob-header (~market-clear-product-admin-oob))) + (<> product-header oob-header (~layouts/clear-product-admin-oob))) ;; Content wrappers -(defcomp ~market-content-padded (&key content) +(defcomp ~layouts/content-padded (&key content) (<> content (div :class "pb-8"))) diff --git a/market/sx/meta.sx b/market/sx/meta.sx index 5675fe4..9e18138 100644 --- a/market/sx/meta.sx +++ b/market/sx/meta.sx @@ -1,21 +1,21 @@ ;; Market meta/SEO components -(defcomp ~market-meta-title (&key (title :as string)) +(defcomp ~meta/title (&key (title :as string)) (title title)) -(defcomp ~market-meta-description (&key (description :as string)) +(defcomp ~meta/description (&key (description :as string)) (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)) -(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)) -(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)) -(defcomp ~market-meta-jsonld (&key (json :as string)) +(defcomp ~meta/jsonld (&key (json :as string)) (script :type "application/ld+json" (~rich-text :html json))) @@ -23,30 +23,30 @@ ;; 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?) (site-title :as string) (brand :as string?) (price :as string?) (price-currency :as string?) (jsonld-json :as string)) (<> - (~market-meta-title :title title) - (~market-meta-description :description description) - (when canonical (~market-meta-canonical :href canonical)) + (~meta/title :title title) + (~meta/description :description description) + (when canonical (~meta/canonical :href canonical)) ;; OpenGraph - (~market-meta-og :property "og:site_name" :content site-title) - (~market-meta-og :property "og:type" :content "product") - (~market-meta-og :property "og:title" :content title) - (~market-meta-og :property "og:description" :content description) - (when canonical (~market-meta-og :property "og:url" :content canonical)) - (when image-url (~market-meta-og :property "og:image" :content image-url)) + (~meta/og :property "og:site_name" :content site-title) + (~meta/og :property "og:type" :content "product") + (~meta/og :property "og:title" :content title) + (~meta/og :property "og:description" :content description) + (when canonical (~meta/og :property "og:url" :content canonical)) + (when image-url (~meta/og :property "og:image" :content image-url)) (when (and price price-currency) - (<> (~market-meta-og :property "product:price:amount" :content price) - (~market-meta-og :property "product:price:currency" :content price-currency))) - (when brand (~market-meta-og :property "product:brand" :content brand)) + (<> (~meta/og :property "product:price:amount" :content price) + (~meta/og :property "product:price:currency" :content price-currency))) + (when brand (~meta/og :property "product:brand" :content brand)) ;; Twitter - (~market-meta-twitter :name "twitter:card" + (~meta/twitter :name "twitter:card" :content (if image-url "summary_large_image" "summary")) - (~market-meta-twitter :name "twitter:title" :content title) - (~market-meta-twitter :name "twitter:description" :content description) - (when image-url (~market-meta-twitter :name "twitter:image" :content image-url)) + (~meta/twitter :name "twitter:title" :content title) + (~meta/twitter :name "twitter:description" :content description) + (when image-url (~meta/twitter :name "twitter:image" :content image-url)) ;; JSON-LD - (~market-meta-jsonld :json jsonld-json))) + (~meta/jsonld :json jsonld-json))) diff --git a/market/sx/navigation.sx b/market/sx/navigation.sx index 05e6c07..5255885 100644 --- a/market/sx/navigation.sx +++ b/market/sx/navigation.sx @@ -1,6 +1,6 @@ ;; 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" (a :href href :sx-get href :sx-target "#main-panel" :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) 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" 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))) -(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" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" :aria-selected (if active "true" "false") :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"))) -(defcomp ~market-mobile-chevron () +(defcomp ~navigation/mobile-chevron () (svg :class "w-4 h-4 shrink-0 transition-transform group-open/cat:rotate-180" :viewBox "0 0 20 20" :fill "currentColor" (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"))) -(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) (a :href href :sx-get href :sx-target "#main-panel" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" @@ -37,7 +37,7 @@ (div :aria-label count-label count-str)) 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") :aria-selected (if active "true" "false") :href href :sx-get href :sx-target "#main-panel" @@ -45,20 +45,20 @@ (div label) (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 :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" 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" (a :class "px-2 py-1 rounded hover:bg-stone-100 block" :href href :sx-get href :sx-target "#main-panel" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" "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 summary subs)) @@ -67,25 +67,25 @@ ;; 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)) - (~market-mobile-nav-wrapper :items +(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)) + (~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) (map (lambda (cat) - (~market-mobile-cat-details + (~navigation/mobile-cat-details :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" "") :href (get cat "href") :hx-select hx-select :select-colours select-colours :cat-name (get cat "name") :count-label (str (get cat "count") " products") :count-str (str (get cat "count")) - :chevron (~market-mobile-chevron)) + :chevron (~navigation/mobile-chevron)) :subs (if (get cat "subs") - (~market-mobile-subs-panel :links + (~navigation/mobile-subs-panel :links (<> (map (lambda (sub) - (~market-mobile-sub-link + (~navigation/mobile-sub-link :select-colours select-colours :active (get sub "active") :href (get sub "href") :hx-select hx-select @@ -93,5 +93,5 @@ :count-label (str (get sub "count") " products") :count-str (str (get sub "count")))) (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)))) diff --git a/market/sx/prices.sx b/market/sx/prices.sx index 45c4f8e..ce96c41 100644 --- a/market/sx/prices.sx +++ b/market/sx/prices.sx @@ -1,36 +1,36 @@ ;; 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)) -(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)) -(defcomp ~market-price-regular (&key (price :as string)) +(defcomp ~prices/regular (&key (price :as string)) (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)) -(defcomp ~market-header-price-special-label () +(defcomp ~prices/header-price-special-label () (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)) -(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)) -(defcomp ~market-header-price-regular-label () +(defcomp ~prices/header-price-regular-label () (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)) -(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))) -(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)) @@ -38,31 +38,31 @@ ;; 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) (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 - (~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)) :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 - (<> (~market-header-price-special-label) - (~market-header-price-special :price sp-str) - (when rp-val (~market-header-price-strike :price rp-str)))) + (<> (~prices/header-price-special-label) + (~prices/header-price-special :price sp-str) + (when rp-val (~prices/header-price-strike :price rp-str)))) (when (and (not sp-val) rp-val) - (<> (~market-header-price-regular-label) - (~market-header-price-regular :price rp-str))) - (when rrp-str (~market-header-rrp :rrp rrp-str))))) + (<> (~prices/header-price-regular-label) + (~prices/header-price-regular :price rp-str))) + (when rrp-str (~prices/header-rrp :rrp rrp-str))))) ;; 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?)) - (~market-price-line :inner +(defcomp ~prices/card-price-from-data (&key (sp-val :as number?) (sp-str :as string?) (rp-val :as number?) (rp-str :as string?)) + (~prices/line :inner (<> (when sp-val - (<> (~market-price-special :price sp-str) - (when rp-val (~market-price-regular-strike :price rp-str)))) + (<> (~prices/special :price sp-str) + (when rp-val (~prices/regular-strike :price rp-str)))) (when (and (not sp-val) rp-val) - (~market-price-regular :price rp-str))))) + (~prices/regular :price rp-str))))) diff --git a/market/sxc/pages/market.sx b/market/sxc/pages/market.sx index 220b315..8280987 100644 --- a/market/sxc/pages/market.sx +++ b/market/sxc/pages/market.sx @@ -13,10 +13,10 @@ :layout :root :data (all-markets-data) :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") - (~market-markets-grid - :cards (~market-cards-content + (~grids/markets-grid + :cards (~cards/content :markets market-data :page market-page :has-more has-more :next-url next-url)))) @@ -26,10 +26,10 @@ :layout :post :data (page-markets-data) :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") - (~market-markets-grid - :cards (~market-cards-content + (~grids/markets-grid + :cards (~cards/content :markets market-data :page market-page :has-more has-more :next-url next-url)))) @@ -38,24 +38,24 @@ :auth :admin :layout (:post-admin :selected "markets") :data (page-admin-data) - :content (~market-admin-content-wrap - :inner (~crud-panel + :content (~grids/admin-content-wrap + :inner (~shared:misc/crud-panel :list-id "markets-list" :form (when can-create - (~crud-create-form + (~shared:misc/crud-create-form :create-url create-url :csrf csrf :errors-id "market-create-errors" :list-id "markets-list" :placeholder "e.g. Suma, Craft Fair" :btn-label "Add market")) :list (if admin-markets (<> (map (fn (m) - (~crud-item + (~shared:misc/crud-item :href (get m "href") :name (get m "name") :slug (get m "slug") :del-url (get m "del-url") :csrf-hdr (get m "csrf-hdr") :list-id "markets-list" :confirm-title "Delete market?" :confirm-text "Products will be hidden (soft delete)")) admin-markets)) - (~empty-state + (~shared:misc/empty-state :message "No markets yet. Create one above." :cls "text-gray-500 mt-4"))))) @@ -64,7 +64,7 @@ :auth :public :layout :market :data (market-home-data) - :content (~market-landing-from-data + :content (~cards/landing-from-data :excerpt excerpt :feature-image feature-image :html html)) (defpage market-admin diff --git a/orders/sx/checkout.sx b/orders/sx/checkout.sx index 75294d2..a17b6fc 100644 --- a/orders/sx/checkout.sx +++ b/orders/sx/checkout.sx @@ -1,6 +1,6 @@ ;; 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" (div :class "space-y-1" (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.") (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 "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."))) -(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" (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 " (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" (p :class "font-medium" "All done!") (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" (div (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 "ml-4 font-medium" price))) -(defcomp ~checkout-return-tickets (&key items) +(defcomp ~checkout/return-tickets (&key items) (section :class "mt-6 space-y-3" (h2 :class "text-base sm:text-lg font-semibold" "Event tickets in this order") (ul :class "divide-y divide-stone-200 rounded-2xl border border-stone-200 bg-white/80" items))) ;; Data-driven ticket items (replaces Python loop) -(defcomp ~checkout-return-tickets-from-data (&key (tickets :as list)) - (~checkout-return-tickets +(defcomp ~checkout/return-tickets-from-data (&key (tickets :as list)) + (~checkout/return-tickets :items (<> (map (lambda (tk) - (~checkout-return-ticket + (~checkout/return-ticket :name (get tk "name") :pill (get tk "pill") :state (get tk "state") :type-name (get tk "type_name") :date-str (get tk "date_str") :code (get tk "code") :price (get tk "price"))) (or tickets (list)))))) -(defcomp ~checkout-return-content (&key summary items calendar tickets status-message) +(defcomp ~checkout/return-content (&key summary items calendar tickets status-message) (div :class "max-w-full px-1 py-1" (when summary (div :class "rounded-2xl border border-stone-200 bg-white/80 p-4 sm:p-6 space-y-2" summary)) diff --git a/orders/sx/handlers/account-nav-item.sx b/orders/sx/handlers/account-nav-item.sx index 9d4f007..c003e3a 100644 --- a/orders/sx/handlers/account-nav-item.sx +++ b/orders/sx/handlers/account-nav-item.sx @@ -4,6 +4,6 @@ ;; Renders the "orders" link for the account dashboard nav. (defhandler account-nav-item (&key) - (~account-nav-item + (~shared:fragments/account-nav-item :href (app-url "orders" "/") :label "orders")) diff --git a/orders/sx/layouts.sx b/orders/sx/layouts.sx index 5985cfb..e95fb12 100644 --- a/orders/sx/layouts.sx +++ b/orders/sx/layouts.sx @@ -3,40 +3,40 @@ ;; --- 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) - (~header-child-sx + (~shared:layout/header-child-sx :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) - (~oob-header-sx + (~shared:layout/oob-header-sx :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))) -(defcomp ~orders-layout-mobile () +(defcomp ~layouts/mobile () (~root-mobile-auto)) ;; --- 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) - (~order-detail-header-stack + (~shared:orders/detail-header-stack :auth (~auth-header-row-auto) - :orders (~orders-header-row :list-url (or list-url "/")) - :order (~menu-row-sx :id "order-row" :level 3 :colour "sky" + :orders (~shared:auth/orders-header-row :list-url (or list-url "/")) + :order (~shared:layout/menu-row-sx :id "order-row" :level 3 :colour "sky" :link-href (or detail-url "/") :link-label "Order" :icon "fa fa-gbp")))) -(defcomp ~order-detail-layout-oob (&key (detail-url :as string)) - (<> (~oob-header-sx +(defcomp ~layouts/order-detail-layout-oob (&key (detail-url :as string)) + (<> (~shared:layout/oob-header-sx :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" :icon "fa fa-gbp" :oob true)) (~root-header-auto true))) -(defcomp ~order-detail-layout-mobile () +(defcomp ~layouts/order-detail-layout-mobile () (~root-mobile-auto)) diff --git a/orders/sxc/pages/orders.sx b/orders/sxc/pages/orders.sx index 8c5bec6..7340e4e 100644 --- a/orders/sxc/pages/orders.sx +++ b/orders/sxc/pages/orders.sx @@ -13,14 +13,14 @@ :page (or (request-arg "page" "1") "1")) :layout (:orders :list-url (str (route-prefix) (url-for "defpage_orders_list"))) - :filter (~order-list-header - :search-mobile (~search-mobile + :filter (~shared:orders/list-header + :search-mobile (~shared:controls/search-mobile :current-local-href "/" :search (or search "") :search-count (or search-count "") :hx-select "#main-panel" :search-headers-mobile "{\"X-Origin\":\"search-mobile\",\"X-Search\":\"true\"}")) - :aside (~search-desktop + :aside (~shared:controls/search-desktop :current-local-href "/" :search (or search "") :search-count (or search-count "") @@ -30,7 +30,7 @@ (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))) (rows-url (str pfx (url-for "orders.orders_rows")))) - (~orders-list-content + (~shared:orders/list-content :orders orders :page page :total-pages total-pages @@ -49,12 +49,12 @@ :list-url (str (route-prefix) (url-for "defpage_orders_list")) :detail-url (str (route-prefix) (url-for "defpage_order_detail" :order-id order-id))) :filter (let* ((pfx (route-prefix))) - (~order-detail-filter-content + (~shared:orders/detail-filter-content :order order :list-url (str pfx (url-for "defpage_orders_list")) :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)) :csrf (csrf-token))) - :content (~order-detail-content + :content (~shared:orders/detail-content :order order :calendar-entries calendar-entries)) diff --git a/relations/sx/handlers/container-nav.sx b/relations/sx/handlers/container-nav.sx index fb580db..4029544 100644 --- a/relations/sx/handlers/container-nav.sx +++ b/relations/sx/handlers/container-nav.sx @@ -39,7 +39,7 @@ (href (if svc-name (app-url svc-name path) path))) - (~relation-nav + (~shared:navigation/relation-nav :href href :name (or (get child "label") "") :icon (or (get defn "nav_icon") "") diff --git a/shared/browser/app/errors.py b/shared/browser/app/errors.py index 79d78de..524f9c3 100644 --- a/shared/browser/app/errors.py +++ b/shared/browser/app/errors.py @@ -62,7 +62,7 @@ def _sx_error_page(errnum: str, message: str, image: str | None = None) -> str: from shared.sx.page import 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", message=message, image=image, diff --git a/shared/sx/deps.py b/shared/sx/deps.py index 8488709..d2eec5c 100644 --- a/shared/sx/deps.py +++ b/shared/sx/deps.py @@ -170,7 +170,7 @@ def compute_all_deps(env: dict[str, Any]) -> None: def scan_components_from_sx(source: str) -> set[str]: """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(): from .ref.sx_ref import scan_components_from_source as _ref_sc diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 97fb3c3..1036a7a 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -90,7 +90,7 @@ def mobile_menu_sx(*sections: str) -> SxExpr: 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 "" auth_menu = ctx.get("auth_menu") or "" 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. 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", parent_id=parent_id, @@ -636,7 +636,7 @@ def sx_response(source: str, status: int = 200, # --------------------------------------------------------------------------- # 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. @@ -780,7 +780,7 @@ async def sx_page(ctx: dict, page_sx: str, *, renders everything client-side. CSS rules are scanned from the sx source and component defs, then injected as a