;; Blog index components (defcomp ~index/no-pages () (div :class "col-span-full mt-8 text-center text-stone-500" "No pages found.")) (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" :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 ~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 ~index/main-panel-posts (&key tabs toggle grid-cls cards) (<> tabs toggle (div :class grid-cls cards) (div :class "pb-8"))) (defcomp ~index/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 ~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 ~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 (~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) (~cards/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) (~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)))) (~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" "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"))) (~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 (~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 (~shared:misc/list-svg) :tile-svg (~shared:misc/tile-svg)) :grid-cls grid-cls :cards (<> (map (lambda (card) (if (= view "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") :publish-requested (get card "publish_requested") :status-timestamp (get card "status_timestamp") :excerpt (get card "excerpt") :tags (get card "tags") :authors (get card "authors")) (~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") :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))) (~index/sentinel :page page :total-pages total-pages :current-local-href current-local-href))))))) ;; Sentinel for blog index infinite scroll (defcomp ~index/sentinel (&key page total-pages current-local-href) (when (< page total-pages) (let* ((next-url (str current-local-href "?page=" (+ page 1)))) (~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 ~index/actions (&key is-admin has-user hx-select draft-count drafts new-post-href new-page-href current-local-href) (~filters/action-buttons-wrapper :inner (<> (when is-admin (<> (~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") (~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 (~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"))) (~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 ~index/tag-groups-filter (&key tag-groups is-any-group hx-select) (~filters/nav :items (<> (~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") :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 (~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))))) (~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 ~index/authors-filter (&key authors is-any-author hx-select) (~filters/nav :items (<> (~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") :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"))) (~filters/author-li :cls cls :hx-get (str "?author=" (get a "slug") "&page=1") :hx-select hx-select :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 ~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) (~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 (~index/tag-groups-filter :tag-groups tag-groups :is-any-group is-any-group :hx-select hx-select) :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 ~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) (~shared:controls/mobile-filter :filter-summary (<> (~shared:controls/search-mobile) (when (not (= tg-summary "")) (~filters/summary :text tg-summary)) (when (not (= au-summary "")) (~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 (<> (~index/tag-groups-filter :tag-groups tag-groups :is-any-group is-any-group :hx-select hx-select) (~index/authors-filter :authors authors :is-any-author is-any-author :hx-select hx-select))))