BlogPageService.index_data() assembles all data (cards, filters, actions) and 7 new .sx defcomps handle rendering: main content, aside, filter, actions, tag groups filter, authors filter, and sentinel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
254 lines
13 KiB
Plaintext
254 lines
13 KiB
Plaintext
;; Blog index components
|
|
|
|
(defcomp ~blog-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)
|
|
(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"
|
|
:class (str "px-4 py-1.5 rounded-t text-sm font-medium transition-colors " posts-cls) "Posts")
|
|
(a :href pages-href :sx-get pages-href :sx-target "#main-panel"
|
|
: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)
|
|
(<> 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)
|
|
(<> tabs
|
|
toggle
|
|
(div :class grid-cls cards)
|
|
(div :class "pb-8")))
|
|
|
|
(defcomp ~blog-aside (&key search action-buttons tag-groups-filter authors-filter)
|
|
(<> search
|
|
action-buttons
|
|
(div :id "category-summary-desktop" :hxx-swap-oob "outerHTML"
|
|
tag-groups-filter
|
|
authors-filter)
|
|
(div :id "filter-summary-desktop" :hxx-swap-oob "outerHTML")))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Data-driven composition defcomps — replace Python sx_components functions
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
;; Helper: CSS class for filter item based on selection state
|
|
(defcomp ~blog-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
|
|
current-local-href hx-select blog-url-base)
|
|
(let* ((posts-href (str blog-url-base "/index"))
|
|
(pages-href (str posts-href "?type=pages"))
|
|
(posts-cls (if (not (= content-type "pages"))
|
|
"bg-stone-700 text-white" "bg-stone-100 text-stone-600 hover:bg-stone-200"))
|
|
(pages-cls (if (= content-type "pages")
|
|
"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
|
|
: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
|
|
:href (get card "href") :hx-select hx-select
|
|
:title (get card "title")
|
|
:has-calendar (get card "has_calendar")
|
|
:has-market (get card "has_market")
|
|
:pub-timestamp (get card "pub_timestamp")
|
|
:feature-image (get card "feature_image")
|
|
:excerpt (get card "excerpt")))
|
|
(or cards (list)))
|
|
(if (< page total-pages)
|
|
(~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)))))
|
|
;; 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"
|
|
"max-w-full px-3 py-3 space-y-3"))
|
|
(list-href current-local-href)
|
|
(tile-href (str current-local-href
|
|
(if (contains? current-local-href "?") "&" "?") "view=tile"))
|
|
(list-cls (if (not (= view "tile"))
|
|
"bg-stone-200 text-stone-800"
|
|
"text-stone-400 hover:text-stone-600"))
|
|
(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
|
|
:posts-href posts-href :pages-href pages-href
|
|
:hx-select hx-select :posts-cls posts-cls :pages-cls pages-cls)
|
|
:toggle (~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))
|
|
:grid-cls grid-cls
|
|
:cards (<>
|
|
(map (lambda (card)
|
|
(if (= view "tile")
|
|
(~blog-card-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")
|
|
:publish-requested (get card "publish_requested")
|
|
:status-timestamp (get card "status_timestamp")
|
|
:excerpt (get card "excerpt")
|
|
:tags (get card "tags") :authors (get card "authors"))
|
|
(~blog-card
|
|
: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")
|
|
:publish-requested (get card "publish_requested")
|
|
:status-timestamp (get card "status_timestamp")
|
|
:has-like (get card "has_like") :liked (get card "liked")
|
|
:like-url (get card "like_url") :csrf-token (get card "csrf_token")
|
|
:tags (get card "tags") :authors (get card "authors")
|
|
:widget (get card "widget"))))
|
|
(or cards (list)))
|
|
(~blog-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)
|
|
(when (< page total-pages)
|
|
(let* ((next-url (str current-local-href "?page=" (+ page 1))))
|
|
(~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
|
|
new-post-href new-page-href current-local-href)
|
|
(~blog-action-buttons-wrapper
|
|
:inner (<>
|
|
(when is-admin
|
|
(<>
|
|
(~blog-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
|
|
: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
|
|
: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
|
|
: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
|
|
:items (<>
|
|
(~blog-filter-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")
|
|
:hx-select hx-select)
|
|
(map (lambda (grp)
|
|
(let* ((is-on (get grp "is_selected"))
|
|
(cls (if is-on
|
|
"bg-stone-900 text-white border-stone-900"
|
|
"bg-white text-stone-600 border-stone-300 hover:bg-stone-50"))
|
|
(fi (get grp "feature_image"))
|
|
(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
|
|
:style (if colour
|
|
(str "background-color: " colour "; color: white;")
|
|
"background-color: #e7e5e4; color: #57534e;")
|
|
:initial (slice (or name "?") 0 1)))))
|
|
(~blog-filter-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
|
|
:items (<>
|
|
(~blog-filter-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")
|
|
:hx-select hx-select)
|
|
(map (lambda (a)
|
|
(let* ((is-on (get a "is_selected"))
|
|
(cls (if is-on
|
|
"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
|
|
: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")))
|
|
: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
|
|
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
|
|
: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 tag-groups :is-any-group is-any-group :hx-select hx-select)
|
|
:authors-filter (~blog-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
|
|
new-post-href new-page-href current-local-href
|
|
tag-groups authors is-any-group is-any-author
|
|
tg-summary au-summary)
|
|
(~mobile-filter
|
|
:filter-summary (<>
|
|
(~search-mobile)
|
|
(when (not (= tg-summary ""))
|
|
(~blog-filter-summary :text tg-summary))
|
|
(when (not (= au-summary ""))
|
|
(~blog-filter-summary :text au-summary)))
|
|
:action-buttons (~blog-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
|
|
:tag-groups tag-groups :is-any-group is-any-group :hx-select hx-select)
|
|
(~blog-index-authors-filter
|
|
:authors authors :is-any-author is-any-author :hx-select hx-select))))
|