481 lines
15 KiB
Vue
481 lines
15 KiB
Vue
<template>
|
|
<!-- <SectionShopPageCurrencyRatesBar
|
|
class="mb-[25px] sm:mb-[55px] xl:mb-[75px]"
|
|
/> -->
|
|
<UiContainer>
|
|
<div class="flex flex-col gap-[25px] sm:gap-10 xl:flex-row">
|
|
<!-- button to open categories -->
|
|
<div class="xl:hidden flex items-center w-full">
|
|
<button
|
|
class="h-[40px] w-full cursor-pointer rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover"
|
|
@click="openCategories = !openCategories">
|
|
Otevřené kategorie a filtry
|
|
</button>
|
|
</div>
|
|
|
|
<Transition>
|
|
<div v-if="openCategories" class="min-w-[250px] px-5 sm:p-0 xl:hidden">
|
|
<h1 class="font-bounded leading-[140%] font-bold text-[24px] mb-[25px]">
|
|
{{ $t("category") }}
|
|
</h1>
|
|
<div class="flex flex-col gap-[25px]">
|
|
<div>
|
|
<div v-if="categoriesList && categoriesList.length < 1" class="animate-pulse">
|
|
<div class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24">
|
|
<div class="w-32 h-4 bg-gray-200 rounded" />
|
|
<div class="w-4 h-4 bg-gray-200 rounded-full" />
|
|
</div>
|
|
</div>
|
|
<CategoryTree :data="categoriesList" :active="categoryId" @change-category="changeCategory($event)" />
|
|
</div>
|
|
<div>
|
|
<p class="mb-[25px] text-lg font-extrabold text-black dark:text-white">
|
|
{{ $t("filtered_by") }}
|
|
</p>
|
|
|
|
<div v-for="(item, itemIndex) in filters" :key="itemIndex" :class="[
|
|
'mb-[30px]',
|
|
visibleFeatures[item.feature] && 'border-b border-block pb-2',
|
|
]">
|
|
<span class="flex justify-between items-center font-bold cursor-pointer mb-[25px] text-base"
|
|
@click="toggleFeature(item.feature)">
|
|
{{ item.feature }}
|
|
<span :class="[
|
|
visibleFeatures[item.feature] && 'rotate-180',
|
|
'transition-all',
|
|
]"><i class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto" /></span>
|
|
</span>
|
|
<ul v-show="visibleFeatures[item.feature]" class="flex flex-col gap-5">
|
|
<li v-for="filter in item.feature_values" :key="filter.value_id"
|
|
class="flex items-center gap-[10px] cursor-pointer">
|
|
<input :id="`${filter.value_id}`" v-model="selectedFilters"
|
|
:value="`${filter.parent}.${filter.value_id}`" type="checkbox" class="border-button !bg-inherit">
|
|
<label :for="`${filter.value_id}`"
|
|
class="cursor-pointer flex items-center justify-between w-full text-base">
|
|
<span>{{ filter.value }}</span>
|
|
<span>12</span>
|
|
</label>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
|
|
<!-- categories -->
|
|
<div class="min-w-[250px] hidden xl:block">
|
|
<h1 class="font-bounded leading-[140%] font-bold text-[40px] mb-[55px]">
|
|
{{ $t("category") }}
|
|
</h1>
|
|
<div class="flex flex-col gap-12">
|
|
<div>
|
|
<div v-if="categoriesList && categoriesList.length < 1" class="animate-pulse">
|
|
<div class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24">
|
|
<div class="w-32 h-4 bg-gray-200 rounded" />
|
|
<div class="w-4 h-4 bg-gray-200 rounded-full" />
|
|
</div>
|
|
</div>
|
|
<CategoryTree :data="categoriesList" :active="categoryId" @change-category="changeCategory($event)" />
|
|
</div>
|
|
<div>
|
|
<p class="mb-10 text-2xl font-extrabold text-black dark:text-white">
|
|
{{ $t("filtered_by") }}
|
|
</p>
|
|
|
|
<!-- <div v-if="filters.length < 1" class="mb-8 text-white animate-pulse">
|
|
<div v-for="i in 5"
|
|
class="mt-10 flex justify-between font-bold text-black cursor-pointer 2xl:pr-24 dark:text-gray">
|
|
<div class="w-32 h-4 bg-gray-200 rounded"></div>
|
|
<div class="w-4 h-4 bg-gray-200 rounded"></div>
|
|
</div>
|
|
</div> -->
|
|
|
|
<div v-for="(item, itemIndex) in filters" :key="itemIndex" :class="[
|
|
'mb-[30px]',
|
|
visibleFeatures[item.feature] && 'border-b border-block pb-2',
|
|
]">
|
|
<span class="flex justify-between items-center font-bold cursor-pointer mb-[25px]"
|
|
@click="toggleFeature(item.feature)">
|
|
{{ item.feature }}
|
|
<span :class="[
|
|
visibleFeatures[item.feature] && 'rotate-180',
|
|
'transition-all',
|
|
]"><i class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto" /></span>
|
|
</span>
|
|
<ul v-show="visibleFeatures[item.feature]" class="flex flex-col gap-5">
|
|
<li v-for="filter in item.feature_values" :key="filter.value_id"
|
|
class="flex items-center gap-[10px] cursor-pointer">
|
|
<!-- <input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`"
|
|
v-model="selectedFilters" type="checkbox" class="border-button !bg-inherit" />
|
|
<label :for="`${filter.value_id}`" class="cursor-pointer">{{ filter.value }}</label> -->
|
|
|
|
<input :id="`${filter.value_id}`" v-model="selectedFilters"
|
|
:value="`${filter.parent}.${filter.value_id}`" type="checkbox" class="border-button !bg-inherit">
|
|
<label :for="`${filter.value_id}`" class="cursor-pointer flex items-center justify-between w-full">
|
|
<span>{{ filter.value }}</span>
|
|
<span>12</span>
|
|
</label>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-full space-y-10">
|
|
<!-- pop-up -->
|
|
<div v-if="isInfo"
|
|
class="w-full xl:w-[70%] mx-auto border-y border-block py-[15px] sm:p-[30px] flex gap-[55px] relative">
|
|
<UButton variant="ghost"
|
|
class="p-0 absolute right-0 top-2 sm:right-2 sm:top-2 cursor-pointer text-button font-light hover:bg-inherit hover:text-button-hover"
|
|
size="xl" icon="i-lucide-x" @click="closeElement()" />
|
|
|
|
<div class="flex flex-col sm:flex-row gap-[25px]">
|
|
<div class="flex flex-col justify-between gap-[25px]">
|
|
<h4 class="font-inter text-lg sm:text-[24px] leading-[150%] md:leading-[120%] font-bold">
|
|
{{ component.front_section_lang[0].data.title }}
|
|
</h4>
|
|
<p>{{ component.front_section_lang[0].data.description }}</p>
|
|
</div>
|
|
<img class="max-w-[150px] mx-auto" :src="`/api/public/file/${component.img[0]}_m.webp')`">
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="products.length < 1" class="grid gap-12 pt-32 pb-16 md:grid-cols-2 2xl:grid-cols-3">
|
|
<!-- <TheProductSkeleton v-for="index in 6" :key="index" /> -->
|
|
</div>
|
|
|
|
<!-- products -->
|
|
<div v-else ref="loadingElement" class="flex flex-wrap justify-center gap-5 sm:gap-10">
|
|
<Product v-for="product in products" :key="product.id" :product="product" />
|
|
</div>
|
|
|
|
<div v-if="reachedEnd" class="w-full flex justify-center">
|
|
<p>
|
|
{{ $t("FrontTranslations", "You reached end of the list.") }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UiContainer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import Product from './Product.vue'
|
|
import CategoryTree from './CategoryTree.vue'
|
|
import type {
|
|
Feature,
|
|
GenericResponse,
|
|
GenericResponseChildren,
|
|
GenericResponseItems,
|
|
ProductType,
|
|
} from '~/types'
|
|
|
|
const { $session } = useNuxtApp()
|
|
|
|
watch(
|
|
() => $session.cookieData,
|
|
async () => await getProducts(),
|
|
{ deep: true },
|
|
)
|
|
defineProps<{
|
|
component: {
|
|
id: number
|
|
name: string
|
|
img: string[]
|
|
component_name: string
|
|
is_no_lang: boolean
|
|
page_name: string
|
|
front_section_lang: {
|
|
data: {
|
|
title: string
|
|
description: string
|
|
}
|
|
id_front_section: number
|
|
id_lang: number
|
|
}[]
|
|
}
|
|
}>()
|
|
|
|
const openCategories = ref(false)
|
|
const isInfo = ref<boolean>(true)
|
|
const selectedFilters = ref<string[]>([])
|
|
const categoryId = ref<number>(1)
|
|
|
|
const loading = ref(false)
|
|
const reachedEnd = ref(false)
|
|
|
|
const loadingElement = ref<HTMLElement | null>(null)
|
|
|
|
const page = ref(1)
|
|
const elems = ref(12)
|
|
const maxElements = ref(0)
|
|
|
|
const products = ref([] as ProductType[])
|
|
async function getProducts() {
|
|
try {
|
|
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
|
`/api/public/products/category/${categoryId.value}?p=${page.value}&elems=${elems.value}`,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
onErrorOccured: (_, status) => {
|
|
throw new Error(`HTTP error: ${status}`)
|
|
},
|
|
},
|
|
)
|
|
|
|
products.value = data.items
|
|
maxElements.value = data.items_count + 1
|
|
}
|
|
catch (error) {
|
|
console.error('getProducts error:', error)
|
|
}
|
|
}
|
|
|
|
const filters = ref([] as Feature[])
|
|
async function getCategory() {
|
|
try {
|
|
const { data } = await useMyFetch<GenericResponse<object>>(
|
|
`/api/public/products/category/1/classification`,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
onErrorOccured: (_, status) => {
|
|
throw new Error(`HTTP error: ${status}`)
|
|
},
|
|
},
|
|
)
|
|
|
|
filters.value = data as Feature[]
|
|
filters.value.forEach((el: Feature) => {
|
|
const parentId = el.feature_id
|
|
el.feature_values.forEach((el) => {
|
|
el.parent = parentId
|
|
})
|
|
})
|
|
}
|
|
catch (error) {
|
|
console.error('getCategory error:', error)
|
|
}
|
|
}
|
|
|
|
const categoriesList = ref()
|
|
async function getCategoryTree() {
|
|
try {
|
|
const { data } = await useMyFetch<GenericResponseChildren<ProductType[]>>(
|
|
`/api/public/categories/tree`,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
onErrorOccured: (_, status) => {
|
|
throw new Error(`HTTP error: ${status}`)
|
|
},
|
|
},
|
|
)
|
|
|
|
categoriesList.value = data.children
|
|
}
|
|
catch (error) {
|
|
console.error('getCategory error:', error)
|
|
}
|
|
}
|
|
|
|
getProducts()
|
|
getCategory()
|
|
getCategoryTree()
|
|
|
|
const closeElement = () => {
|
|
isInfo.value = false
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('scroll', scrollEvent)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('scroll', scrollEvent)
|
|
})
|
|
|
|
async function scrollEvent() {
|
|
const maxScrollY
|
|
= window.scrollY
|
|
|| document.documentElement.scrollHeight
|
|
- document.documentElement.clientHeight
|
|
|
|
if (
|
|
window.scrollY >= maxScrollY - 500
|
|
&& !reachedEnd.value
|
|
&& !loading.value
|
|
) {
|
|
loading.value = true
|
|
await loadMoreProducts()
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
filters.value.forEach((item) => {
|
|
visibleFeatures[item.feature] = false
|
|
})
|
|
|
|
const visibleFeatures = reactive<Record<string, boolean>>({})
|
|
function toggleFeature(feature: string) {
|
|
if (feature in visibleFeatures) {
|
|
visibleFeatures[feature] = !visibleFeatures[feature]
|
|
}
|
|
else {
|
|
visibleFeatures[feature] = true
|
|
}
|
|
}
|
|
|
|
class FilteredQueryString extends URLSearchParams {
|
|
override append(name: string, value: string): void {
|
|
if (value == null) {
|
|
return
|
|
}
|
|
|
|
super.append(name, value)
|
|
}
|
|
}
|
|
|
|
async function loadMoreProducts() {
|
|
const qParams = new FilteredQueryString()
|
|
|
|
page.value = page.value + 1
|
|
|
|
qParams.append('p', `${page.value}`)
|
|
qParams.append('elems', `${elems.value}`)
|
|
qParams.append(
|
|
'features',
|
|
selectedFilters.value.length > 0 ? selectedFilters.value : null,
|
|
)
|
|
|
|
try {
|
|
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
|
`/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
onErrorOccured: (_, status) => {
|
|
throw new Error(`HTTP error: ${status}`)
|
|
},
|
|
},
|
|
)
|
|
|
|
maxElements.value = data.items_count
|
|
|
|
if (data.items) {
|
|
products.value.push(...(data.items as ProductType[]))
|
|
}
|
|
else {
|
|
reachedEnd.value = true
|
|
}
|
|
|
|
if (products.value.length >= maxElements.value) {
|
|
reachedEnd.value = true
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('getCategory error:', error)
|
|
}
|
|
}
|
|
|
|
interface CategoryItem {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
const changeCategory = (item: CategoryItem) => {
|
|
categoryId.value = item.id
|
|
}
|
|
|
|
watch(selectedFilters, async (newQuestion: string) => {
|
|
if (newQuestion) {
|
|
page.value = 1
|
|
reachedEnd.value = false
|
|
loadingElement.value?.scrollIntoView()
|
|
|
|
const qParams = new FilteredQueryString()
|
|
|
|
qParams.append('p', `${page.value}`)
|
|
qParams.append('elems', `${elems.value}`)
|
|
qParams.append(
|
|
'features',
|
|
selectedFilters.value.length > 0 ? selectedFilters.value : null,
|
|
)
|
|
|
|
try {
|
|
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
|
`/api/public/products/category/1?${qParams.toString()}`,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
onErrorOccured: (_, status) => {
|
|
throw new Error(`HTTP error: ${status}`)
|
|
},
|
|
},
|
|
)
|
|
|
|
products.value = data.items
|
|
maxElements.value = data.items_count
|
|
}
|
|
catch (error) {
|
|
console.error('selectedFilters error:', error)
|
|
}
|
|
}
|
|
})
|
|
|
|
watch(categoryId, async (newCategoryId) => {
|
|
if (newCategoryId) {
|
|
page.value = 1
|
|
reachedEnd.value = false
|
|
loadingElement.value?.scrollIntoView()
|
|
|
|
const qParams = new FilteredQueryString()
|
|
qParams.append('p', `${page.value}`)
|
|
qParams.append('elems', `${elems.value}`)
|
|
qParams.append(
|
|
'features',
|
|
selectedFilters.value.length > 0 ? selectedFilters.value : null,
|
|
)
|
|
|
|
try {
|
|
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
|
`api/public/products/category/${newCategoryId}?${qParams.toString()}`,
|
|
{
|
|
headers: { 'Content-Type': 'application/json' },
|
|
onErrorOccured: (_, status) => {
|
|
throw new Error(`HTTP error: ${status}`)
|
|
},
|
|
},
|
|
)
|
|
|
|
products.value = data.items
|
|
maxElements.value = data.items_count
|
|
}
|
|
catch (error) {
|
|
console.error('getCategory error:', error)
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.v-enter-active,
|
|
.v-leave-active {
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
|
|
.v-enter-from,
|
|
.v-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|