UNPKG

@salla.sa/twilight-components

Version:
288 lines (283 loc) 15.3 kB
/*! * Crafted with ❤ by Salla */ import { proxyCustomElement, HTMLElement, h, Host } from '@stencil/core/internal/client'; import { d as defineCustomElement$3 } from './salla-button2.js'; import { d as defineCustomElement$2 } from './salla-slider2.js'; const sallaCartCouponsCss = ""; const SallaCartCoupons$1 = /*@__PURE__*/ proxyCustomElement(class SallaCartCoupons extends HTMLElement { constructor() { super(); this.__registerHost(); this.availableCoupons = []; this.cartTotal = 0; this.currentCoupon = ''; this.couponInputValue = ''; this.couponError = ''; this.isApplying = false; this.isHandlingInternalError = false; // Store cart event listener references for cleanup this.cartUpdatedHandler = (cartData) => { this.cartTotal = cartData.sub_total ?? cartData.total ?? 0; // Update current coupon state const couponCode = cartData.coupon || ''; // Only overwrite the user's typed value when the cart-level coupon itself changes if (couponCode !== this.currentCoupon) { this.currentCoupon = couponCode; this.couponInputValue = couponCode; } // Clear error when cart updates this.couponError = ''; }; this.couponAddedHandler = () => { // Coupon was successfully added - state is managed by cartUpdatedHandler }; this.couponAdditionFailedHandler = (error) => { // Only handle external coupon failures when no internal operation is handling errors if (!this.isApplying && !this.isHandlingInternalError) { this.showCouponError(error?.message || salla.lang.get('pages.cart.coupon_application_error')); } // Internal operations handle their own errors to prevent double display }; this.couponDeletedHandler = () => { // Coupon was removed - state is managed by cartUpdatedHandler }; // Arrow functions to maintain context for event listeners this.onKeyUp = (event) => { if (event.key === 'Enter') { this.couponButtonRef?.click(); } this.couponError = ''; }; this.onInput = (event) => { this.couponInputValue = event.target.value; }; } async componentWillLoad() { await Salla.onReady(); await Salla.lang.onLoaded(); //then set your logic here to avoid any race condition this.loadCouponsData(); // Load async without blocking render - shows empty until loaded this.loadCartTotal(); this.setupCartEventListeners(); // Note: No store feature gate needed - coupon availability controlled by backend API response } componentDidLoad() { this.setupCouponInputEvents(); } setupCartEventListeners() { // Listen for cart updates to refresh cart total salla.event.on('cart::updated', this.cartUpdatedHandler); // Listen for coupon events salla.event.on('cart::coupon.added', this.couponAddedHandler); salla.event.on('cart::coupon.addition.failed', this.couponAdditionFailedHandler); salla.event.on('cart::coupon.deleted', this.couponDeletedHandler); } disconnectedCallback() { // Clean up DOM event listeners this.couponInputRef?.removeEventListener('keyup', this.onKeyUp); this.couponInputRef?.removeEventListener('input', this.onInput); // Clean up cart event listeners using reliable global event system salla.event.off('cart::updated', this.cartUpdatedHandler); salla.event.off('cart::coupon.added', this.couponAddedHandler); salla.event.off('cart::coupon.addition.failed', this.couponAdditionFailedHandler); salla.event.off('cart::coupon.deleted', this.couponDeletedHandler); } /** * Business Decision: No visual selected state needed * UX Flow: Click coupon → populate input → auto-apply (streamlined experience) */ async selectCoupon(couponCode) { const selectedCoupon = this.availableCoupons.find(coupon => coupon.code === couponCode); if (selectedCoupon) { if (!this.isEligible(selectedCoupon)) { throw new Error(`Coupon not eligible: minimum cart amount not met`); } try { // Actually apply the coupon to the cart first await this.applyCoupon(couponCode); // Only dispatch event after successful API call this.host.dispatchEvent(new CustomEvent('salla::cart-coupons.coupon.selected', { detail: selectedCoupon, bubbles: true })); } catch (error) { // Rethrow to let caller handle the error throw error; } } return selectedCoupon; } async loadCouponsData() { try { // Load coupons from API const response = await salla.api.request('coupons'); const raw = response?.data; const coupons = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : []); this.availableCoupons = coupons; } catch (error) { console.warn('Failed to load coupons:', error); // No coupons available on error this.availableCoupons = []; } } loadCartTotal() { // Get cart total and current coupon from Salla storage const cart = salla.storage.get('cart'); // Check summary first (more accurate), then cart level, fallback to 0 this.cartTotal = cart?.summary?.sub_total ?? cart?.sub_total ?? 0; this.currentCoupon = cart?.coupon || ''; this.couponInputValue = cart?.coupon || ''; } setupCouponInputEvents() { if (!this.couponInputRef || !this.couponButtonRef) { return; } this.couponInputRef.addEventListener('keyup', this.onKeyUp); this.couponInputRef.addEventListener('input', this.onInput); } async handleCouponButtonClick() { if (this.isApplying) { return; } const hasCoupon = !!this.currentCoupon; if (!hasCoupon && !this.couponInputValue.trim()) { this.showCouponError(salla.lang.get('pages.cart.enter_coupon_code')); return; } this.isApplying = true; this.isHandlingInternalError = true; try { if (hasCoupon) { // Remove existing coupon await salla.cart.deleteCoupon(); } else { // Apply new coupon await salla.cart.addCoupon(this.couponInputValue.trim()); } } catch (error) { this.showCouponError(error.response?.data?.error?.message || salla.lang.get('pages.cart.coupon_application_error')); } finally { this.isApplying = false; // Keep internal error flag active briefly to prevent race condition with SDK event setTimeout(() => { this.isHandlingInternalError = false; }, 100); } } showCouponError(message) { this.couponError = message; } handleCouponClick(coupon) { this.applyCoupon(coupon.code, false).catch(() => { // Error already displayed by applyCoupon }); } async applyCoupon(couponCode, shouldThrow = true) { if (this.isApplying) { if (shouldThrow) { throw new Error('Coupon application already in progress'); } else { return; // Silent return for card clicks when operation is already in progress } } // Update the input field with the coupon code this.couponInputValue = couponCode; // Clear any existing errors this.couponError = ''; this.isApplying = true; this.isHandlingInternalError = true; try { // Apply the coupon directly without checking for existing coupon await salla.cart.addCoupon(couponCode.trim()); } catch (error) { this.showCouponError(error.response?.data?.error?.message || salla.lang.get('pages.cart.coupon_application_error')); // Re-throw error only if requested (for selectCoupon API) if (shouldThrow) { throw error; } } finally { this.isApplying = false; // Keep internal error flag active briefly to prevent race condition with SDK event setTimeout(() => { this.isHandlingInternalError = false; }, 100); } } formatMainAmount(coupon) { if (coupon.type === 'percentage') { // For percentage coupons, amount should be a string or number const percentageValue = typeof coupon.amount === 'object' ? coupon.amount.amount : coupon.amount; return `${salla.helpers.number(percentageValue)}%`; } else { // For fixed coupons, amount is an object with amount and currency const amountData = typeof coupon.amount === 'object' ? coupon.amount : { amount: coupon.amount, currency: coupon.minimum_amount?.currency || salla.config.get('store.currency.code', 'SAR') }; return (h("span", { innerHTML: salla.money(amountData.amount, amountData.currency) })); } } isEligible(coupon) { if (coupon.minimum_amount && this.cartTotal < parseFloat(coupon.minimum_amount.amount)) { return false; // Coupon is not eligible due to minimum amount requirement } return true; } renderCouponCard(coupon) { const isEligible = this.isEligible(coupon); // Note: Card notches (ticket-style pseudo-elements) match card background, not page background - this is intentional design return (h("div", { class: "s-cart-coupons-slide-one-fourth swiper-slide" }, h("div", { class: `s-cart-coupons-card ${!isEligible || this.isApplying ? 's-cart-coupons-disabled' : ''}`, onClick: () => isEligible && !this.isApplying && this.handleCouponClick(coupon) }, h("div", { class: "s-cart-coupons-top-section" }, h("div", { class: "s-cart-coupons-discount-values" }, h("div", { class: "s-cart-coupons-main-amount" }, this.formatMainAmount(coupon)), coupon.free_shipping ? (h("div", { class: "s-cart-coupons-secondary-info" }, "+ ", salla.lang.get('pages.cart.free_shipping'))) : coupon.maximum_amount && (h("div", { class: "s-cart-coupons-secondary-info" }, salla.lang.get('pages.cart.up_to'), " ", h("span", { innerHTML: salla.money(coupon.maximum_amount.amount, coupon.maximum_amount.currency) })))), h("div", { class: "s-cart-coupons-code" }, coupon.code)), h("div", { class: "s-cart-coupons-divider" }), h("div", { class: "s-cart-coupons-bottom-section" }, h("div", { class: "s-cart-coupons-terms" }, coupon.remaining_days !== undefined && coupon.remaining_days !== null && (h("div", { class: `s-cart-coupons-expiry ${coupon.remaining_days <= 3 ? 's-cart-coupons-expiry-warning' : ''}` }, coupon.remaining_days === 0 ? salla.lang.get('pages.cart.expires_today') : `${salla.lang.get('pages.cart.expires_after')} ${coupon.remaining_days} ${salla.lang.get('pages.cart.day')}`)), coupon.minimum_amount && parseFloat(coupon.minimum_amount.amount) > 0 && (h("div", { class: "s-cart-coupons-minimum" }, salla.lang.get('pages.cart.minimum'), " ", h("span", { innerHTML: salla.money(coupon.minimum_amount.amount, coupon.minimum_amount.currency) })))), h("div", { class: "s-cart-coupons-action" }, isEligible ? salla.lang.get('pages.cart.save_coupon') : salla.lang.get('pages.cart.coupon_not_compatible')))))); } render() { return (h(Host, { key: 'f3f3d943ab2c1a6488a1da02d2cc031dbc88b482' }, h("div", { key: 'abf88828017041b19a6c54ba2f99e6c246bd8d04', class: "s-cart-coupons-wrapper" }, h("div", { key: 'c2946579a7cca935722997229f50ede603acb8b2', class: "s-cart-coupons-input-section" }, h("label", { key: '50042d827c86daf853babd4e41ba3a920aceda9d', htmlFor: "coupon-input", class: "s-cart-coupons-coupon-label" }, salla.lang.get('pages.cart.have_coupon')), h("div", { key: '4b01047727bd3a83f6cfbac37221e7732076248f', class: "s-cart-coupons-coupon-input-container" }, h("input", { key: '555108f68b7a60c618d9a904905ef321cd274c7a', type: "text", id: "coupon-input", name: "coupon", class: `form-input ${this.couponError ? 's-coupon-input-error' : ''}`, placeholder: salla.lang.get('pages.cart.coupon_placeholder'), value: this.couponInputValue, disabled: !!this.currentCoupon || this.isApplying, ref: el => this.couponInputRef = el, "aria-label": salla.lang.get('pages.cart.apply_coupon') }), h("salla-button", { key: '0416bbaf029f7d8951ca6a419cdc676ae50ee792', ref: el => this.couponButtonRef = el, class: `s-cart-coupons-coupon-button ${this.currentCoupon ? 's-cart-coupons-coupon-button-remove' : 's-cart-coupons-coupon-button-apply'}`, color: this.currentCoupon ? 'danger' : 'primary', loading: this.isApplying, "loader-position": "center", onClick: () => this.handleCouponButtonClick() }, this.currentCoupon ? (h("i", { class: "sicon-cancel" })) : (h("span", null, salla.lang.get('pages.cart.save_coupon'))))), this.couponError && (h("span", { key: 'f7c77e8676cb07b4820b571dbd8ee25dca79a1b1', class: "s-cart-coupons-coupon-error" }, this.couponError))), this.availableCoupons && this.availableCoupons.length > 0 && (h("salla-slider", { key: 'daee9c6705de734d5f81ff86d92ad6952b5a7332', type: "carousel", showControls: false, class: "s-cart-coupons-slider" }, h("div", { key: '5d5ef30a6ff7f7c2b406cc5cec6622c2f028d17e', slot: "items" }, this.availableCoupons.map(coupon => this.renderCouponCard(coupon)))))))); } get host() { return this; } static get style() { return sallaCartCouponsCss; } }, [0, "salla-cart-coupons", { "availableCoupons": [32], "cartTotal": [32], "currentCoupon": [32], "couponInputValue": [32], "couponError": [32], "isApplying": [32], "selectCoupon": [64] }]); function defineCustomElement$1() { if (typeof customElements === "undefined") { return; } const components = ["salla-cart-coupons", "salla-button", "salla-slider"]; components.forEach(tagName => { switch (tagName) { case "salla-cart-coupons": if (!customElements.get(tagName)) { customElements.define(tagName, SallaCartCoupons$1); } break; case "salla-button": if (!customElements.get(tagName)) { defineCustomElement$3(); } break; case "salla-slider": if (!customElements.get(tagName)) { defineCustomElement$2(); } break; } }); } defineCustomElement$1(); const SallaCartCoupons = SallaCartCoupons$1; const defineCustomElement = defineCustomElement$1; export { SallaCartCoupons, defineCustomElement };