UNPKG

@salla.sa/twilight-components

Version:
178 lines (173 loc) 9.31 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-skeleton2.js'; const sallaBoughtTogetherCss = ":host{display:block}.s-bought-together-checkbox:checked~.s-bought-together-checkmark{background-color:var(--color-primary, #2ECC71);border-color:var(--color-primary, #2ECC71)}.s-bought-together-checkbox:checked~.s-bought-together-checkmark::after{content:\"\";display:block;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg);margin-top:-2px}"; // Read-only recommendations key confirmed safe for client-side use by the API team — no write access or sensitive scope. const XSELL_API_KEY = 'VOL5WaZu2YROp0RplCZAr1RplhL9FFGQ'; const SallaBoughtTogether$1 = /*@__PURE__*/ proxyCustomElement(class SallaBoughtTogether extends HTMLElement { constructor() { super(); this.__registerHost(); /** * Maximum number of recommendations to fetch. Max is 4. */ this.limit = 3; this.recommendations = []; this.selectedIds = new Set(); this.isLoading = true; this.canRender = true; this.isAdding = false; this.title = salla.lang.get('pages.products.bought_together_title'); this.subtitle = salla.lang.get('pages.products.bought_together_subtitle'); this.buyTogetherFor = salla.lang.get('pages.products.buy_together_for'); this.resolvedProductId = 0; salla.lang.onLoaded(() => { this.title = salla.lang.get('pages.products.bought_together_title'); this.subtitle = salla.lang.get('pages.products.bought_together_subtitle'); this.buyTogetherFor = salla.lang.get('pages.products.buy_together_for'); }); } componentWillLoad() { salla.onReady() .then(() => { if (!salla.config.get('store.settings.product.bought_together')) { this.canRender = false; return; } const id = salla.config.get('page.id'); if (!id) { this.canRender = false; return; } this.resolvedProductId = id; return salla.api.request('https://api.salla.dev/1/indexes/*/recommendations', { requests: [{ indexName: 'products', model: 'xsell-v1', priceThreshold: 2, objectID: String(id), maxRecommendations: this.limit, }] }, 'post', { headers: { 'X-Algolia-Api-Key': XSELL_API_KEY, 'X-Query-Enrichment': 'false', 'X-source': 'store' } }); }) .then((response) => { if (!response) return; const hits = response?.results?.[0]?.hits ?? []; if (!hits.length) { this.canRender = false; return; } const ids = [String(this.resolvedProductId), ...hits.map(h => h.objectID)]; return salla.product.fetch({ source: 'selected', source_value: ids }); }) .then((response) => { if (!response) return; const products = response?.data ?? []; if (!products.length) { this.canRender = false; return; } const mainProduct = products.find(p => String(p.id) === String(this.resolvedProductId)); const rest = products.filter(p => String(p.id) !== String(this.resolvedProductId)); if (!rest.length) { this.canRender = false; return; } this.recommendations = mainProduct ? [mainProduct, ...rest] : rest; this.selectedIds = new Set(this.recommendations.map((p) => p.id)); }) .catch(() => { this.canRender = false; }) .finally(() => { this.isLoading = false; }); } toggleProduct(id) { const next = new Set(this.selectedIds); next.has(id) ? next.delete(id) : next.add(id); this.selectedIds = next; } get totalPrice() { const total = this.recommendations .filter(p => this.selectedIds.has(p.id)) // p.price is a plain float in the API response (e.g. 1150.58) — the TS type (any[]) is misleading. .reduce((sum, p) => sum + (p.price ?? 0), 0); const decimals = salla.config.get('store.currency.decimals') ?? 2; const factor = Math.pow(10, decimals); return salla.money(Math.round(total * factor) / factor); } async addSelectedToCart() { if (this.isAdding || !this.selectedIds.size) return; this.isAdding = true; try { // Intentional: quickAdd is used for all items including the main product without options — business decision by product team. const results = await Promise.allSettled(Array.from(this.selectedIds).map(id => salla.cart.quickAdd(id))); if (results.some(r => r.status === 'rejected')) { salla.error(salla.lang.get('common.messages.error')); } } finally { this.isAdding = false; } } getSkeletonView() { return (h(Host, { class: "s-bought-together-entry" }, h("div", { class: "s-bought-together-skeleton" }, h("div", { class: "s-bought-together-skeleton-header" }, h("salla-skeleton", { height: "16px", width: "35%" }), h("salla-skeleton", { height: "10px", width: "60%" })), Array(4).fill(null).map((_, i) => (h("div", { key: i, class: "s-bought-together-skeleton-item" }, h("salla-skeleton", { height: "22px", width: "22px" }), h("salla-skeleton", { height: "48px", width: "48px" }), h("div", { class: "s-bought-together-skeleton-info" }, h("salla-skeleton", { height: "12px", width: "55%", style: { marginBottom: '3px' } }), h("salla-skeleton", { height: "10px", width: "25%" }))))), h("salla-skeleton", { height: "44px", width: "100%" })))); } render() { if (!this.canRender) return null; if (this.isLoading) return this.getSkeletonView(); if (!this.recommendations.length) return null; return (h(Host, { class: "s-bought-together-entry" }, h("div", { class: "s-bought-together-header" }, h("h3", { class: "s-bought-together-title" }, this.title), h("p", { class: "s-bought-together-subtitle" }, this.subtitle)), h("div", { class: "s-bought-together-list" }, this.recommendations.map((product) => (h("div", { key: product.id, class: `s-bought-together-item${!this.selectedIds.has(product.id) ? ' s-bought-together-item--unchecked' : ''}` }, h("label", { class: "s-bought-together-checkbox-label" }, h("input", { type: "checkbox", class: "s-bought-together-checkbox", checked: this.selectedIds.has(product.id), onChange: () => this.toggleProduct(product.id) }), h("span", { class: "s-bought-together-checkmark" })), h("a", { class: "s-bought-together-item-link", href: product.url }, h("img", { class: "s-bought-together-item-img", src: product.image?.url || salla.url.cdn('images/s-empty.png'), alt: product.image?.alt || product.name, loading: "lazy", decoding: "async", onError: e => { e.currentTarget.onerror = null; e.currentTarget.src = salla.url.cdn('images/s-empty.png'); } }), h("span", { class: "s-bought-together-item-name" }, product.name)), h("span", { class: "s-bought-together-item-price", innerHTML: product.price != null ? salla.money(product.price) : '' }))))), h("salla-button", { class: "s-bought-together-btn", disabled: !this.selectedIds.size || this.isAdding, loading: this.isAdding, onClick: () => this.addSelectedToCart() }, h("span", { innerHTML: `${this.buyTogetherFor} ${this.totalPrice}` })))); } static get style() { return sallaBoughtTogetherCss; } }, [0, "salla-bought-together", { "limit": [2], "recommendations": [32], "selectedIds": [32], "isLoading": [32], "canRender": [32], "isAdding": [32], "title": [32], "subtitle": [32], "buyTogetherFor": [32] }]); function defineCustomElement$1() { if (typeof customElements === "undefined") { return; } const components = ["salla-bought-together", "salla-button", "salla-skeleton"]; components.forEach(tagName => { switch (tagName) { case "salla-bought-together": if (!customElements.get(tagName)) { customElements.define(tagName, SallaBoughtTogether$1); } break; case "salla-button": if (!customElements.get(tagName)) { defineCustomElement$3(); } break; case "salla-skeleton": if (!customElements.get(tagName)) { defineCustomElement$2(); } break; } }); } defineCustomElement$1(); const SallaBoughtTogether = SallaBoughtTogether$1; const defineCustomElement = defineCustomElement$1; export { SallaBoughtTogether, defineCustomElement };