{% if products|length %} {% hook 'brands:single.items.start' %} - +
+ {% include 'pages.partials.product.skeleton' with { type: 'grid', count: 8 } %} + +
{% hook 'brands:single.items.end' %} {% else %}
diff --git a/src/views/pages/partials/product/skeleton.twig b/src/views/pages/partials/product/skeleton.twig new file mode 100644 index 000000000..accf6418d --- /dev/null +++ b/src/views/pages/partials/product/skeleton.twig @@ -0,0 +1,30 @@ +{# +| Variable | Type | Description | +|--------------|---------|--------------------------------------------------------------| +| type | string | slider, grid, filters (grid beside the filters) or horizontal | +| count | ?int | Number of placeholder cards, default 4 | +| title | ?bool | Reserve the slider title row | +| source | ?string | Products source, to skip sources that can never load | +| source_value | ?array | Products source value | +#} +{# these sources fetch nothing without a value, so there is nothing to wait for #} +{% set never_loads = source|default and source not in ['offers', 'latest', 'sales', 'top-rated'] and source_value is empty %} +{% if not never_loads %} + +{% endif %} diff --git a/src/views/pages/product/index.twig b/src/views/pages/product/index.twig index 81d140227..957ad849d 100644 --- a/src/views/pages/product/index.twig +++ b/src/views/pages/product/index.twig @@ -71,7 +71,8 @@ {% set has_selected_sort = sort_list|filter(sort => sort.is_selected)|length %} {% set fallback_sort = (sort_list|length and not has_selected_sort and first_sort.id != 'ourSuggest') ? first_sort.id : '' %} -
+
+ {% include 'pages.partials.product.skeleton' with { type: filters ? 'filters' : 'grid', count: 8 } %}
From 62586d199953ad97b7775c1cfbdcd3160f9a76a4 Mon Sep 17 00:00:00 2001 From: Eslam Magdy <32875906+eslamoo@users.noreply.github.com> Date: Mon, 28 Sep 2026 04:33:05 +0300 Subject: [PATCH 2/2] Give product card images their width and height from the image URL so cards hold their size while images load --- public/product-card.js | 2 +- src/assets/js/partials/product-card.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/public/product-card.js b/public/product-card.js index 9d5fcdff4..16d303db7 100644 --- a/public/product-card.js +++ b/public/product-card.js @@ -1 +1 @@ -(()=>{"use strict";class t extends HTMLElement{constructor(){super()}connectedCallback(){this.product=this.product||JSON.parse(this.getAttribute("product")),"ready"===window.app?.status?this.onReady():document.addEventListener("theme::ready",()=>this.onReady())}onReady(){this.fitImageHeight=salla.config.get("store.settings.product.fit_type"),this.placeholder=salla.url.asset(salla.config.get("theme.settings.placeholder")),this.getProps(),salla.lang.onLoaded(()=>{this.remained=salla.lang.get("pages.products.remained"),this.donationAmount=salla.lang.get("pages.products.donation_amount"),this.startingPrice=salla.lang.get("pages.products.starting_price"),this.addToCart=salla.lang.get("pages.cart.add_to_cart"),this.outOfStock=salla.lang.get("pages.products.out_of_stock"),this.render()}),this.render()}initCircleBar(){let t=100-this.product.quantity/(this.product.quantity>100?2*this.product.quantity:100)*100;this.querySelector(".s-product-card-content-pie-svg-bar").style.strokeDashoffset=t}formatDate(t){let s=new Date(t);return`${s.getFullYear()}-${s.getMonth()+1}-${s.getDate()}`}getProductBadge(){return this.product?.preorder?.label?`
${this.product.preorder.label}
`:this.product.promotion_title?`
${this.product.promotion_title}
`:this.showQuantity&&this.product?.quantity?`${this.remained} ${salla.helpers.number(this.product?.quantity)}
`:this.showQuantity&&this.product?.is_out_of_stock?`
${this.outOfStock}
`:""}getPriceFormat(t){return t&&0!=t?salla.money(t):salla.config.get("store.settings.product.show_price_as_dash")?"-":""}getProductPrice(){let t="";return t=this.product.is_on_sale?`
\n

${this.getPriceFormat(this.product.sale_price)}

\n ${this.getPriceFormat(this.product?.regular_price)}\n
`:this.product.starting_price?`
\n

${this.startingPrice}

\n

${this.getPriceFormat(this.product?.starting_price)}

\n
`:`

${this.getPriceFormat(this.product?.price)}

`,t}getAddButtonLabel(){return this.product.has_preorder_campaign?salla.lang.get("pages.products.pre_order_now"):"sale"===this.product.status&&"booking"===this.product.type?salla.lang.get("pages.cart.book_now"):"sale"===this.product.status?salla.lang.get("pages.cart.add_to_cart"):"donating"!==this.product.type?salla.lang.get("pages.products.out_of_stock"):salla.lang.get("pages.products.donation_exceed")}getProps(){this.horizontal=this.hasAttribute("horizontal"),this.shadowOnHover=this.hasAttribute("shadowOnHover"),this.hideAddBtn=this.hasAttribute("hideAddBtn"),this.fullImage=this.hasAttribute("fullImage"),this.minimal=this.hasAttribute("minimal"),this.isSpecial=this.hasAttribute("isSpecial"),this.showQuantity=this.hasAttribute("showQuantity")}escapeHTML(t=""){return String(t).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}render(){this.classList.add("s-product-card-entry"),this.setAttribute("id",this.product.id),!(this.horizontal||this.fullImage||this.minimal)&&this.classList.add("s-product-card-vertical"),this.horizontal&&!this.fullImage&&!this.minimal&&this.classList.add("s-product-card-horizontal"),this.fitImageHeight&&!this.isSpecial&&!this.fullImage&&!this.minimal&&this.classList.add("s-product-card-fit-height"),this.isSpecial&&this.classList.add("s-product-card-special"),this.fullImage&&this.classList.add("s-product-card-full-image"),this.minimal&&this.classList.add("s-product-card-minimal"),this.product?.donation&&this.classList.add("s-product-card-donation"),this.shadowOnHover&&this.classList.add("s-product-card-shadow"),this.product?.is_out_of_stock&&this.classList.add("s-product-card-out-of-stock"),this.isInWishlist=!salla.config.isGuest()&&salla.storage.get("salla::wishlist",[]).includes(Number(this.product.id)),this.effectiveStatus=this.product.is_out_of_stock&&window.notify_when_available_in_card&&!["donating","financial_support"].includes(this.product?.type)?"out-and-notify":this.product.status,this.innerHTML=`\n
\n \n ${this.escapeHTML(this.product?.image?.alt||this.product.name)}\n ${this.fullImage||this.minimal?"":this.getProductBadge()}\n \n ${this.fullImage?``:""}\n ${this.horizontal||this.fullImage?"":`\n \n `}\n
\n
\n ${this.isSpecial&&this.product?.quantity?`
\n \n ${salla.helpers.number(this.product?.quantity)}\n ${this.remained}\n \n \n \n \n \n
`:""}\n\n
\n

\n ${this.product?.name}\n

\n\n ${this.product?.subtitle&&!this.minimal?`

${this.product?.subtitle}

`:""}\n
\n ${!this.product?.donation||this.minimal||this.fullImage?"":`\n
\n ${this.product?.donation?.can_donate&&this.product?.donation?.custom_amount_enabled?`\n `:""}\n
`}\n
\n ${this.product?.donation?.can_donate?"":this.getProductPrice()}\n ${this.product?.rating?.stars?`
\n \n ${this.product.rating.stars}\n
`:""}\n
\n\n ${this.isSpecial&&this.product.discount_ends?``:""}\n\n\n ${this.hideAddBtn?"":``}\n
\n `,this.querySelectorAll('[name="donating_amount"]').forEach(t=>{t.addEventListener("input",t=>{t.target.closest(".s-product-card-content").querySelector("salla-add-product-button").setAttribute("donating-amount",t.target.value)})}),this.product?.quantity&&this.isSpecial&&this.initCircleBar(),this.querySelectorAll(".s-product-card-wishlist-btn").forEach(t=>{t.addEventListener("click",()=>{const s=!t.classList.contains("s-product-card-wishlist-added");app.toggleElementClassIf(t,"s-product-card-wishlist-added","not-added",()=>s),app.toggleElementClassIf(t,"pulse-anime","un-favorited",()=>s)})})}}customElements.define("custom-salla-product-card",t)})(); \ No newline at end of file +(()=>{"use strict";class t extends HTMLElement{constructor(){super()}connectedCallback(){this.product=this.product||JSON.parse(this.getAttribute("product")),"ready"===window.app?.status?this.onReady():document.addEventListener("theme::ready",()=>this.onReady())}onReady(){this.fitImageHeight=salla.config.get("store.settings.product.fit_type"),this.placeholder=salla.url.asset(salla.config.get("theme.settings.placeholder")),this.getProps(),salla.lang.onLoaded(()=>{this.remained=salla.lang.get("pages.products.remained"),this.donationAmount=salla.lang.get("pages.products.donation_amount"),this.startingPrice=salla.lang.get("pages.products.starting_price"),this.addToCart=salla.lang.get("pages.cart.add_to_cart"),this.outOfStock=salla.lang.get("pages.products.out_of_stock"),this.render()}),this.render()}initCircleBar(){let t=100-this.product.quantity/(this.product.quantity>100?2*this.product.quantity:100)*100;this.querySelector(".s-product-card-content-pie-svg-bar").style.strokeDashoffset=t}formatDate(t){let s=new Date(t);return`${s.getFullYear()}-${s.getMonth()+1}-${s.getDate()}`}getProductBadge(){return this.product?.preorder?.label?`
${this.product.preorder.label}
`:this.product.promotion_title?`
${this.product.promotion_title}
`:this.showQuantity&&this.product?.quantity?`${this.remained} ${salla.helpers.number(this.product?.quantity)}
`:this.showQuantity&&this.product?.is_out_of_stock?`
${this.outOfStock}
`:""}getPriceFormat(t){return t&&0!=t?salla.money(t):salla.config.get("store.settings.product.show_price_as_dash")?"-":""}getProductPrice(){let t="";return t=this.product.is_on_sale?`
\n

${this.getPriceFormat(this.product.sale_price)}

\n ${this.getPriceFormat(this.product?.regular_price)}\n
`:this.product.starting_price?`
\n

${this.startingPrice}

\n

${this.getPriceFormat(this.product?.starting_price)}

\n
`:`

${this.getPriceFormat(this.product?.price)}

`,t}getAddButtonLabel(){return this.product.has_preorder_campaign?salla.lang.get("pages.products.pre_order_now"):"sale"===this.product.status&&"booking"===this.product.type?salla.lang.get("pages.cart.book_now"):"sale"===this.product.status?salla.lang.get("pages.cart.add_to_cart"):"donating"!==this.product.type?salla.lang.get("pages.products.out_of_stock"):salla.lang.get("pages.products.donation_exceed")}getProps(){this.horizontal=this.hasAttribute("horizontal"),this.shadowOnHover=this.hasAttribute("shadowOnHover"),this.hideAddBtn=this.hasAttribute("hideAddBtn"),this.fullImage=this.hasAttribute("fullImage"),this.minimal=this.hasAttribute("minimal"),this.isSpecial=this.hasAttribute("isSpecial"),this.showQuantity=this.hasAttribute("showQuantity")}escapeHTML(t=""){return String(t).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}getImageSize(t){const s=t?.match(/-(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)-/);return s?`width="${Math.round(s[1])}" height="${Math.round(s[2])}"`:""}render(){this.classList.add("s-product-card-entry"),this.setAttribute("id",this.product.id),!(this.horizontal||this.fullImage||this.minimal)&&this.classList.add("s-product-card-vertical"),this.horizontal&&!this.fullImage&&!this.minimal&&this.classList.add("s-product-card-horizontal"),this.fitImageHeight&&!this.isSpecial&&!this.fullImage&&!this.minimal&&this.classList.add("s-product-card-fit-height"),this.isSpecial&&this.classList.add("s-product-card-special"),this.fullImage&&this.classList.add("s-product-card-full-image"),this.minimal&&this.classList.add("s-product-card-minimal"),this.product?.donation&&this.classList.add("s-product-card-donation"),this.shadowOnHover&&this.classList.add("s-product-card-shadow"),this.product?.is_out_of_stock&&this.classList.add("s-product-card-out-of-stock"),this.isInWishlist=!salla.config.isGuest()&&salla.storage.get("salla::wishlist",[]).includes(Number(this.product.id)),this.effectiveStatus=this.product.is_out_of_stock&&window.notify_when_available_in_card&&!["donating","financial_support"].includes(this.product?.type)?"out-and-notify":this.product.status,this.innerHTML=`\n
\n \n ${this.escapeHTML(this.product?.image?.alt||this.product.name)}\n ${this.fullImage||this.minimal?"":this.getProductBadge()}\n \n ${this.fullImage?``:""}\n ${this.horizontal||this.fullImage?"":`\n \n `}\n
\n
\n ${this.isSpecial&&this.product?.quantity?`
\n \n ${salla.helpers.number(this.product?.quantity)}\n ${this.remained}\n \n \n \n \n \n
`:""}\n\n
\n

\n ${this.product?.name}\n

\n\n ${this.product?.subtitle&&!this.minimal?`

${this.product?.subtitle}

`:""}\n
\n ${!this.product?.donation||this.minimal||this.fullImage?"":`\n
\n ${this.product?.donation?.can_donate&&this.product?.donation?.custom_amount_enabled?`\n `:""}\n
`}\n
\n ${this.product?.donation?.can_donate?"":this.getProductPrice()}\n ${this.product?.rating?.stars?`
\n \n ${this.product.rating.stars}\n
`:""}\n
\n\n ${this.isSpecial&&this.product.discount_ends?``:""}\n\n\n ${this.hideAddBtn?"":``}\n
\n `,this.querySelectorAll('[name="donating_amount"]').forEach(t=>{t.addEventListener("input",t=>{t.target.closest(".s-product-card-content").querySelector("salla-add-product-button").setAttribute("donating-amount",t.target.value)})}),this.product?.quantity&&this.isSpecial&&this.initCircleBar(),this.querySelectorAll(".s-product-card-wishlist-btn").forEach(t=>{t.addEventListener("click",()=>{const s=!t.classList.contains("s-product-card-wishlist-added");app.toggleElementClassIf(t,"s-product-card-wishlist-added","not-added",()=>s),app.toggleElementClassIf(t,"pulse-anime","un-favorited",()=>s)})})}}customElements.define("custom-salla-product-card",t)})(); \ No newline at end of file diff --git a/src/assets/js/partials/product-card.js b/src/assets/js/partials/product-card.js index 5d8f9e3a9..c9bed0388 100644 --- a/src/assets/js/partials/product-card.js +++ b/src/assets/js/partials/product-card.js @@ -164,6 +164,12 @@ class ProductCard extends HTMLElement { .replace(/>/g, ">"); } + // salla image urls carry their size, e.g. -500x400-, so the card can hold its height before the image loads + getImageSize(url) { + const size = url?.match(/-(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)-/); + return size ? `width="${Math.round(size[1])}" height="${Math.round(size[2])}"` : ''; + } + render(){ this.classList.add('s-product-card-entry'); this.setAttribute('id', this.product.id); @@ -190,6 +196,7 @@ class ProductCard extends HTMLElement { ? this.fitImageHeight : 'cover'}" src="${this.product?.image?.url || this.product?.thumbnail || this.placeholder || ''}" + ${this.getImageSize(this.product?.image?.url || this.product?.thumbnail)} alt="${this.escapeHTML(this.product?.image?.alt || this.product.name)}" loading="lazy" />