Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/app.css

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions public/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/product-card.js

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions public/product.js

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions src/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class App extends AppHelpers {
this.initiateDropdowns();
this.initiateModals();
this.initiateCollapse();
this.initProductsSkeletons();

// Ensure #more-menu-dropdown exists before running changeMenuDirection
const menuDirInterval = setInterval(() => {
Expand Down Expand Up @@ -63,6 +64,36 @@ class App extends AppHelpers {
}, 1000);
}

initProductsSkeletons() {
app.all('.products-reserve', reserve => {
const skeleton = reserve.querySelector(':scope > .products-skeleton');
const products = reserve.querySelector(':scope > salla-products-slider, :scope > salla-products-list');
if (!skeleton || !products) return;

const isSlider = products.matches('salla-products-slider');
// cards are inserted empty and drawn by product-card.js, so wait for a drawn one
const isReady = () => {
const hasCard = products.querySelector('custom-salla-product-card > *');
if (isSlider) {
const isEmpty = products.querySelector('salla-slider') && !products.querySelector('custom-salla-product-card');
return isEmpty || (hasCard && products.querySelector('.swiper-initialized'));
}
return hasCard || products.querySelector('.s-products-list-placeholder');
};

const observer = new MutationObserver(() => isReady() && release());
const timeout = setTimeout(() => release(), 30000);
const release = () => {
observer.disconnect();
clearTimeout(timeout);
skeleton.remove();
};

if (isReady()) return release();
observer.observe(products, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
});
}

loadModalImgOnclick(){
document.querySelectorAll('.load-img-onclick').forEach(link => {
link.addEventListener('click', (event) => {
Expand Down
7 changes: 7 additions & 0 deletions src/assets/js/partials/product-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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"
/>
Expand Down
96 changes: 96 additions & 0 deletions src/assets/styles/04-components/products-skeleton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Server-rendered placeholder for product sliders and lists (views/pages/partials/product/skeleton.twig).
// app.js removes it once the products are laid out; the sizes restate twilight's so the swap moves nothing.
.products-reserve {
position: relative;
}

// the products lay out on top of the skeleton, out of flow, until it is removed
.products-reserve:has(> .products-skeleton) > :is(salla-products-slider, salla-products-list) {
position: absolute;
top: 0;
right: 0;
left: 0;
opacity: 0;
pointer-events: none;
}

.products-skeleton {
@apply w-full mb-8 animate-pulse;

&__title {
@apply flex items-center h-14 mb-4 md:mb-8;

span {
@apply block w-32 h-5 rounded bg-gray-200;
}
}

&__cards {
display: grid;
@apply grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2.5 sm:gap-4 md:gap-6;
}

&__card {
@apply flex flex-col overflow-hidden rounded bg-white;
}

&__image {
@apply w-full aspect-square bg-gray-100;
}

&__content {
@apply p-3 sm:p-5;
}

&__line {
@apply block w-3/4 h-4 mb-[1.125rem] rounded bg-gray-200;

&--short {
@apply w-1/3 mb-6;
}
}

&__button {
@apply block h-10 rounded bg-gray-100;
}

// equal product card size setting
&--fit &__image {
@apply aspect-auto h-44 sm:h-60;
}

&--filters &__cards {
@apply md:grid-cols-3 lg:grid-cols-3;
}

&--horizontal {
.products-skeleton__cards {
@apply grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-8;
}

.products-skeleton__card {
@apply flex-row;
}

.products-skeleton__image {
@apply aspect-auto h-auto w-32 sm:w-56 shrink-0;
}

.products-skeleton__content {
@apply flex-1;
}
}

// twilight's slide: fixed widths in a single row, the gutter is the slide padding
&--slider {
@apply mb-8 md:mb-20;

.products-skeleton__cards {
@apply flex gap-0;
}

.products-skeleton__card {
@apply flex-none w-full sm:w-1/2 md:w-1/3 lg:w-1/4 max-w-[250px] sm:max-w-[320px] px-[5px] sm:px-2.5 bg-clip-content;
}
}
}
2 changes: 2 additions & 0 deletions src/assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* Home Blocks..........Specific styles for home blocks.
* Slider...............Specific styles for sliders.
* Product..............Specific styles for single product page and it's elements.
* Products Skeleton....Loading placeholder for product sliders and lists.
* Brands...............Specific styles for brands page.
* Placeholder..........Specific styles for no content placeholders.
* Gifting..............Specific styles for gifting system.
Expand Down Expand Up @@ -79,6 +80,7 @@
@import './04-components/home-blocks';
@import './04-components/slider';
@import './04-components/product';
@import './04-components/products-skeleton';
@import './04-components/brands';
@import './04-components/no-content-placeholder';
@import './04-components/gifting';
Expand Down
21 changes: 12 additions & 9 deletions src/views/components/home/fixed-products.twig
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@
</div>
{% endif %}

<salla-products-list
data-testid="store-home-fixed-products-list"
source="{{ products.source }}"
limit="{{ limit }}"
source-value="{{ products.source_value|json_encode }}"
class="{{is_vertical ? 'vertical-products' : 'horizontal-products'}}"
{{is_vertical ? '' : 'horizontal-cards'}}
>
</salla-products-list>
<div class="products-reserve">
{% include 'pages.partials.product.skeleton' with { type: is_vertical ? 'grid' : 'horizontal', count: min(limit|default(8), 8), source: products.source, source_value: products.source_value } %}
<salla-products-list
data-testid="store-home-fixed-products-list"
source="{{ products.source }}"
limit="{{ limit }}"
source-value="{{ products.source_value|json_encode }}"
class="{{is_vertical ? 'vertical-products' : 'horizontal-products'}}"
{{is_vertical ? '' : 'horizontal-cards'}}
>
</salla-products-list>
</div>
</section>


5 changes: 4 additions & 1 deletion src/views/components/home/latest-products.twig
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
<h2>{{ trans('blocks.home.latest_products') }}</h2>
</div>

<salla-products-list data-testid="store-home-latest-products-list" source="latest"></salla-products-list>
<div class="products-reserve">
{% include 'pages.partials.product.skeleton' with { type: 'grid', count: 8 } %}
<salla-products-list data-testid="store-home-latest-products-list" source="latest"></salla-products-list>
</div>
</section>
25 changes: 14 additions & 11 deletions src/views/components/home/products-slider.twig
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@

<section component-id="{{ componentId }}" data-testid="store-home-products-slider" id="best-offers-{{ position }}-slider" class="s-block s-block--best-offers container overflow-hidden">
{% if products|length %}
<salla-products-slider
data-testid="store-home-products-slider-widget"
source="{{ products.source }}"
limit="{{ limit }}"
autoplay
source-value="{{ products.source_value|json_encode }}"
display-all-url="{{display_all_url}}"
block-title="{{ title }}"
block-subTitle="{{ sub_title|raw }}"
slider-id="slider-with-bg-{{ position }}"
></salla-products-slider>
<div class="products-reserve">
{% include 'pages.partials.product.skeleton' with { type: 'slider', title: title, source: products.source, source_value: products.source_value } %}
<salla-products-slider
data-testid="store-home-products-slider-widget"
source="{{ products.source }}"
limit="{{ limit }}"
autoplay
source-value="{{ products.source_value|json_encode }}"
display-all-url="{{display_all_url}}"
block-title="{{ title }}"
block-subTitle="{{ sub_title|raw }}"
slider-id="slider-with-bg-{{ position }}"
></salla-products-slider>
</div>
{% endif %}
</section>
5 changes: 4 additions & 1 deletion src/views/pages/brands/single.twig
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@
<div class="main-content w-full">
{% if products|length %}
{% hook 'brands:single.items.start' %}
<salla-products-list data-testid="store-brand-products" source="{{page.slug}}" source-value="{{page.id}}"></salla-products-list>
<div class="products-reserve">
{% include 'pages.partials.product.skeleton' with { type: 'grid', count: 8 } %}
<salla-products-list data-testid="store-brand-products" source="{{page.slug}}" source-value="{{page.id}}"></salla-products-list>
</div>
{% hook 'brands:single.items.end' %}
{% else %}
<div class="no-content-placeholder">
Expand Down
30 changes: 30 additions & 0 deletions src/views/pages/partials/product/skeleton.twig
Original file line number Diff line number Diff line change
@@ -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 %}
<div class="products-skeleton products-skeleton--{{ type }}{{ store.settings.product.fit_type ? ' products-skeleton--fit' : '' }}" aria-hidden="true">
{% if title %}
<div class="products-skeleton__title"><span></span></div>
{% endif %}
<div class="products-skeleton__cards">
{% for i in 1..count|default(4) %}
<div class="products-skeleton__card">
<div class="products-skeleton__image"></div>
<div class="products-skeleton__content">
<span class="products-skeleton__line"></span>
<span class="products-skeleton__line products-skeleton__line--short"></span>
<span class="products-skeleton__button"></span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
3 changes: 2 additions & 1 deletion src/views/pages/product/index.twig
Original file line number Diff line number Diff line change
Expand Up @@ -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 : '' %}

<div class="flex">
<div class="products-reserve flex">
{% include 'pages.partials.product.skeleton' with { type: filters ? 'filters' : 'grid', count: 8 } %}
<salla-products-list data-testid="store-products-list" class="flex-1 min-w-0" source="{{ page.slug }}" source-value="{{ page.id }}" sort-by="{{ fallback_sort }}" {{ filters ?'filters-Results':'' }}></salla-products-list>
</div>

Expand Down