;; Events page-level components (slots, ticket types, buy form, cart, posts nav) (defcomp ~page/slot-days-pills (&key days-inner) (div :class "flex flex-wrap gap-1" days-inner)) (defcomp ~page/slot-day-pill (&key day) (span :class "px-2 py-0.5 rounded-full text-xs bg-slate-200" day)) (defcomp ~page/slot-no-days () (span :class "text-xs text-slate-400" "No days")) (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") (div :class "mt-1" days)) (div :class "flex flex-col" (div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Flexible") (div :class "mt-1" flexible)) (div :class "grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm" (div :class "flex flex-col" (div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Time") (div :class "mt-1" time-str)) (div :class "flex flex-col" (div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Cost") (div :class "mt-1" cost-str))) (button :type "button" :class pre-action :sx-get edit-url :sx-target (str "#slot-" slot-id) :sx-swap "outerHTML" "Edit"))) (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 ~page/slots-empty-row () (tr (td :colspan "5" :class "p-3 text-stone-500" "No slots yet."))) (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" (div :class "font-medium" (a :href slot-href :class pill-cls :sx-get slot-href :sx-target "#main-panel" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" slot-name)) (p :class "text-stone-500 whitespace-pre-line break-all w-full" description)) (td :class "p-2 align-top w-1/6" flexible) (td :class "p-2 align-top w-1/6" days) (td :class "p-2 align-top w-1/6" time-str) (td :class "p-2 align-top w-1/6" cost-str) (td :class "p-2 align-top w-1/6" (button :class action-btn :type "button" :data-confirm "true" :data-confirm-title "Delete slot?" :data-confirm-text "This action cannot be undone." :data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it" :data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed" :sx-delete del-url :sx-target "#slots-table" :sx-select "#slots-table" :sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed" (i :class "fa-solid fa-trash"))))) (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" (tr (th :class "p-2 text-left w-1/6" "Name") (th :class "p-2 text-left w-1/6" "Flexible") (th :class "text-left p-2 w-1/6" "Days") (th :class "text-left p-2 w-1/6" "Time") (th :class "text-left p-2 w-1/6" "Cost") (th :class "text-left p-2 w-1/6" "Actions"))) (tbody rows)) (div :id "slot-add-container" :class "mt-4" (button :type "button" :class pre-action :sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML" "+ Add slot")))) ;; --------------------------------------------------------------------------- ;; Composition defcomps — receive data, compose slot/table trees ;; --------------------------------------------------------------------------- ;; Days pills from data — replaces Python loop (defcomp ~page/days-pills-from-data (&key days) (if (empty? (or days (list))) (~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 ~page/slot-panel-from-data (&key slot-id list-container days flexible time-str cost-str pre-action edit-url description oob) (<> (~page/slot-panel :slot-id slot-id :list-container list-container :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 (~page/slot-description-oob :description (or description ""))))) ;; Slots table from data (defcomp ~page/slots-table-from-data (&key list-container slots pre-action add-url tr-cls pill-cls action-btn hx-select csrf-hdr) (~page/slots-table :list-container list-container :rows (if (empty? (or slots (list))) (~page/slots-empty-row) (<> (map (lambda (s) (~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 (~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 ~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 ~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 ~page/ticket-types-empty-row () (tr (td :colspan "4" :class "p-3 text-stone-500" "No ticket types yet."))) (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" (div :class "font-medium" (a :href tt-href :class pill-cls :sx-get tt-href :sx-target "#main-panel" :sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" tt-name))) (td :class "p-2 align-top w-1/4" cost-str) (td :class "p-2 align-top w-1/4" count) (td :class "p-2 align-top w-1/6" (button :class action-btn :type "button" :data-confirm "true" :data-confirm-title "Delete ticket type?" :data-confirm-text "This action cannot be undone." :data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it" :data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed" :sx-delete del-url :sx-target "#tickets-table" :sx-select "#tickets-table" :sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed" (i :class "fa-solid fa-trash"))))) (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" (tr (th :class "p-2 text-left w-1/3" "Name") (th :class "text-left p-2 w-1/4" "Cost") (th :class "text-left p-2 w-1/4" "Count") (th :class "text-left p-2 w-1/6" "Actions"))) (tbody rows)) (div :id "ticket-add-container" :class "mt-4" (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 ~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:") (span :class "font-medium text-green-600" price-str)) (div :class "flex items-center gap-2" (span :class "text-sm font-medium text-stone-700" "Available:") (span :class "font-medium text-blue-600" count-str)) (button :type "button" :class "text-xs text-blue-600 hover:text-blue-800 underline" :onclick show-js "Edit ticket config"))) (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 ~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) (div (label :for (str "ticket-price-" entry-id) :class "block text-sm font-medium text-stone-700 mb-1" "Ticket Price (£)") (input :type "number" :id (str "ticket-price-" entry-id) :name "ticket_price" :step "0.01" :min "0" :value price-val :class "w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" :placeholder "e.g., 5.00")) (div (label :for (str "ticket-count-" entry-id) :class "block text-sm font-medium text-stone-700 mb-1" "Total Tickets") (input :type "number" :id (str "ticket-count-" entry-id) :name "ticket_count" :min "0" :value count-val :class "w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" :placeholder "Leave empty for unlimited")) (div :class "flex gap-2" (button :type "submit" :class "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm" "Save") (button :type "button" :class "px-4 py-2 bg-stone-200 text-stone-700 rounded hover:bg-stone-300 text-sm" :onclick hide-js "Cancel")))) ;; Data-driven buy form — Python passes pre-resolved data, .sx does layout + iteration (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") (~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" (h3 :class "text-sm font-semibold text-stone-700 mb-3" (i :class "fa fa-ticket mr-1" :aria-hidden "true") "Tickets") ;; Info bar (when (or info-sold info-remaining info-basket) (div :class "flex items-center gap-3 mb-3 text-xs text-stone-500" (when info-sold (span (str info-sold " sold"))) (when info-remaining (span (str info-remaining " remaining"))) (when info-basket (span :class "text-emerald-600 font-medium" (i :class "fa fa-shopping-cart text-[0.6rem]" :aria-hidden "true") (str " " info-basket " in basket"))))) ;; Body — multi-type or default (if (and ticket-types (not (empty? ticket-types))) (div :class "space-y-2" (map (fn (tt) (let ((tt-count (if user-ticket-counts-by-type (get user-ticket-counts-by-type (str (get tt "id")) 0) 0)) (tt-id (get tt "id"))) (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"))) (~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"))) (~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 ~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) (input :type "hidden" :name "entry_id" :value entry-id) (when ticket-type-id (input :type "hidden" :name "ticket_type_id" :value (str ticket-type-id))) (input :type "hidden" :name "count" :value "1") (button :type "submit" :class "relative inline-flex items-center justify-center text-sm font-medium text-stone-500 hover:bg-emerald-50 rounded p-1" (i :class "fa fa-cart-plus text-2xl" :aria-hidden "true"))) (div :class "flex items-center gap-2" (form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" (input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "entry_id" :value entry-id) (when ticket-type-id (input :type "hidden" :name "ticket_type_id" :value (str ticket-type-id))) (input :type "hidden" :name "count" :value (str (- count 1))) (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" "-")) (a :class "relative inline-flex items-center justify-center text-emerald-700" :href my-tickets-href (span :class "relative inline-flex items-center justify-center" (i :class "fa-solid fa-shopping-cart text-2xl" :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" (str count))))) (form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" (input :type "hidden" :name "csrf_token" :value csrf) (input :type "hidden" :name "entry_id" :value entry-id) (when ticket-type-id (input :type "hidden" :name "ticket_type_id" :value (str ticket-type-id))) (input :type "hidden" :name "count" :value (str (+ count 1))) (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 ~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 ~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" (div :class "flex items-center gap-2 mb-3" (i :class "fa fa-check-circle text-emerald-600" :aria-hidden "true") (span :class "font-semibold text-emerald-800" (str count " ticket" suffix " reserved"))) (div :class "space-y-2 mb-4" (map (fn (t) (a :href (get t "href") :class "flex items-center justify-between p-2 rounded-lg bg-white border border-emerald-100 hover:border-emerald-300 transition text-sm" (div :class "flex items-center gap-2" (i :class "fa fa-ticket text-emerald-500" :aria-hidden "true") (span :class "font-mono text-xs text-stone-500" (get t "code_short"))) (span :class "text-xs text-emerald-600 font-medium" "View ticket"))) tickets)) (when (not (nil? remaining)) (let ((r-suffix (if (= remaining 1) "" "s"))) (p :class "text-xs text-stone-500" (str remaining " ticket" r-suffix " remaining")))) (div :class "mt-3 flex gap-2" (a :href my-tickets-href :class "text-sm text-emerald-700 hover:text-emerald-900 underline" "View all my tickets"))))) ;; Single response wrappers for POST routes (include OOB cart icon) (defcomp ~page/buy-response (&key entry-id tickets remaining my-tickets-href cart-count blog-href cart-href logo) (<> (~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 ~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) (<> (~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 :adjust-url adjust-url :csrf csrf :state state :my-tickets-href my-tickets-href))) ;; Unified OOB cart icon — picks logo or badge based on count (defcomp ~page/cart-icon (&key cart-count blog-href cart-href logo) (if (= cart-count 0) (~page/cart-icon-logo :blog-href blog-href :logo logo) (~page/cart-icon-badge :cart-href cart-href :count (str cart-count)))) (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 ~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") (span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5" count)))) ;; Inline ticket widget (for all-events/page-summary cards) (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 ~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 ~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 ~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 ~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 ~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 ~page/entry-posts-panel (&key posts search-url entry-id) (div :class "space-y-2" posts (div :class "mt-3 pt-3 border-t" (label :class "block text-xs font-medium text-stone-700 mb-1" "Add Post") (input :type "text" :placeholder "Search posts..." :class "w-full px-3 py-2 border rounded text-sm" :sx-get search-url :sx-trigger "keyup changed delay:300ms, load" :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 ~page/entry-posts-list (&key items) (div :class "space-y-2" items)) (defcomp ~page/entry-posts-none () (p :class "text-sm text-stone-400" "No posts associated")) (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" :data-confirm "true" :data-confirm-title "Remove post?" :data-confirm-text (str "This will remove " title " from this entry") :data-confirm-icon "warning" :data-confirm-confirm-text "Yes, remove it" :data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed" :sx-delete del-url :sx-trigger "confirmed" :sx-target (str "#entry-posts-" entry-id) :sx-swap "innerHTML" :sx-headers csrf-hdr (i :class "fa fa-times") " Remove"))) (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 ~page/post-img-placeholder () (div :class "w-8 h-8 rounded-full bg-stone-200 flex-shrink-0")) ;; Entry posts nav OOB (defcomp ~page/entry-posts-nav-oob-empty () (div :id "entry-posts-nav-wrapper" :sx-swap-oob "true")) (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 ~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 ~page/post-nav-oob-empty () (div :id "entries-calendars-nav-wrapper" :sx-swap-oob "true")) (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 ~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 ~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" :aria-label "Scroll left" :_ "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft - 200" (i :class "fa fa-chevron-left")) (div :id "associated-items-container" :class "overflow-y-auto sm:overflow-x-auto sm:overflow-y-visible scrollbar-hide max-h-[50vh] sm:max-h-none" :style "scroll-behavior: smooth;" :_ hyperscript (div :class "flex flex-col sm:flex-row gap-1" items)) (style ".scrollbar-hide::-webkit-scrollbar { display: none; } .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }") (button :class "entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded" :aria-label "Scroll right" :_ "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft + 200" (i :class "fa fa-chevron-right")))) ;; Entry nav post link (with image) (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)))) ;; --------------------------------------------------------------------------- ;; Composition defcomps — nav OOB, posts panel from data ;; --------------------------------------------------------------------------- ;; Post image helper from data (defcomp ~page/post-img-from-data (&key src alt) (if src (~page/post-img :src src :alt alt) (~page/post-img-placeholder))) ;; Entry posts nav OOB from data (defcomp ~page/entry-posts-nav-oob-from-data (&key nav-btn posts) (if (empty? (or posts (list))) (~page/entry-posts-nav-oob-empty) (~page/entry-posts-nav-oob :items (<> (map (lambda (p) (~page/entry-nav-post :href (get p "href") :nav-btn nav-btn :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 ~page/entry-posts-nav-inner-from-data (&key posts) (when (not (empty? (or posts (list)))) (~page/entry-posts-nav-oob :items (<> (map (lambda (p) (~page/entry-nav-post-link :href (get p "href") :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 ~page/post-nav-wrapper-from-data (&key nav-btn entries calendars hyperscript) (if (and (empty? (or entries (list))) (empty? (or calendars (list)))) (~page/post-nav-oob-empty) (~page/post-nav-wrapper :items (<> (map (lambda (e) (~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) (~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 ~page/entry-posts-panel-from-data (&key entry-id posts search-url) (~page/entry-posts-panel :posts (if (empty? (or posts (list))) (~page/entry-posts-none) (~page/entry-posts-list :items (<> (map (lambda (p) (~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"))) posts)))) :search-url search-url :entry-id entry-id)) ;; CRUD list/panel from data — shared by calendars + markets (defcomp ~page/crud-list-from-data (&key items empty-msg list-id) (if (empty? (or items (list))) (~shared:misc/empty-state :message empty-msg :cls "text-gray-500 mt-4") (<> (map (lambda (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 :confirm-title (get item "confirm-title") :confirm-text (get item "confirm-text"))) items)))) (defcomp ~page/crud-panel-from-data (&key can-create create-url csrf errors-id list-id placeholder btn-label items empty-msg) (~shared:misc/crud-panel :form (when can-create (~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 (~page/crud-list-from-data :items items :empty-msg empty-msg :list-id list-id) :list-id list-id)) ;; Post nav admin cog (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 ~page/post-nav-from-data (&key calendars container-nav select-colours has-admin admin-href aclass) (<> (map (lambda (c) (~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 (~page/post-nav-admin-cog :href admin-href :aclass aclass)))) ;; Calendar nav from data — slots + admin link (defcomp ~page/calendar-nav-from-data (&key slots-href admin-href select-colours is-admin) (<> (~shared:layout/nav-link :href slots-href :icon "fa fa-clock" :label "Slots" :select-colours select-colours) (when is-admin (~shared:layout/nav-link :href admin-href :icon "fa fa-cog" :select-colours select-colours)))) ;; Calendar admin nav from data (defcomp ~page/calendar-admin-nav-from-data (&key links select-colours) (<> (map (lambda (l) (~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 ~page/day-nav-from-data (&key entries is-admin admin-href) (<> (when (not (empty? (or entries (list)))) (~day/entries-nav :inner (<> (map (lambda (e) (~day/entry-link :href (get e "href") :name (get e "name") :time-str (get e "time-str"))) entries)))) (when is-admin (~shared:layout/nav-link :href admin-href :icon "fa fa-cog")))) ;; Post search results from data (defcomp ~page/post-search-results-from-data (&key items page next-url has-more) (<> (map (lambda (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 (~page/post-img-from-data :src (get item "img") :alt (get item "title")) :title (get item "title"))) (or items (list))) (cond (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 ~page/entry-options-from-data (&key entry-id state buttons) (~admin/entry-options :entry-id entry-id :buttons (<> (map (lambda (b) (~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") :confirm-title (get b "confirm-title") :confirm-text (get b "confirm-text") :label (get b "label") :is-btn (get b "is-btn"))) (or buttons (list))))))