All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m20s
Replace all 676 inline sexp() string calls across 7 services with render(component_name, **kwargs) calls backed by 46 external .sexpr component definition files (587 defcomps total). - Add render() function to shared/sexp/jinja_bridge.py - Add load_service_components() helper and update load_sexp_dir() for *.sexpr - Update parser keyword regex to support HTMX hx-on::event syntax - Convert remaining inline HTML in route files to render() calls - Add shared/sexp/templates/misc.sexp for cross-service utility components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
9.7 KiB
Plaintext
179 lines
9.7 KiB
Plaintext
;; Blog admin panel components
|
|
|
|
(defcomp ~blog-cache-panel (&key clear-url csrf)
|
|
(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 :hx-post clear-url :hx-trigger "submit" :hx-target "#cache-status" :hx-swap "innerHTML"
|
|
(input :type "hidden" :name "csrf_token" :value csrf)
|
|
(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-html)
|
|
(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" (raw! list-html))))
|
|
|
|
(defcomp ~blog-snippets-empty ()
|
|
(div :class "bg-white rounded-lg shadow"
|
|
(div :class "p-8 text-center text-stone-400"
|
|
(i :class "fa fa-puzzle-piece text-4xl mb-2")
|
|
(p "No snippets yet. Create one from the blog editor."))))
|
|
|
|
(defcomp ~blog-snippet-visibility-select (&key patch-url hx-headers options-html cls)
|
|
(select :name "visibility" :hx-patch patch-url :hx-target "#snippets-list" :hx-swap "innerHTML"
|
|
:hx-headers hx-headers :class "text-sm border border-stone-300 rounded px-2 py-1"
|
|
(raw! options-html)))
|
|
|
|
(defcomp ~blog-snippet-option (&key value selected label)
|
|
(option :value value :selected selected label))
|
|
|
|
(defcomp ~blog-snippet-delete-button (&key confirm-text delete-url hx-headers)
|
|
(button :type "button" :data-confirm "" :data-confirm-title "Delete snippet?"
|
|
:data-confirm-text confirm-text :data-confirm-icon "warning"
|
|
:data-confirm-confirm-text "Yes, delete" :data-confirm-cancel-text "Cancel"
|
|
:data-confirm-event "confirmed"
|
|
:hx-delete delete-url :hx-trigger "confirmed" :hx-target "#snippets-list" :hx-swap "innerHTML"
|
|
:hx-headers hx-headers
|
|
:class "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0"
|
|
(i :class "fa fa-trash") " Delete"))
|
|
|
|
(defcomp ~blog-snippet-row (&key name owner badge-cls visibility extra-html)
|
|
(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)
|
|
(div :class "text-xs text-stone-500" owner))
|
|
(span :class (str "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium " badge-cls) visibility)
|
|
(raw! extra-html)))
|
|
|
|
(defcomp ~blog-snippets-list (&key rows-html)
|
|
(div :class "bg-white rounded-lg shadow" (div :class "divide-y" (raw! rows-html))))
|
|
|
|
(defcomp ~blog-menu-items-panel (&key new-url list-html)
|
|
(div :class "max-w-4xl mx-auto p-6"
|
|
(div :class "mb-6 flex justify-end items-center"
|
|
(button :type "button" :hx-get new-url :hx-target "#menu-item-form" :hx-swap "innerHTML"
|
|
:class "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
(i :class "fa fa-plus") " Add Menu Item"))
|
|
(div :id "menu-item-form" :class "mb-6")
|
|
(div :id "menu-items-list" (raw! list-html))))
|
|
|
|
(defcomp ~blog-menu-items-empty ()
|
|
(div :class "bg-white rounded-lg shadow"
|
|
(div :class "p-8 text-center text-stone-400"
|
|
(i :class "fa fa-inbox text-4xl mb-2")
|
|
(p "No menu items yet. Add one to get started!"))))
|
|
|
|
(defcomp ~blog-menu-item-image (&key src label)
|
|
(if src (img :src src :alt label :class "w-12 h-12 rounded-full object-cover flex-shrink-0")
|
|
(div :class "w-12 h-12 rounded-full bg-stone-200 flex-shrink-0")))
|
|
|
|
(defcomp ~blog-menu-item-row (&key img-html label slug sort-order edit-url delete-url confirm-text 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"))
|
|
(raw! img-html)
|
|
(div :class "flex-1 min-w-0"
|
|
(div :class "font-medium truncate" label)
|
|
(div :class "text-xs text-stone-500 truncate" slug))
|
|
(div :class "text-sm text-stone-500" (str "Order: " sort-order))
|
|
(div :class "flex gap-2 flex-shrink-0"
|
|
(button :type "button" :hx-get edit-url :hx-target "#menu-item-form" :hx-swap "innerHTML"
|
|
:class "px-3 py-1 text-sm bg-stone-200 hover:bg-stone-300 rounded"
|
|
(i :class "fa fa-edit") " Edit")
|
|
(button :type "button" :data-confirm "" :data-confirm-title "Delete menu item?"
|
|
:data-confirm-text confirm-text :data-confirm-icon "warning"
|
|
:data-confirm-confirm-text "Yes, delete" :data-confirm-cancel-text "Cancel"
|
|
:data-confirm-event "confirmed"
|
|
:hx-delete delete-url :hx-trigger "confirmed" :hx-target "#menu-items-list" :hx-swap "innerHTML"
|
|
:hx-headers hx-headers
|
|
:class "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800"
|
|
(i :class "fa fa-trash") " Delete"))))
|
|
|
|
(defcomp ~blog-menu-items-list (&key rows-html)
|
|
(div :class "bg-white rounded-lg shadow" (div :class "divide-y" (raw! rows-html))))
|
|
|
|
;; Tag groups admin
|
|
|
|
(defcomp ~blog-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")
|
|
(div :class "flex flex-col sm:flex-row gap-3"
|
|
(input :type "text" :name "name" :placeholder "Group name" :required "" :class "flex-1 border rounded px-3 py-2 text-sm")
|
|
(input :type "text" :name "colour" :placeholder "#colour" :class "w-28 border rounded px-3 py-2 text-sm")
|
|
(input :type "number" :name "sort_order" :placeholder "Order" :value "0" :class "w-20 border rounded px-3 py-2 text-sm"))
|
|
(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)
|
|
(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)
|
|
(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-html edit-href name slug sort-order)
|
|
(li :class "border rounded p-3 bg-white flex items-center gap-3"
|
|
(raw! icon-html)
|
|
(div :class "flex-1"
|
|
(a :href edit-href :class "font-medium text-stone-800 hover:underline" name)
|
|
(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-html)
|
|
(ul :class "space-y-2" (raw! items-html)))
|
|
|
|
(defcomp ~blog-tag-groups-empty ()
|
|
(p :class "text-stone-500 text-sm" "No tag groups yet."))
|
|
|
|
(defcomp ~blog-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-html)
|
|
(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" (raw! spans-html))))
|
|
|
|
(defcomp ~blog-tag-groups-main (&key form-html groups-html unassigned-html)
|
|
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-8"
|
|
(raw! form-html) (raw! groups-html) (raw! unassigned-html)))
|
|
|
|
;; Tag group edit
|
|
|
|
(defcomp ~blog-tag-checkbox (&key tag-id checked img-html name)
|
|
(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")
|
|
(raw! img-html) (span name)))
|
|
|
|
(defcomp ~blog-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 csrf name colour sort-order feature-image tags-html)
|
|
(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"
|
|
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Name")
|
|
(input :type "text" :name "name" :value name :required "" :class "w-full border rounded px-3 py-2 text-sm"))
|
|
(div :class "flex gap-3"
|
|
(div :class "flex-1" (label :class "block text-xs font-medium text-stone-600 mb-1" "Colour")
|
|
(input :type "text" :name "colour" :value colour :placeholder "#hex" :class "w-full border rounded px-3 py-2 text-sm"))
|
|
(div :class "w-24" (label :class "block text-xs font-medium text-stone-600 mb-1" "Order")
|
|
(input :type "number" :name "sort_order" :value sort-order :class "w-full border rounded px-3 py-2 text-sm")))
|
|
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Feature Image URL")
|
|
(input :type "text" :name "feature_image" :value feature-image :placeholder "https://..." :class "w-full border rounded px-3 py-2 text-sm")))
|
|
(div (label :class "block text-xs font-medium text-stone-600 mb-2" "Assign Tags")
|
|
(div :class "grid grid-cols-1 sm:grid-cols-2 gap-1 max-h-64 overflow-y-auto border rounded p-2"
|
|
(raw! tags-html)))
|
|
(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 csrf)
|
|
(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-html delete-form-html)
|
|
(div :class "max-w-2xl mx-auto px-4 py-6 space-y-6"
|
|
(raw! edit-form-html) (raw! delete-form-html)))
|