This commit is contained in:
Parsa Nazer
2025-06-22 16:05:05 +03:30
35 changed files with 606 additions and 243 deletions
+2 -2
View File
@@ -273,9 +273,9 @@
/* CONTAINER */
* {
/* * {
scroll-behavior: smooth !important;
}
} */
@utility container {
@apply mx-auto px-[var(--app-container-padding)] w-full max-sm:max-w-[var(--breakpoint-xs)] max-md:max-w-[var(--breakpoint-sm)] max-lg:max-w-[var(--breakpoint-md)] max-xl:max-w-[var(--breakpoint-lg)] max-w-[var(--breakpoint-2xl)];
+5 -5
View File
@@ -29,20 +29,20 @@ const brands = ref([
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
</p>
</div>
<div class="-rotate-z-2 z-20 w-[110%]">
<div class="-rotate-z-2 z-20 w-[110%] shadow-2xl shadow-black/7">
<Marquee
class="bg-black h-full"
class="bg-blue-500 h-full"
:clone="true"
dir="ltr"
:duration="3"
>
<div class="flex items-center gap-12 sm:gap-20 px-6 sm:px-10 h-[90px] sm:h-[140px]">
<div class="text-[30px] lg:text-[40px] mt-2 text-white whitespace-nowrap font-semibold opacity-85">
<div class="text-[30px] text-white lg:text-[40px] mt-2 whitespace-nowrap font-semibold opacity-85">
HEYMLZ
</div>
<NuxtImg
src="/img/heymlz/heymlz-logo.png"
class="h-[25px] sm:h-[45px] invert opacity-85"
class="h-[25px] sm:h-[45px] invert"
/>
</div>
</Marquee>
@@ -63,7 +63,7 @@ const brands = ref([
>
<NuxtImg
:src="brand"
class="h-[25px] sm:h-[45px]"
class="h-[25px] sm:h-[45px] opacity-25"
/>
</div>
</Marquee>
+5 -2
View File
@@ -33,7 +33,7 @@ const emit = defineEmits(["update:modelValue"]);
// state
const value = ref<OptionChildren>();
const value = ref<OptionChildren | Option>();
// watch
@@ -49,7 +49,10 @@ watch(
watch(
() => modelValue.value,
(newValue) => {
const target = options.value.flatMap((option) => option.children).find((child) => child.id == newValue);
let target;
target = newValue?.toString().startsWith("category")
? options.value.find((child) => child.slug == newValue)
: options.value.flatMap((option) => option.children).find((child) => child.slug == newValue);
value.value = target || undefined;
},
+10 -8
View File
@@ -35,14 +35,16 @@ const progressStyle = computed(() => {
// };
const onAssetFinished = () => {
gsap.to("#loading-overlay", {
opacity: 0,
onComplete: () => {
shouldRenderLoadingOverlay.value = false;
isWindowScrollLocked.value = false;
isSiteLoadingDisabled.value = true;
},
});
if (!isSiteLoadingDisabled.value) {
gsap.to("#loading-overlay", {
opacity: 0,
onComplete: () => {
shouldRenderLoadingOverlay.value = false;
isWindowScrollLocked.value = false;
isSiteLoadingDisabled.value = true;
},
});
}
};
// watch
+28 -11
View File
@@ -11,6 +11,7 @@ type Props = {
type: string;
value: number;
}[];
perPage: number;
};
// props
@@ -21,6 +22,9 @@ defineProps<Props>();
const params: any = inject("params");
const router = useRouter();
const route = useRoute();
const { isMobile } = useRatio();
const { y } = useWindowScroll({ behavior: "smooth" });
@@ -28,9 +32,12 @@ const { y } = useWindowScroll({ behavior: "smooth" });
// computed
const page = computed({
get: () => (params?.page ? Number(params.page) : 1),
get: () => (route.query["page"] ? Number(route.query["page"]) : 1),
set: (value: number) => {
params.page = value;
router.push({
query: { ...route.query, page: value },
});
y.value = 0;
},
});
@@ -40,24 +47,30 @@ const page = computed({
<PaginationRoot
:total="total"
:sibling-count="isMobile ? 0 : 1"
:items-per-page="15"
:items-per-page="perPage"
show-edges
v-model:page="page"
class="max-w-full"
>
<PaginationList
v-slot="{ items }"
class="flex items-center gap-2"
>
<PaginationFirst
class="px-2 h-9 font-light flex items-center whitespace-nowrap justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
برو اول
<Icon
name="bi:chevron-double-right"
class="**:fill-back"
size="18px"
/>
</PaginationFirst>
<PaginationPrev
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-slate-100 transition mr-4 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
<Icon
name="ci:chevron-right"
name="bi:chevron-right"
class="**:fill-back"
size="18px"
/>
@@ -67,7 +80,7 @@ const page = computed({
<PaginationListItem
v-if="page.type === 'page'"
:key="index"
class="w-9 h-9 cursor-pointer bg-slate-100 rounded-lg data-[selected]:!bg-black data-[selected]:text-white data-[selected]:shadow-sm hover:bg-slate-200 transition"
class="w-9 h-9 shrink-0 cursor-pointer bg-slate-100 rounded-lg data-[selected]:!bg-black data-[selected]:text-white data-[selected]:shadow-sm hover:bg-slate-200 transition"
:value="page.value"
>
{{ page.value }}
@@ -83,19 +96,23 @@ const page = computed({
</template>
<PaginationNext
class="w-9 h-9 ml-4 flex items-center justify-center cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="w-9 h-9 flex items-center justify-center cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
<Icon
name="ci:chevron-left"
name="bi:chevron-left"
class="**:fill-back"
size="18px"
/>
</PaginationNext>
<PaginationLast
class="px-2 h-9 font-light whitespace-nowrap flex items-center justify-center bg-transparent cursor-pointer transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
class="w-9 h-9 flex items-center justify-center bg-transparent cursor-pointer hover:bg-slate-100 transition disabled:opacity-50 disabled:cursor-not-allowed rounded-lg"
>
برو آخر
<Icon
name="bi:chevron-double-left"
class="**:fill-back"
size="18px"
/>
</PaginationLast>
</PaginationList>
</PaginationRoot>
+14 -10
View File
@@ -34,18 +34,22 @@ withDefaults(defineProps<Props>(), {
</NuxtLink>
</div>
<ul 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
<li
class="w-full"
v-for="product in products"
:key="product.id"
:id="product.id"
:slug="product.slug"
:title="product.name"
:picture="product.variants[0].images[0].image"
:colors="product.colors"
:price="product.variants[0].price"
:rate="product.rating"
:dark-layer="true"
/>
>
<ProductCard
:id="product.id"
:slug="product.slug"
:title="product.name"
:picture="product.variants[0].images[0].image"
:colors="product.colors"
:price="product.variants[0].price"
:rate="product.rating"
:dark-layer="true"
/>
</li>
</ul>
</section>
</template>
@@ -57,12 +57,17 @@ const changeSlide = (id: number) => {
name="zoom"
mode="out-in"
>
<NuxtImg
:key="selectedSlideDetail.id"
class="size-full absolute object-contain"
:src="selectedSlideDetail.image"
:alt="selectedSlideDetail.name"
/>
<div :key="selectedSlideDetail.id">
<vue-image-zoomer
class="size-full absolute object-contain"
:show-message="false"
:show-message-touch="false"
click-message="برای مشاهده کلیک کنید"
hover-message="برای مشاهده موس را وارد عکس کنید"
:regular="selectedSlideDetail.image"
:alt="selectedSlideDetail.name"
/>
</div>
</Transition>
</div>
@@ -4,7 +4,14 @@
import Tag from "~/components/global/Tag.vue";
import Rate from "~/components/global/Rate.vue";
import ColorCircle from "~/components/global/ColorCircle.vue";
import { useImageColor } from "~/composables/global/useImageColor";
import { useRatio } from "~/composables/global/useRatio";
// state
const containerEl = ref<HTMLElement | null>(null);
const { isOutside: isMouseOutsideContainer } = useMouseInElement(containerEl);
const parallax = reactive(useParallax(containerEl));
const { isMobile } = useRatio();
// types
@@ -25,22 +32,36 @@ type Props = {
const props = defineProps<Props>();
const { id, colors } = toRefs(props);
// state
const { colorObject } = useImageColor(`#product-image-${id.value}`);
// computed
const limitedColors = computed(() => {
return colors.value.slice(0, 3);
});
const parallaxStyle = computed(() => {
if (isMobile.value || isMouseOutsideContainer.value) {
return {
transform: `rotateX(0deg) rotateY(0deg)`,
transition: "0.3s ease-out all",
};
}
return {
transform: `rotateX(${parallax.roll * 20}deg) rotateY(${parallax.tilt * 20}deg)`,
transition: "0.3s ease-out all",
};
});
</script>
<template>
<li class="w-full">
<NuxtLink :to="'/product/' + slug">
<div class="@container group">
<NuxtLink :to="{ name: 'product-id', params: { id: slug } }">
<div
class="@container group"
ref="containerEl"
>
<div class="perspective-midrange">
<div
:style="parallaxStyle"
class="group relative size-full aspect-square rounded-2xl bg-white brightness-[95%] overflow-hidden p-6"
>
<NuxtImg
@@ -81,18 +102,18 @@ const limitedColors = computed(() => {
</div>
</div>
</div>
</div>
<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 }}
<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="flex items-center justify-between w-full mt-1">
<span class="typo-p-xs !font-bold whitespace-nowrap">
{{ price }}
</span>
<div class="flex items-center justify-between w-full mt-1">
<span class="typo-p-xs !font-bold whitespace-nowrap">
{{ price }}
</span>
</div>
</div>
</div>
</NuxtLink>
</li>
</div>
</NuxtLink>
</template>
+221 -117
View File
@@ -2,135 +2,239 @@
// import
import useHomeData from "~/composables/api/home/useHomeData";
import { motion } from "motion-v";
import useSlider from "~/composables/global/useSlider";
// state
const { data: homeData } = useHomeData();
const { $gsap: gsap, $ScrollTrigger: ScrollTrigger } = useNuxtApp();
let gsapTimeline: gsap.core.Timeline;
let scrollTrigger: ScrollTrigger;
// lifecycle
onMounted(() => {
gsapTimeline = gsap.timeline();
const showcaseElements = gsap.utils.toArray<HTMLElement>(".showcase-slide");
setTimeout(() => {
showcaseElements.forEach((element, index) => {
gsapTimeline.fromTo(
element,
index === 0
? {
opacity: 1,
scale: 1,
// rotateX: -25,
zIndex: 1,
top: 0,
ease: "none",
}
: {
opacity: 0,
scale: 1,
// rotateX: -25,
zIndex: 1,
top: 20,
ease: "none",
},
{
opacity: 1,
scale: 1,
// rotateX: 0,
zIndex: 5,
top: 0,
ease: "none",
},
index === 0 ? "-=0%" : undefined
);
if (index < showcaseElements.length - 1) {
gsapTimeline.to(element, {
opacity: 0,
scale: 1.03,
// rotateX: 25,
top: -20,
ease: "none",
});
}
});
scrollTrigger = ScrollTrigger.create({
trigger: "#products-showcase-container",
animation: gsapTimeline,
scrub: 1,
pin: true,
start: "top top",
anticipatePin: 1,
// markers: true,
end: "bottom top",
});
setTimeout(() => {
scrollTrigger.update();
scrollTrigger.refresh();
}, 1000);
}, 1000);
const { nextSlide, prevSlide, slideTo, activeSlide, progress } = useSlider({
duration: 10_000,
count: homeData.value!.show_case_slider.length,
});
onUnmounted(() => {
gsapTimeline.progress(1).pause();
gsapTimeline.kill();
});
const variants = {
hide: { opacity: 0, y: -200 },
show: {
opacity: 1,
y: 0,
transition: {
when: "beforeChildren",
staggerChildren: 0.15,
},
},
exit: (slidesCount: number) => {
return {
opacity: 0,
y: 200,
transition: {
when: "afterChildren",
delay: slidesCount * 0.21,
staggerChildren: 0.1,
staggerDirection: 1,
},
};
},
};
const childContentVariants = {
hide: {
filter: "blur(20px)",
opacity: 0,
},
show: {
filter: "blur(0px)",
opacity: 1,
},
exit: {
filter: "blur(20px)",
opacity: 0,
},
};
const childImageVariants = {
hide: {
filter: "blur(20px)",
y: 70,
scale: 0.65,
opacity: 0,
},
show: {
filter: "blur(0px)",
y: 0,
scale: 1,
opacity: 1,
transition: {
type: "spring",
damping: 20,
},
},
exit: {
filter: "blur(20px)",
y: 70,
scale: 0.65,
opacity: 0,
transition: {
default: {
type: "spring",
damping: 20,
},
opacity: {
duration: 0.1,
},
},
},
};
</script>
<template>
<section
id="products-showcase-container"
class="perspective-midrange relative z-[999]"
>
<div class="w-full min-h-[120svh] lg:min-h-[102svh] bg-black">
<div
<section class="relative z-[999] h-[115svh] min-h-[1000px] pb-20 bg-black overflow-y-hidden flex-center">
<AnimatePresence mode="popLayout">
<template
v-for="(slide, index) in homeData!.show_case_slider"
:key="index"
class="showcase-slide origin-bottom absolute size-full bg-black flex items-center justify-center max-lg:-mt-16 lg:mt-5"
:key="slide.id"
>
<NuxtImg
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
:src="slide.image"
:style="{
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%)',
}"
alt=""
/>
<div class="flex flex-col items-center justify-center gap-6 text-center absolute z-20 mt-20">
<span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3">
{{ slide.title }}
</span>
<p
class="text-white max-w-[320px] xs:max-w-[360px] sm:max-w-[480px] lg:max-w-[550px] xl:max-w-[750px] typo-p-sm lg:typo-p-md xl:typo-p-lg"
>
{{ slide.description }}
</p>
<NuxtLink
:to="`/resellers/category/${slide.id}`"
class="relative"
>
<NuxtImg
src="/img/heymlz/heymlz-falling.gif"
class="absolute top-[101px] sm:top-[100px] lg:top-[117px] left-1/2 -translate-1/2 w-[200px] lg:w-[250px] drop-shadow-md"
/>
<Button
variant="primary"
end-icon="ci:arrow-left"
class="mt-8 max-sm:py-2 max-lg:typo-label-xs px-10 rounded-full hover:bg-transparent"
<motion.div
v-if="activeSlide === index"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
:exit="{ opacity: 0 }"
:transition="{ duration: 1 }"
class="absolute size-full inset-0 -z-10"
>
<NuxtImg
:src="slide.background_image"
class="absolute size-full object-cover"
/>
</motion.div>
</template>
</AnimatePresence>
<AnimatePresence mode="popLayout">
<template
v-for="(slide, index) in homeData!.show_case_slider"
:key="slide.id"
>
<motion.div
v-if="activeSlide === index"
:custom="homeData!.show_case_slider.length"
:variants="variants"
initial="hide"
animate="show"
exit="exit"
class="size-full flex flex-col gap-20 items-center justify-center"
>
<div class="flex items-center sm:gap-2 lg:gap-6 perspective-midrange">
<motion.div
:variants="childImageVariants"
class="origin-bottom-left"
>
مشاهده دسته بندی
</Button>
</NuxtLink>
</div>
</div>
</div>
<NuxtImg
class="w-[130px] sm:w-[180px] lg:w-[250px] xl:w-[300px] z-20 mt-40"
:src="slide.image3"
alt=""
/>
</motion.div>
<motion.div
:variants="childImageVariants"
class="origin-bottom"
>
<NuxtImg
class="w-[130px] sm:w-[180px] lg:w-[250px] xl:w-[300px] z-20"
:src="slide.image2"
alt=""
/>
</motion.div>
<motion.div
:variants="childImageVariants"
class="origin-bottom-right"
>
<NuxtImg
class="w-[130px] sm:w-[180px] lg:w-[250px] xl:w-[300px] z-20 mt-40"
:src="slide.image1"
alt=""
/>
</motion.div>
</div>
<motion.div
:variants="childContentVariants"
class="flex flex-col items-center justify-center gap-6 text-center"
>
<div class="flex items-center justify-center gap-4 sm:gap-6">
<button
@click="nextSlide"
class="relative"
>
<div class="size-8 blur-xl bg-white absolute ping-animation max-sm:hidden"></div>
<Icon
class="**:stroke-white cursor-pointer size-6 md:size-8"
name="ci:arrow-right"
/>
</button>
<div class="flex items-center flex-row-reverse gap-3 xs:gap-4">
<button
v-for="(_slide, index) in homeData!.show_case_slider"
@click="slideTo(index)"
class="h-12 flex-center cursor-pointer active:scale-90 transition-transform"
>
<div
dir="ltr"
:class="activeSlide === index ? 'bg-blue-500/50' : 'bg-white/15'"
class="h-1 w-8 xs:w-12 md:w-16 rounded-full"
>
<div
:class="activeSlide === index ? 'bg-blue-500' : 'bg-transparent w-full'"
class="h-full transition-all"
:style="{ width: `${progress}%` }"
></div>
</div>
</button>
</div>
<button
@click="prevSlide"
class="relative"
>
<div class="size-8 blur-xl bg-white absolute ping-animation max-sm:hidden"></div>
<Icon
class="**:stroke-white cursor-pointer size-6 md:size-8"
name="ci:arrow-left"
/>
</button>
</div>
<span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3">
{{ slide.title }}
</span>
<p
class="text-white max-w-[320px] xs:max-w-[360px] sm:max-w-[480px] lg:max-w-[550px] xl:max-w-[750px] typo-p-sm lg:typo-p-md xl:typo-p-lg"
>
{{ slide.description }}
</p>
<NuxtLink
:to="`/resellers/category/${slide.id}`"
class="relative"
>
<NuxtImg
src="/img/heymlz/heymlz-falling.gif"
class="absolute top-[101px] sm:top-[100px] lg:top-[117px] left-1/2 -translate-1/2 w-[200px] lg:w-[250px] drop-shadow-md"
/>
<Button
variant="primary"
end-icon="ci:arrow-left"
class="mt-8 max-sm:py-2 max-lg:typo-label-xs px-10 rounded-full hover:bg-transparent"
>
مشاهده دسته بندی
</Button>
</NuxtLink>
</motion.div>
</motion.div>
</template>
</AnimatePresence>
</section>
</template>
@@ -0,0 +1,67 @@
<script lang="ts" setup>
// types
type Props = {
description: string;
};
// props
defineProps<Props>();
// state
const descriptionEl = ref<HTMLDivElement | null>(null);
const descriptionElScrollHeight = ref(0);
const isDescriptionCollapsed = ref(true);
const descriptionMaxHeight = 200;
// watch
watch(
() => descriptionEl.value?.scrollHeight,
(nv) => {
descriptionElScrollHeight.value = nv ?? 0;
},
{
immediate: true,
}
);
</script>
<template>
<div
:class="descriptionElScrollHeight > descriptionMaxHeight ? 'mb-20' : ''"
class="relative"
>
<div
ref="descriptionEl"
:class="
descriptionElScrollHeight > descriptionMaxHeight && isDescriptionCollapsed
? 'mask-b-from-20% mask-b-to-90% overflow-y-hidden max-h-[300px] select-none'
: ''
"
class="py-8 leading-[200%] max-sm:text-sm text-slate-500 text-justify [&_a]:text-blue-400 [&_strong]:font-bold [&_u]:text-red-400"
v-html="description"
/>
<div
v-if="descriptionElScrollHeight > descriptionMaxHeight"
:class="isDescriptionCollapsed ? 'absolute' : ''"
class="bottom-0 inset-x-0 flex items-center"
>
<button
@click="isDescriptionCollapsed = !isDescriptionCollapsed"
variant="ghost"
size="md"
class="rounded-full max-xs:typo-label-sm flex items-center justify-center gap-1.5"
>
{{ isDescriptionCollapsed ? 'نمایش بیشتر' : 'نمایش کمتر' }}
<Icon
name="ci:chevron-left"
size="16"
/>
</button>
</div>
</div>
</template>
@@ -73,8 +73,8 @@ watch(
to="#"
class="typo-label-sm"
>
{{ product!.category.name }}</NuxtLink
>
{{ product!.category.name }}
</NuxtLink>
<h1 class="typo-h-6 xs:typo-h-5 sm:typo-h-4 lg:typo-h-2">
{{ product!.name }}
</h1>
@@ -161,10 +161,7 @@ watch(
/>
</div>
<div
class="py-8 leading-[200%] max -sm:text-sm text-slate-500 text-justify [&_a]:text-blue-400 [&_strong]:font-bold [&_u]:text-red-400"
v-html="product!.description"
/>
<ProductDescription :description="product!.description" />
<div class="flex items-center gap-4">
<span class="typo-md sm:typo-p-lg"> تنوع رنگی : </span>
@@ -83,8 +83,9 @@ const resetFilters = () => {
sliderValue.value = [PRODUCT_RANGE.min, PRODUCT_RANGE.max];
has_discount.value = false;
in_stock.value = false;
params.page = 1;
router.push({ path: `/products/`, query: { ...route.query } });
router.push({ path: `/products/`, query: { ...route.query, page: 1 } });
};
watch(
+31 -29
View File
@@ -7,37 +7,39 @@ import type { GetArticleResponse } from "~/composables/api/blog/useGetArticle";
// types
export type GetHomeDataResponse = {
"sliders": {
"id": number,
"link": string,
"title": string,
"description": string,
"image": string | null,
"video": string | null
}[],
"main_categories": Category[],
"products": ProductListItem[],
"difreance_section": {
"image1": string,
"image2": string,
"title1": string,
"title2": string,
"description1": string,
"description2": string,
"link1": string,
"link2": string
},
"show_case_slider" : {
"id": number,
"title": string,
"description": string,
"link": string,
"image": string,
}[]
sliders: {
id: number;
link: string;
title: string;
description: string;
image: string | null;
video: string | null;
}[];
main_categories: Category[];
products: ProductListItem[];
difreance_section: {
image1: string;
image2: string;
title1: string;
title2: string;
description1: string;
description2: string;
link1: string;
link2: string;
};
show_case_slider: {
id: number;
title: string;
description: string;
link: string;
image1: string;
image2: string;
image3: string;
background_image: string;
}[];
};
const useHomeData = () => {
// state
const { $axios: axios } = useNuxtApp();
@@ -51,7 +53,7 @@ const useHomeData = () => {
return useQuery({
queryKey: [QUERY_KEYS.home],
queryFn: () => handleHomeData()
queryFn: () => handleHomeData(),
});
};
@@ -2,6 +2,7 @@
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
import { useAuth } from "../auth/useAuth";
// types
@@ -12,17 +13,18 @@ const useGetCartOrders = () => {
const { $axios: axios } = useNuxtApp();
const { token } = useAuth();
// methods
const handleGetCartOrders = async () => {
const { data } = await axios.get<GetCartOrdersResponse>(
API_ENDPOINTS.orders.cart.get_all
);
const { data } = await axios.get<GetCartOrdersResponse>(API_ENDPOINTS.orders.cart.get_all);
return data;
};
return useQuery({
queryKey: [QUERY_KEYS.cart],
enabled: !!token.value,
queryFn: () => handleGetCartOrders(),
});
};
@@ -21,17 +21,14 @@ const useGetAllTickets = (params: ComputedRef<GetAllTicketsRequest>) => {
// methods
const handleGetAllTickets = async (params: GetAllTicketsRequest) => {
const { data } = await axios.get<GetAllTicketsResponse>(
API_ENDPOINTS.tickets.get_all,
{
params: {
sort: params.sort,
filter: params.status,
offset: Number(params.page) * 10 - 10,
limit: 10,
},
}
);
const { data } = await axios.get<GetAllTicketsResponse>(API_ENDPOINTS.tickets.get_all, {
params: {
sort: params.sort,
filter: params.status,
offset: Number(params.page) * 7 - 7,
limit: 7,
},
});
return data;
};
+91
View File
@@ -0,0 +1,91 @@
type Props = {
duration?: number;
count: number;
};
const useSlider = ({ duration = 0, count }: Props) => {
// states
const sliderTimer = ref<NodeJS.Timeout | null>(null);
const progressTimer = ref<NodeJS.Timeout | null>(null);
const progress = ref(0);
const activeSlide = ref(0);
// methods
const slideTo = (index: number) => {
activeSlide.value = index;
restartSliderTimer();
};
const nextSlide = () => {
if (activeSlide.value > count - 2) {
activeSlide.value = 0;
} else {
activeSlide.value = activeSlide.value + 1;
}
};
const prevSlide = () => {
if (activeSlide.value < 1) {
activeSlide.value = count - 1;
} else {
activeSlide.value = activeSlide.value - 1;
}
};
const prevSlideHandler = () => {
restartSliderTimer();
runProgress();
prevSlide();
};
const nextSlideHandler = () => {
restartSliderTimer();
runProgress();
nextSlide();
};
const runProgress = () => {
const delay = duration / 100;
if (progressTimer.value) clearInterval(progressTimer.value);
progress.value = 0;
progressTimer.value = setInterval(() => {
if (progress.value < 100) {
progress.value = progress.value + 1;
}
}, delay);
};
const restartSliderTimer = () => {
if (sliderTimer.value) clearInterval(sliderTimer.value);
runProgress();
if (duration > 0) {
sliderTimer.value = setInterval(() => {
runProgress();
nextSlide();
}, duration);
}
};
onMounted(() => {
restartSliderTimer();
});
onUnmounted(() => {});
return {
activeSlide,
progress,
slideTo,
nextSlide: nextSlideHandler,
prevSlide: prevSlideHandler,
restart: restartSliderTimer,
};
};
export default useSlider;
+7 -7
View File
@@ -1,14 +1,14 @@
export type ToastOptions = {
description?: string;
duration?: number;
status?: "success" | "error" | "info" | "warning",
}
status?: "success" | "error" | "info" | "warning";
};
type Toast = {
id: number;
message: string;
options?: ToastOptions
}
options?: ToastOptions;
};
const toasts = ref<Toast[]>([]);
@@ -20,12 +20,12 @@ export function useToast() {
};
const destroyToast = (id: number) => {
toasts.value = toasts.value.filter(toast => toast.id !== id);
toasts.value = toasts.value.filter((toast) => toast.id !== id);
};
return {
toasts,
addToast,
destroyToast
destroyToast,
};
}
}
+30 -2
View File
@@ -16,6 +16,20 @@ export default defineNuxtConfig({
name: "fade",
mode: "out-in",
},
head: {
meta: [
{
name: "mobile-web-app-capable",
content: "yes",
},
],
link: [
{
rel: "apple-touch-icon",
href: "/logo/apple-icon-180.png",
},
],
},
// layoutTransition: {
// name: "fade",
// mode: "out-in",
@@ -83,14 +97,28 @@ export default defineNuxtConfig({
theme_color: "#ffffff",
icons: [
{
src: "/logo/logo-192x192.png",
src: "/logo/manifest-icon-192.maskable.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/logo/logo-512x512.png",
src: "/logo/manifest-icon-192.maskable.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
src: "/logo/manifest-icon-512.maskable.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "/logo/manifest-icon-512.maskable.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
+2 -1
View File
@@ -27,7 +27,7 @@
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/integrations": "^12.7.0",
"@vueuse/nuxt": "^12.7.0",
"@vueuse/nuxt": "^13.3.0",
"animate.css": "^4.1.1",
"axios": "^1.8.1",
"date-fns-jalali": "^4.1.0-0",
@@ -42,6 +42,7 @@
"swiper": "^11.2.6",
"universal-cookie": "^7.2.2",
"vue": "latest",
"vue-image-zoomer": "^2.4.4",
"vue-router": "latest",
"vue-scrollto": "^2.20.0",
"vue-skeletor": "^1.0.6",
+2 -1
View File
@@ -36,7 +36,7 @@ const filters = computed(() => {
in_stock: params.in_stock ?? false,
has_discount: params.has_discount ?? false,
category: Array.isArray(route.params.slug) ? route.params.slug[1] ?? undefined : undefined,
page: params.page ?? 1,
page: route.query["page"] ?? 1,
};
});
@@ -153,6 +153,7 @@ watch(
<Pagination
:items="paginationData"
:total="data.count"
:per-page="15"
/>
</div>
</div>
+7 -1
View File
@@ -16,7 +16,12 @@ definePageMeta({
// state
const params = useUrlSearchParams("history") as GetAllTicketsRequest;
const params: GetAllTicketsRequest = useUrlSearchParams("history", {
initialValue: {
page: 1,
},
writeMode: "push",
});
const filters = computed(() => {
return {
@@ -236,6 +241,7 @@ const clearFilters = () => {
<Pagination
:items="paginationData"
:total="data?.count"
:per-page="7"
/>
</div>
</div>
+7
View File
@@ -0,0 +1,7 @@
// @ts-ignore
import VueImageZoomer from 'vue-image-zoomer';
import 'vue-image-zoomer/dist/style.css';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueImageZoomer, { name: "ImageZoomer" });
});
+9 -2
View File
@@ -1,11 +1,18 @@
export default defineNuxtPlugin((nuxtApp) => {
let previousPath: string | null = null;
let isFirstLoad = true;
nuxtApp.hook("page:finish", () => {
const currentPath = useRoute().fullPath;
const { fullPath: currentPath } = useRoute();
if (isFirstLoad) {
previousPath = currentPath;
isFirstLoad = false;
return;
}
if (previousPath !== currentPath) {
window.scrollTo(0, 0);
window.scrollTo({ top: 0, behavior: "auto" });
previousPath = currentPath;
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB