This commit is contained in:
2025-06-16 15:53:57 +02:00
committed by Marek Goc
parent c7479b1aa6
commit 7b0d9b9c7b
12 changed files with 1993 additions and 1544 deletions

View File

@ -0,0 +1,44 @@
<template>
<div v-if="hasMainCategory" :class="['flex flex-col gap-[25px]', isOpen && 'border-b border-block pb-[10px]']">
<div @click="toggle" class="flex items-center justify-between rounded-lg cursor-pointer">
<div class="flex items-center justify-between w-full">
<p class="text-2xl font-extrabold text-black dark:text-white">
{{ mainCategoryName }}
</p>
<span :class="[isOpen && 'rotate-180', 'transition-all']"> <i
class="uil uil-angle-down text-3xl text-button font-light cursor-pointer"></i></span>
</div>
</div>
<ul class="flex flex-col gap-[25px] text-black dark:text-gray" v-show="isOpen">
<li @click="$emit('change-category', child)" v-for="child in subcategories" :key="child.id" class="cursor-pointer"
:class="child.id === props.active ? 'text-yellow' : ''">
{{ child.langs[0].Name }}
</li>
</ul>
</div>
<div v-else>
<!-- Optional: Placeholder or message when main category is not available -->
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
// Define the props
const props = defineProps({
data: Array,
active: Number
});
const isOpen = ref(false);
const toggle = () => {
isOpen.value = !isOpen.value;
};
// Computed properties
const hasMainCategory = computed(() => props.data.length > 0 && props.data[0].langs && props.data[0].langs.length > 0);
const mainCategoryName = computed(() => hasMainCategory.value ? props.data[0].langs[0].Name : '');
const subcategories = computed(() => hasMainCategory.value && props.data[0].children ? props.data[0].children : []);
</script>

View File

@ -0,0 +1,53 @@
<template>
<NuxtLink>
<li class="bg-block rounded-2xl min-h-[420px] w-[330px] list-none overflow-hidden p-5">
<article class="group h-full">
<div class="h-full flex flex-col">
<div class="h-[205px]">
<img :src="`https://www.yourgold.cz/api/public/file/${props.product?.cover_picture_uuid}.webp`"
:alt="props.product?.description" class="h-full object-contain w-auto block mx-auto">
</div>
<div class="flex flex-col items-start text-start mt-[50px] justify-between h-full">
<div class="space-y-[15px]">
<h4 class="font-bold">{{ props.product?.name }}</h4>
<p class="text-sm font-normal">{{ props.product?.tax_name }}</p>
</div>
<div class="flex items-center justify-between w-full">
<p class="font-bold text-accent-green-light text-sm sm:text-2xl">{{ props.product?.formatted_price }}</p>
<div class="w-12 h-12 bg-button flex items-center justify-center rounded-[10px]">
<i class="uil uil-shopping-cart text-[31px] cursor-pointer text-white"></i>
</div>
</div>
</div>
</div>
<!-- <div v-if="!props.product?.is_sale_active"
class="absolute top-0 flex items-center justify-center w-full h-full px-4 bg-black bg-opacity-90 cursor-progress">
<div class="flex flex-col items-center gap-4">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#ffcc00" d="M5.7 22H42.5V26H5.7z" transform="rotate(-45.001 24.036 24.037)"></path>
<path fill="#ffcc00"
d="M24,4C13,4,4,13,4,24s9,20,20,20s20-9,20-20S35,4,24,4z M24,40c-8.8,0-16-7.2-16-16S15.2,8,24,8 s16,7.2,16,16S32.8,40,24,40z">
</path>
</svg>
<span class="font-bold text-center uppercase text-yellow">
{{ $t("FrontTranslations", "not available") }}
</span>
</div>
</div> -->
</article>
</li>
</NuxtLink>
</template>
<script setup lang="ts">
// import imgUrl from "~/utils/imgUrl";
const props = defineProps({
product: Object,
});
// const addToCartAndPreventNavigation = (event: any) => {
// event.preventDefault();
// useCartStore().addToCart(props.product);
// };
</script>

View File

@ -0,0 +1,377 @@
<template>
<UiContainer>
<div class="flex justify-between 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-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(
`/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(
`/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(
`/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(
`/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(
`/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(
`/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>