;; 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)))