This commit is contained in:
Mamalizz
2025-05-12 15:28:12 +03:30
34 changed files with 350 additions and 281 deletions
+25 -18
View File
@@ -10,6 +10,7 @@ type Props = {
description: string;
picture: string;
darkLayer?: boolean;
isActive: boolean;
};
// props
@@ -23,39 +24,45 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
</script>
<template>
<NuxtLink :to="`/products?category=${id}`">
<NuxtLink :to="`/products/category/${id}`">
<div class="group relative rounded-150 overflow-hidden w-full aspect-square bg-white brightness-[97%]">
<NuxtImg
:id="`category-image-${id}`"
class="group-hover:scale-105 transition-transform duration-200 absolute object-contain size-full"
:src="picture"
alt=""
/>
<Transition name="fade">
<video
v-if="isActive"
src="/video/category.mp4"
autoplay
muted
loop
playsinline
webkit-playsinline
class="group-hover:scale-105 transition-transform duration-200 absolute object-contain size-full"
/>
<NuxtImg
v-else
:id="`category-image-${id}`"
class="group-hover:scale-105 transition-transform duration-200 absolute object-contain size-full"
:src="picture"
alt=""
/>
</Transition>
<div
v-if="darkLayer"
class="bg-linear-to-t from-black/50 to-transparent to-40% absolute z-10 size-full"
/>
<div
class="absolute z-20 bottom-0 p-4 md:p-6 flex items-center justify-between w-full"
>
<div class="absolute z-20 bottom-0 p-4 md:p-6 flex items-center justify-between w-full">
<div
:class="colorObject?.isLight && !darkLayer ? 'text-black': 'text-white'"
:class="colorObject?.isLight && !darkLayer ? 'text-black' : 'text-white'"
class="typo-sub-h-sm md:typo-sub-h-md"
>
{{ category }}
</div>
<Icon
name="ci:arrow-left"
class="size-5 md:size-6"
:class="
colorObject?.isLight && !darkLayer
? '**:stroke-black'
: '**:stroke-white'
"
:class="colorObject?.isLight && !darkLayer ? '**:stroke-black' : '**:stroke-white'"
/>
</div>
</div>
+14 -14
View File
@@ -27,12 +27,12 @@ const progressStyle = computed(() => {
// methods
const onAssetLoaded = () => {
clearInterval(progressInterval.value!);
criticalLoad.value = false;
assetLoadingProgress.value = 100;
isAssetLoaded.value = true;
};
// const onAssetLoaded = () => {
// clearInterval(progressInterval.value!);
// criticalLoad.value = false;
// assetLoadingProgress.value = 100;
// isAssetLoaded.value = true;
// };
const onAssetFinished = () => {
gsap.to("#loading-overlay", {
@@ -59,15 +59,15 @@ onMounted(() => {
if (!isSiteLoadingDisabled.value) {
isWindowScrollLocked.value = true;
const heymlzLoadingAnimation = document.querySelector("#heymlz-loading-animation") as HTMLVideoElement;
// const heymlzLoadingAnimation = document.querySelector("#heymlz-loading-animation") as HTMLVideoElement;
if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
onAssetLoaded();
}
// if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
// onAssetLoaded();
// }
progressInterval.value = setInterval(() => {
assetLoadingProgress.value += Math.random() * 10;
}, 250);
assetLoadingProgress.value += Math.random() * 50;
}, 150);
gsap.to("#loading-overlay", {
opacity: 1,
@@ -100,7 +100,7 @@ onMounted(() => {
</div>
</div>
<video
<!-- <video
id="heymlz-loading-animation"
muted
autoplay
@@ -114,6 +114,6 @@ onMounted(() => {
:style="{
mask: 'linear-gradient(to bottom, rgba(0,0,0,0) 0%, black 20%, black 80%, rgba(0,0,0,0) 100%)',
}"
/>
/> -->
</div>
</template>
+1 -1
View File
@@ -34,7 +34,7 @@ const page = computed({
<PaginationRoot
:total="total"
:sibling-count="1"
:items-per-page="9"
:items-per-page="15"
show-edges
v-model:page="page"
>
+1 -1
View File
@@ -35,7 +35,7 @@ withDefaults(defineProps<Props>(), {
</NuxtLink>
</div>
<ul
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8"
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-y-8 gap-5 sm:gap-8"
>
<ProductCard
v-for="product in products"
@@ -49,7 +49,7 @@ const changeSlide = (id: number) => {
<template>
<div class="sticky top-10">
<div class="flex flex-col relative gap-6">
<div class="flex flex-col relative gap-4">
<div
class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-[12px] md:rounded-200"
>
@@ -74,7 +74,6 @@ const changeSlide = (id: number) => {
class="w-full"
>
<SwiperSlide
class="py-4"
v-for="slide in slides"
:key="slide.id"
>
@@ -94,7 +93,6 @@ const changeSlide = (id: number) => {
v-if="emptySlidesCount > 0"
v-for="slide in emptySlidesCount"
:key="slide"
class="py-4"
>
<div
class="brightness-[97%] flex-center bg-white aspect-square rounded-[12px] md:rounded-200 w-full"
@@ -38,9 +38,9 @@ const limitedColors = computed(() => {
<template>
<li class="w-full">
<NuxtLink :to="'/product/' + id">
<div class="@container">
<div class="@container group">
<div
class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[98%] overflow-hidden p-6"
class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[95%] overflow-hidden p-6"
>
<NuxtImg
:id="`product-image-${id}`"
@@ -49,10 +49,10 @@ const limitedColors = computed(() => {
alt="product-background"
/>
<div
<!-- <div
v-if="darkLayer"
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-55% absolute z-10 size-full"
/>
/> -->
<div
class="flex justify-between items-center absolute px-4 @[280px]:px-6 pt-4 @[280px]:pt-6 top-0 w-full inset-x-0"
@@ -66,38 +66,27 @@ const limitedColors = computed(() => {
</Tag>
</div>
<div
:class="colorObject?.isLight && !darkLayer ? 'text-black' : 'text-white'"
class="absolute inset-x-0 bottom-0 pb-4 @[280px]:pb-6 px-4 @[280px]:px-6 flex flex-row-reverse justify-between items-end z-10"
class="absolute opacity-0 group-hover:opacity-100 bg-gradient-to-t transition-all group-hover:from-black/30 to-transparent inset-x-0 bottom-0 pb-4 @[280px]:pb-6 px-4 @[280px]:px-6 flex flex-row-reverse justify-between items-end z-10"
>
<div class="flex flex-col gap-2 items-start w-full">
<span class="@max-[280px]:hidden typo-sub-h-md @[280px]:typo-sub-h-lg truncate w-full">
{{ title }}
</span>
<div class="flex items-center justify-between w-full mt-1">
<div class="flex items-center gap-2 @[280px]:mt-1">
<ColorCircle
v-for="color in limitedColors"
:key="color"
:style="{ backgroundColor: color }"
class="!size-5 @[280px]:!size-6"
/>
</div>
<span
class="@max-[280px]:hidden typo-p-xs @[280px]:typo-p-md !font-semibold whitespace-nowrap"
>
{{ price }}
</span>
</div>
<div
class="items-center flex gap-2 @[280px]:mt-1 transition-all translate-y-1 group-hover:translate-y-0 "
>
<ColorCircle
v-for="color in limitedColors"
:key="color"
:style="{ backgroundColor: color }"
class="!size-5 @[280px]:!size-6"
/>
</div>
</div>
</div>
<div class="flex flex-col gap-1 px-2 items-start w-full text-black mt-4 @[280px]:hidden">
<span class="typo-sub-h-sm w-full truncate">
<div class="flex flex-col gap-1 px-2 items-start w-full text-black mt-4">
<span class="typo-sub-h-sm font-normal w-full truncate">
{{ title }}
</span>
<div class="@[280px]:hidden flex items-center justify-between w-full mt-1">
<span class="typo-p-xs !font-semibold whitespace-nowrap">
<div class="flex items-center justify-between w-full mt-1">
<span class="typo-p-xs !font-bold whitespace-nowrap">
{{ price }}
</span>
</div>
@@ -2,34 +2,39 @@
// imports
import useGetCategories from "~/composables/api/product/useGetCategories";
import useGetProducts, {
type GetProductsFilters,
} from "~/composables/api/products/useGetProducts";
import useGetProducts, { type GetProductsFilters } from "~/composables/api/products/useGetProducts";
import { PRODUCT_RANGE } from "~/constants";
// state
const route = useRoute();
const router = useRouter();
const params = inject("params") as GetProductsFilters;
const currentCategory = computed({
get: () => {
return Array.isArray(route.params.slug) ? route.params.slug[1] ?? undefined : undefined;
},
set: (newValue) => {
router.push({ path: `/products/category/${newValue}`, query: { ...route.query } });
},
});
const sort_filter = ref([
{ title: "جدیدترین ها", value: "newest" },
{ title: "گران ترین ها", value: "price" },
{ title: "ارزان ترین ها", value: "-price" },
]);
const sliderValue = ref([
params.price_gte ?? PRODUCT_RANGE.min,
params.price_lte ?? PRODUCT_RANGE.max,
]);
const sliderValue = ref([params.price_gte ?? PRODUCT_RANGE.min, params.price_lte ?? PRODUCT_RANGE.max]);
const has_discount = ref(Boolean(params.has_discount) ?? false);
const in_stock = ref(Boolean(params.in_stock) ?? false);
const sliderValueDebounced = refDebounced(sliderValue, 1000);
const filtersSuccessMessage = ref<{ title: string; status: string } | null>(
null
);
const filtersSuccessMessage = ref<{ title: string; status: string } | null>(null);
// queries
@@ -41,7 +46,7 @@ const filters = computed(() => {
price_lte: params.price_lte ?? PRODUCT_RANGE.max,
in_stock: params.in_stock ?? false,
has_discount: params.has_discount ?? false,
category: params.category ?? undefined,
category: currentCategory.value,
page: params.page ?? 1,
};
});
@@ -50,8 +55,7 @@ const { data: categories, suspense } = useGetCategories();
await suspense();
const { isPending: productsIsPending, status: productsStatus } =
useGetProducts(filters);
const { isPending: productsIsPending, status: productsStatus } = useGetProducts(filters);
// computed
@@ -77,10 +81,9 @@ const resetFilters = () => {
sliderValue.value = [PRODUCT_RANGE.min, PRODUCT_RANGE.max];
has_discount.value = false;
in_stock.value = false;
params.category = undefined;
};
// watch
router.push({ path: `/products/`, query: { ...route.query } });
};
watch(
() => sliderValueDebounced.value,
@@ -123,10 +126,11 @@ watch(
<div class="size-full flex flex-col gap-14 justify-between">
<div class="w-full flex flex-col gap-10">
<div class="flex flex-col items-center w-full gap-5">
<div
class="flex items-center justify-start gap-2 text-lg w-full"
>
<Icon name="ci:filter-list" size="24" />
<div class="flex items-center justify-start gap-2 text-lg w-full">
<Icon
name="ci:filter-list"
size="24"
/>
ترتیب بر اساس
</div>
<div class="w-full flex items-center gap-2">
@@ -134,11 +138,7 @@ watch(
v-for="(sort, index) in sort_filter"
:key="index"
@click="params.sort = sort.value"
:class="
params.sort == sort.value
? 'bg-black text-white'
: 'bg-slate-100'
"
:class="params.sort == sort.value ? 'bg-black text-white' : 'bg-slate-100'"
class="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-md text-sm"
>
{{ sort.title }}
@@ -147,20 +147,25 @@ watch(
</div>
<div class="flex flex-col w-full gap-5">
<div
class="flex items-center justify-start gap-2 text-lg w-full"
>
<Icon name="ci:grid" size="24" />
<div class="flex items-center justify-start gap-2 text-lg w-full">
<Icon
name="ci:grid"
size="24"
/>
دسته بندی
</div>
<ComboBox :options="allCategories" v-model="params.category" />
<ComboBox
:options="allCategories"
v-model="currentCategory"
/>
</div>
<div class="flex flex-col w-full gap-5">
<div
class="flex items-center justify-start gap-2 text-lg w-full"
>
<Icon name="ci:scan-box" size="24" />
<div class="flex items-center justify-start gap-2 text-lg w-full">
<Icon
name="ci:scan-box"
size="24"
/>
محدوده قیمت
</div>
<SliderRoot
@@ -171,12 +176,8 @@ watch(
:max="PRODUCT_RANGE.max"
:step="1000"
>
<SliderTrack
class="bg-black/10 relative grow rounded-full h-[3px]"
>
<SliderRange
class="absolute bg-black rounded-full h-full"
/>
<SliderTrack class="bg-black/10 relative grow rounded-full h-[3px]">
<SliderRange class="absolute bg-black rounded-full h-full" />
</SliderTrack>
<SliderThumb
v-for="thumb in Object.keys(PRODUCT_RANGE)"
@@ -227,15 +228,9 @@ watch(
: ' text-danger-600 bg-danger-100 border-danger-600'
"
>
<span class="text-sm">{{
filtersSuccessMessage.title
}}</span>
<span class="text-sm">{{ filtersSuccessMessage.title }}</span>
<Icon
:name="
filtersSuccessMessage.status == 'success'
? 'bi:check'
: 'bi:x'
"
:name="filtersSuccessMessage.status == 'success' ? 'bi:check' : 'bi:x'"
size="20"
/>
</div>
@@ -246,14 +241,29 @@ watch(
@click="resetFilters"
class="w-full rounded-full py-4 !cursor-pointer disabled:pointer-events-none z-[3]"
>
<Transition name="fade" mode="out-in">
<span v-if="productsIsPending" class="flex-center gap-3">
<Transition
name="fade"
mode="out-in"
>
<span
v-if="productsIsPending"
class="flex-center gap-3"
>
در حال دریافت اطلاعات
<Icon name="svg-spinners:3-dots-bounce" size="20" />
<Icon
name="svg-spinners:3-dots-bounce"
size="20"
/>
</span>
<span v-else class="flex-center gap-3">
<span
v-else
class="flex-center gap-3"
>
بازنشانی به پیش فرض
<Icon name="ci:close" size="20" />
<Icon
name="ci:close"
size="20"
/>
</span>
</Transition>
</Button>
+32 -5
View File
@@ -4,17 +4,27 @@
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
import useHomeData from "~/composables/api/home/useHomeData";
import { EffectCoverflow } from "swiper/modules";
// state
const { data: homeData } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null);
const activeIndex = ref(0);
const slideElement = ref<HTMLDivElement | null>(null);
const { width: slideWidth } = useElementSize(slideElement);
// methods
const onSwiper = (swiper: SwiperClass) => {
swiper_instance.value = swiper;
};
const onSlideChange = (swiper: SwiperClass) => {
activeIndex.value = swiper.realIndex;
};
</script>
<template>
@@ -23,7 +33,7 @@ const onSwiper = (swiper: SwiperClass) => {
class="flex flex-col justify-center gap-4 bg-black sm:min-h-[110svh] relative overflow-hidden shrink-0 py-24 lg:py-32"
>
<div class="w-full relative flex-center z-10 container">
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4 2xl:typo-h-2"> دسته بندی ها </span>
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4 min-[2000px]:typo-h-2"> دسته بندی ها </span>
</div>
<div class="w-full mt-44 lg:mt-64 relative">
@@ -38,8 +48,17 @@ const onSwiper = (swiper: SwiperClass) => {
:loop="true"
:centered-slides="true"
:slides-per-view="1.5"
:space-between="20"
@swiper="onSwiper"
@slideChange="onSlideChange"
:modules="[EffectCoverflow]"
:effect="'coverflow'"
:coverflowEffect="{
rotate: 10,
stretch: -100,
depth: 200,
modifier: 1,
slideShadows: true,
}"
:breakpoints="{
640: {
centeredSlides: true,
@@ -52,7 +71,8 @@ const onSwiper = (swiper: SwiperClass) => {
}"
>
<SwiperSlide
v-for="slide in homeData!.sub_categories"
ref="slideElement"
v-for="(slide, index) in homeData!.sub_categories"
:key="slide.id"
>
<CategoryCard
@@ -61,6 +81,7 @@ const onSwiper = (swiper: SwiperClass) => {
:category="slide.name"
:picture="slide.image"
:count="slide.product_count"
:isActive="activeIndex === index"
description="توضیحات دسته بندی"
/>
</SwiperSlide>
@@ -69,7 +90,10 @@ const onSwiper = (swiper: SwiperClass) => {
<div
v-if="!swiper_instance?.isBeginning"
@click="swiper_instance?.slidePrev()"
class="max-xs:hidden absolute z-20 right-10 xs:right-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] translate-y-1/2 bg-white rounded-full size-10 xs:size-11.5 flex justify-center items-center"
:style="{
right: `calc(50% - ${slideWidth / 2}px - 20px)`,
}"
class="max-xs:hidden absolute z-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] translate-y-1/2 bg-white rounded-full size-10 xs:size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-right"
@@ -80,7 +104,10 @@ const onSwiper = (swiper: SwiperClass) => {
<div
v-if="!swiper_instance?.isEnd"
@click="swiper_instance?.slideNext()"
class="max-xs:hidden absolute z-20 left-10 xs:left-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] translate-y-1/2 bg-white rounded-full size-10 xs:size-11.5 flex justify-center items-center"
:style="{
left: `calc(50% - ${slideWidth / 2}px - 20px)`,
}"
class="max-xs:hidden absolute z-20 shadow-lg cursor-pointer shadow-black/25 bottom-[50%] translate-y-1/2 bg-white rounded-full size-10 xs:size-11.5 flex justify-center items-center"
>
<Icon
name="ci:arrow-left"