Files
your-gold/components/section/ShopMain.vue
2025-06-27 16:02:00 +02:00

558 lines
16 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 type {
Feature,
GenericResponse,
GenericResponseChildren,
GenericResponseItems,
ProductType,
} from "~/types";
import CategoryTree from "./CategoryTree.vue";
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<any>([]);
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);
}
}
const changeCategory = (item: any) => {
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>