merage and slider router

This commit is contained in:
Parsa Nazer
2025-05-19 20:46:24 +03:30
59 changed files with 590 additions and 582 deletions
+9 -1
View File
@@ -5,6 +5,14 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
// state
useSeoMeta({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} | فروشگاه هی‌ ملز` : "فروشگاه هی‌ ملز";
},
ogImage: "/img/heymlz/global-cover.jpg",
twitterImage: "/img/heymlz/global-cover.jpg",
});
const { $updateAvailable: updateAvailable, $handleUpdate: handleUpdate } = useNuxtApp();
const closeModal = () => {
@@ -37,7 +45,7 @@ const closeModal = () => {
<VueQueryDevtools
dir="ltr"
buttonPosition="top-right"
buttonPosition="top-left"
/>
</div>
</template>
-8
View File
@@ -131,8 +131,6 @@
--breakpoint-3xl: 1700px;
/* ANIMATIONS */
--animate-marquee: marquee 20s linear infinite;
--animate-marquee-reverse: marquee 20s linear infinite reverse;
--animate-fade-in: fadeIn 350ms ease-in-out;
--animate-slide-down: slideDown 300ms ease-out;
@@ -149,12 +147,6 @@
--animate-toast-in: toastSlideIn 600ms cubic-bezier(0.16, 1, 0.3, 1);
--animate-toast-out: toastSlideOut 200ms ease-out;
@keyframes marquee {
to {
transform: translateX(50%);
}
}
@keyframes fadeIn {
from {
opacity: 0;
+54 -117
View File
@@ -9,6 +9,15 @@ type Props = {
const props = defineProps<Props>();
const {} = toRefs(props);
const brands = ref([
"/img/brands/brand-1.png",
"/img/brands/brand-2.png",
"/img/brands/brand-3.png",
"/img/brands/brand-4.png",
"/img/brands/brand-5.png",
"/img/brands/brand-6.png",
]);
</script>
<template>
@@ -20,136 +29,64 @@ const {} = toRefs(props);
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
</p>
</div>
<div class="-rotate-z-2 z-20">
<div
class="bg-black flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee-reverse h-[90px] sm:h-[140px]"
<div class="-rotate-z-2 z-20 w-[110%]">
<Marquee
class="bg-black h-full"
:clone="true"
dir="ltr"
:duration="3"
>
<template v-for="i in 10">
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
<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">
HEYMLZ
</div>
<NuxtImg
src="/img/heymlz/heymlz-logo.png"
class="h-[25px] sm:h-[45px] invert opacity-85"
/>
</template>
<template v-for="i in 10">
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
HEYMLZ
</div>
<NuxtImg
src="/img/heymlz/heymlz-logo.png"
class="h-[25px] sm:h-[45px] invert opacity-85"
/>
</template>
</div>
</div>
</Marquee>
</div>
<div class="rotate-z-2 z-10">
<div
class="bg-slate-100/70 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee h-[90px] sm:h-[140px]"
<div class="rotate-z-2 z-10 w-[110%]">
<Marquee
class="bg-slate-100/70"
:direction="'reverse'"
:clone="true"
dir="ltr"
:duration="10"
>
<template v-for="i in 1">
<div
v-for="brand in brands"
:key="brand"
class="flex items-center px-6 sm:px-10 h-[90px] sm:h-[140px]"
>
<NuxtImg
src="/img/brands/brand-1.png"
:src="brand"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-2.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-3.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-4.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-5.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-6.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-1.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-2.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-3.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-4.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-5.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-6.png"
class="h-[25px] sm:h-[45px]"
/>
</template>
<template v-for="i in 1">
<NuxtImg
src="/img/brands/brand-1.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-2.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-3.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-4.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-5.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-6.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-1.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-2.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-3.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-4.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-5.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-6.png"
class="h-[25px] sm:h-[45px]"
/>
</template>
</div>
</div>
</Marquee>
</div>
</div>
</template>
<!-- <NuxtImg
src="/img/brands/brand-2.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-3.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-4.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-5.png"
class="h-[25px] sm:h-[45px]"
/>
<NuxtImg
src="/img/brands/brand-6.png"
class="h-[25px] sm:h-[45px]"
/> -->
+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>
+35 -17
View File
@@ -47,9 +47,7 @@ watch(
watch(
() => modelValue.value,
(newValue) => {
const target = options.value
.flatMap((option) => option.children)
.find((child) => child.id == newValue);
const target = options.value.flatMap((option) => option.children).find((child) => child.id == newValue);
value.value = target || undefined;
},
@@ -58,9 +56,13 @@ watch(
</script>
<template>
<ComboboxRoot class="relative" dir="rtl" v-model="value">
<ComboboxRoot
class="relative"
dir="rtl"
v-model="value"
>
<ComboboxAnchor
class="w-full inline-flex items-center justify-between rounded-xl border-[1.5px] border-slate-200 focus:border-slate-800 px-[1rem] text-sm leading-none py-3.5 gap-[5px] bg-slate-50 text-black hover:border-black transition-all data-[placeholder]:text-black/80 typo-label-sm outline-none"
class="w-full inline-flex items-center justify-between rounded-xl border-[1.5px] border-slate-200 focus:border-slate-800 leading-none px-4 py-2.5 lg:py-3.5 gap-[5px] bg-slate-50 text-black hover:border-black transition-all data-[placeholder]:text-black/80 text-xs lg:text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal outline-none"
>
<ComboboxInput
:display-value="(v) => (!!v ? v.name : '')"
@@ -68,29 +70,39 @@ watch(
:placeholder="placeholder"
/>
<ComboboxTrigger class="cursor-pointer">
<Icon name="ci:chevron-down" class="size-5" />
<Icon
name="ci:chevron-down"
class="size-5"
/>
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxContent
class="absolute z-10 w-full mt-4 bg-slate-50 overflow-hidden rounded-xl shadow-sm border border-slate-200 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=bottom]:animate-slideUpAndFade"
class="absolute z-10 w-full max-h-[25rem] mt-4 bg-slate-50 overflow-hidden rounded-xl shadow-sm border border-slate-200 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=bottom]:animate-slideUpAndFade"
>
<ComboboxViewport class="p-[1rem]">
<ComboboxEmpty
class="text-mauve8 text-xs font-medium text-center py-5"
/>
<ComboboxEmpty class="placeholder-slate-400 text-xs font-medium text-center py-5" />
<template v-for="(group, index) in options" :key="group.name">
<template
v-for="(group, index) in options"
:key="group.name"
>
<ComboboxGroup>
<ComboboxSeparator v-if="index !== 0" class="h-6" />
<ComboboxSeparator
v-if="index !== 0"
class="h-6"
/>
<ComboboxLabel
class="flex items-center justify-between px-[1.2rem] w-full text-md text-black bg-slate-200/50 leading-[25px] py-3 rounded-lg"
class="flex items-center justify-between px-[1.2rem] w-full max-lg:text-sm text-black bg-slate-200/50 leading-[25px] py-2 lg:py-3 rounded-lg"
>
<span>
{{ group.name }}
</span>
<Icon name="ci:delivery-boxes" size="18px" />
<Icon
name="ci:delivery-boxes"
class="text-lg"
/>
</ComboboxLabel>
<ComboboxItem
@@ -102,10 +114,16 @@ watch(
<ComboboxItemIndicator
class="absolute left-3 w-[25px] inline-flex items-center justify-center"
>
<Icon name="ci:checkmark" size="18" />
<Icon
name="ci:checkmark"
size="18"
/>
</ComboboxItemIndicator>
<div class="flex items-center gap-2">
<Icon name="ci:minus" class="opacity-50" />
<div class="flex items-center gap-2 max-lg:text-xs">
<Icon
name="ci:minus"
class="opacity-50"
/>
<span>
{{ option.name }}
</span>
+1 -1
View File
@@ -5,7 +5,7 @@
alt=""
class="absolute z-10 object-cover opacity-45"
:style="{
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%',
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%',
}"
/>
+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>
+24 -11
View File
@@ -1,4 +1,8 @@
<script setup lang="ts">
// imports
import { useRatio } from "~/composables/global/useRatio";
// types
type Props = {
@@ -17,6 +21,8 @@ defineProps<Props>();
const params: any = inject("params");
const { isMobile } = useRatio();
const { y } = useWindowScroll({ behavior: "smooth" });
// computed
@@ -33,26 +39,29 @@ const page = computed({
<template>
<PaginationRoot
:total="total"
:sibling-count="1"
:items-per-page="9"
show-edges
:sibling-count="isMobile ? 0 : 1"
:items-per-page="15"
v-model:page="page"
>
<PaginationList v-slot="{ items }" class="flex items-center gap-2">
<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"
>
برو اول
</PaginationFirst>
<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"
<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"
>
<Icon
name="ci:chevron-right"
class="**:fill-back"
size="18px"
/>
</PaginationNext>
</PaginationPrev>
<template v-for="(page, index) in items">
<PaginationListItem
@@ -73,11 +82,15 @@ const page = computed({
</PaginationEllipsis>
</template>
<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"
<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"
>
<Icon name="ci:chevron-left" class="**:fill-back" size="18px" />
</PaginationPrev>
<Icon
name="ci: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"
+10 -4
View File
@@ -13,11 +13,17 @@ defineProps<Props>();
<template>
<div
class="w-full flex-col flex-grow py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-xl flex-center"
class="w-full flex-col flex-grow py-[10rem] lg:py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-2xl flex-center"
>
<Icon :name="icon" size="50" class="**:fill-gray-500" />
<span class="text-lg text-gray-500"> {{ title }} </span>
<slot v-if="$slots['actions']" name="actions" />
<Icon
:name="icon"
class="**:fill-gray-400 text-3xl"
/>
<span class="lg:text-lg text-gray-400"> {{ title }} </span>
<slot
v-if="$slots['actions']"
name="actions"
/>
</div>
</template>
+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"
+2 -2
View File
@@ -44,7 +44,7 @@ watch(
<div
class="w-full flex justify-between items-center py-[1.5rem] lg:py-[2.5rem] px-[1.5rem] lg:px-[3rem]"
>
<span class="typo-h-5">
<span class="typo-h-5 lg:typo-h-4">
{{ title }}
</span>
@@ -61,7 +61,7 @@ watch(
</div>
<div
class="size-full flex flex-col grow overflow-y-auto py-[1.5rem] lg:py-[2.5rem] px-[1.5rem] lg:px-[3rem] pt-0"
class="size-full flex flex-col grow overflow-y-auto py-[1.5rem] mb-10 px-[1.5rem] lg:px-[3rem]"
>
<slot />
</div>
@@ -3,12 +3,12 @@
import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react";
import useHomeData from "~/composables/api/home/useHomeData";
// types
type Props = {
title: string;
products: ProductListItem[];
};
// props
@@ -17,14 +17,8 @@ defineProps<Props>();
// state
const { data: homeData, suspense } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null);
// queries
await suspense();
// methods
const onSwiper = (swiper: SwiperClass) => {
@@ -100,7 +94,7 @@ const onSwiper = (swiper: SwiperClass) => {
}"
>
<SwiperSlide
v-for="product in [...homeData!.products,...homeData!.products]"
v-for="product in products"
:key="product.id"
>
<ProductCard
@@ -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"
@@ -42,13 +42,8 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
</span>
<ul class="list-disc w-full ps-5">
<li
v-for="detail in [
item.detail_text1,
item.detail_text2,
item.detail_text3,
item.detail_text4,
]"
class="text-slate-500 typo-p-md"
v-for="detail in item.texts"
class="text-slate-500 text-sm leading-[175%] mt-1.5"
>
{{ detail }}
</li>
@@ -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-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,
@@ -121,12 +124,13 @@ watch(
<template>
<div class="size-full flex flex-col gap-14 justify-between">
<div class="w-full flex flex-col gap-10">
<div class="w-full flex flex-col gap-8 lg: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 max-lg:text-sm w-full">
<Icon
name="ci:filter-list"
class="text-xl"
/>
ترتیب بر اساس
</div>
<div class="w-full flex items-center gap-2">
@@ -134,12 +138,8 @@ 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="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-md text-sm"
: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-xs lg:text-sm"
>
{{ sort.title }}
</button>
@@ -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 max-lg:text-sm w-full">
<Icon
name="ci:grid"
class="text-xl"
/>
دسته بندی
</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 max-lg:text-sm w-full">
<Icon
name="ci:scan-box"
class="text-xl"
/>
محدوده قیمت
</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)"
@@ -185,15 +186,15 @@ watch(
/>
</SliderRoot>
<div class="flex items-center justify-between">
<div class="flex-center gap-2">
<span class="text-sm text-black">حداقل</span>
<span class="text-sm text-black">
<div class="flex-center gap-2 text-xs lg:text-sm">
<span class="text-black">حداقل</span>
<span class="text-black">
{{ sliderValue[0].toLocaleString() }}
</span>
</div>
<div class="flex-center gap-2">
<span class="text-sm text-black">حداکثر</span>
<span class="text-sm text-black">
<div class="flex-center gap-2 text-xs lg:text-sm">
<span class="text-black">حداکثر</span>
<span class="text-black">
{{ sliderValue[1].toLocaleString() }}
</span>
</div>
@@ -201,13 +202,13 @@ watch(
</div>
<div class="flex items-center justify-between w-full gap-5">
<span class="text-black">فقط کالاهای تخفیف دار</span>
<span class="text-black max-lg:text-sm">فقط کالاهای تخفیف دار</span>
<Switch v-model="has_discount" />
</div>
<div class="flex items-center justify-between w-full gap-5">
<span class="text-black">فقط کالاهای موجود</span>
<span class="text-black max-lg:text-sm">فقط کالاهای موجود</span>
<Switch v-model="in_stock" />
</div>
@@ -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>
+34 -7
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,23 +33,32 @@ 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"> دسته بندی ها </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">
<NuxtImg
class="aspect-square w-[210px] sm:w-[240px] md:w-[300px] lg:w-[350px] translate-y-[-164px] md:translate-y-[-206px] lg:translate-y-[-240px] absolute left-1/2 -translate-x-1/2 z-10"
class="aspect-square w-[210px] sm:w-[240px] md:w-[300px] lg:w-[350px] 2xl:w-[420px] translate-y-[-136px] sm:translate-y-[-156px] md:translate-y-[-195px] lg:translate-y-[-228px] 2xl:translate-y-[-273px] absolute left-1/2 -translate-x-1/2 z-10"
:style="{
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))',
}"
src="/img/heymlz/heymlz-seat.gif"
src="/img/heymlz/heymlz-category-seat.gif"
/>
<Swiper
: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"
+25 -5
View File
@@ -179,9 +179,11 @@ onUnmounted(() => {
<div
id="header-slider-wrapper"
class="relative"
:class="swiper_instance ? '' : 'bg-black min-h-svh'"
>
<Swiper
ref="observerTarget"
:class="swiper_instance ? '' : 'opacity-0'"
:slides-per-view="slidesPerView"
:loop="true"
:centered-slides="true"
@@ -242,7 +244,10 @@ onUnmounted(() => {
/>
</button>
</div>
<NuxtLink :to="slide.link" class="typo-h-6 md:typo-h-4 lg:typo-h-1 tracking-[-2px] text-white">
<NuxtLink
:to="slide.link"
class="typo-h-6 md:typo-h-4 lg:typo-h-1 tracking-[-2px] text-white"
>
{{ slide.title }}
</NuxtLink>
</div>
@@ -250,10 +255,21 @@ onUnmounted(() => {
<span class="truncate typo-p-xs md:typo-p-sm lg:typo-p-lg text-white">
{{ slide.description }}
</span>
<NuxtLink :to="slide.link">
<NuxtLink
:to="slide.link"
class="relative max-sm:hidden"
>
<NuxtImg
class="aspect-square w-[110px] lg:w-[120px] translate-y-[-75px] lg:translate-y-[-82px] absolute left-1/2 -translate-x-1/2 z-10"
:style="{
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))',
}"
src="/img/heymlz/heymlz-seat.gif"
/>
<Button
variant="primary"
class="max-sm:hidden max-lg:typo-label-xs px-7 rounded-full hover:bg-transparent"
class="max-lg:typo-label-xs px-12 rounded-full hover:bg-blue-500 hover:text-white"
>
مشاهده
</Button>
@@ -270,7 +286,9 @@ onUnmounted(() => {
@click="swiper_instance?.slidePrev()"
class="relative"
>
<div class="size-8 blur-xl bg-white absolute ping-animation max-sm:hidden"></div>
<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"
@@ -289,7 +307,9 @@ onUnmounted(() => {
@click="swiper_instance?.slideNext()"
class="relative"
>
<div class="size-8 blur-xl bg-white absolute ping-animation max-sm:hidden"></div>
<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"
+28 -49
View File
@@ -14,12 +14,12 @@ const activeSlideVideo = ref<"left" | "right" | "none">("none");
const draggableEl = ref<HTMLElement | null>(null);
const previewContainerEl = ref<HTMLElement | null>(null);
const heymlzElement = useTemplateRef<HTMLDivElement>("heymlzElement");
const heymlzElementIsVisible = useElementVisibility(heymlzElement, {
rootMargin: "0px 0px -40% 0px",
});
// const heymlzElement = useTemplateRef<HTMLDivElement>("heymlzElement");
// const heymlzElementIsVisible = useElementVisibility(heymlzElement, {
// rootMargin: "0px 0px -40% 0px",
// });
const showHeymlzAnimation = ref(false);
// const showHeymlzAnimation = ref(false);
const { x: dragAxisX } = useDraggable(draggableEl, {
initialValue: { x: 0, y: 0 },
@@ -28,22 +28,22 @@ const { x: dragAxisX } = useDraggable(draggableEl, {
// watch
watch(
heymlzElementIsVisible,
(newValue) => {
if (newValue) {
setTimeout(() => {
showHeymlzAnimation.value = true;
setTimeout(() => {
showHeymlzAnimation.value = false;
}, 3200);
}, 400);
}
},
{
once: true,
}
);
// watch(
// heymlzElementIsVisible,
// (newValue) => {
// if (newValue) {
// setTimeout(() => {
// showHeymlzAnimation.value = true;
// setTimeout(() => {
// showHeymlzAnimation.value = false;
// }, 3200);
// }, 400);
// }
// },
// {
// once: true,
// }
// );
watch(
() => clipPathPercent.value,
@@ -64,7 +64,7 @@ watch(
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
const percent = clientRect.width / 100;
const clipPercent = (newValue + draggableEl.value!.clientWidth / 2 - clientRect.x - 8) / percent;
if (clipPercent >= 5 && clipPercent <= 95) {
if (clipPercent >= 1 && clipPercent <= 99) {
clipPathPercent.value = clipPercent;
}
}
@@ -72,7 +72,7 @@ watch(
</script>
<template>
<div class="container mb-40 lg:mb-40 max-lg:mt-20 lg:-mt-32">
<div class="container select-none mb-40 lg:mb-40 max-lg:mt-20 lg:-mt-32">
<div>
<div class="flex flex-col items-center gap-3 mb-10 lg:mb-16">
<span class="typo-p-sm md:typo-p-md text-slate-500"> مقایسه محصولات </span>
@@ -86,8 +86,7 @@ watch(
<NuxtImg
v-if="activeSlideVideo !== 'right'"
:src="homeData!.difreance_section.image1"
:class="showHeymlzAnimation ? 'brightness-25 blur-sm' : 'brightness-[95%] blur-[0px]'"
class="select-none absolute size-full object-cover transition-[filter] duration-250"
class="select-none absolute size-full object-cover transition-[filter] duration-250 brightness-[95%]"
:alt="homeData!.difreance_section.title1"
/>
<video
@@ -109,8 +108,7 @@ watch(
<NuxtImg
v-if="activeSlideVideo !== 'left'"
:src="homeData!.difreance_section.image2"
:class="showHeymlzAnimation ? 'brightness-25 blur-sm' : 'brightness-[95%] blur-[0px]'"
class="overlay-image select-none absolute object-cover size-full transition-[filter] duration-250"
class="overlay-image select-none absolute object-cover size-full transition-[filter] duration-250 brightness-[95%]"
:alt="homeData!.difreance_section.title2"
/>
<video
@@ -124,47 +122,28 @@ watch(
/>
</Transition>
<Transition
name="fade"
:duration="250"
>
<NuxtImg
v-if="showHeymlzAnimation"
src="/img/heymlz/heymlz-pullingg.gif"
class="size-[250px] sm:size-[400px] absolute translate-x-[-62px] sm:translate-x-[-107px] z-10 top-[50%] -translate-y-1/2"
:style="{
left: `${clipPathPercent}%`,
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.3))',
}"
/>
</Transition>
<div
:style="{
left: `${clipPathPercent}%`,
}"
:class="[
activeSlideVideo !== 'none' ? 'opacity-10' : '',
showHeymlzAnimation ? 'bg-neutral-200' : 'bg-black',
]"
class="select-none w-[5px] sm:w-2 h-full absolute left-0 flex items-center justify-center transition-opacity duration-250"
class="select-none w-[5px] sm:w-2 bg-black h-full absolute left-0 flex items-center justify-center transition-opacity duration-250"
>
<div
ref="draggableEl"
:class="showHeymlzAnimation ? 'bg-neutral-200' : 'bg-black'"
class="touch-none cursor-grab hover:scale-115 transition-transform rounded-full absolute size-9 sm:size-11 flex items-center justify-center"
class="touch-none cursor-grab bg-black hover:scale-115 transition-transform rounded-full absolute size-9 sm:size-11 flex items-center justify-center"
>
<Icon
name="ci:arrows"
class="transition-all size-5 sm:size-6"
:class="showHeymlzAnimation ? '**:stroke-black' : '**:stroke-white'"
class="transition-all size-5 sm:size-6 **:stroke-white"
/>
</div>
</div>
</div>
<div
:class="showHeymlzAnimation ? 'opacity-0' : 'opacity-100'"
class="absolute bottom-0 p-6 md:p-10 w-full flex justify-between items-end transition-opacity"
>
<div
@@ -92,7 +92,7 @@ onUnmounted(() => {
v-for="slide in homeData!.show_case_slider"
:key="slide.id"
:to="slide.link"
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center max-lg:-mt-16 lg:mt-5"
class="showcase-slide origin-bottom absolute size-full bg-black flex items-center justify-center max-lg:-mt-16 lg:mt-5"
>
<NuxtImg
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
@@ -204,7 +204,7 @@ whenever(
<div class="flex flex-col gap-4 items-center">
<span class="text-center typo-p-xl font-bold">سلام دوست عزیز!</span>
<p class="text-center typo-p-md">
من میتونم هر سوالی رو درمورد این محصول جواب بدم اگه میخوای شروع کنیم روی دکمه زیر کلیک کن
من میتونم هر سوالی رو درمورد این محصول جواب بدم اگه میخوای شروع کنیم وارد وبسایت شو
</p>
</div>
<div class="flex-center gap-4">
+11 -10
View File
@@ -62,7 +62,7 @@ const limitedComments = computed(() => {
>
<textarea
:disabled="!token"
class="w-full min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
class="w-full min-h-[125px] resize-none sm:min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
v-model="userComment"
placeholder="نظر خود را بنویسید..."
/>
@@ -98,15 +98,6 @@ const limitedComments = computed(() => {
:username="'منصور مرزبان'"
/>
<div
class="h-[400px] lg:flex-grow w-full border-[0.5px] flex-col-center border-slate-200 bg-white rounded-xl"
>
<NuxtImg
src="/img/heymlz/heymlz-contact-us.gif"
class="w-[200px] lg:w-[300px] translate-y-[-25px]"
/>
<span class="text-xl text-black font-semibold translate-y-[-25px]"> هیچ نظری ثبت نشده است </span>
</div>
<div
v-if="comments!.count > 0"
class="flex items-center justify-center w-full"
@@ -127,6 +118,16 @@ const limitedComments = computed(() => {
نمایش همه
</Button>
</div>
<div
v-else
class="h-[400px] lg:flex-grow w-full border-[0.5px] flex-col-center border-slate-200 bg-white rounded-xl"
>
<NuxtImg
src="/img/heymlz/heymlz-contact-us.gif"
class="w-[200px] lg:w-[300px] translate-y-[-25px]"
/>
<span class="text-xl text-black font-semibold translate-y-[-25px]"> هیچ نظری ثبت نشده است </span>
</div>
</div>
</div>
</section>
+1 -1
View File
@@ -162,7 +162,7 @@ watch(
</div>
<div
class="py-8 typo-sm max-sm:leading-[175%] sm:typo-p-md text-slate-500 text-justify [&_a]:text-blue-400 [&_strong]:font-bold [&_u]:text-red-400"
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"
/>
@@ -39,8 +39,8 @@ const useGetProducts = (params?: ComputedRef<GetProductsFilters>) => {
category: params?.category,
price_gte: params?.price_gte,
price_lte: params?.price_lte,
offset: Number(params?.page) * 9 - 9,
limit: 9
offset: Number(params?.page) * 15 - 15,
limit: 15
}
}
);
-5
View File
@@ -1,5 +0,0 @@
export default defineNuxtRouteMiddleware((to, from) => {
if (to.path !== from.path && process.client) {
window.scrollTo(0, 0);
}
});
+7 -10
View File
@@ -3,11 +3,7 @@ export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
ssr: true,
devtools: { enabled: true },
css: [
"~/assets/css/tailwind.css",
"swiper/css",
"animate.css/animate.min.css",
],
css: ["~/assets/css/tailwind.css", "swiper/css", "animate.css/animate.min.css"],
routeRules: {
"/products": { prerender: false, ssr: false },
@@ -16,9 +12,6 @@ export default defineNuxtConfig({
},
app: {
head: {
title: "فروشگاه هی ملز",
},
pageTransition: {
name: "fade",
mode: "out-in",
@@ -71,14 +64,18 @@ export default defineNuxtConfig({
"@formkit/auto-animate/nuxt",
"@vite-pwa/nuxt",
"@nuxt/image",
"@nuxtjs/seo",
],
sitemap: {
enabled: false,
},
pwa: {
strategies: "injectManifest",
srcDir: "public",
filename: "sw.js",
registerType:
process.env.NODE_ENV === "production" ? "autoUpdate" : "prompt",
registerType: process.env.NODE_ENV === "production" ? "autoUpdate" : "prompt",
manifest: {
name: "Heymlz",
short_name: "Heymlz",
+1
View File
@@ -19,6 +19,7 @@
"@nuxt/icon": "^1.10.3",
"@nuxt/image": "^1.10.0",
"@nuxtjs/google-fonts": "^3.2.0",
"@nuxtjs/seo": "^3.0.3",
"@tanstack/vue-query": "^5.62.2",
"@tanstack/vue-query-devtools": "^5.62.3",
"@vite-pwa/nuxt": "^0.10.6",
+42 -31
View File
@@ -1,5 +1,4 @@
<script lang="ts" setup>
// import
import useGetArticle from "~/composables/api/blog/useGetArticle";
@@ -12,6 +11,14 @@ const id = route.params.id as string | undefined;
const { data: article, suspense } = useGetArticle(id);
useSeoMeta({
title: `مقاله ${article.value?.title}`,
ogImage: article.value?.cover_image,
twitterImage: article.value?.cover_image,
ogDescription: article.value?.summery,
twitterDescription: article.value?.summery,
});
// ssr
const response = await suspense();
@@ -19,16 +26,19 @@ const response = await suspense();
if (response.isError) {
throw createError({
statusCode: 500,
statusMessage: `Error in categories page prefetch`
statusMessage: `Error in categories page prefetch`,
});
}
</script>
<template>
<div class="container">
<div class="w-full h-[80svh] rounded-3xl relative overflow-hidden">
<NuxtImg class="absolute object-cover size-full" :alt="article!.title" :src="article!.cover_image" />
<NuxtImg
class="absolute object-cover size-full"
:alt="article!.title"
:src="article!.cover_image"
/>
<div class="absolute bg-linear-to-t from-black/75 to-transparent size-full" />
<div class="absolute pl-10 right-10 bottom-10 flex flex-col gap-6">
<h1 class="typo-h-4 text-white pl-8">
@@ -41,13 +51,13 @@ if (response.isError) {
/>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div
class="w-fit pr-2 pl-5 h-[50px] rounded-full flex items-center justify-center gap-3 bg-white">
class="w-fit pr-2 pl-5 h-[50px] rounded-full flex items-center justify-center gap-3 bg-white"
>
<div
class="relative flex items-center justify-center rounded-full overflow-hidden size-[35px]">
class="relative flex items-center justify-center rounded-full overflow-hidden size-[35px]"
>
<NuxtImg
class="size-full object-cover absolute"
:src="article!.author.profile_photo"
@@ -55,50 +65,51 @@ if (response.isError) {
/>
</div>
<span class="typo-label-sm">
{{ article!.author.full_name }}
</span>
{{ article!.author.full_name }}
</span>
</div>
<div
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white">
<span class="typo-label-sm mt-0.5">
دسته بندی موبایل
</span>
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white"
>
<span class="typo-label-sm mt-0.5"> دسته بندی موبایل </span>
</div>
</div>
<div class="flex items-center gap-4">
<div
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white">
<Icon name="ci:calendar" size="24px" class="**:stroke-white" />
<span class="typo-label-sm mt-0.5">
۲۴ مهر 1403
</span>
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white"
>
<Icon
name="ci:calendar"
size="24px"
class="**:stroke-white"
/>
<span class="typo-label-sm mt-0.5"> ۲۴ مهر 1403 </span>
</div>
<div
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white">
<Icon name="ci:eye-open" size="24px" class="**:stroke-white" />
class="w-fit pr-4 pl-5 h-[50px] rounded-full flex items-center justify-center gap-2 border-[1.5px] border-white text-white"
>
<Icon
name="ci:eye-open"
size="24px"
class="**:stroke-white"
/>
<span class="typo-label-sm mt-0.5">
{{ article!.views }}
</span>
{{ article!.views }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-4 mt-8">
<div
class="p-8 flex-1 text-zinc-800 flex flex-col gap-6 [&_p,ul]:text-zinc-500 [&_h1]:typo-h-4 [&_h2]:typo-h-5 [&_h3]:typo-h-6 [&_p]:typo-p-md [&_ul]:list-disc [&_ul]:typo-p-md [&_ul]:space-y-2"
v-html="article!.content"
/>
<aside class="mt-8 p-8 h-fit bg-slate-100 w-[400px] sticky top-4 rounded-3xl">
asdsa
</aside>
<aside class="mt-8 p-8 h-fit bg-slate-100 w-[400px] sticky top-4 rounded-3xl">asdsa</aside>
</div>
</div>
</template>
</template>
+4
View File
@@ -7,6 +7,10 @@ import ArticlesList from "~/components/articles/ArticlesList.vue";
// state
useSeoMeta({
title : "مقالات"
});
const page = ref(1);
const search = ref("");
const debouncedSearch = refDebounced(search, 700);
+4 -1
View File
@@ -3,10 +3,13 @@
const route = useRoute();
useSeoMeta({
title: "ثبت سفارش",
});
definePageMeta({
layout: "cart",
middleware: "check-is-logged-in",
pageTitle: "ثبت سفارش",
prevPage: { name: "cart-delivery", label: "انتخاب آدرس" },
nextPage: { name: "payment", label: "پرداخت" },
});
+5 -1
View File
@@ -7,10 +7,14 @@ import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// meta
useSeoMeta({
title: "انتخاب آدرس",
});
definePageMeta({
layout: "cart",
middleware: "check-is-logged-in",
pageTitle: "انتخاب آدرس",
prevPage: { name: "cart", label: "سبد خرید" },
nextPage: { name: "cart-checkout", label: "تسویه حساب", query: "ZARINPAL" },
});
+4 -1
View File
@@ -5,10 +5,13 @@ import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// meta
useSeoMeta({
title : "سبد خرید"
});
definePageMeta({
layout: "cart",
middleware: "check-is-logged-in",
pageTitle: "سبد خرید",
prevPage: { name: "index", label: "بازگشت به خانه" },
nextPage: { name: "cart-delivery", label: "انتخاب آدرس" },
});
+4
View File
@@ -5,6 +5,10 @@ import useGetCategories from "~/composables/api/product/useGetCategories";
// state
useSeoMeta({
title : "دسته بندی ها"
});
const { data: categories, suspense } = useGetCategories();
const search = ref("");
+4
View File
@@ -1,6 +1,10 @@
<script setup lang="ts">
// state
useSeoMeta({
title : "ارتباط با ما"
});
const contactInfo = ref({
name: "",
email: "",
+1 -3
View File
@@ -4,8 +4,6 @@
import useHomeData from "~/composables/api/home/useHomeData";
import ProductsGrid from "~/components/global/ProductsGrid.vue";
import { useStorage } from "@vueuse/core";
// state
const { data: homeData, suspense } = useHomeData();
@@ -36,7 +34,7 @@ onMounted(() => {
<ProductsShowcase class="lg:mb-12" />
<ProductsGrid
title="محصولات پرفروش"
:products="[...homeData!.products,...homeData!.products]"
:products="homeData!.products"
/>
<Categories class="mt-12" />
<Brands />
+10 -1
View File
@@ -4,6 +4,7 @@
import ChatButton from "~/components/product/ChatBox/ChatButton.vue";
import useGetProduct from "~/composables/api/product/useGetProduct";
import useGetComments from "~/composables/api/product/useGetComments";
import ProductsSlider from "~/components/global/product-detail/ProductsSlider.vue";
// state
@@ -15,6 +16,14 @@ const page = ref(1);
const { suspense: suspenseProduct, data: product } = useGetProduct(id);
const { suspense: suspenseComments } = useGetComments(id, page);
useSeoMeta({
title: `محصول ${product.value?.name}`,
ogImage: product.value?.variants[0].images[0].image,
twitterImage: product.value?.variants[0].images[0].image,
ogDescription: product.value?.description,
twitterDescription: product.value?.description,
});
const selectedVariant = ref<ProductVariant>();
const showChatButton = ref(true);
@@ -52,7 +61,7 @@ if (productResponse.isError || commentsResponse.isError) {
<ProductVideo v-model:showChatButton="showChatButton" />
<ProductComments />
<ProductDetails />
<ProductsGrid
<ProductsSlider
title="محصولات مشابه"
:products="product!.related_products"
/>
@@ -6,6 +6,22 @@ import { PRODUCT_RANGE } from "~/constants";
// state
const route = useRoute();
useSeoMeta({
title: "محصولات",
});
definePageMeta({
validate: (route) => {
if (Array.isArray(route.params.slug)) {
return route.params.slug.length === 2 && route.params.slug[0] === "category";
}
return true;
},
});
const params: GetProductsFilters = useUrlSearchParams("history", {
removeFalsyValues: true,
removeNullishValues: true,
@@ -19,7 +35,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: Array.isArray(route.params.slug) ? route.params.slug[1] ?? undefined : undefined,
page: params.page ?? 1,
};
});
@@ -95,11 +111,11 @@ watch(
</div>
<ul
v-if="productsIsLoading"
class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-y-8 gap-5 sm:gap-8 w-full"
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 w-full"
>
<div
class="w-full flex flex-col gap-3"
v-for="i in 8"
v-for="i in 10"
:key="i"
>
<Skeleton
@@ -107,9 +123,10 @@ watch(
:key="i"
class="w-full"
:class="{
'!h-[11.75rem] lg:!h-[22.5rem] !rounded-2xl': i == 1,
'!h-[1.35rem] lg:!h-[1.5rem] !rounded-sm lg:!hidden': [2, 3].includes(i),
'!w-1/2': i == 2,
'!h-[11.9rem] lg:!h-[17.25rem] !rounded-2xl': i == 1,
'!h-[1.4rem] lg:!h-[1.5rem] !rounded-sm': [2, 3].includes(i),
'!w-1/2 lg:!w-full': i == 2,
'lg:!w-1/2': i == 3,
}"
/>
</div>
@@ -133,8 +150,8 @@ watch(
class="!p-0"
/>
<div
v-if="data && paginationData && data.count > 10"
class="w-full flex-center py-10"
v-if="data && paginationData && data.count > 15"
class="w-full flex-center py-10 mt-5 lg:mt-10"
>
<Pagination
:items="paginationData"
+4
View File
@@ -5,6 +5,10 @@ import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
// meta
useSeoMeta({
title : "پنل کاربری آدرس ها"
});
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
+17 -23
View File
@@ -11,6 +11,10 @@ import { QUERY_KEYS } from "~/constants";
// meta
useSeoMeta({
title: "پنل کاربری",
});
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
@@ -63,16 +67,6 @@ const formRules = computed(() => {
helpers.regex(/^0?[1-9][0-9]{9}$/)
),
},
gender: {
required: helpers.withMessage("فیلد جنسیت الزامی می باشد", required),
},
email: {
required: helpers.withMessage("فیلد حساب الکترونیکی الزامی می باشد", required),
email: helpers.withMessage("حساب الکترونیکی وارد شده معتبر نمی باشد", email),
},
birth_date: {
required: helpers.withMessage("فیلد تاریخ تولد الزامی می باشد", required),
},
};
});
@@ -228,24 +222,18 @@ const handleSubmit = (withValidation: boolean) => {
<DataField
id="personal-data-gender"
label="جنسیت"
:error="formValidator$.gender"
>
<Select
v-model="personalData.gender!"
:options="['مرد', 'زن']"
variant="outlined"
:error="formValidator$.gender.$error"
/>
</DataField>
<DataField
id="personal-data-birth-date"
label="تاریsخ تولد"
:error="formValidator$.birth_date"
label="تاریخ تولد"
>
<Datepicker
v-model="personalData.birth_date!"
:error="formValidator$.birth_date.$error"
/>
<Datepicker v-model="personalData.birth_date!" />
</DataField>
<DataField
id="personal-data-phone"
@@ -261,20 +249,26 @@ const handleSubmit = (withValidation: boolean) => {
<DataField
id="personal-email"
label="حساب الکترونیکی"
:error="formValidator$.email"
>
<Input
v-model="personalData.email!"
variant="outlined"
:error="formValidator$.email.$error"
/>
</DataField>
</div>
<div class="w-full flex items-start justify-start gap-2 mt-5 px-2">
<Icon
name="bi:info-circle-fill"
class="**:fill-slate-400 mt-0.5"
/>
<p class="text-slate-400 text-[13px] font-medium">
با پر کردن فیلد های جنسیت, حساب الکترونیکی, تاریخ تولد مارا در خدمات رسانی شخصی سازی شده به شما
مشتریان عزیز یاری کنید
</p>
</div>
</ProfileSection>
</div>
<!-- <div class="w-fill grid grid-cols-1 lg:grid-cols-2">
</div> -->
</div>
</template>
+4
View File
@@ -1,6 +1,10 @@
<script setup lang="ts">
// meta
useSeoMeta({
title : "پنل کاربری اعلان ها"
});
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
@@ -5,6 +5,10 @@ import useGetAllOrders, {
// meta
useSeoMeta({
title : "پنل کاربری سفارشات"
});
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
+4
View File
@@ -13,6 +13,10 @@ import { QUERY_KEYS } from "~/constants";
// meta
useSeoMeta({
title : "پنل کاربری تیکت"
});
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
+4
View File
@@ -7,6 +7,10 @@ import useGetAllTickets, {
// meta
useSeoMeta({
title : "پنل کاربری تیکت ها"
});
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
+35 -67
View File
@@ -2,9 +2,7 @@
// imports
import useGetAllOrders from "~/composables/api/orders/useGetAllOrders";
import useCreateTicket, {
type CreateTicketRequest,
} from "~/composables/api/tickets/useCreateTicket";
import useCreateTicket, { type CreateTicketRequest } from "~/composables/api/tickets/useCreateTicket";
import useUploadAttachment from "~/composables/api/tickets/useUploadAttachment";
import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants";
@@ -14,6 +12,10 @@ import type { GetAllOrdersRequest } from "~/composables/api/orders/useGetAllOrde
// meta
useSeoMeta({
title: "پنل کاربری تیکت جدید",
});
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
@@ -83,44 +85,26 @@ const ordersFilter = computed<GetAllOrdersRequest>(() => {
// queries
const { data: orders, isLoading: ordersIsLoading } =
useGetAllOrders(ordersFilter);
const { data: orders, isLoading: ordersIsLoading } = useGetAllOrders(ordersFilter);
const { mutateAsync: createTicket, isPending: createTicketIsPending } =
useCreateTicket();
const { mutateAsync: createTicket, isPending: createTicketIsPending } = useCreateTicket();
const { mutateAsync: uploadAttachment, isPending: uploadAttachmentIsPending } =
useUploadAttachment();
const { mutateAsync: uploadAttachment, isPending: uploadAttachmentIsPending } = useUploadAttachment();
// computed
const formRules = computed(() => {
return {
ticket_category: {
required: helpers.withMessage(
"فیلد دسته بندی الزامی می باشد",
required
),
required: helpers.withMessage("فیلد دسته بندی الزامی می باشد", required),
},
subject: {
required: helpers.withMessage(
"فیلد عنوان تیکت الزامی می باشد",
required
),
minLength: helpers.withMessage(
"فیلد عنوان تیکت حداقل ۵ کرکتر می باشد",
minLength(5)
),
required: helpers.withMessage("فیلد عنوان تیکت الزامی می باشد", required),
minLength: helpers.withMessage("فیلد عنوان تیکت حداقل ۵ کرکتر می باشد", minLength(5)),
},
content: {
required: helpers.withMessage(
"فیلد متن تیکت الزامی می باشد",
required
),
minLength: helpers.withMessage(
"فیلد متن تیکت حداقل ۵ کرکتر می باشد",
minLength(5)
),
required: helpers.withMessage("فیلد متن تیکت الزامی می باشد", required),
minLength: helpers.withMessage("فیلد متن تیکت حداقل ۵ کرکتر می باشد", minLength(5)),
},
};
});
@@ -138,9 +122,7 @@ const handleUploadAttachment = (file: File) => {
},
onError: (error) => {
addToast({
message: error.message
? error.message
: "خطایی در آپلود پیوست رخ داد",
message: error.message ? error.message : "خطایی در آپلود پیوست رخ داد",
options: {
status: "error",
description: "لطفا مجدد تلاش کنید",
@@ -166,8 +148,7 @@ const handleSubmit = async () => {
message: "تیکت شما با موفقیت ثبت شد",
options: {
status: "success",
description:
"پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
description: "پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
},
});
},
@@ -188,7 +169,10 @@ const handleSubmit = async () => {
<template>
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="تیکت جدید" icon="bi:ticket" />
<ProfilePageTitle
title="تیکت جدید"
icon="bi:ticket"
/>
<ProfileSection title="ارتباط با پشتیبانی">
<template #button>
@@ -218,9 +202,7 @@ const handleSubmit = async () => {
<template #content>
<SelectGroup>
<SelectItem
v-for="(
category, index
) in ticketCategories"
v-for="(category, index) in ticketCategories"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
:value="category.value"
@@ -228,11 +210,12 @@ const handleSubmit = async () => {
<SelectItemIndicator
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
>
<Icon name="bi:check" size="20" />
<Icon
name="bi:check"
size="20"
/>
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
>
<SelectItemText class="text-end font-iran-yekan-x text-sm">
{{ category.title }}
</SelectItemText>
</SelectItem>
@@ -240,7 +223,11 @@ const handleSubmit = async () => {
</template>
</Select>
</DataField>
<DataField id="orders" :required="true" label="خرید یا سفارش">
<DataField
id="orders"
:required="true"
label="خرید یا سفارش"
>
<Select
placeholder="انتخاب کنید"
variant="outlined"
@@ -249,18 +236,10 @@ const handleSubmit = async () => {
>
<template #trigger>
<SelectValue
:class="
ticketData.order_id
? 'text-black'
: 'text-slate-400'
"
:class="ticketData.order_id ? 'text-black' : 'text-slate-400'"
class="font-iran-yekan-x text-sm text-start placeholder-slate-400"
>
{{
ticketData.order_id
? `شماره سفارش : ${ticketData.order_id}`
: "وارد نشده"
}}
{{ ticketData.order_id ? `شماره سفارش : ${ticketData.order_id}` : "وارد نشده" }}
</SelectValue>
</template>
@@ -283,15 +262,8 @@ const handleSubmit = async () => {
size="32px"
/>
<div
class="flex items-start gap-1 text-[10px]"
>
<span
>{{
order.count
}}
محصول</span
>
<div class="flex items-start gap-1 text-[10px]">
<span>{{ order.count }} محصول</span>
|
<span>
شماره سفارش :
@@ -353,11 +325,7 @@ const handleSubmit = async () => {
>
<Icon
v-if="createTicketIsPending"
:name="
createTicketIsPending
? 'svg-spinners:3-dots-bounce'
: 'bi:send'
"
:name="createTicketIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:send'"
/>
<span v-else>ارسال تیکت</span>
</Button>
+4
View File
@@ -26,6 +26,10 @@ definePageMeta({
// state
useSeoMeta({
title : "ورود به فروشگاه"
});
const { addToast } = useToast();
const { updateToken, updateRefreshToken } = useAuth();
+23 -46
View File
@@ -6,6 +6,12 @@ import usePersianDate from "~/composables/global/usePersianDate";
// meta
useSeoMeta({
title: "نتیجه تراکنش",
description : "",
keywords : ""
})
definePageMeta({
layout: "none",
});
@@ -22,11 +28,7 @@ const tracking_code = computed(() => route.query["tc"] as string);
// queries
const {
data: transaction,
isLoading: transactionIsLoading,
suspense,
} = useGetTransaction(tracking_code);
const { data: transaction, isLoading: transactionIsLoading, suspense } = useGetTransaction(tracking_code);
await suspense();
@@ -37,8 +39,7 @@ const statusVariants = computed(() => {
return {
background_color: "bg-success-500",
text_color: "text-white",
after_background_color:
"bg-success-600/50 shadow-[0px_40px_175px_1px] shadow-success-100",
after_background_color: "bg-success-600/50 shadow-[0px_40px_175px_1px] shadow-success-100",
icon: "bi:check",
title: "تراکنش موفق",
hue_deg: "[filter:_hue-rotate(260deg)] ",
@@ -47,8 +48,7 @@ const statusVariants = computed(() => {
return {
background_color: "bg-danger-500",
text_color: "text-white",
after_background_color:
"bg-danger-600/50 shadow-[0px_40px_175px_1px] shadow-danger-100",
after_background_color: "bg-danger-600/50 shadow-[0px_40px_175px_1px] shadow-danger-100",
icon: "bi:x",
title: "تراکنش ناموفق",
hue_deg: "[filter:_hue-rotate(120deg)]",
@@ -57,8 +57,7 @@ const statusVariants = computed(() => {
return {
background_color: "bg-slate-300",
text_color: "text-black",
after_background_color:
"bg-slate-600/50 shadow-[0px_40px_175px_1px] shadow-slate-100",
after_background_color: "bg-slate-600/50 shadow-[0px_40px_175px_1px] shadow-slate-100",
icon: "bi:question-circle",
title: "تراکنش معلق",
hue_deg: "[filter:_hue-rotate(0deg)]",
@@ -104,10 +103,7 @@ const statusTitle = computed(() => {
>
<div
class="w-full h-[4rem] lg:h-[5.2rem] absolute left-0 top-0 flex-center gap-2"
:class="[
statusVariants.background_color,
statusVariants.text_color,
]"
:class="[statusVariants.background_color, statusVariants.text_color]"
>
<Icon
:name="statusVariants.icon"
@@ -118,68 +114,48 @@ const statusTitle = computed(() => {
</h1>
</div>
<div
class="w-full flex flex-col gap-4 lg:gap-5 pt-[4.5rem] lg:pt-[5.5rem] p-1"
>
<div class="w-full flex flex-col gap-4 lg:gap-5 pt-[4.5rem] lg:pt-[5.5rem] p-1">
<div
v-if="transaction?.bank_result?.bank_type"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
>
<span class="font-medium">درگاه پرداخت</span>
<span class="opacity-50">{{
transaction?.bank_result?.bank_type
}}</span>
<span class="opacity-50">{{ transaction?.bank_result?.bank_type }}</span>
</div>
<div
v-if="transaction?.bank_result?.tracking_code"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
>
<span class="font-medium">کد پیگیری</span>
<span class="opacity-50 underline"
>#{{
transaction?.bank_result?.tracking_code
}}</span
>
<span class="opacity-50 underline">#{{ transaction?.bank_result?.tracking_code }}</span>
</div>
<div
v-if="transaction?.bank_result?.reference_number"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
>
<span class="font-medium">کد ارجاع</span>
<span class="opacity-50 underline"
>#{{
transaction?.bank_result?.reference_number
}}</span
>
<span class="opacity-50 underline">#{{ transaction?.bank_result?.reference_number }}</span>
</div>
<div
v-if="transaction?.bank_result?.amount"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
>
<span class="font-medium">مبلغ</span>
<span class="opacity-50">{{
transaction?.bank_result?.amount
}}</span>
<span class="opacity-50">{{ transaction?.bank_result?.amount }}</span>
</div>
<div
v-if="transaction?.bank_result?.created_at"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
>
<span class="font-medium">تاریخ</span>
<span class="opacity-50">{{
formatToPersian(
transaction?.bank_result?.created_at
)
}}</span>
<span class="opacity-50">{{ formatToPersian(transaction?.bank_result?.created_at) }}</span>
</div>
<div
v-if="transaction?.bank_result?.response_result"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
>
<span class="font-medium">وضعیت پرداخت</span>
<span class="opacity-50">{{
transaction?.bank_result?.status_detail
}}</span>
<span class="opacity-50">{{ transaction?.bank_result?.status_detail }}</span>
</div>
</div>
@@ -190,10 +166,11 @@ const statusTitle = computed(() => {
{{ transaction?.detail }}
</div>
<div
class="w-full flex flex-col-reverse lg:flex-row items-center justify-between gap-4 lg:gap-5"
>
<NuxtLink to="/" class="w-full">
<div class="w-full flex flex-col-reverse lg:flex-row items-center justify-between gap-4 lg:gap-5">
<NuxtLink
to="/"
class="w-full"
>
<Button
class="w-full rounded-full max-lg:py-2"
start-icon="ci:left-rotation"
+5
View File
@@ -0,0 +1,5 @@
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook("page:finish", () => {
window.scrollTo(0, 0);
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

-1
View File
@@ -1 +0,0 @@
Binary file not shown.
+1 -4
View File
@@ -62,10 +62,7 @@ declare global {
type ProductDetailItem = {
id: number;
title: string;
detail_text1: string;
detail_text2: string;
detail_text3: string;
detail_text4: string;
texts: string[];
};
type ProductDetail = {