218 lines
No EOL
5.7 KiB
Vue
218 lines
No EOL
5.7 KiB
Vue
<template>
|
|
<div class="store">
|
|
<store-filter
|
|
v-if="filters.length"
|
|
:filterableAttributes="filters"
|
|
:isOpen="showFilter"
|
|
@update:selected="onFiltersChange"
|
|
@close="showFilter = false"
|
|
/>
|
|
<div class="store__inner">
|
|
<store-top
|
|
v-model="orderBy"
|
|
@toggle-filter="onFilterToggle"
|
|
:isFilters="filters.length > 0"
|
|
/>
|
|
<client-only>
|
|
<div
|
|
class="store__list"
|
|
:class="[
|
|
{ 'store__list-grid': productView === 'grid' },
|
|
{ 'store__list-list': productView === 'list' }
|
|
]"
|
|
>
|
|
<cards-product
|
|
v-if="products.length && !pending"
|
|
v-for="product in products"
|
|
:key="product.node.uuid"
|
|
:product="product.node"
|
|
:isList="productView === 'list'"
|
|
/>
|
|
<skeletons-cards-product
|
|
v-for="idx in 12"
|
|
:key="idx"
|
|
:isList="productView === 'list'"
|
|
/>
|
|
</div>
|
|
</client-only>
|
|
<div class="store__list-observer" ref="observer"></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {useFilters, useStore} from '@composables/store';
|
|
import {useCategoryBySlug} from '@composables/categories';
|
|
import {useBrandBySlug} from '@composables/brands';
|
|
import {useDefaultSeo} from '@composables/seo';
|
|
|
|
const { locale } = useI18n();
|
|
const { $appHelpers } = useNuxtApp();
|
|
|
|
const productView = useCookie<string>(
|
|
$appHelpers.COOKIES_PRODUCT_VIEW_KEY as string,
|
|
{
|
|
default: () => 'grid',
|
|
path: '/',
|
|
}
|
|
);
|
|
|
|
const categorySlug = useRouteParams<string>('categorySlug');
|
|
const brandSlug = useRouteParams<number>('brandSlug');
|
|
const attributes = useRouteQuery<string>('attributes', '');
|
|
const orderBy = useRouteQuery<string>('orderBy', 'created');
|
|
const minPrice = useRouteQuery<number>('minPrice', 0);
|
|
const maxPrice = useRouteQuery<number>('maxPrice', 50000);
|
|
const observer = ref(null);
|
|
|
|
const categoryData = categorySlug.value
|
|
? await useCategoryBySlug(categorySlug.value)
|
|
: { category: ref(null), seoMeta: ref(null), filters: ref([]) };
|
|
|
|
const brandData = brandSlug.value
|
|
? await useBrandBySlug(brandSlug.value)
|
|
: { brand: ref(null), seoMeta: ref(null) };
|
|
|
|
const { category, seoMeta: categorySeoMeta, filters } = categoryData;
|
|
const { brand, seoMeta: brandSeoMeta } = brandData;
|
|
|
|
const seoMeta = computed(() => categorySeoMeta.value || brandSeoMeta.value);
|
|
|
|
const meta = useDefaultSeo(seoMeta.value || null);
|
|
|
|
if (meta) {
|
|
useSeoMeta({
|
|
title: meta.title || $appHelpers.APP_NAME,
|
|
description: meta.description || meta.title || $appHelpers.APP_NAME,
|
|
ogTitle: meta.og.title || undefined,
|
|
ogDescription: meta.og.description || meta.title || $appHelpers.APP_NAME,
|
|
ogType: meta.og.type || undefined,
|
|
ogUrl: meta.og.url || undefined,
|
|
ogImage: meta.og.image || undefined,
|
|
twitterCard: meta.twitter.card || undefined,
|
|
twitterTitle: meta.twitter.title || undefined,
|
|
twitterDescription: meta.twitter.description || undefined,
|
|
robots: meta.robots,
|
|
});
|
|
|
|
useHead({
|
|
link: [
|
|
meta.canonical ? { rel: 'canonical', href: meta.canonical } : {},
|
|
].filter(Boolean) as any,
|
|
meta: [{ property: 'og:locale', content: locale.value }],
|
|
script: meta.jsonLd.map((obj: any) => ({
|
|
type: 'application/ld+json',
|
|
innerHTML: JSON.stringify(obj),
|
|
})),
|
|
__dangerouslyDisableSanitizersByTagID: Object.fromEntries(
|
|
meta.jsonLd.map((_, i: number) => [`ldjson-${i}`, ['innerHTML']])
|
|
),
|
|
});
|
|
}
|
|
|
|
watch(
|
|
() => category.value,
|
|
(cat) => {
|
|
if (cat && !useRoute().query.maxPrice) {
|
|
maxPrice.value = cat.minMaxPrices.maxPrice;
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const { pending, products, pageInfo, variables, getProducts } = useStore({
|
|
orderBy: orderBy.value,
|
|
categoriesSlugs: categorySlug.value,
|
|
productAfter: '',
|
|
minPrice: minPrice.value,
|
|
maxPrice: maxPrice.value,
|
|
brand: brand.value?.name,
|
|
attributes: attributes.value
|
|
});
|
|
await getProducts();
|
|
|
|
const { buildAttributesString } = useFilters(filters);
|
|
const showFilter = ref<boolean>(false);
|
|
|
|
function onFilterToggle() {
|
|
showFilter.value = true;
|
|
}
|
|
|
|
async function onFiltersChange(newFilters: Record<string, string[]>) {
|
|
const attrString = buildAttributesString(newFilters);
|
|
attributes.value = attrString;
|
|
variables.attributes = attrString;
|
|
await getProducts();
|
|
}
|
|
|
|
useIntersectionObserver(
|
|
observer,
|
|
async ([{ isIntersecting }]) => {
|
|
if (isIntersecting && pageInfo.value?.hasNextPage && !pending.value) {
|
|
variables.productAfter = pageInfo.value.endCursor;
|
|
await getProducts();
|
|
}
|
|
},
|
|
);
|
|
|
|
watch(orderBy, async (newVal) => {
|
|
variables.orderBy = newVal || '';
|
|
variables.productAfter = '';
|
|
products.value = [];
|
|
await getProducts();
|
|
});
|
|
watch(attributes, async (newVal) => {
|
|
variables.attributes = newVal;
|
|
variables.productAfter = '';
|
|
products.value = [];
|
|
await getProducts();
|
|
});
|
|
watch(minPrice, async (newVal) => {
|
|
variables.minPrice = newVal || 0;
|
|
variables.productAfter = '';
|
|
products.value = [];
|
|
await getProducts();
|
|
});
|
|
watch(maxPrice, async (newVal) => {
|
|
variables.maxPrice = newVal || 500000;
|
|
variables.productAfter = '';
|
|
products.value = [];
|
|
await getProducts();
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.store {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 32px;
|
|
|
|
&__inner {
|
|
width: 100%;
|
|
}
|
|
|
|
&__list {
|
|
margin-top: 50px;
|
|
|
|
&-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 25px;
|
|
}
|
|
|
|
&-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 20px;
|
|
}
|
|
|
|
&-observer {
|
|
background-color: transparent;
|
|
width: 100%;
|
|
height: 10px;
|
|
}
|
|
}
|
|
}
|
|
</style> |