merage and slider router
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]"
|
||||
/> -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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%',
|
||||
}"
|
||||
/>
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ const progressStyle = computed(() => {
|
||||
|
||||
// methods
|
||||
|
||||
const onAssetLoaded = () => {
|
||||
clearInterval(progressInterval.value!);
|
||||
criticalLoad.value = false;
|
||||
assetLoadingProgress.value = 100;
|
||||
isAssetLoaded.value = true;
|
||||
};
|
||||
// const onAssetLoaded = () => {
|
||||
// clearInterval(progressInterval.value!);
|
||||
// criticalLoad.value = false;
|
||||
// assetLoadingProgress.value = 100;
|
||||
// isAssetLoaded.value = true;
|
||||
// };
|
||||
|
||||
const onAssetFinished = () => {
|
||||
gsap.to("#loading-overlay", {
|
||||
@@ -59,15 +59,15 @@ onMounted(() => {
|
||||
if (!isSiteLoadingDisabled.value) {
|
||||
isWindowScrollLocked.value = true;
|
||||
|
||||
const heymlzLoadingAnimation = document.querySelector("#heymlz-loading-animation") as HTMLVideoElement;
|
||||
// const heymlzLoadingAnimation = document.querySelector("#heymlz-loading-animation") as HTMLVideoElement;
|
||||
|
||||
if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
|
||||
onAssetLoaded();
|
||||
}
|
||||
// if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
|
||||
// onAssetLoaded();
|
||||
// }
|
||||
|
||||
progressInterval.value = setInterval(() => {
|
||||
assetLoadingProgress.value += Math.random() * 10;
|
||||
}, 250);
|
||||
assetLoadingProgress.value += Math.random() * 50;
|
||||
}, 150);
|
||||
|
||||
gsap.to("#loading-overlay", {
|
||||
opacity: 1,
|
||||
@@ -100,7 +100,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video
|
||||
<!-- <video
|
||||
id="heymlz-loading-animation"
|
||||
muted
|
||||
autoplay
|
||||
@@ -114,6 +114,6 @@ onMounted(() => {
|
||||
:style="{
|
||||
mask: 'linear-gradient(to bottom, rgba(0,0,0,0) 0%, black 20%, black 80%, rgba(0,0,0,0) 100%)',
|
||||
}"
|
||||
/>
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
if (to.path !== from.path && process.client) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: "پرداخت" },
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
|
||||
@@ -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: "انتخاب آدرس" },
|
||||
});
|
||||
|
||||
@@ -5,6 +5,10 @@ import useGetCategories from "~/composables/api/product/useGetCategories";
|
||||
|
||||
// state
|
||||
|
||||
useSeoMeta({
|
||||
title : "دسته بندی ها"
|
||||
});
|
||||
|
||||
const { data: categories, suspense } = useGetCategories();
|
||||
|
||||
const search = ref("");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// state
|
||||
|
||||
useSeoMeta({
|
||||
title : "ارتباط با ما"
|
||||
});
|
||||
|
||||
const contactInfo = ref({
|
||||
name: "",
|
||||
email: "",
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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"
|
||||
@@ -5,6 +5,10 @@ import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
|
||||
|
||||
// meta
|
||||
|
||||
useSeoMeta({
|
||||
title : "پنل کاربری آدرس ها"
|
||||
});
|
||||
|
||||
definePageMeta({
|
||||
middleware: "check-is-logged-in",
|
||||
layout: "profile",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -13,6 +13,10 @@ import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
// meta
|
||||
|
||||
useSeoMeta({
|
||||
title : "پنل کاربری تیکت"
|
||||
});
|
||||
|
||||
definePageMeta({
|
||||
middleware: "check-is-logged-in",
|
||||
layout: "profile",
|
||||
|
||||
@@ -7,6 +7,10 @@ import useGetAllTickets, {
|
||||
|
||||
// meta
|
||||
|
||||
useSeoMeta({
|
||||
title : "پنل کاربری تیکت ها"
|
||||
});
|
||||
|
||||
definePageMeta({
|
||||
middleware: "check-is-logged-in",
|
||||
layout: "profile",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,6 +26,10 @@ definePageMeta({
|
||||
|
||||
// state
|
||||
|
||||
useSeoMeta({
|
||||
title : "ورود به فروشگاه"
|
||||
});
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const { updateToken, updateRefreshToken } = useAuth();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook("page:finish", () => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 19 KiB |
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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 = {
|
||||
|
||||