;; Blog card components — pure data, no HTML injection (defcomp ~cards/like-button (&key like-url hx-headers heart) (div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl" (~detail/like-toggle :like-url like-url :hx-headers hx-headers :heart heart))) (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 ~cards/published-status (&key (timestamp :as string)) (p :class "text-sm text-stone-500" (str "Published: " timestamp))) ;; Tag components — accept data, not HTML (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 ~cards/tag-item (&key src name initial) (li (a :class "flex items-center gap-1" (~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 ~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) (~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) (~cards/author-item :image (get a "image") :name (get a "name"))) authors))))))) ;; Author components (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 ~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?) (has-like :as boolean) (tags :as list?) (authors :as list?) widget) (article :class "border-b pb-6 last:border-b-0 relative" (when has-like (~cards/like-button :like-url like-url :hx-headers {:X-CSRFToken csrf-token} :heart (if liked "❤️" "🤍"))) (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) (if is-draft (~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 (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) (~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) (~cards/author-item :image (get a "image") :name (get a "name"))) authors)))))))) (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" (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" (when feature-image (div (img :src feature-image :alt "" :class "w-full aspect-video object-cover"))) (div :class "p-3 text-center" (h2 :class "text-lg font-bold text-stone-900" title) (if is-draft (~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) (~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) (~cards/author-item :image (get a "image") :name (get a "name"))) authors)))))))) ;; Data-driven blog cards list (replaces Python _blog_cards_sx loop) (defcomp ~cards/from-data (&key (posts :as list?) (view :as string?) sentinel) (<> (map (lambda (p) (if (= view "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")) (~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") :publish-requested (get p "publish_requested") :status-timestamp (get p "status_timestamp") :has-like (get p "has_like") :liked (get p "liked") :like-url (get p "like_url") :csrf-token (get p "csrf_token") :tags (get p "tags") :authors (get p "authors") :widget (when (get p "widget") (~rich-text :html (get p "widget")))))) (or posts (list))) sentinel)) ;; Data-driven page cards list (replaces Python _page_cards_sx loop) (defcomp ~cards/page-cards-from-data (&key (pages :as list?) sentinel) (<> (map (lambda (pg) (~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") :pub-timestamp (get pg "pub_timestamp") :feature-image (get pg "feature_image") :excerpt (get pg "excerpt"))) (or pages (list))) sentinel)) (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 ~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) (~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)))))