UNPKG

@salla.sa/twilight-components

Version:
398 lines (393 loc) 24.5 kB
/*! * Crafted with ❤ by Salla */ import { r as registerInstance, h, a as getElement } from './index-BQQ2x3w_.js'; var PageType; (function (PageType) { PageType["ProductDetail"] = "product.single"; PageType["Cart"] = "cart"; })(PageType || (PageType = {})); var OfferType; (function (OfferType) { OfferType["Conditional"] = "conditional"; OfferType["PercentageOrFixed"] = "fixed"; OfferType["DiscountsTable"] = "discounts_table"; OfferType["Bank"] = "bank"; OfferType["BuyXGetY"] = "buy_x_get_y"; OfferType["SpecialPrice"] = "special_price"; OfferType["Percentage"] = "percentage"; OfferType["FixedAmount"] = "fixed_amount"; })(OfferType || (OfferType = {})); const sallaOfferCss = ":host .s-drawer-close:where([dir=rtl],[dir=rtl] *){left:0}"; const SallaOffer = class { constructor(hostRef) { registerInstance(this, hostRef); /** * Custom Card Component for the Salla Products List. * * This component allows you to customize the appearance of individual product cards within a Salla Products List. * * @example * <salla-products-list product-card-component="my-custom-card-style1" ... * <salla-products-list product-card-component="my-custom-card-style2" ... */ this.productCardComponent = 'salla-product-card'; // Declare component state variables this.offersList = []; this.canRender = false; this.showOffer = salla.config.get('store.settings.product.show_special_offers'); this.showAllOffersModal = false; this.showProductsModal = false; this.selectedOfferProducts = []; this.collapsedOffers = {}; this.expandedCategories = {}; this.productsLoaded = false; // Default translated texts this.offer_with_price_text = salla.lang.get('pages.offer.with_price', { price: '' }); this.with_discount_text = salla.lang.get('pages.products.with_a_discount'); this.product_discount_text = salla.lang.get('pages.products.discount'); this.special_offer_text = salla.lang.get('pages.products.special_offer'); this.buy_quantity_text = (quantity) => salla.lang.get('pages.offer.buy_quantity', { quantity }); this.products_link_text = salla.lang.get('pages.offer.included_products', 'المنتجات المشمولة'); this.min_spend_text = (amount, currency) => salla.lang.get('pages.offer.min_spend_amount', { amount, currency }, `بحد أدنى ${amount} ${currency} من مبلغ الشراء`); this.min_items_text = (items) => salla.lang.get('pages.offer.min_items', { items }, `بحد أدنى ${items} منتجات`); this.product_text = salla.lang.get('common.elements.product', 'منتج'); // Language salla.lang.onLoaded(() => { this.offer_with_price_text = salla.lang.get('pages.offer.with_price'); this.with_discount_text = salla.lang.get('pages.products.with_a_discount'); this.product_discount_text = salla.lang.get('pages.products.discount'); this.special_offer_text = salla.lang.get('pages.products.special_offer'); this.buy_quantity_text(0); this.products_link_text = salla.lang.get('pages.offer.included_products', 'المنتجات المشمولة'); this.min_spend_text = (amount, currency) => salla.lang.get('pages.offer.min_spend_amount', { amount, currency }, `بحد أدنى ${amount} ${currency} من مبلغ الشراء`); this.min_items_text = (items) => salla.lang.get('pages.offer.min_items', { items }, `بحد أدنى ${items} منتجات`); this.product_text = salla.lang.get('common.elements.product', 'منتج'); }); salla.onReady(() => { this.currentPage = salla.config.get('page.slug'); const currencies = salla.config.get('currencies') || {}; const currencyCode = salla.config.get('user.currency_code'); this.userCurrency = currencies[currencyCode] || { symbol: '', code: currencyCode || 'SAR', name: '' }; }); } async getEndpointByPageName() { if (this.currentPage == PageType.Cart) { const cartId = await Salla.cart.getCurrentCartId(); return `offers/cart/${cartId}`; } if (this.currentPage == PageType.ProductDetail) { return `offers/product/${salla.config.get('page.id')}`; } return "offers"; } /** * Emits a promotion viewed event for analytics tracking */ emitPromotionViewed() { if (!(this.offersList.length && this.canRender && this.showOffer)) return; // Emit analytics for all visible offers (first 5) with correct positions const visibleOffers = this.offersList.slice(0, 5); const promotionDataArray = visibleOffers.map((offer, index) => ({ id: offer.id, name: offer.title, creative: offer.description || '', position: index + 1 })); salla.event.emit('salla::offer.promotion.viewed', promotionDataArray); } /** * Emits a promotion clicked event */ emitPromotionClicked(offer, position = 1) { if (!(this.offersList.length && this.canRender && this.showOffer)) return; const targetOffer = offer || this.offersList[0]; // Transform offer data to match analytics expectations const promotionData = { id: targetOffer.id, name: targetOffer.title, creative: targetOffer.description || '', position: position }; salla.event.emit('salla::offer.promotion.clicked', [promotionData]); } componentWillLoad() { this.hasCustomComponent = !!customElements.get(this.productCardComponent); return (new Promise(resolve => salla.onReady(resolve))) .then(() => { this.showOffer = !salla.url.is_page('product.single') || salla.config.get('store.settings.product.show_special_offers'); if (this.showOffer) { return; } throw new Error("Merchant disabled showing the offers on product page"); }) .then(async () => salla.api.request(await this.getEndpointByPageName())) .then(async (res) => { if (!(this.offersList = res.data).length) { throw new Error('salla-offers:: There is no offers!'); } // Filter to only supported offer types this.offersList = this.offersList.filter(offer => [OfferType.SpecialPrice, OfferType.Bank, OfferType.BuyXGetY, OfferType.DiscountsTable, OfferType.PercentageOrFixed, OfferType.Percentage, OfferType.FixedAmount].includes(offer.type)); if (!this.offersList.length) { throw new Error('salla-offers:: No supported offer types found!'); } // Fetch all categories once if any offer needs them let allCategories = null; const needsCategories = this.offersList.some(offer => { const details = offer.details; return (details.apply_to === 'category' && details.targets) || (details.apply_to === 'categories' && details.targets) || (offer.type === OfferType.BuyXGetY && (details.get?.source === 'categories' || details.get?.source === 'category')); }); if (needsCategories) { const res = await salla.product.api.categories(); allCategories = res.data; } // Collect all unique product IDs to make single batched API call const productIds = new Set(); for (const offer of this.offersList) { const details = offer.details; if ((details.apply_to === 'product' || details.apply_to === 'products') && details.targets) { details.targets.forEach(id => productIds.add(id)); } if (offer.type === OfferType.BuyXGetY && details.get?.source === 'products' && details.get?.source_value) { details.get.source_value.forEach(id => productIds.add(id)); } if (offer.type === OfferType.BuyXGetY && (details.buy?.source === 'products' || details.buy?.source === 'product') && details.buy?.source_value) { details.buy.source_value.forEach(id => productIds.add(id)); } } // Single batched API call with source="selected" prevents redundant requests let allProducts = {}; if (productIds.size > 0) { try { const response = await salla.product.api.fetch({ source: "selected", source_value: Array.from(productIds) }); allProducts = Object.fromEntries(response.data.map(p => [p.id, p])); } catch (error) { allProducts = {}; } } // Process each offer using cached data for (const offer of this.offersList) { const details = offer.details; const shouldFetchCategories = (details.apply_to === 'category' && details.targets) || (details.apply_to === 'categories' && details.targets); if (shouldFetchCategories && allCategories) { details.categories = this.findCategories(allCategories, details.targets); } if ((details.apply_to === 'product' || details.apply_to === 'products') && details.targets) { details.products = details.targets.map(id => allProducts[id]).filter(Boolean); } if (offer.type === OfferType.BuyXGetY && details.get) { const getY = details.get; if (getY.source === 'products' && getY.source_value) { getY.products = getY.source_value.map(id => allProducts[id]).filter(Boolean); } else if ((getY.source === 'categories' || getY.source === 'category') && allCategories && getY.source_value) { getY.categories = this.findCategories(allCategories, getY.source_value); details.categories = getY.categories; } } if (offer.type === OfferType.BuyXGetY && details.buy) { const buyX = details.buy; if ((buyX.source === 'products' || buyX.source === 'product') && buyX.source_value) { buyX.products = buyX.source_value.map(id => allProducts[id]).filter(Boolean); } } } return this.offersList; }) .then(() => { this.canRender = true; }) .catch((error) => { salla.logger.warn(error); }); } componentDidLoad() { let nav = this.host.querySelector('.s-slider-block__title-nav'); nav?.classList.add("s-offer-bank-payment-nav"); this.emitPromotionViewed(); } findCategories(categories, ids) { let found = []; for (const category of categories) { if (ids.includes(category.id_ || category.id)) { //here we are using || because we are planning to drop `id_` found.push(category); } if (category.sub_categories?.length > 0) { found = found.concat(this.findCategories(category.sub_categories, ids)); } } return found; } render() { // Check if the offers list is empty or if the component is not ready to render if (!this.offersList.length || !this.canRender || !this.showOffer) return null; const blockTitle = salla.lang.get('pages.offer.offers_title', 'عروض المنتج'); const blockSubTitle = salla.lang.get('pages.offer.offers_subtitle', 'احصل على المنتج بسعر مخفض مع أفضل العروض'); const titles = { 'block-title': blockTitle, 'block-subTitle': blockSubTitle, 'show-controls': this.offersList.length > 1 }; return [ h("div", { class: "s-offer-wrapper" }, h("salla-slider", { type: "carousel", id: "offers-slider", ...titles }, h("div", { slot: "items" }, this.offersList.slice(0, 5).map((offer, index) => this.renderOfferCard(offer, index + 1)), this.offersList.length > 5 && this.renderShowMoreCard()))), // Always render drawers but control visibility through visible prop this.renderAllOffersModal(), this.renderProductsModal() ]; } renderShowMoreCard() { return (h("div", { class: "s-offer-slide-one-fourth swiper-slide", onClick: (e) => { e.preventDefault(); e.stopPropagation(); this.openAllOffersModal(); } }, h("div", { class: "s-offer-card s-offer-show-more-card" }, h("i", { class: "sicon-add s-offer-show-more-icon" }), h("h3", { class: "s-offer-show-more-title" }, salla.lang.get('pages.offer.show_more_offers', 'عرض المزيد من العروض'))))); } openAllOffersModal() { this.showAllOffersModal = true; } closeAllOffersModal() { this.showAllOffersModal = false; } closeProductsModal() { this.showProductsModal = false; this.selectedOfferProducts = []; this.productsLoaded = false; } handleAccordionToggle(offerId, collapsed) { this.collapsedOffers = { ...this.collapsedOffers, [offerId]: collapsed }; } getOfferProducts(offer) { const details = offer.details; // Handle products from any offer type if (details.products && details.products.length > 0) { return details.products; } // Handle BuyXGetY special case if (offer.type === OfferType.BuyXGetY && details.get?.products) { return details.get.products; } // For categories, we'll return empty array as we handle categories separately return []; } renderProductsLinkForModalCard(offer, isInDrawer) { const products = this.getOfferProducts(offer); if (!products || products.length === 0) { return null; } const handleProductsClick = (e) => { e.stopPropagation(); this.selectedOfferProducts = products; this.showProductsModal = true; }; return (h("div", { class: "s-offer-products-link", onClick: handleProductsClick }, h("span", { class: "s-offer-products-link-text" }, this.products_link_text), isInDrawer && h("i", { class: "sicon-keyboard_arrow_down s-offer-products-link-icon" }))); } renderAllOffersModal() { return (h("salla-drawer", { visible: this.showAllOffersModal, position: "right", width: "md", "no-padding": true, "drawer-title": salla.lang.get('pages.offer.all_offers_title', 'جميع العروض'), onDrawerVisibilityChanged: (e) => !e.detail && this.closeAllOffersModal() }, h("div", { class: "s-offer-drawer-content" }, this.offersList.map((offer, index) => this.renderModalOfferCard(offer, index + 1))))); } renderProductsModal() { return (h("salla-drawer", { visible: this.showProductsModal, width: "md", position: "right", "no-padding": true, "drawer-title": this.products_link_text, onDrawerVisibilityChanged: (e) => !e.detail && this.closeProductsModal() }, h("div", { class: "s-offer-products-drawer-content" }, this.selectedOfferProducts?.length > 0 ? (h("div", null, !this.productsLoaded && (h("div", { class: "flex items-center justify-center p-8" }, h("salla-loading", { size: "24" }))), h("salla-products-list", { source: "selected", "source-value": JSON.stringify(this.selectedOfferProducts.map(p => p.id)), "product-card-component": this.productCardComponent, "horizontal-cards": true, "compact-cards": true, includes: ["images"], onProductsFetched: () => this.productsLoaded = true }))) : (h("div", { class: "p-4 text-center text-gray-500" }, salla.lang.get('blocks.products.no_products_found', 'لا توجد منتجات')))))); } renderModalOfferCard(offer, position = 1) { const products = this.getOfferProducts(offer); const hasProducts = products && products.length > 0; const isCollapsed = this.collapsedOffers[offer.id] !== false; // Default to collapsed return (h("div", { class: "s-offer-drawer-card" }, h("div", { class: "s-offer-card-main", onClick: () => this.emitPromotionClicked(offer, position) }, h("div", { class: "s-offer-card-icon" }, this.getOfferIcon(offer.type)), h("div", { class: "s-offer-card-content" }, h("h3", { class: "s-offer-card-title", title: offer.title }, offer.title), offer.description && h("p", { class: "s-offer-card-description" }, offer.description), offer.type === OfferType.DiscountsTable ? (h("div", { class: "s-offer-card-details" }, this.renderDiscountTableOfferContent(offer))) : (offer.type === OfferType.PercentageOrFixed || offer.type === OfferType.Percentage || offer.type === OfferType.FixedAmount) ? (h("div", { class: "s-offer-card-details" }, this.renderPercentageOrFixedOfferContent(offer), this.renderOfferCategories(offer))) : (h("div", { class: "s-offer-card-details" }, this.renderOfferCategories(offer))))), hasProducts && (h("salla-accordion", { collapsed: isCollapsed, collapsible: true, size: "sm", onClick: (e) => e.stopPropagation(), onAccordionToggle: (e) => this.handleAccordionToggle(offer.id, e.detail.payload.collapsed) }, h("salla-accordion-head", { collapsible: true, collapsed: isCollapsed }, h("div", { slot: "title", class: "s-offer-products-thumbnails" }, h("span", { class: "s-offer-products-title" }, this.products_link_text, h("i", { class: `sicon-keyboard_arrow_${isCollapsed ? 'down' : 'up'} s-offer-products-link-icon` })), h("div", { class: "s-offer-products-thumbnails-stack" }, products.slice(0, 3).map((product, index) => (h("div", { class: "s-offer-product-thumbnail", style: { zIndex: `${index + 1}` } }, h("img", { src: product.thumbnail || product.image?.url || salla.url.cdn('images/s-empty.png'), alt: product.name })))), products.length > 3 && (h("div", { class: "s-offer-products-more-count", style: { zIndex: '7' } }, "+", products.length - 3))))), h("salla-accordion-body", null, h("div", { class: "s-offer-products-content" }, h("salla-products-list", { source: "selected", "source-value": JSON.stringify(products.map(p => p.id)), "product-card-component": this.productCardComponent, "horizontal-cards": true, "compact-cards": true, includes: ["images"] }))))))); } renderOfferCard(offer, position = 1) { return (h("div", { class: "s-offer-slide-one-fourth swiper-slide", onClick: () => this.emitPromotionClicked(offer, position) }, h("div", { class: "s-offer-card" }, h("div", { class: "s-offer-card-icon" }, this.getOfferIcon(offer.type)), h("div", { class: "s-offer-card-content" }, h("h3", { class: "s-offer-card-title", title: offer.title }, offer.title), offer.description && h("p", { class: "s-offer-card-description" }, offer.description), h("div", { class: "s-offer-card-details" }, offer.type === OfferType.DiscountsTable ? this.renderDiscountTableOfferContent(offer) : (offer.type === OfferType.PercentageOrFixed || offer.type === OfferType.Percentage || offer.type === OfferType.FixedAmount) ? [ this.renderPercentageOrFixedOfferContent(offer), this.renderOfferCategories(offer) ] : this.renderOfferCategories(offer), this.renderProductsLinkForModalCard(offer, false)))))); } getOfferIcon(offerType) { const iconMap = { [OfferType.DiscountsTable]: 'sicon-discount-calculator', [OfferType.Bank]: 'sicon-bank', [OfferType.SpecialPrice]: 'sicon-fire', [OfferType.BuyXGetY]: 'sicon-gift', [OfferType.PercentageOrFixed]: 'sicon-special-discount', [OfferType.Percentage]: 'sicon-special-discount', [OfferType.FixedAmount]: 'sicon-special-discount' }; return h("i", { class: iconMap[offerType] || 'sicon-discount' }); } renderDiscountTableOfferContent(offer) { const details = offer.details; const discounts = details.discounts || []; if (!discounts.length) return null; return (h("div", null, h("div", { class: "s-offer-card-description" }, salla.lang.get('pages.offer.discount_table_subtitle', 'وفر اكتر بشراء منتجات أكثر')), h("table", { class: "s-offer-discount-table" }, h("tbody", null, this.groupDiscountsByRows(discounts, 3).map((row, rowIndex) => (h("tr", { key: rowIndex }, row.map((discount, colIndex) => (h("td", { key: colIndex }, h("div", { class: "s-offer-discount-percentage" }, discount.percentage, "%"), h("div", { class: "s-offer-discount-condition" }, this.formatDiscountCondition(discount)))))))))))); } renderPercentageOrFixedOfferContent(offer) { const details = offer.details; if (!details.discount_value && !(details.min_spend > 0) && !(details.min_items > 0)) return null; return (h("div", { class: "s-offer-percentage-fixed-content" }, details.min_spend > 0 && (h("div", { class: "s-offer-min-spend" }, this.min_spend_text(details.min_spend, this.userCurrency?.symbol || ''))), details.min_items > 0 && (h("div", { class: "s-offer-min-items" }, this.min_items_text(details.min_items))))); } groupDiscountsByRows(discounts, itemsPerRow) { const rows = []; for (let i = 0; i < discounts.length; i += itemsPerRow) { rows.push(discounts.slice(i, i + itemsPerRow)); } return rows; } formatDiscountCondition(discount) { // Format the condition text based on discount amount or quantity if (discount.discounted_amount) { return `${discount.discounted_amount} ${this.userCurrency?.symbol || ''}`; } if (discount.quantity != null) { return this.buy_quantity_text(discount.quantity); } return `1 ${this.product_text}`; } renderOfferCategories(offer) { const details = offer.details; // Get categories from different locations based on offer type let categories = details.categories || []; // For BuyXGetY offers, also check get.categories if (offer.type === OfferType.BuyXGetY && details.get?.categories) { categories = details.get.categories; } if (!categories.length) return null; const maxVisible = 3; // Show only first 3 categories initially const isExpanded = this.expandedCategories[offer.id]; const shouldShowMore = categories.length > maxVisible; const visibleCategories = isExpanded ? categories : categories.slice(0, maxVisible); return (h("div", { class: "s-offer-categories" }, h("div", { class: "s-offer-categories-list" }, visibleCategories.map((category, index) => (h("a", { key: index, class: "s-offer-category-item", href: category.url || salla.url.create('categories', category.id || category.id_) }, category.name))), shouldShowMore && (h("span", { class: "s-offer-categories-toggle", onClick: (e) => { e.preventDefault(); e.stopPropagation(); this.toggleCategoryExpansion(offer.id); } }, isExpanded ? (h("span", null, h("i", { class: "sicon-keyboard_arrow_up" }), ' ', salla.lang.get('common.elements.hide', 'إخفاء'))) : (h("span", null, salla.lang.get('common.elements.show', 'عرض'), " ", `+${categories.length - maxVisible}`, ' ', h("i", { class: "sicon-keyboard_arrow_down" })))))))); } /** * Toggle category expansion for a specific offer */ toggleCategoryExpansion(offerId) { this.expandedCategories = { ...this.expandedCategories, [offerId]: !this.expandedCategories[offerId] }; } get host() { return getElement(this); } }; SallaOffer.style = sallaOfferCss; export { SallaOffer as salla_offer };