merge
@@ -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)];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 13 KiB |