This commit is contained in:
Mamalizz
2025-05-12 15:28:12 +03:30
34 changed files with 350 additions and 281 deletions
+8
View File
@@ -5,6 +5,14 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
// state // 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 { $updateAvailable: updateAvailable, $handleUpdate: handleUpdate } = useNuxtApp();
const closeModal = () => { const closeModal = () => {
+25 -18
View File
@@ -10,6 +10,7 @@ type Props = {
description: string; description: string;
picture: string; picture: string;
darkLayer?: boolean; darkLayer?: boolean;
isActive: boolean;
}; };
// props // props
@@ -23,39 +24,45 @@ const { colorObject } = useImageColor(`#category-image-${id.value}`);
</script> </script>
<template> <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%]"> <div class="group relative rounded-150 overflow-hidden w-full aspect-square bg-white brightness-[97%]">
<NuxtImg <Transition name="fade">
:id="`category-image-${id}`" <video
class="group-hover:scale-105 transition-transform duration-200 absolute object-contain size-full" v-if="isActive"
:src="picture" src="/video/category.mp4"
alt="" 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 <div
v-if="darkLayer" v-if="darkLayer"
class="bg-linear-to-t from-black/50 to-transparent to-40% absolute z-10 size-full" class="bg-linear-to-t from-black/50 to-transparent to-40% absolute z-10 size-full"
/> />
<div <div class="absolute z-20 bottom-0 p-4 md:p-6 flex items-center justify-between w-full">
class="absolute z-20 bottom-0 p-4 md:p-6 flex items-center justify-between w-full"
>
<div <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" class="typo-sub-h-sm md:typo-sub-h-md"
> >
{{ category }} {{ category }}
</div> </div>
<Icon <Icon
name="ci:arrow-left" name="ci:arrow-left"
class="size-5 md:size-6" class="size-5 md:size-6"
:class=" :class="colorObject?.isLight && !darkLayer ? '**:stroke-black' : '**:stroke-white'"
colorObject?.isLight && !darkLayer
? '**:stroke-black'
: '**:stroke-white'
"
/> />
</div> </div>
</div> </div>
+14 -14
View File
@@ -27,12 +27,12 @@ const progressStyle = computed(() => {
// methods // methods
const onAssetLoaded = () => { // const onAssetLoaded = () => {
clearInterval(progressInterval.value!); // clearInterval(progressInterval.value!);
criticalLoad.value = false; // criticalLoad.value = false;
assetLoadingProgress.value = 100; // assetLoadingProgress.value = 100;
isAssetLoaded.value = true; // isAssetLoaded.value = true;
}; // };
const onAssetFinished = () => { const onAssetFinished = () => {
gsap.to("#loading-overlay", { gsap.to("#loading-overlay", {
@@ -59,15 +59,15 @@ onMounted(() => {
if (!isSiteLoadingDisabled.value) { if (!isSiteLoadingDisabled.value) {
isWindowScrollLocked.value = true; 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) { // if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
onAssetLoaded(); // onAssetLoaded();
} // }
progressInterval.value = setInterval(() => { progressInterval.value = setInterval(() => {
assetLoadingProgress.value += Math.random() * 10; assetLoadingProgress.value += Math.random() * 50;
}, 250); }, 150);
gsap.to("#loading-overlay", { gsap.to("#loading-overlay", {
opacity: 1, opacity: 1,
@@ -100,7 +100,7 @@ onMounted(() => {
</div> </div>
</div> </div>
<video <!-- <video
id="heymlz-loading-animation" id="heymlz-loading-animation"
muted muted
autoplay autoplay
@@ -114,6 +114,6 @@ onMounted(() => {
:style="{ :style="{
mask: 'linear-gradient(to bottom, rgba(0,0,0,0) 0%, black 20%, black 80%, rgba(0,0,0,0) 100%)', mask: 'linear-gradient(to bottom, rgba(0,0,0,0) 0%, black 20%, black 80%, rgba(0,0,0,0) 100%)',
}" }"
/> /> -->
</div> </div>
</template> </template>
+1 -1
View File
@@ -34,7 +34,7 @@ const page = computed({
<PaginationRoot <PaginationRoot
:total="total" :total="total"
:sibling-count="1" :sibling-count="1"
:items-per-page="9" :items-per-page="15"
show-edges show-edges
v-model:page="page" v-model:page="page"
> >
+1 -1
View File
@@ -35,7 +35,7 @@ withDefaults(defineProps<Props>(), {
</NuxtLink> </NuxtLink>
</div> </div>
<ul <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 <ProductCard
v-for="product in products" v-for="product in products"
@@ -49,7 +49,7 @@ const changeSlide = (id: number) => {
<template> <template>
<div class="sticky top-10"> <div class="sticky top-10">
<div class="flex flex-col relative gap-6"> <div class="flex flex-col relative gap-4">
<div <div
class="bg-white brightness-[97%] w-full relative aspect-square overflow-hidden rounded-[12px] md:rounded-200" 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" class="w-full"
> >
<SwiperSlide <SwiperSlide
class="py-4"
v-for="slide in slides" v-for="slide in slides"
:key="slide.id" :key="slide.id"
> >
@@ -94,7 +93,6 @@ const changeSlide = (id: number) => {
v-if="emptySlidesCount > 0" v-if="emptySlidesCount > 0"
v-for="slide in emptySlidesCount" v-for="slide in emptySlidesCount"
:key="slide" :key="slide"
class="py-4"
> >
<div <div
class="brightness-[97%] flex-center bg-white aspect-square rounded-[12px] md:rounded-200 w-full" class="brightness-[97%] flex-center bg-white aspect-square rounded-[12px] md:rounded-200 w-full"
@@ -38,9 +38,9 @@ const limitedColors = computed(() => {
<template> <template>
<li class="w-full"> <li class="w-full">
<NuxtLink :to="'/product/' + id"> <NuxtLink :to="'/product/' + id">
<div class="@container"> <div class="@container group">
<div <div
class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[98%] overflow-hidden p-6" class="group relative size-full aspect-square rounded-xl @[280px]:rounded-2xl bg-white brightness-[95%] overflow-hidden p-6"
> >
<NuxtImg <NuxtImg
:id="`product-image-${id}`" :id="`product-image-${id}`"
@@ -49,10 +49,10 @@ const limitedColors = computed(() => {
alt="product-background" alt="product-background"
/> />
<div <!-- <div
v-if="darkLayer" v-if="darkLayer"
class="bg-linear-to-t inset-0 from-black/50 to-transparent to-55% absolute z-10 size-full" class="bg-linear-to-t inset-0 from-black/50 to-transparent to-55% absolute z-10 size-full"
/> /> -->
<div <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" 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> </Tag>
</div> </div>
<div <div
:class="colorObject?.isLight && !darkLayer ? 'text-black' : 'text-white'" 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"
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"
> >
<div class="flex flex-col gap-2 items-start w-full"> <div
<span class="@max-[280px]:hidden typo-sub-h-md @[280px]:typo-sub-h-lg truncate w-full"> class="items-center flex gap-2 @[280px]:mt-1 transition-all translate-y-1 group-hover:translate-y-0 "
{{ title }} >
</span> <ColorCircle
<div class="flex items-center justify-between w-full mt-1"> v-for="color in limitedColors"
<div class="flex items-center gap-2 @[280px]:mt-1"> :key="color"
<ColorCircle :style="{ backgroundColor: color }"
v-for="color in limitedColors" class="!size-5 @[280px]:!size-6"
: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> </div>
</div> </div>
</div> </div>
<div class="flex flex-col gap-1 px-2 items-start w-full text-black mt-4 @[280px]:hidden"> <div class="flex flex-col gap-1 px-2 items-start w-full text-black mt-4">
<span class="typo-sub-h-sm w-full truncate"> <span class="typo-sub-h-sm font-normal w-full truncate">
{{ title }} {{ title }}
</span> </span>
<div class="@[280px]:hidden flex items-center justify-between w-full mt-1"> <div class="flex items-center justify-between w-full mt-1">
<span class="typo-p-xs !font-semibold whitespace-nowrap"> <span class="typo-p-xs !font-bold whitespace-nowrap">
{{ price }} {{ price }}
</span> </span>
</div> </div>
@@ -2,34 +2,39 @@
// imports // imports
import useGetCategories from "~/composables/api/product/useGetCategories"; import useGetCategories from "~/composables/api/product/useGetCategories";
import useGetProducts, { import useGetProducts, { type GetProductsFilters } from "~/composables/api/products/useGetProducts";
type GetProductsFilters,
} from "~/composables/api/products/useGetProducts";
import { PRODUCT_RANGE } from "~/constants"; import { PRODUCT_RANGE } from "~/constants";
// state // state
const route = useRoute();
const router = useRouter();
const params = inject("params") as GetProductsFilters; 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([ const sort_filter = ref([
{ title: "جدیدترین ها", value: "newest" }, { title: "جدیدترین ها", value: "newest" },
{ title: "گران ترین ها", value: "price" }, { title: "گران ترین ها", value: "price" },
{ title: "ارزان ترین ها", value: "-price" }, { title: "ارزان ترین ها", value: "-price" },
]); ]);
const sliderValue = ref([ const sliderValue = ref([params.price_gte ?? PRODUCT_RANGE.min, params.price_lte ?? PRODUCT_RANGE.max]);
params.price_gte ?? PRODUCT_RANGE.min,
params.price_lte ?? PRODUCT_RANGE.max,
]);
const has_discount = ref(Boolean(params.has_discount) ?? false); const has_discount = ref(Boolean(params.has_discount) ?? false);
const in_stock = ref(Boolean(params.in_stock) ?? false); const in_stock = ref(Boolean(params.in_stock) ?? false);
const sliderValueDebounced = refDebounced(sliderValue, 1000); const sliderValueDebounced = refDebounced(sliderValue, 1000);
const filtersSuccessMessage = ref<{ title: string; status: string } | null>( const filtersSuccessMessage = ref<{ title: string; status: string } | null>(null);
null
);
// queries // queries
@@ -41,7 +46,7 @@ const filters = computed(() => {
price_lte: params.price_lte ?? PRODUCT_RANGE.max, price_lte: params.price_lte ?? PRODUCT_RANGE.max,
in_stock: params.in_stock ?? false, in_stock: params.in_stock ?? false,
has_discount: params.has_discount ?? false, has_discount: params.has_discount ?? false,
category: params.category ?? undefined, category: currentCategory.value,
page: params.page ?? 1, page: params.page ?? 1,
}; };
}); });
@@ -50,8 +55,7 @@ const { data: categories, suspense } = useGetCategories();
await suspense(); await suspense();
const { isPending: productsIsPending, status: productsStatus } = const { isPending: productsIsPending, status: productsStatus } = useGetProducts(filters);
useGetProducts(filters);
// computed // computed
@@ -77,10 +81,9 @@ const resetFilters = () => {
sliderValue.value = [PRODUCT_RANGE.min, PRODUCT_RANGE.max]; sliderValue.value = [PRODUCT_RANGE.min, PRODUCT_RANGE.max];
has_discount.value = false; has_discount.value = false;
in_stock.value = false; in_stock.value = false;
params.category = undefined;
};
// watch router.push({ path: `/products/`, query: { ...route.query } });
};
watch( watch(
() => sliderValueDebounced.value, () => sliderValueDebounced.value,
@@ -123,10 +126,11 @@ watch(
<div class="size-full flex flex-col gap-14 justify-between"> <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-10">
<div class="flex flex-col items-center w-full gap-5"> <div class="flex flex-col items-center w-full gap-5">
<div <div class="flex items-center justify-start gap-2 text-lg w-full">
class="flex items-center justify-start gap-2 text-lg w-full" <Icon
> name="ci:filter-list"
<Icon name="ci:filter-list" size="24" /> size="24"
/>
ترتیب بر اساس ترتیب بر اساس
</div> </div>
<div class="w-full flex items-center gap-2"> <div class="w-full flex items-center gap-2">
@@ -134,11 +138,7 @@ watch(
v-for="(sort, index) in sort_filter" v-for="(sort, index) in sort_filter"
:key="index" :key="index"
@click="params.sort = sort.value" @click="params.sort = sort.value"
:class=" :class="params.sort == sort.value ? 'bg-black text-white' : 'bg-slate-100'"
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="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-md text-sm"
> >
{{ sort.title }} {{ sort.title }}
@@ -147,20 +147,25 @@ watch(
</div> </div>
<div class="flex flex-col w-full gap-5"> <div class="flex flex-col w-full gap-5">
<div <div class="flex items-center justify-start gap-2 text-lg w-full">
class="flex items-center justify-start gap-2 text-lg w-full" <Icon
> name="ci:grid"
<Icon name="ci:grid" size="24" /> size="24"
/>
دسته بندی دسته بندی
</div> </div>
<ComboBox :options="allCategories" v-model="params.category" /> <ComboBox
:options="allCategories"
v-model="currentCategory"
/>
</div> </div>
<div class="flex flex-col w-full gap-5"> <div class="flex flex-col w-full gap-5">
<div <div class="flex items-center justify-start gap-2 text-lg w-full">
class="flex items-center justify-start gap-2 text-lg w-full" <Icon
> name="ci:scan-box"
<Icon name="ci:scan-box" size="24" /> size="24"
/>
محدوده قیمت محدوده قیمت
</div> </div>
<SliderRoot <SliderRoot
@@ -171,12 +176,8 @@ watch(
:max="PRODUCT_RANGE.max" :max="PRODUCT_RANGE.max"
:step="1000" :step="1000"
> >
<SliderTrack <SliderTrack class="bg-black/10 relative grow rounded-full h-[3px]">
class="bg-black/10 relative grow rounded-full h-[3px]" <SliderRange class="absolute bg-black rounded-full h-full" />
>
<SliderRange
class="absolute bg-black rounded-full h-full"
/>
</SliderTrack> </SliderTrack>
<SliderThumb <SliderThumb
v-for="thumb in Object.keys(PRODUCT_RANGE)" v-for="thumb in Object.keys(PRODUCT_RANGE)"
@@ -227,15 +228,9 @@ watch(
: ' text-danger-600 bg-danger-100 border-danger-600' : ' text-danger-600 bg-danger-100 border-danger-600'
" "
> >
<span class="text-sm">{{ <span class="text-sm">{{ filtersSuccessMessage.title }}</span>
filtersSuccessMessage.title
}}</span>
<Icon <Icon
:name=" :name="filtersSuccessMessage.status == 'success' ? 'bi:check' : 'bi:x'"
filtersSuccessMessage.status == 'success'
? 'bi:check'
: 'bi:x'
"
size="20" size="20"
/> />
</div> </div>
@@ -246,14 +241,29 @@ watch(
@click="resetFilters" @click="resetFilters"
class="w-full rounded-full py-4 !cursor-pointer disabled:pointer-events-none z-[3]" class="w-full rounded-full py-4 !cursor-pointer disabled:pointer-events-none z-[3]"
> >
<Transition name="fade" mode="out-in"> <Transition
<span v-if="productsIsPending" class="flex-center gap-3"> 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>
<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> </span>
</Transition> </Transition>
</Button> </Button>
+32 -5
View File
@@ -4,17 +4,27 @@
import { Swiper, SwiperSlide } from "swiper/vue"; import { Swiper, SwiperSlide } from "swiper/vue";
import type { SwiperClass } from "swiper/react"; import type { SwiperClass } from "swiper/react";
import useHomeData from "~/composables/api/home/useHomeData"; import useHomeData from "~/composables/api/home/useHomeData";
import { EffectCoverflow } from "swiper/modules";
// state // state
const { data: homeData } = useHomeData(); const { data: homeData } = useHomeData();
const swiper_instance = ref<SwiperClass | null>(null); const swiper_instance = ref<SwiperClass | null>(null);
const activeIndex = ref(0);
const slideElement = ref<HTMLDivElement | null>(null);
const { width: slideWidth } = useElementSize(slideElement);
// methods // methods
const onSwiper = (swiper: SwiperClass) => { const onSwiper = (swiper: SwiperClass) => {
swiper_instance.value = swiper; swiper_instance.value = swiper;
}; };
const onSlideChange = (swiper: SwiperClass) => {
activeIndex.value = swiper.realIndex;
};
</script> </script>
<template> <template>
@@ -23,7 +33,7 @@ const onSwiper = (swiper: SwiperClass) => {
class="flex flex-col justify-center gap-4 bg-black sm:min-h-[110svh] relative overflow-hidden shrink-0 py-24 lg:py-32" 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"> <div class="w-full relative flex-center z-10 container">
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4 2xl:typo-h-2"> دسته بندی ها </span> <span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4 min-[2000px]:typo-h-2"> دسته بندی ها </span>
</div> </div>
<div class="w-full mt-44 lg:mt-64 relative"> <div class="w-full mt-44 lg:mt-64 relative">
@@ -38,8 +48,17 @@ const onSwiper = (swiper: SwiperClass) => {
:loop="true" :loop="true"
:centered-slides="true" :centered-slides="true"
:slides-per-view="1.5" :slides-per-view="1.5"
:space-between="20"
@swiper="onSwiper" @swiper="onSwiper"
@slideChange="onSlideChange"
:modules="[EffectCoverflow]"
:effect="'coverflow'"
:coverflowEffect="{
rotate: 10,
stretch: -100,
depth: 200,
modifier: 1,
slideShadows: true,
}"
:breakpoints="{ :breakpoints="{
640: { 640: {
centeredSlides: true, centeredSlides: true,
@@ -52,7 +71,8 @@ const onSwiper = (swiper: SwiperClass) => {
}" }"
> >
<SwiperSlide <SwiperSlide
v-for="slide in homeData!.sub_categories" ref="slideElement"
v-for="(slide, index) in homeData!.sub_categories"
:key="slide.id" :key="slide.id"
> >
<CategoryCard <CategoryCard
@@ -61,6 +81,7 @@ const onSwiper = (swiper: SwiperClass) => {
:category="slide.name" :category="slide.name"
:picture="slide.image" :picture="slide.image"
:count="slide.product_count" :count="slide.product_count"
:isActive="activeIndex === index"
description="توضیحات دسته بندی" description="توضیحات دسته بندی"
/> />
</SwiperSlide> </SwiperSlide>
@@ -69,7 +90,10 @@ const onSwiper = (swiper: SwiperClass) => {
<div <div
v-if="!swiper_instance?.isBeginning" v-if="!swiper_instance?.isBeginning"
@click="swiper_instance?.slidePrev()" @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 <Icon
name="ci:arrow-right" name="ci:arrow-right"
@@ -80,7 +104,10 @@ const onSwiper = (swiper: SwiperClass) => {
<div <div
v-if="!swiper_instance?.isEnd" v-if="!swiper_instance?.isEnd"
@click="swiper_instance?.slideNext()" @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 <Icon
name="ci:arrow-left" name="ci:arrow-left"
@@ -39,8 +39,8 @@ const useGetProducts = (params?: ComputedRef<GetProductsFilters>) => {
category: params?.category, category: params?.category,
price_gte: params?.price_gte, price_gte: params?.price_gte,
price_lte: params?.price_lte, price_lte: params?.price_lte,
offset: Number(params?.page) * 12 - 12, offset: Number(params?.page) * 15 - 15,
limit: 12 limit: 15
} }
} }
); );
+5 -3
View File
@@ -12,9 +12,6 @@ export default defineNuxtConfig({
}, },
app: { app: {
head: {
title: "فروشگاه هی ملز",
},
pageTransition: { pageTransition: {
name: "fade", name: "fade",
mode: "out-in", mode: "out-in",
@@ -67,8 +64,13 @@ export default defineNuxtConfig({
"@formkit/auto-animate/nuxt", "@formkit/auto-animate/nuxt",
"@vite-pwa/nuxt", "@vite-pwa/nuxt",
"@nuxt/image", "@nuxt/image",
"@nuxtjs/seo",
], ],
sitemap: {
enabled: false,
},
pwa: { pwa: {
strategies: "injectManifest", strategies: "injectManifest",
srcDir: "public", srcDir: "public",
+1
View File
@@ -19,6 +19,7 @@
"@nuxt/icon": "^1.10.3", "@nuxt/icon": "^1.10.3",
"@nuxt/image": "^1.10.0", "@nuxt/image": "^1.10.0",
"@nuxtjs/google-fonts": "^3.2.0", "@nuxtjs/google-fonts": "^3.2.0",
"@nuxtjs/seo": "^3.0.3",
"@tanstack/vue-query": "^5.62.2", "@tanstack/vue-query": "^5.62.2",
"@tanstack/vue-query-devtools": "^5.62.3", "@tanstack/vue-query-devtools": "^5.62.3",
"@vite-pwa/nuxt": "^0.10.6", "@vite-pwa/nuxt": "^0.10.6",
+42 -31
View File
@@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
// import // import
import useGetArticle from "~/composables/api/blog/useGetArticle"; 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); 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 // ssr
const response = await suspense(); const response = await suspense();
@@ -19,16 +26,19 @@ const response = await suspense();
if (response.isError) { if (response.isError) {
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: `Error in categories page prefetch` statusMessage: `Error in categories page prefetch`,
}); });
} }
</script> </script>
<template> <template>
<div class="container"> <div class="container">
<div class="w-full h-[80svh] rounded-3xl relative overflow-hidden"> <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 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"> <div class="absolute pl-10 right-10 bottom-10 flex flex-col gap-6">
<h1 class="typo-h-4 text-white pl-8"> <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 justify-between">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div <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 <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 <NuxtImg
class="size-full object-cover absolute" class="size-full object-cover absolute"
:src="article!.author.profile_photo" :src="article!.author.profile_photo"
@@ -55,50 +65,51 @@ if (response.isError) {
/> />
</div> </div>
<span class="typo-label-sm"> <span class="typo-label-sm">
{{ article!.author.full_name }} {{ article!.author.full_name }}
</span> </span>
</div> </div>
<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"> 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="typo-label-sm mt-0.5"> دسته بندی موبایل </span>
</span>
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<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"> 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"> <Icon
۲۴ مهر 1403 name="ci:calendar"
</span> size="24px"
class="**:stroke-white"
/>
<span class="typo-label-sm mt-0.5"> ۲۴ مهر 1403 </span>
</div> </div>
<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"> 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" /> >
<Icon
name="ci:eye-open"
size="24px"
class="**:stroke-white"
/>
<span class="typo-label-sm mt-0.5"> <span class="typo-label-sm mt-0.5">
{{ article!.views }} {{ article!.views }}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="flex gap-4 mt-8"> <div class="flex gap-4 mt-8">
<div <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" 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" v-html="article!.content"
/> />
<aside class="mt-8 p-8 h-fit bg-slate-100 w-[400px] sticky top-4 rounded-3xl"> <aside class="mt-8 p-8 h-fit bg-slate-100 w-[400px] sticky top-4 rounded-3xl">asdsa</aside>
asdsa
</aside>
</div> </div>
</div> </div>
</template> </template>
+4
View File
@@ -7,6 +7,10 @@ import ArticlesList from "~/components/articles/ArticlesList.vue";
// state // state
useSeoMeta({
title : "مقالات"
});
const page = ref(1); const page = ref(1);
const search = ref(""); const search = ref("");
const debouncedSearch = refDebounced(search, 700); const debouncedSearch = refDebounced(search, 700);
+4 -1
View File
@@ -3,10 +3,13 @@
const route = useRoute(); const route = useRoute();
useSeoMeta({
title: "ثبت سفارش",
});
definePageMeta({ definePageMeta({
layout: "cart", layout: "cart",
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
pageTitle: "ثبت سفارش",
prevPage: { name: "cart-delivery", label: "انتخاب آدرس" }, prevPage: { name: "cart-delivery", label: "انتخاب آدرس" },
nextPage: { name: "payment", label: "پرداخت" }, nextPage: { name: "payment", label: "پرداخت" },
}); });
+5 -1
View File
@@ -7,10 +7,14 @@ import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// meta // meta
useSeoMeta({
title: "انتخاب آدرس",
});
definePageMeta({ definePageMeta({
layout: "cart", layout: "cart",
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
pageTitle: "انتخاب آدرس",
prevPage: { name: "cart", label: "سبد خرید" }, prevPage: { name: "cart", label: "سبد خرید" },
nextPage: { name: "cart-checkout", label: "تسویه حساب", query: "ZARINPAL" }, nextPage: { name: "cart-checkout", label: "تسویه حساب", query: "ZARINPAL" },
}); });
+4 -1
View File
@@ -5,10 +5,13 @@ import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// meta // meta
useSeoMeta({
title : "سبد خرید"
});
definePageMeta({ definePageMeta({
layout: "cart", layout: "cart",
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
pageTitle: "سبد خرید",
prevPage: { name: "index", label: "بازگشت به خانه" }, prevPage: { name: "index", label: "بازگشت به خانه" },
nextPage: { name: "cart-delivery", label: "انتخاب آدرس" }, nextPage: { name: "cart-delivery", label: "انتخاب آدرس" },
}); });
+4
View File
@@ -5,6 +5,10 @@ import useGetCategories from "~/composables/api/product/useGetCategories";
// state // state
useSeoMeta({
title : "دسته بندی ها"
});
const { data: categories, suspense } = useGetCategories(); const { data: categories, suspense } = useGetCategories();
const search = ref(""); const search = ref("");
+4
View File
@@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
// state // state
useSeoMeta({
title : "ارتباط با ما"
});
const contactInfo = ref({ const contactInfo = ref({
name: "", name: "",
email: "", email: "",
-2
View File
@@ -4,8 +4,6 @@
import useHomeData from "~/composables/api/home/useHomeData"; import useHomeData from "~/composables/api/home/useHomeData";
import ProductsGrid from "~/components/global/ProductsGrid.vue"; import ProductsGrid from "~/components/global/ProductsGrid.vue";
import { useStorage } from "@vueuse/core";
// state // state
const { data: homeData, suspense } = useHomeData(); const { data: homeData, suspense } = useHomeData();
+8
View File
@@ -16,6 +16,14 @@ const page = ref(1);
const { suspense: suspenseProduct, data: product } = useGetProduct(id); const { suspense: suspenseProduct, data: product } = useGetProduct(id);
const { suspense: suspenseComments } = useGetComments(id, page); 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 selectedVariant = ref<ProductVariant>();
const showChatButton = ref(true); const showChatButton = ref(true);
@@ -6,6 +6,22 @@ import { PRODUCT_RANGE } from "~/constants";
// state // 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", { const params: GetProductsFilters = useUrlSearchParams("history", {
removeFalsyValues: true, removeFalsyValues: true,
removeNullishValues: true, removeNullishValues: true,
@@ -19,7 +35,7 @@ const filters = computed(() => {
price_lte: params.price_lte ?? PRODUCT_RANGE.max, price_lte: params.price_lte ?? PRODUCT_RANGE.max,
in_stock: params.in_stock ?? false, in_stock: params.in_stock ?? false,
has_discount: params.has_discount ?? 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, page: params.page ?? 1,
}; };
}); });
@@ -133,7 +149,7 @@ watch(
class="!p-0" class="!p-0"
/> />
<div <div
v-if="data && paginationData && data.count > 10" v-if="data && paginationData && data.count > 15"
class="w-full flex-center py-10" class="w-full flex-center py-10"
> >
<Pagination <Pagination
+4
View File
@@ -5,6 +5,10 @@ import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
// meta // meta
useSeoMeta({
title : "پنل کاربری آدرس ها"
});
definePageMeta({ definePageMeta({
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
layout: "profile", layout: "profile",
+4
View File
@@ -11,6 +11,10 @@ import { QUERY_KEYS } from "~/constants";
// meta // meta
useSeoMeta({
title : "پنل کاربری"
});
definePageMeta({ definePageMeta({
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
layout: "profile", layout: "profile",
+4
View File
@@ -1,6 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
// meta // meta
useSeoMeta({
title : "پنل کاربری اعلان ها"
});
definePageMeta({ definePageMeta({
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
layout: "profile", layout: "profile",
@@ -5,6 +5,10 @@ import useGetAllOrders, {
// meta // meta
useSeoMeta({
title : "پنل کاربری سفارشات"
});
definePageMeta({ definePageMeta({
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
layout: "profile", layout: "profile",
+4
View File
@@ -13,6 +13,10 @@ import { QUERY_KEYS } from "~/constants";
// meta // meta
useSeoMeta({
title : "پنل کاربری تیکت"
});
definePageMeta({ definePageMeta({
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
layout: "profile", layout: "profile",
+4
View File
@@ -7,6 +7,10 @@ import useGetAllTickets, {
// meta // meta
useSeoMeta({
title : "پنل کاربری تیکت ها"
});
definePageMeta({ definePageMeta({
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
layout: "profile", layout: "profile",
+35 -67
View File
@@ -2,9 +2,7 @@
// imports // imports
import useGetAllOrders from "~/composables/api/orders/useGetAllOrders"; import useGetAllOrders from "~/composables/api/orders/useGetAllOrders";
import useCreateTicket, { import useCreateTicket, { type CreateTicketRequest } from "~/composables/api/tickets/useCreateTicket";
type CreateTicketRequest,
} from "~/composables/api/tickets/useCreateTicket";
import useUploadAttachment from "~/composables/api/tickets/useUploadAttachment"; import useUploadAttachment from "~/composables/api/tickets/useUploadAttachment";
import { useToast } from "~/composables/global/useToast"; import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants"; import { QUERY_KEYS } from "~/constants";
@@ -14,6 +12,10 @@ import type { GetAllOrdersRequest } from "~/composables/api/orders/useGetAllOrde
// meta // meta
useSeoMeta({
title: "پنل کاربری تیکت جدید",
});
definePageMeta({ definePageMeta({
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
layout: "profile", layout: "profile",
@@ -83,44 +85,26 @@ const ordersFilter = computed<GetAllOrdersRequest>(() => {
// queries // queries
const { data: orders, isLoading: ordersIsLoading } = const { data: orders, isLoading: ordersIsLoading } = useGetAllOrders(ordersFilter);
useGetAllOrders(ordersFilter);
const { mutateAsync: createTicket, isPending: createTicketIsPending } = const { mutateAsync: createTicket, isPending: createTicketIsPending } = useCreateTicket();
useCreateTicket();
const { mutateAsync: uploadAttachment, isPending: uploadAttachmentIsPending } = const { mutateAsync: uploadAttachment, isPending: uploadAttachmentIsPending } = useUploadAttachment();
useUploadAttachment();
// computed // computed
const formRules = computed(() => { const formRules = computed(() => {
return { return {
ticket_category: { ticket_category: {
required: helpers.withMessage( required: helpers.withMessage("فیلد دسته بندی الزامی می باشد", required),
"فیلد دسته بندی الزامی می باشد",
required
),
}, },
subject: { subject: {
required: helpers.withMessage( required: helpers.withMessage("فیلد عنوان تیکت الزامی می باشد", required),
"فیلد عنوان تیکت الزامی می باشد", minLength: helpers.withMessage("فیلد عنوان تیکت حداقل ۵ کرکتر می باشد", minLength(5)),
required
),
minLength: helpers.withMessage(
"فیلد عنوان تیکت حداقل ۵ کرکتر می باشد",
minLength(5)
),
}, },
content: { content: {
required: helpers.withMessage( required: helpers.withMessage("فیلد متن تیکت الزامی می باشد", required),
"فیلد متن تیکت الزامی می باشد", minLength: helpers.withMessage("فیلد متن تیکت حداقل ۵ کرکتر می باشد", minLength(5)),
required
),
minLength: helpers.withMessage(
"فیلد متن تیکت حداقل ۵ کرکتر می باشد",
minLength(5)
),
}, },
}; };
}); });
@@ -138,9 +122,7 @@ const handleUploadAttachment = (file: File) => {
}, },
onError: (error) => { onError: (error) => {
addToast({ addToast({
message: error.message message: error.message ? error.message : "خطایی در آپلود پیوست رخ داد",
? error.message
: "خطایی در آپلود پیوست رخ داد",
options: { options: {
status: "error", status: "error",
description: "لطفا مجدد تلاش کنید", description: "لطفا مجدد تلاش کنید",
@@ -166,8 +148,7 @@ const handleSubmit = async () => {
message: "تیکت شما با موفقیت ثبت شد", message: "تیکت شما با موفقیت ثبت شد",
options: { options: {
status: "success", status: "success",
description: description: "پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
"پس از بررسی پشتیبانی به شما اطلاع رسانی می شود",
}, },
}); });
}, },
@@ -188,7 +169,10 @@ const handleSubmit = async () => {
<template> <template>
<div class="w-full flex flex-col gap-5"> <div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="تیکت جدید" icon="bi:ticket" /> <ProfilePageTitle
title="تیکت جدید"
icon="bi:ticket"
/>
<ProfileSection title="ارتباط با پشتیبانی"> <ProfileSection title="ارتباط با پشتیبانی">
<template #button> <template #button>
@@ -218,9 +202,7 @@ const handleSubmit = async () => {
<template #content> <template #content>
<SelectGroup> <SelectGroup>
<SelectItem <SelectItem
v-for="( v-for="(category, index) in ticketCategories"
category, index
) in ticketCategories"
:key="index" :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" 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" :value="category.value"
@@ -228,11 +210,12 @@ const handleSubmit = async () => {
<SelectItemIndicator <SelectItemIndicator
class="absolute left-0 w-[25px] inline-flex items-center justify-center" 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> </SelectItemIndicator>
<SelectItemText <SelectItemText class="text-end font-iran-yekan-x text-sm">
class="text-end font-iran-yekan-x text-sm"
>
{{ category.title }} {{ category.title }}
</SelectItemText> </SelectItemText>
</SelectItem> </SelectItem>
@@ -240,7 +223,11 @@ const handleSubmit = async () => {
</template> </template>
</Select> </Select>
</DataField> </DataField>
<DataField id="orders" :required="true" label="خرید یا سفارش"> <DataField
id="orders"
:required="true"
label="خرید یا سفارش"
>
<Select <Select
placeholder="انتخاب کنید" placeholder="انتخاب کنید"
variant="outlined" variant="outlined"
@@ -249,18 +236,10 @@ const handleSubmit = async () => {
> >
<template #trigger> <template #trigger>
<SelectValue <SelectValue
:class=" :class="ticketData.order_id ? 'text-black' : 'text-slate-400'"
ticketData.order_id
? 'text-black'
: 'text-slate-400'
"
class="font-iran-yekan-x text-sm text-start placeholder-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> </SelectValue>
</template> </template>
@@ -283,15 +262,8 @@ const handleSubmit = async () => {
size="32px" size="32px"
/> />
<div <div class="flex items-start gap-1 text-[10px]">
class="flex items-start gap-1 text-[10px]" <span>{{ order.count }} محصول</span>
>
<span
>{{
order.count
}}
محصول</span
>
| |
<span> <span>
شماره سفارش : شماره سفارش :
@@ -353,11 +325,7 @@ const handleSubmit = async () => {
> >
<Icon <Icon
v-if="createTicketIsPending" v-if="createTicketIsPending"
:name=" :name="createTicketIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:send'"
createTicketIsPending
? 'svg-spinners:3-dots-bounce'
: 'bi:send'
"
/> />
<span v-else>ارسال تیکت</span> <span v-else>ارسال تیکت</span>
</Button> </Button>
+4
View File
@@ -26,6 +26,10 @@ definePageMeta({
// state // state
useSeoMeta({
title : "ورود به فروشگاه"
});
const { addToast } = useToast(); const { addToast } = useToast();
const { updateToken, updateRefreshToken } = useAuth(); const { updateToken, updateRefreshToken } = useAuth();
+23 -46
View File
@@ -6,6 +6,12 @@ import usePersianDate from "~/composables/global/usePersianDate";
// meta // meta
useSeoMeta({
title: "نتیجه تراکنش",
description : "",
keywords : ""
})
definePageMeta({ definePageMeta({
layout: "none", layout: "none",
}); });
@@ -22,11 +28,7 @@ const tracking_code = computed(() => route.query["tc"] as string);
// queries // queries
const { const { data: transaction, isLoading: transactionIsLoading, suspense } = useGetTransaction(tracking_code);
data: transaction,
isLoading: transactionIsLoading,
suspense,
} = useGetTransaction(tracking_code);
await suspense(); await suspense();
@@ -37,8 +39,7 @@ const statusVariants = computed(() => {
return { return {
background_color: "bg-success-500", background_color: "bg-success-500",
text_color: "text-white", text_color: "text-white",
after_background_color: after_background_color: "bg-success-600/50 shadow-[0px_40px_175px_1px] shadow-success-100",
"bg-success-600/50 shadow-[0px_40px_175px_1px] shadow-success-100",
icon: "bi:check", icon: "bi:check",
title: "تراکنش موفق", title: "تراکنش موفق",
hue_deg: "[filter:_hue-rotate(260deg)] ", hue_deg: "[filter:_hue-rotate(260deg)] ",
@@ -47,8 +48,7 @@ const statusVariants = computed(() => {
return { return {
background_color: "bg-danger-500", background_color: "bg-danger-500",
text_color: "text-white", text_color: "text-white",
after_background_color: after_background_color: "bg-danger-600/50 shadow-[0px_40px_175px_1px] shadow-danger-100",
"bg-danger-600/50 shadow-[0px_40px_175px_1px] shadow-danger-100",
icon: "bi:x", icon: "bi:x",
title: "تراکنش ناموفق", title: "تراکنش ناموفق",
hue_deg: "[filter:_hue-rotate(120deg)]", hue_deg: "[filter:_hue-rotate(120deg)]",
@@ -57,8 +57,7 @@ const statusVariants = computed(() => {
return { return {
background_color: "bg-slate-300", background_color: "bg-slate-300",
text_color: "text-black", text_color: "text-black",
after_background_color: after_background_color: "bg-slate-600/50 shadow-[0px_40px_175px_1px] shadow-slate-100",
"bg-slate-600/50 shadow-[0px_40px_175px_1px] shadow-slate-100",
icon: "bi:question-circle", icon: "bi:question-circle",
title: "تراکنش معلق", title: "تراکنش معلق",
hue_deg: "[filter:_hue-rotate(0deg)]", hue_deg: "[filter:_hue-rotate(0deg)]",
@@ -104,10 +103,7 @@ const statusTitle = computed(() => {
> >
<div <div
class="w-full h-[4rem] lg:h-[5.2rem] absolute left-0 top-0 flex-center gap-2" class="w-full h-[4rem] lg:h-[5.2rem] absolute left-0 top-0 flex-center gap-2"
:class="[ :class="[statusVariants.background_color, statusVariants.text_color]"
statusVariants.background_color,
statusVariants.text_color,
]"
> >
<Icon <Icon
:name="statusVariants.icon" :name="statusVariants.icon"
@@ -118,68 +114,48 @@ const statusTitle = computed(() => {
</h1> </h1>
</div> </div>
<div <div class="w-full flex flex-col gap-4 lg:gap-5 pt-[4.5rem] lg:pt-[5.5rem] p-1">
class="w-full flex flex-col gap-4 lg:gap-5 pt-[4.5rem] lg:pt-[5.5rem] p-1"
>
<div <div
v-if="transaction?.bank_result?.bank_type" v-if="transaction?.bank_result?.bank_type"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs" class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
> >
<span class="font-medium">درگاه پرداخت</span> <span class="font-medium">درگاه پرداخت</span>
<span class="opacity-50">{{ <span class="opacity-50">{{ transaction?.bank_result?.bank_type }}</span>
transaction?.bank_result?.bank_type
}}</span>
</div> </div>
<div <div
v-if="transaction?.bank_result?.tracking_code" v-if="transaction?.bank_result?.tracking_code"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs" class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
> >
<span class="font-medium">کد پیگیری</span> <span class="font-medium">کد پیگیری</span>
<span class="opacity-50 underline" <span class="opacity-50 underline">#{{ transaction?.bank_result?.tracking_code }}</span>
>#{{
transaction?.bank_result?.tracking_code
}}</span
>
</div> </div>
<div <div
v-if="transaction?.bank_result?.reference_number" v-if="transaction?.bank_result?.reference_number"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs" class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
> >
<span class="font-medium">کد ارجاع</span> <span class="font-medium">کد ارجاع</span>
<span class="opacity-50 underline" <span class="opacity-50 underline">#{{ transaction?.bank_result?.reference_number }}</span>
>#{{
transaction?.bank_result?.reference_number
}}</span
>
</div> </div>
<div <div
v-if="transaction?.bank_result?.amount" v-if="transaction?.bank_result?.amount"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs" class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
> >
<span class="font-medium">مبلغ</span> <span class="font-medium">مبلغ</span>
<span class="opacity-50">{{ <span class="opacity-50">{{ transaction?.bank_result?.amount }}</span>
transaction?.bank_result?.amount
}}</span>
</div> </div>
<div <div
v-if="transaction?.bank_result?.created_at" v-if="transaction?.bank_result?.created_at"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs" class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
> >
<span class="font-medium">تاریخ</span> <span class="font-medium">تاریخ</span>
<span class="opacity-50">{{ <span class="opacity-50">{{ formatToPersian(transaction?.bank_result?.created_at) }}</span>
formatToPersian(
transaction?.bank_result?.created_at
)
}}</span>
</div> </div>
<div <div
v-if="transaction?.bank_result?.response_result" v-if="transaction?.bank_result?.response_result"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs" class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
> >
<span class="font-medium">وضعیت پرداخت</span> <span class="font-medium">وضعیت پرداخت</span>
<span class="opacity-50">{{ <span class="opacity-50">{{ transaction?.bank_result?.status_detail }}</span>
transaction?.bank_result?.status_detail
}}</span>
</div> </div>
</div> </div>
@@ -190,10 +166,11 @@ const statusTitle = computed(() => {
{{ transaction?.detail }} {{ transaction?.detail }}
</div> </div>
<div <div class="w-full flex flex-col-reverse lg:flex-row items-center justify-between gap-4 lg:gap-5">
class="w-full flex flex-col-reverse lg:flex-row items-center justify-between gap-4 lg:gap-5" <NuxtLink
> to="/"
<NuxtLink to="/" class="w-full"> class="w-full"
>
<Button <Button
class="w-full rounded-full max-lg:py-2" class="w-full rounded-full max-lg:py-2"
start-icon="ci:left-rotation" start-icon="ci:left-rotation"
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

-1
View File
@@ -1 +0,0 @@
Binary file not shown.