Files
your-gold/components/section/shop-page/ShopMain.vue
2025-06-17 15:57:00 +02:00

420 lines
16 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<UiContainer>
<div class="flex justify-between mb-[25px] sm:mb-[55px] xl:mb-[75px] whitespace-nowrap gap-[100px]">
<p>CZK cena (na EUR). 25,2380 +0,0030 (+0.01%)</p>
<p>Cena zlata na trhu. 2 852,1450 -21,6520 (-0.75%)</p>
<p>Cena stříbra na trhu. 28,6500 -0,1570 (-0.54%)</p>
<p>PLN cena (na EUR). 4,2550</p>
</div>
<div class="flex flex-col gap-10 xl:flex-row">
<Transition>
<div v-if="openCategories" class="z-40 block w-full pt-8 xl:w-1/4 xl:pt-36 xl:hidden">
<BaseTitle>
{{ $t("FrontTranslations", "Categories") }}
</BaseTitle>
<div class="mt-14">
<div class="flex flex-col gap-12">
<div>
<CategoryTree :data="categoriesList" @change-category="changeCategory($event)"
:active="categoryId" />
</div>
<div>
<p class="mb-8 text-2xl font-extrabold text-black dark:text-white">
{{ $t("FrontTranslations", "Filtered by") }}
</p>
<div v-for="(item, itemIndex) in filters" :key="itemIndex" class="mb-8 text-white">
<span
class="flex justify-between font-bold text-black cursor-pointer 2xl:pr-24 dark:text-gray"
@click="toggleFeature(item.feature)">
{{ item.feature }}
<span class="w-4 h-4 text-yellow"><i
class="uil uil-angle-down text-2xl font-light cursor-pointer"></i></span>
</span>
<ul v-show="visibleFeatures[item.feature]"
class="flex flex-col gap-8 pl-8 mt-8 text-black dark:text-gray">
<li v-for="filter in item.feature_values" :key="filter.value_id"
class="flex gap-1">
<input :id="`${filter.value_id}`"
:value="`${filter.parent}.${filter.value_id}`" v-model="selectedFilters"
type="checkbox" />
<label :for="`${filter.value_id}`">{{ filter.value }}</label>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</Transition>
<!-- button to open categories -->
<div class="xl:hidden flex items-center w-full">
<button @click="openCategories = !openCategories"
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">
Otevřené kategorie a filtry
</button>
</div>
<!-- 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.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>
<div class="w-4 h-4 bg-gray-200 rounded-full"></div>
</div>
</div>
<CategoryTree :data="categoriesList" @change-category="changeCategory($event)"
:active="categoryId" />
</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] text-white', visibleFeatures[item.feature] && 'border-b border-block pb-[10px]']">
<span
class="flex justify-between items-center font-bold text-black cursor-pointer dark:text-gray mb-[25px]"
@click="toggleFeature(item.feature)">
{{ item.feature }}
<span :class="[visibleFeatures[item.feature] && 'rotate-180', 'transition-all']"> <i
class="uil uil-angle-down text-3xl text-button font-light cursor-pointer"></i></span>
</span>
<ul v-show="visibleFeatures[item.feature]"
class="flex flex-col gap-5 text-black dark:text-gray">
<li v-for="filter in item.feature_values" :key="filter.value_id"
class="flex gap-[10px] cursor-pointer">
<input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`"
v-model="selectedFilters" type="checkbox" />
<label :for="`${filter.value_id}`" class="cursor-pointer">{{ filter.value }}</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 @click="closeElement()" size="xl" icon="i-lucide-x" 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" />
<div class="flex flex-col 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.section_lang_data.title }}
</h4>
<p>{{ component.section_lang_data.description }}</p>
</div>
<img class="max-w-[150px] mx-auto"
:src="`/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=640x0')`"
alt="" />
</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"></TheProductSkeleton>
</div>
<!-- products -->
<div v-else ref="loadingElement"
class="xl:grid gap-5 sm:gap-10 grid grid-cols-2 sm:flex sm:flex-wrap justify-center xl:grid-cols-4">
<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, ProductType } from "~/types";
import CategoryTree from "./CategoryTree.vue";
defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
description: string
};
};
const openCategories = ref(false);
const isInfo = ref<boolean>(true);
const selectedFilters = ref<any>([]);
const categoryId = ref<number>(1);
const itemsCount = ref(0);
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 res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/${categoryId.value}?p=${page.value}&elems=${elems.value}`,
{
headers: {
"Content-Type": "application/json",
},
}
);
const data = await res.json();
products.value = data.data.items;
maxElements.value = data.data.items_count + 1;
} catch (error) {
console.error("getProducts error:", error);
}
}
const filters = ref([] as Feature[]);
async function getCategory() {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/1/classification`,
{
headers: {
"Content-Type": "application/json",
},
}
);
const data = await res.json();
filters.value = data.data as Feature[];
filters.value.forEach((el) => {
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 res = await fetch(
`http://127.0.0.1:4000/api/public/categories/tree`,
{
headers: {
"Content-Type": "application/json",
},
}
);
const data = await res.json();
categoriesList.value = data.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(e: Event) {
let 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<any>({});
function toggleFeature(feature: any) {
if (visibleFeatures.hasOwnProperty(feature)) {
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() {
let 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 res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
},
}
);
const data = await res.json();
maxElements.value = data.data.items_count;
if (data.data.items) {
products.value.push(...(data.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) => {
if (newQuestion) {
page.value = 1;
reachedEnd.value = false;
loadingElement.value?.scrollIntoView();
let 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 res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/1?${qParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
},
}
);
const data = await res.json();
products.value = data.data.items;
maxElements.value = data.data.items_count;
} catch (error) {
console.error("selectedFilters error:", error);
}
}
});
watch(categoryId, async (newQuestion) => {
if (newQuestion) {
page.value = 1;
reachedEnd.value = false;
loadingElement.value?.scrollIntoView();
let 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 res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
},
}
);
const data = await res.json();
products.value = data.data.items;
maxElements.value = data.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>