;; Cart item components (defcomp ~cart-item-img (&key src alt) (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 ~cart-item-price (&key text) (p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (defcomp ~cart-item-price-was (&key text) (p :class "text-xs text-stone-400 line-through" text)) (defcomp ~cart-item-no-price () (p :class "text-xs text-stone-500" "No price")) (defcomp ~cart-item-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 ~cart-item-brand (&key brand) (p :class "mt-0.5 text-[0.7rem] sm:text-xs text-stone-500" brand)) (defcomp ~cart-item-line-total (&key text) (p :class "text-sm sm:text-base font-semibold text-stone-900" text)) (defcomp ~cart-item (&key id img prod-url title brand deleted price qty-url csrf minus qty plus 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 ~cart-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 ~cart-item-from-data (&key item) (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)))) (~cart-item :id (str "cart-item-" slug) :img (if image (~cart-item-img :src image :alt title) (~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 (~cart-item-brand :brand brand)) :deleted (when is-deleted (~cart-item-deleted)) :price (if unit-price (<> (~cart-item-price :text (str symbol (format-decimal unit-price 2))) (when (and special-price (!= special-price regular-price)) (~cart-item-price-was :text (str symbol (format-decimal regular-price 2))))) (~cart-item-no-price)) :qty-url qty-url :csrf csrf :minus (str (- quantity 1)) :qty (str quantity) :plus (str (+ quantity 1)) :line-total (when line-total (~cart-item-line-total :text (str "Line total: " symbol (format-decimal line-total 2))))))) ;; Assembled calendar entries section — replaces Python _calendar_entries_sx (defcomp ~cart-cal-section-from-data (&key entries) (when (not (empty? entries)) (~cart-cal-section :items (map (lambda (e) (let* ((name (or (get e "name") "")) (date-str (or (get e "date_str") ""))) (~cart-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 ~cart-tickets-section-from-data (&key ticket-groups) (when (not (empty? ticket-groups)) (let* ((csrf (csrf-token)) (qty-url (url-for "cart_global.update_ticket_quantity"))) (~cart-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") ""))) (~cart-ticket-article :name name :type-name (when tt-name (~cart-ticket-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 (~cart-ticket-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 ~cart-summary-from-data (&key item-count grand-total symbol is-logged-in checkout-action login-href user-email) (~cart-summary-panel :item-count (str item-count) :subtotal (str symbol (format-decimal grand-total 2)) :checkout (if is-logged-in (~cart-checkout-form :action checkout-action :csrf (csrf-token) :label (str " Checkout as " user-email)) (~cart-checkout-signin :href login-href)))) ;; Assembled page cart content — replaces Python _page_cart_main_panel_sx (defcomp ~cart-page-cart-content (&key cart-items cal-entries ticket-groups 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" (~empty-state :icon "fa fa-shopping-cart" :message "Your cart is empty" :cls "text-center")))) (~cart-page-panel :items (map (lambda (item) (~cart-item-from-data :item item)) (or cart-items (list))) :cal (when (not (empty? (or cal-entries (list)))) (~cart-cal-section-from-data :entries cal-entries)) :tickets (when (not (empty? (or ticket-groups (list)))) (~cart-tickets-section-from-data :ticket-groups ticket-groups)) :summary summary)))