@salla.sa/twilight-components
Version:
Salla Web Component
398 lines (393 loc) • 24.5 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
import { r as registerInstance, h, a as getElement } from './index-Dbv0I4re.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 };