Component names now reflect filesystem location using / as path separator and : as namespace separator for shared components: ~sx-header → ~layouts/header ~layout-app-body → ~shared:layout/app-body ~blog-admin-dashboard → ~admin/dashboard 209 files, 4,941 replacements across all services. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
8.8 KiB
Plaintext
166 lines
8.8 KiB
Plaintext
;; Cart item components
|
|
|
|
(defcomp ~items/img (&key (src :as string) (alt :as string))
|
|
(img :src src :alt alt :class "w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100" :loading "lazy"))
|
|
|
|
(defcomp ~items/price (&key (text :as string))
|
|
(p :class "text-sm sm:text-base font-semibold text-stone-900" text))
|
|
|
|
(defcomp ~items/price-was (&key (text :as string))
|
|
(p :class "text-xs text-stone-400 line-through" text))
|
|
|
|
(defcomp ~items/no-price ()
|
|
(p :class "text-xs text-stone-500" "No price"))
|
|
|
|
(defcomp ~items/deleted ()
|
|
(p :class "mt-2 inline-flex items-center gap-1 text-[0.65rem] sm:text-xs font-medium text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5"
|
|
(i :class "fa-solid fa-triangle-exclamation text-[0.6rem]" :aria-hidden "true")
|
|
" This item is no longer available or price has changed"))
|
|
|
|
(defcomp ~items/brand (&key (brand :as string))
|
|
(p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand))
|
|
|
|
(defcomp ~items/line-total (&key (text :as string))
|
|
(p :class "text-sm sm:text-base font-semibold text-stone-900" text))
|
|
|
|
(defcomp ~items/index (&key (id :as string) img (prod-url :as string) (title :as string) brand deleted price (qty-url :as string) (csrf :as string) (minus :as string) (qty :as string) (plus :as string) line-total)
|
|
(article :id id :class "flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5"
|
|
(div :class "w-full sm:w-32 shrink-0 flex justify-center sm:block" (when img img))
|
|
(div :class "flex-1 min-w-0"
|
|
(div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3"
|
|
(div :class "min-w-0"
|
|
(h2 :class "text-sm sm:text-base md:text-lg font-semibold text-stone-900"
|
|
(a :href prod-url :class "hover:text-emerald-700" title))
|
|
(when brand brand) (when deleted deleted))
|
|
(div :class "text-left sm:text-right" (when price price)))
|
|
(div :class "mt-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2 sm:gap-4"
|
|
(div :class "flex items-center gap-2 text-xs sm:text-sm text-stone-700"
|
|
(span :class "text-[0.65rem] sm:text-xs uppercase tracking-wide text-stone-500" "Quantity")
|
|
(form :action qty-url :method "post" :sx-post qty-url :sx-swap "none"
|
|
(input :type "hidden" :name "csrf_token" :value csrf)
|
|
(input :type "hidden" :name "count" :value 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" "-"))
|
|
(span :class "inline-flex items-center justify-center px-2 py-1 rounded-full bg-stone-100 text-[0.7rem] sm:text-xs font-medium" qty)
|
|
(form :action qty-url :method "post" :sx-post qty-url :sx-swap "none"
|
|
(input :type "hidden" :name "csrf_token" :value csrf)
|
|
(input :type "hidden" :name "count" :value 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" "+")))
|
|
(div :class "flex items-center justify-between sm:justify-end gap-3" (when line-total line-total))))))
|
|
|
|
(defcomp ~items/page-panel (&key items cal tickets summary)
|
|
(div :class "max-w-full px-3 py-3 space-y-3"
|
|
(div :id "cart"
|
|
(div (section :class "space-y-3 sm:space-y-4" items cal tickets)
|
|
summary))))
|
|
|
|
;; Assembled cart item from serialized data — replaces Python _cart_item_sx
|
|
(defcomp ~items/from-data (&key (item :as dict))
|
|
(let* ((slug (or (get item "slug") ""))
|
|
(title (or (get item "title") ""))
|
|
(image (get item "image"))
|
|
(brand (get item "brand"))
|
|
(is-deleted (get item "is_deleted"))
|
|
(unit-price (get item "unit_price"))
|
|
(special-price (get item "special_price"))
|
|
(regular-price (get item "regular_price"))
|
|
(currency (or (get item "currency") "GBP"))
|
|
(symbol (if (= currency "GBP") "\u00a3" currency))
|
|
(quantity (or (get item "quantity") 1))
|
|
(product-id (get item "product_id"))
|
|
(prod-url (or (get item "product_url") ""))
|
|
(qty-url (or (get item "qty_url") ""))
|
|
(csrf (csrf-token))
|
|
(line-total (when unit-price (* unit-price quantity))))
|
|
(~items/index
|
|
:id (str "cart-item-" slug)
|
|
:img (if image
|
|
(~items/img :src image :alt title)
|
|
(~shared:misc/img-or-placeholder :src nil
|
|
:size-cls "w-24 h-24 sm:w-32 sm:h-28 rounded-xl border border-dashed border-stone-300"
|
|
:placeholder-text "No image"))
|
|
:prod-url prod-url
|
|
:title title
|
|
:brand (when brand (~items/brand :brand brand))
|
|
:deleted (when is-deleted (~items/deleted))
|
|
:price (if unit-price
|
|
(<>
|
|
(~items/price :text (str symbol (format-decimal unit-price 2)))
|
|
(when (and special-price (!= special-price regular-price))
|
|
(~items/price-was :text (str symbol (format-decimal regular-price 2)))))
|
|
(~items/no-price))
|
|
:qty-url qty-url :csrf csrf
|
|
:minus (str (- quantity 1))
|
|
:qty (str quantity)
|
|
:plus (str (+ quantity 1))
|
|
:line-total (when line-total
|
|
(~items/line-total :text (str "Line total: " symbol (format-decimal line-total 2)))))))
|
|
|
|
;; Assembled calendar entries section — replaces Python _calendar_entries_sx
|
|
(defcomp ~items/cal-section-from-data (&key (entries :as list))
|
|
(when (not (empty? entries))
|
|
(~calendar/cal-section
|
|
:items (map (lambda (e)
|
|
(let* ((name (or (get e "name") ""))
|
|
(date-str (or (get e "date_str") "")))
|
|
(~calendar/cal-entry
|
|
:name name :date-str date-str
|
|
:cost (str "\u00a3" (format-decimal (or (get e "cost") 0) 2)))))
|
|
entries))))
|
|
|
|
;; Assembled ticket groups section — replaces Python _ticket_groups_sx
|
|
(defcomp ~items/tickets-section-from-data (&key (ticket-groups :as list))
|
|
(when (not (empty? ticket-groups))
|
|
(let* ((csrf (csrf-token))
|
|
(qty-url (url-for "cart_global.update_ticket_quantity")))
|
|
(~tickets/section
|
|
:items (map (lambda (tg)
|
|
(let* ((name (or (get tg "entry_name") ""))
|
|
(tt-name (get tg "ticket_type_name"))
|
|
(price (or (get tg "price") 0))
|
|
(quantity (or (get tg "quantity") 0))
|
|
(line-total (or (get tg "line_total") 0))
|
|
(entry-id (str (or (get tg "entry_id") "")))
|
|
(tt-id (get tg "ticket_type_id"))
|
|
(date-str (or (get tg "date_str") "")))
|
|
(~tickets/article
|
|
:name name
|
|
:type-name (when tt-name (~tickets/type-name :name tt-name))
|
|
:date-str date-str
|
|
:price (str "\u00a3" (format-decimal price 2))
|
|
:qty-url qty-url :csrf csrf
|
|
:entry-id entry-id
|
|
:type-hidden (when tt-id (~tickets/type-hidden :value (str tt-id)))
|
|
:minus (str (max (- quantity 1) 0))
|
|
:qty (str quantity)
|
|
:plus (str (+ quantity 1))
|
|
:line-total (str "Line total: \u00a3" (format-decimal line-total 2)))))
|
|
ticket-groups)))))
|
|
|
|
;; Assembled cart summary — replaces Python _cart_summary_sx
|
|
(defcomp ~items/summary-from-data (&key (item-count :as number) (grand-total :as number) (symbol :as string) (is-logged-in :as boolean) (checkout-action :as string) (login-href :as string) (user-email :as string?))
|
|
(~summary/panel
|
|
:item-count (str item-count)
|
|
:subtotal (str symbol (format-decimal grand-total 2))
|
|
:checkout (if is-logged-in
|
|
(~summary/checkout-form
|
|
:action checkout-action :csrf (csrf-token)
|
|
:label (str " Checkout as " user-email))
|
|
(~summary/checkout-signin :href login-href))))
|
|
|
|
;; Assembled page cart content — replaces Python _page_cart_main_panel_sx
|
|
(defcomp ~items/page-cart-content (&key (cart-items :as list?) (cal-entries :as list?) (ticket-groups :as list?) summary)
|
|
(if (and (empty? (or cart-items (list)))
|
|
(empty? (or cal-entries (list)))
|
|
(empty? (or ticket-groups (list))))
|
|
(div :class "max-w-full px-3 py-3 space-y-3"
|
|
(div :id "cart"
|
|
(div :class "rounded-2xl border border-dashed border-stone-300 bg-white/80 p-6 sm:p-8 text-center"
|
|
(~shared:misc/empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center"))))
|
|
(~items/page-panel
|
|
:items (map (lambda (item) (~items/from-data :item item)) (or cart-items (list)))
|
|
:cal (when (not (empty? (or cal-entries (list))))
|
|
(~items/cal-section-from-data :entries cal-entries))
|
|
:tickets (when (not (empty? (or ticket-groups (list))))
|
|
(~items/tickets-section-from-data :ticket-groups ticket-groups))
|
|
:summary summary)))
|