377 lines
14 KiB
Vue
377 lines
14 KiB
Vue
<template>
|
||
<UiContainer>
|
||
<div class="flex justify-between mb-[75px] whitespace-nowrap gap-[100px]">
|
||
<p>CZK cena (na EUR). Kč 25,2380 Kč +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). zł 4,2550</p>
|
||
</div>
|
||
<div class="flex flex-col gap-16 px-4 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> -->
|
||
|
||
<div class="min-w-[275px]">
|
||
<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">
|
||
<ThePartnerInfo v-if="isInfo" @close-element="closeElement()" />
|
||
|
||
<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>
|
||
|
||
<div v-else ref="loadingElement" class="grid gap-12 pt-32 pb-16 md:grid-cols-2 2xl:grid-cols-4">
|
||
<Product v-for="product in products" :key="product.id" :product="product" />
|
||
<div v-if="reachedEnd"
|
||
class="md:col-span-2 2xl:col-span-3 border-2 border-yellow border-r-0 border-l-0 mt-10">
|
||
<p class="text-black dark:text-gray text-center text-lg p-2">
|
||
{{ $t("FrontTranslations", "You reached end of the list.") }}
|
||
</p>
|
||
</div>
|
||
</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";
|
||
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;
|
||
}
|
||
}
|
||
|
||
const visibleFeatures = reactive<any>({});
|
||
|
||
filters.value.forEach((item) => {
|
||
visibleFeatures[item.feature] = false;
|
||
});
|
||
|
||
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> |