merage and test docker compsoe

This commit is contained in:
Parsa Nazer
2025-03-28 12:28:09 +03:30
50 changed files with 397 additions and 165 deletions
+1 -1
View File
@@ -35,6 +35,6 @@ const closeModal = () => {
/> />
</ToastProvider> </ToastProvider>
<VueQueryDevtools dir="ltr" buttonPosition="bottom-right" /> <VueQueryDevtools dir="ltr" buttonPosition="top-right"/>
</div> </div>
</template> </template>
+1 -1
View File
@@ -53,7 +53,7 @@
} }
&:hover { &:hover {
@apply bg-blue-200 border-blue-500 text-blue-500; @apply bg-transparent border-blue-500 text-blue-500;
svg[class~="iconify"] path { svg[class~="iconify"] path {
@apply stroke-blue-500; @apply stroke-blue-500;
@@ -0,0 +1,52 @@
<script setup lang="ts">
// types
type Props = {
gateway: PaymentGateway;
isSelected: boolean;
};
type Emits = {
select: [value: null];
};
// props
defineProps<Props>();
// emits
const emit = defineEmits<Emits>();
</script>
<template>
<button
@click="emit('select', null)"
:class="
isSelected
? 'ring-2 ring-offset-2 ring-black border-black'
: 'border-slate-200'
"
class="w-full p-5 border rounded-xl flex flex-col gap-4 transition-all cursor-pointer relative overflow-hidden"
>
<div class="aspect-square flex-center">
<NuxtImg :src="gateway.picture" class="object-cover" />
</div>
<span class="typo-label-sm text-right text-black">
{{ gateway.title }}
</span>
<Transition
enter-active-class="animate__animated animate__fadeInLeft animate__faster"
leave-active-class="animate__animated animate__fadeOutLeft animate__faster"
>
<span
v-if="isSelected"
class="bg-black rounded-md p-0.5 text-center bottom-4 left-4 text-slate-200 text-[10px] lg:text-xs absolute"
>
<Icon name="bi:check" size="20" class="**:fill-white" />
</span>
</Transition>
</button>
</template>
<style scoped></style>
@@ -43,6 +43,7 @@ const addressData = ref({
: address.value?.for_me == true : address.value?.for_me == true
? "بله" ? "بله"
: "خیر", : "خیر",
is_main: address.value?.is_main ?? false,
}); });
// queries // queries
@@ -67,6 +68,7 @@ const closeModal = () => {
name: "", name: "",
phone: "", phone: "",
for_me: "بله", for_me: "بله",
is_main: false,
}; };
} }
isShow.value = false; isShow.value = false;
@@ -253,6 +255,16 @@ watch(
class="flex items-center field-sizing-content resize-none bg-slate-50 border-slate-200 hover:border-black focus:border-black max-h-[10rem] text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 outline-none flex-1 !text-sm placeholder-slate-400" class="flex items-center field-sizing-content resize-none bg-slate-50 border-slate-200 hover:border-black focus:border-black max-h-[10rem] text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 outline-none flex-1 !text-sm placeholder-slate-400"
></textarea> ></textarea>
</div> </div>
<div class="flex items-center justify-between w-full gap-2">
<label
for="is_main"
class="text-xs font-semibold lg:text-sm text-gray-900"
>
به عنوان آدرس پیش فرض ثبت شود؟
</label>
<Switch id="is_main" v-model="addressData.is_main" />
</div>
</div> </div>
<div class="py-6 border-t border-slate-200 flex gap-3"> <div class="py-6 border-t border-slate-200 flex gap-3">
@@ -21,17 +21,19 @@ const discountCode = ref(cart.value?.discount_code?.code || "");
const { const {
mutateAsync: submitDiscountCode, mutateAsync: submitDiscountCode,
isPending: submitDiscountCodeIsPending isPending: submitDiscountCodeIsPending,
} = useSubmitDiscountCode(); } = useSubmitDiscountCode();
const { const {
mutateAsync: deleteDiscountCode, mutateAsync: deleteDiscountCode,
isPending: deleteDiscountCodeIsPending isPending: deleteDiscountCodeIsPending,
} = useDeleteDiscountCode(); } = useDeleteDiscountCode();
// computed // computed
const nextPage = computed(() => route.meta.nextPage as { name: string; label: string } | undefined); const nextPage = computed(
() => route.meta.nextPage as { name: string; label: string } | undefined
);
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code); const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
@@ -48,11 +50,11 @@ const handleSubmitDiscountCode = () => {
addToast({ addToast({
message: "خطایی در ثبت کد تخفیف رخ داد", message: "خطایی در ثبت کد تخفیف رخ داد",
options: { options: {
status: "error" status: "error",
} },
}); });
discountCode.value = ""; discountCode.value = "";
} },
} }
); );
}; };
@@ -67,11 +69,11 @@ const handleDeleteDiscountCode = () => {
addToast({ addToast({
message: "خطایی در حذف کد تخفیف رخ داد", message: "خطایی در حذف کد تخفیف رخ داد",
options: { options: {
status: "error" status: "error",
} },
}); });
discountCode.value = ""; discountCode.value = "";
} },
}); });
}; };
</script> </script>
+40 -27
View File
@@ -12,42 +12,55 @@ const {} = toRefs(props);
</script> </script>
<template> <template>
<div class="relative w-full flex flex-col justify-center min-h-[450px] md:h-[80svh]"> <div class="relative w-full flex flex-col justify-center min-h-[450px] h-svh">
<div class="flex-col-center gap-6 mb-32">
<span class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4 text-black">
مجله در ستون و سطرآنچ
</span>
<p class="text-slate-500 text-center max-w-[750px] typo-p-lg xl:typo-p-xl">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
</p>
</div>
<div class="-rotate-z-2 z-20"> <div class="-rotate-z-2 z-20">
<div <div
class="bg-warning-500 flex pr-20 gap-12 sm:gap-20 py-2 w-max animate-marquee-reverse" class="bg-blue-500 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee-reverse h-[140px]"
> >
<span <template v-for="i in 10">
v-for="i in 10" <div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold" HEYMLZ
> </div>
TEST {{ i }} <NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[45px] invert opacity-85" />
</span> </template>
<span <template v-for="i in 10">
v-for="i in 10" <div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
class="text-[40px] lg:text-[50px] text-white whitespace-nowrap font-semibold" HEYMLZ
> </div>
TEST {{ i }} <NuxtImg src="/img/heymlz/heymlz-logo.png" class="h-[45px] invert opacity-85" />
</span> </template>
</div> </div>
</div> </div>
<div class="rotate-z-2 z-10"> <div class="rotate-z-2 z-10">
<div <div
class="bg-slate-50 flex pr-20 gap-12 sm:gap-20 py-2 w-max animate-marquee" class="bg-slate-100/70 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee h-[140px]"
> >
<span <template v-for="i in 1">
v-for="i in 10" <NuxtImg src="/img/brands/brand-1.png" class="h-[45px] grayscale opacity-40" />
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold" <NuxtImg src="/img/brands/brand-2.png" class="h-[45px] grayscale opacity-40" />
> <NuxtImg src="/img/brands/brand-3.png" class="h-[45px] grayscale opacity-40" />
TEST {{ i }} <NuxtImg src="/img/brands/brand-4.png" class="h-[45px] grayscale opacity-40" />
</span> <NuxtImg src="/img/brands/brand-5.png" class="h-[45px] grayscale opacity-40" />
<span <NuxtImg src="/img/brands/brand-6.png" class="h-[45px] grayscale opacity-40" />
v-for="i in 10" </template>
class="text-[40px] lg:text-[50px] text-slate-300 whitespace-nowrap font-semibold" <template v-for="i in 1">
> <NuxtImg src="/img/brands/brand-1.png" class="h-[45px] grayscale opacity-40" />
TEST {{ i }} <NuxtImg src="/img/brands/brand-2.png" class="h-[45px] grayscale opacity-40" />
</span> <NuxtImg src="/img/brands/brand-3.png" class="h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-4.png" class="h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-5.png" class="h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-6.png" class="h-[45px] grayscale opacity-40" />
</template>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -26,7 +26,7 @@ withDefaults(defineProps<Props>(), {
</span> </span>
<NuxtLink to="/products"> <NuxtLink to="/products">
<Button <Button
variant="outlined" variant="primary"
class="rounded-full max-sm:typo-label-sm max-sm:py-2" class="rounded-full max-sm:typo-label-sm max-sm:py-2"
end-icon="ci:arrow-left" end-icon="ci:arrow-left"
> >
+15 -16
View File
@@ -25,28 +25,27 @@ const onSwiper = (swiper: SwiperClass) => {
class="flex flex-col justify-center gap-4 bg-black h-[150svh] relative overflow-hidden" class="flex flex-col justify-center gap-4 bg-black h-[150svh] relative overflow-hidden"
> >
<!-- <div class="w-full flex justify-center items-center relative z-10">--> <div class="w-full relative translate-y-[-200px] z-10">
<!-- <span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">--> <div class="flex-col-center gap-6">
<!-- دسته بندی ها--> <span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">
<!-- </span>--> دسته بندی ها
<!-- </div>--> </span>
<p class="text-slate-300 text-center max-w-[750px] typo-p-lg xl:typo-p-xl">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها
و
متون بلکه روزنامه و مجله در ستون و سطرآنچنان که
</p>
</div>
</div>
<NuxtImg
src="/img/categories-gradient.png"
class="animate-spin [animation-duration:16s] object-cover absolute size-full brightness-45 scale-115 aspect-square"
:style="{
maskImage: 'radial-gradient(black, transparent 50%)'
}"
alt=""
/>
<div class="w-full my-20 relative"> <div class="w-full my-20 relative">
<video <video
class="aspect-square w-[450px] translate-[-253px] absolute left-1/2 -translate-x-1/2 z-10" class="aspect-square w-[450px] translate-[-293px] absolute left-1/2 -translate-x-1/2 z-10"
:style="{ :style="{
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))', filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))',
}" }"
src="/video/heymlz/heymlz-seat-2.webm" src="/video/heymlz/heymlz-handshake-part-1.webm"
autoplay autoplay
playsinline playsinline
webkit-playsinline webkit-playsinline
@@ -109,7 +108,7 @@ const onSwiper = (swiper: SwiperClass) => {
<div class="w-full flex justify-center items-center"> <div class="w-full flex justify-center items-center">
<NuxtLink to="/category"> <NuxtLink to="/category">
<Button variant="solid" class="invert rounded-full max-xs:typo-label-sm !px-4 xs:!px-8" <Button variant="primary" class="rounded-full max-xs:typo-label-sm !px-4 xs:!px-8"
end-icon="ci:arrow-left"> end-icon="ci:arrow-left">
مشاهده همه دسته ها مشاهده همه دسته ها
</Button> </Button>
+1 -1
View File
@@ -20,7 +20,7 @@ await suspense();
مقالات اخیر سایت مقالات اخیر سایت
</span> </span>
<NuxtLink to="/articles"> <NuxtLink to="/articles">
<Button variant="outlined" class="rounded-full max-sm:typo-label-sm max-sm:py-2" <Button variant="primary" class="rounded-full max-sm:typo-label-sm max-sm:py-2"
end-icon="ci:arrow-left"> end-icon="ci:arrow-left">
نمایش همه نمایش همه
</Button> </Button>
+35 -8
View File
@@ -15,6 +15,13 @@ const activeSlideVideo = ref<"left" | "right" | "none">("none");
const draggableEl = ref<HTMLElement | null>(null); const draggableEl = ref<HTMLElement | null>(null);
const previewContainerEl = 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 showHeymlzAnimation = ref(false);
const { x: dragAxisX } = useDraggable(draggableEl, { const { x: dragAxisX } = useDraggable(draggableEl, {
initialValue: { x: 0, y: 0 }, initialValue: { x: 0, y: 0 },
axis: "x" axis: "x"
@@ -22,6 +29,17 @@ const { x: dragAxisX } = useDraggable(draggableEl, {
// watch // watch
watch(heymlzElementIsVisible, (newValue) => {
if (newValue) {
showHeymlzAnimation.value = true;
setTimeout(() => {
showHeymlzAnimation.value = false;
}, 3200);
}
}, {
once: true
});
watch(() => clipPathPercent.value, (newValue) => { watch(() => clipPathPercent.value, (newValue) => {
if (newValue > 80) { if (newValue > 80) {
activeSlideVideo.value = "right"; activeSlideVideo.value = "right";
@@ -62,7 +80,8 @@ watch(
<NuxtImg <NuxtImg
v-if="activeSlideVideo !== 'right'" v-if="activeSlideVideo !== 'right'"
:src="homeData!.difreance_section.image1" :src="homeData!.difreance_section.image1"
class="select-none absolute size-full object-cover brightness-[95%]" :class="showHeymlzAnimation ? 'brightness-35' : 'brightness-[95%]'"
class="select-none absolute size-full object-cover transition-[filter] duration-250"
:alt="homeData!.difreance_section.title1" :alt="homeData!.difreance_section.title1"
/> />
<video <video
@@ -76,13 +95,14 @@ watch(
/> />
</Transition> </Transition>
<div class="absolute size-full right-0 w-full"> <div class="absolute size-full right-0 w-full" ref="heymlzElement">
<Transition name="fade"> <Transition name="fade">
<NuxtImg <NuxtImg
v-if="activeSlideVideo !== 'left'" v-if="activeSlideVideo !== 'left'"
:src="homeData!.difreance_section.image2" :src="homeData!.difreance_section.image2"
class="overlay-image select-none absolute object-cover size-full brightness-[95%]" :class="showHeymlzAnimation ? 'brightness-35' : 'brightness-[95%]'"
class="overlay-image select-none absolute object-cover size-full transition-[filter] duration-250"
:alt="homeData!.difreance_section.title2" :alt="homeData!.difreance_section.title2"
/> />
<video <video
@@ -97,12 +117,14 @@ watch(
</Transition> </Transition>
<video <video
v-if="showHeymlzAnimation"
src="/video/heymlz/heymlz-pulling.webm" src="/video/heymlz/heymlz-pulling.webm"
autoplay autoplay
muted muted
playsinline playsinline
loop
webkit-playsinline webkit-playsinline
class="size-[300px] absolute translate-x-[-100px] z-10 top-[32%] -translate-y-1/2" class="size-[400px] absolute translate-x-[-107px] z-10 top-[50%] -translate-y-1/2"
:style="{ :style="{
left: `${clipPathPercent}%`, left: `${clipPathPercent}%`,
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.3))' filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.3))'
@@ -113,18 +135,23 @@ watch(
:style="{ :style="{
left: `${clipPathPercent}%`, left: `${clipPathPercent}%`,
}" }"
:class="activeSlideVideo !== 'none' ? 'opacity-10' : ''" :class="[
class="select-none w-2 h-full bg-black absolute left-0 flex items-center justify-center transition-opacity" activeSlideVideo !== 'none' ? 'opacity-10' : '',
showHeymlzAnimation ? 'bg-neutral-300' : 'bg-black'
]"
class="select-none w-2 h-full absolute left-0 flex items-center justify-center transition-opacity duration-250"
> >
<div <div
ref="draggableEl" ref="draggableEl"
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute bg-black size-11 flex items-center justify-center" :class="showHeymlzAnimation ? 'bg-neutral-300' : 'bg-black'"
class="cursor-grab hover:scale-115 transition-transform rounded-full absolute size-11 flex items-center justify-center"
> >
<Icon <Icon
name="ci:arrows" name="ci:arrows"
size="24" size="24"
class="**:stroke-white" class="transition-all"
:class="showHeymlzAnimation ? '**:stroke-black' : '**:stroke-white'"
/> />
</div> </div>
</div> </div>
@@ -92,7 +92,6 @@ onUnmounted(() => {
:to="slide.link" :to="slide.link"
class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center" class="showcase-slide origin-bottom absolute size-full bg-transparent flex items-center justify-center"
> >
<NuxtImg <NuxtImg
class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30" class="w-[280px] xs:w-[350px] lg:w-[500px] xl:w-[650px] z-20 mb-30 sm:mb-20 lg:mb-30"
:src="slide.image" :src="slide.image"
@@ -101,7 +100,7 @@ onUnmounted(() => {
}" }"
alt="" alt=""
/> />
<div class="flex flex-col items-center justify-center gap-4 text-center absolute z-20 mt-20"> <div class="flex flex-col items-center justify-center gap-6 text-center absolute z-20 mt-20">
<span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3"> <span class="text-white typo-h-6 sm:typo-h-5 lg:typo-h-4 xl:typo-h-3">
{{ slide.title }} {{ slide.title }}
</span> </span>
@@ -110,8 +109,9 @@ onUnmounted(() => {
</p> </p>
<NuxtLink :to="slide.link"> <NuxtLink :to="slide.link">
<Button <Button
variant="primary"
end-icon="ci:arrow-left" end-icon="ci:arrow-left"
class="mt-6 invert max-sm:hidden max-lg:typo-label-xs px-12 rounded-full hover:bg-transparent" class="mt-8 max-sm:hidden max-lg:typo-label-xs px-10 rounded-full hover:bg-transparent"
> >
مشاهده دسته بندی مشاهده دسته بندی
</Button> </Button>
@@ -21,18 +21,18 @@ const { circle } = toRefs(props);
<div <div
:style="{ :style="{
height: `${size}px`, height: `${size}px`,
width: circle ? `${size}px` : '100%', width: circle ? `${size}px` : '100%'
}" }"
class="relative flex items-center w-full justify-center shrink-0" class="relative flex items-center w-full justify-center shrink-0"
:class="{ :class="{
'rounded-full overflow-hidden': circle, 'rounded-full overflow-hidden': circle,
}" }"
> >
<img <NuxtImg
:style="{ :style="{
maskImage: 'radial-gradient(black, transparent 70%)' maskImage: 'radial-gradient(black, transparent 70%)'
}" }"
src="/public/img/ai-loading-2.gif" src="/img/heymlz/heymlz-idle.gif"
class="size-full object-cover absolute pt-2" class="size-full object-cover absolute pt-2"
alt="ai-loading" alt="ai-loading"
/> />
@@ -20,6 +20,8 @@ const { isLoggedIn } = useAuth();
const route = useRoute(); const route = useRoute();
const id = route.params.id as string | number; const id = route.params.id as string | number;
const scrollToBottomTimer = ref<NodeJS.Timeout | null>(null);
const chatContainerEl = ref<HTMLElement | null>(null); const chatContainerEl = ref<HTMLElement | null>(null);
const lastMessageBeforeUpdate = ref(0); const lastMessageBeforeUpdate = ref(0);
@@ -63,7 +65,10 @@ useInfiniteScroll(
// methods // methods
const scrollToBottom = () => { const scrollToBottom = () => {
if (scrollToBottomTimer.value) clearTimeout(scrollToBottomTimer.value);
scrollToBottomTimer.value = setTimeout(() => {
chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0; chatContainerScrollY.value = chatContainerEl.value?.scrollHeight ?? 0;
}, 50);
}; };
// computed // computed
@@ -137,8 +142,7 @@ whenever(
> >
<div <div
:style="{ :style="{
maskImage: maskImage: 'linear-gradient(to top, transparent, black 5%, black, black)'
'linear-gradient(to top, transparent, black 5%, black, black)',
}" }"
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto" class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
ref="chatContainerEl" ref="chatContainerEl"
@@ -184,11 +188,20 @@ whenever(
</Transition> </Transition>
</template> </template>
<div <div
class="text-black p-4.5 size-full flex justify-center items-center" class="text-black p-6 size-full flex justify-center items-center flex-col"
v-else v-else
> >
<img class="size-[50px]" src="/img/heymlz/heymlz-idle.gif" alt="" /> <NuxtImg class="size-[250px]" src="/img/heymlz/heymlz-loading-1.gif" alt="" />
Please sign in first <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>
<NuxtLink to="/signin">
<Button class="mt-8 rounded-full px-10">ورود به فروشگاه</Button>
</NuxtLink>
</div> </div>
</div> </div>
</Transition> </Transition>
@@ -7,6 +7,9 @@ import { useToast } from "~/composables/global/useToast";
// state // state
const route = useRoute();
const id = route.params.id as string | number;
const { $queryClient: queryClient } = useNuxtApp(); const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast(); const { addToast } = useToast();
@@ -27,7 +30,7 @@ const sendMessage = async () => {
await createMessage({ await createMessage({
new_message: value, new_message: value,
productId: 1, productId: id,
}); });
} catch (e) { } catch (e) {
addToast({ addToast({
@@ -54,13 +54,12 @@ onMounted(() => {
`#chat-message-content-${id.value}`, `#chat-message-content-${id.value}`,
{ {
text: "", text: "",
duration: 2.5,
ease: "none", ease: "none",
}, },
{ {
text: { value: content.value, rtl: false }, text: { value: content.value, rtl: false },
duration: 2.5,
ease: "none", ease: "none",
duration: 2.5,
onUpdate: () => emit("textUpdate"), onUpdate: () => emit("textUpdate"),
} }
); );
@@ -78,9 +77,9 @@ onMounted(() => {
<div <div
class="relative overflow-hidden flex items-center justify-center mt-px bg-slate-300 rounded-full size-[35px] shrink-0" class="relative overflow-hidden flex items-center justify-center mt-px bg-slate-300 rounded-full size-[35px] shrink-0"
> >
<img <NuxtImg
v-if="!reverse" v-if="reverse"
src="/img/footer-bg.jpg" src="/img/heymlz/footer-share.svg"
class="size-full object-cover absolute" class="size-full object-cover absolute"
alt="profile" alt="profile"
/> />
@@ -45,11 +45,11 @@ const {
}); });
const avatars = ref([ const avatars = ref([
"/avatars/1.jpg", "/img/avatars/1.jpg",
"/avatars/2.jpg", "/img/avatars/2.jpg",
"/avatars/3.jpg", "/img/avatars/3.jpg",
"/avatars/4.jpg", "/img/avatars/4.jpg",
"/avatars/5.jpg", "/img/avatars/5.jpg",
]); ]);
// queries // queries
@@ -134,11 +134,7 @@ onFileDialogChange((files: any) => {
</div> </div>
<div class="w-full flex-col-center gap-5"> <div class="w-full flex-col-center gap-5">
<Avatar <Avatar :src="currentProfile" alt="" class="!size-32" />
:src="currentProfile"
alt=""
class="!size-32"
/>
<Button <Button
class="rounded-full w-[8rem]" class="rounded-full w-[8rem]"
@click="openFileDialog" @click="openFileDialog"
@@ -7,7 +7,7 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<Icon name="bi:info-circle" size="20" /> <Icon name="bi:info-circle" size="20" />
<h3 class="typo-sub-h-lg font-semibold">تغییر وضعیت سفارش</h3> <h3 class="typo-sub-h-md font-semibold">تغییر وضعیت سفارش</h3>
| |
<span class="typo-p-xs text-cyan-500 font-semibold"> <span class="typo-p-xs text-cyan-500 font-semibold">
۲۳ تیر ۲۳ تیر
@@ -33,8 +33,8 @@ const profile = computed(() => {
return is_user.value return is_user.value
? account.value?.profile_photo ? account.value?.profile_photo
? account.value?.profile_photo ? account.value?.profile_photo
: "/avatars/1.jpg" : "/img/avatars/1.jpg"
: "/avatars/3.jpg"; : "/img/avatars/3.jpg";
}); });
const username = computed(() => { const username = computed(() => {
@@ -5,7 +5,7 @@ import { API_ENDPOINTS } from "~/constants";
// types // types
export type CreateOrUpdateAddressResponse = Omit<Address, "is_main">; export type CreateOrUpdateAddressResponse = Address;
const useCreateOrUpdateAddress = (update: ComputedRef<Boolean>) => { const useCreateOrUpdateAddress = (update: ComputedRef<Boolean>) => {
// state // state
+4 -4
View File
@@ -6,9 +6,9 @@ import { useAuth } from "~/composables/api/auth/useAuth";
// types // types
export type GetBranchResponse = ApiPaginated<Chat>; export type GetChatResponse = ApiPaginated<Chat>;
const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => { const useGetChat = (productId: string | number, enabled: Ref<boolean>) => {
// state // state
const { $axios: axios } = useNuxtApp(); const { $axios: axios } = useNuxtApp();
@@ -26,7 +26,7 @@ const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => {
limit: number; limit: number;
offset: number; offset: number;
}) => { }) => {
const { data } = await axios.get<GetBranchResponse>( const { data } = await axios.get<GetChatResponse>(
`${API_ENDPOINTS.chat.messages}/${productId}`, `${API_ENDPOINTS.chat.messages}/${productId}`,
{ {
params: { params: {
@@ -65,4 +65,4 @@ const useGetBranch = (productId: string | number, enabled: Ref<boolean>) => {
}); });
}; };
export default useGetBranch; export default useGetChat;
@@ -0,0 +1,33 @@
// imports
import { useMutation } from "@tanstack/vue-query";
import { API_ENDPOINTS } from "~/constants";
// types
export type SetOrderAddressRequest = {
address_id: number;
};
const useSetOrderAddress = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleSetOrderAddress = async (params: SetOrderAddressRequest) => {
const { data } = await axios.post(
API_ENDPOINTS.orders.delivery.set_address,
{ ...params }
);
return data;
};
return useMutation({
mutationFn: (AddressData: SetOrderAddressRequest) =>
handleSetOrderAddress(AddressData),
});
};
export default useSetOrderAddress;
+6
View File
@@ -53,6 +53,12 @@ export const API_ENDPOINTS = {
add_discount: "/order/cart/discount", add_discount: "/order/cart/discount",
delete_discount: "/order/cart/discount", delete_discount: "/order/cart/discount",
}, },
delivery: {
set_address: "/order/cart/set-address",
},
checkout: {
payment: "/order/cart/payment",
},
}, },
}; };
+43 -34
View File
@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
// meta // meta
const route = useRoute();
definePageMeta({ definePageMeta({
layout: "cart", layout: "cart",
middleware: "check-is-logged-in", middleware: "check-is-logged-in",
@@ -9,45 +11,63 @@ definePageMeta({
nextPage: { name: "checkout", label: "پرداخت" }, nextPage: { name: "checkout", label: "پرداخت" },
}); });
// types
type PaymentGateway = {
id: number;
picture: string;
title: string;
};
// state // state
const router = useRouter();
const paymentGateways = ref<PaymentGateway[]>([ const paymentGateways = ref<PaymentGateway[]>([
{ {
id: 1, id: 1,
picture: "/zarinpal.png", picture: "/img/gateways/zarinpal.png",
title: "زرین پال", title: "زرین پال",
type: "ZARINPAL",
}, },
{ {
id: 2, id: 2,
picture: "/saman-bank.png", picture: "/img/gateways/sep.png",
title: "بانک سامان", title: "سپ",
type: "SEP",
}, },
{ {
id: 3, id: 3,
picture: "/mellat-bank.png", picture: "/img/gateways/mellat-bank.png",
title: "بانک ملت", title: "بانک ملت",
type: "MELLAT",
}, },
{ {
id: 4, id: 4,
picture: "/jibimo.png", picture: "/img/gateways/idpay.png",
title: "جیبی مو", title: "آی دی پی",
type: "IDPAY",
}, },
{ {
id: 5, id: 5,
picture: "/idpay.png", picture: "/img/gateways/zibal.png",
title: "آی دی پی", title: "زیبال",
type: "ZIBAL",
},
{
id: 6,
picture: "/img/gateways/bahamta.png",
title: "باهمتا",
type: "BAHAMTA",
},
{
id: 7,
picture: "/img/gateways/bmi.png",
title: "بانک ملی",
type: "BMI",
}, },
]); ]);
const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]); const selectedGateway = computed({
get: () => {
return !!route.query["gw"]
? paymentGateways.value.find((i) => i.type == route.query["gw"])
: paymentGateways.value[0];
},
set: (nv: PaymentGateway) => router.push({ query: { gw: nv.type } }),
});
</script> </script>
<template> <template>
@@ -64,24 +84,13 @@ const selectedGateway = ref<PaymentGateway>(paymentGateways.value[0]);
<div <div
class="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4" class="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"
> >
<div <Gateway
v-for="(gateway, index) in paymentGateways" v-for="(gateway, index) in paymentGateways"
@click="selectedGateway = { ...gateway }" :index="index"
:key="index" :gateway="gateway"
:class=" :isSelected="selectedGateway?.id == gateway.id"
selectedGateway.id == gateway.id @select="selectedGateway = { ...gateway }"
? 'ring-2 ring-offset-2 ring-black border-black' />
: 'border-slate-200'
"
class="w-full p-5 border rounded-xl flex flex-col gap-4 transition-all cursor-pointer"
>
<div class="aspect-square flex-center">
<NuxtImg :src="gateway.picture" class="object-cover" />
</div>
<span class="typo-label-sm text-black">
{{ gateway.title }}
</span>
</div>
</div> </div>
</div> </div>
<OrderSummary /> <OrderSummary />
+57 -7
View File
@@ -2,6 +2,9 @@
// imports // imports
import useGetAllAddress from "~/composables/api/account/useGetAllAddress"; import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
import useSetOrderAddress from "~/composables/api/orders/useSetOrderAddress";
import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants";
// meta // meta
@@ -24,10 +27,26 @@ type DeliveryData = {
}; };
}; };
// queries
const { data: addresses, isLoading: addressesIsLoading } = useGetAllAddress();
const { mutateAsync: setOrderAddress, isPending: setOrderAddressIsPending } =
useSetOrderAddress();
// computed
const selectedAddress = computed(() => {
return addresses.value?.find((i) => i.is_main) ?? null;
});
// state // state
const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast();
const deliveryData = ref<DeliveryData>({ const deliveryData = ref<DeliveryData>({
address: null, address: selectedAddress.value,
deliveryMethod: { deliveryMethod: {
peyk: false, peyk: false,
pishtaz: true, pishtaz: true,
@@ -35,26 +54,57 @@ const deliveryData = ref<DeliveryData>({
}, },
}); });
const { data: addresses, isLoading: addressesIsLoading } = useGetAllAddress();
// methods // methods
const handleSelectAddress = (address: Address) => { const handleSelectAddress = (address: Address) => {
deliveryData.value.address = { ...address }; deliveryData.value.address = { ...address };
}; };
// watch
whenever(
() => deliveryData.value.address,
(nv) => {
setOrderAddress(
{ address_id: nv.id! },
{
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
},
onError: () => {
addToast({
message: "در انتخاب آدرس خطایی رخ داد",
options: {
description: "لطفا مجدد تلاش کنید",
},
});
},
}
);
},
{
deep: true,
}
);
</script> </script>
<template> <template>
<div class="flex flex-col w-full gap-5"> <div class="flex flex-col w-full gap-5">
<AddressItem /> <AddressItem />
<div class="flex flex-col w-full gap-6"> <div class="flex flex-col w-full gap-6">
<div class="flex items-center gap-3">
<span class="typo-sub-h-xl"> آدرس های شما </span> <span class="typo-sub-h-xl"> آدرس های شما </span>
<div v-if="addressesIsLoading" class="flex flex-col gap-6 w-full"> <Icon
<Skeleton name="svg-spinners:180-ring-with-bg"
v-for="i in 3" size="20"
class="w-full !h-[10rem] !rounded-xl" v-if="setOrderAddressIsPending"
/> />
</div> </div>
<div v-if="addressesIsLoading" class="flex flex-col gap-6 w-full">
<Skeleton v-for="i in 3" class="w-full !h-[8rem] !rounded-xl" />
</div>
<template v-else> <template v-else>
<div <div
v-if="!addresses?.length" v-if="!addresses?.length"
+2 -2
View File
@@ -132,8 +132,8 @@ const contactWays = ref([
</div> </div>
</div> </div>
</div> </div>
<div class="w-4/12 h-full bg-red-300"> <div class="w-4/12 h-full flex-center">
<NuxtImg src="/mlz.jpeg" class="-mt-16" /> <NuxtImg src="/logo.png" class="size-2/3 -mt-5" />
</div> </div>
</div> </div>
</div> </div>
-1
View File
@@ -15,7 +15,6 @@ const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
const response = await suspense(); const response = await suspense();
if (response.isError) { if (response.isError) {
console.log(response);
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: `Landing error : ${response.error.message}` statusMessage: `Landing error : ${response.error.message}`
+10 -5
View File
@@ -1,12 +1,12 @@
import type { import type {
DehydratedState, DehydratedState,
VueQueryPluginOptions VueQueryPluginOptions,
} from "@tanstack/vue-query"; } from "@tanstack/vue-query";
import { import {
VueQueryPlugin, VueQueryPlugin,
QueryClient, QueryClient,
hydrate, hydrate,
dehydrate dehydrate,
} from "@tanstack/vue-query"; } from "@tanstack/vue-query";
import { defineNuxtPlugin, useState } from "#imports"; import { defineNuxtPlugin, useState } from "#imports";
@@ -15,7 +15,12 @@ export default defineNuxtPlugin((nuxt) => {
const vueQueryState = useState<DehydratedState | null>("vue-query"); const vueQueryState = useState<DehydratedState | null>("vue-query");
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 5000 } } defaultOptions: {
queries: {
staleTime: 5000,
experimental_prefetchInRender: true,
},
},
}); });
const options: VueQueryPluginOptions = { queryClient }; const options: VueQueryPluginOptions = { queryClient };
@@ -33,7 +38,7 @@ export default defineNuxtPlugin((nuxt) => {
return { return {
provide: { provide: {
queryClient queryClient,
} },
}; };
}); });

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 730 KiB

Before

Width:  |  Height:  |  Size: 809 KiB

After

Width:  |  Height:  |  Size: 809 KiB

Before

Width:  |  Height:  |  Size: 984 KiB

After

Width:  |  Height:  |  Size: 984 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Before

Width:  |  Height:  |  Size: 498 KiB

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

+28 -14
View File
@@ -11,22 +11,22 @@ declare global {
type LogType = { type LogType = {
title: string; title: string;
status?: "success" | "error" | "info" | "warning"; status?: "success" | "error" | "info" | "warning";
message?: string, message?: string;
details?: any details?: any;
} };
type AxiosLogType = { type AxiosLogType = {
url: string, url: string;
method: string, method: string;
status: number, status: number;
code: string, code: string;
requestHeaders: Record<any, any>, requestHeaders: Record<any, any>;
responseHeaders?: Record<any, any>, responseHeaders?: Record<any, any>;
response?: any, response?: any;
payload?: Record<any, any>, payload?: Record<any, any>;
params?: Record<any, any>, params?: Record<any, any>;
date: string date: string;
} };
type Chat = { type Chat = {
id: number; id: number;
@@ -276,4 +276,18 @@ declare global {
updated_at: string; updated_at: string;
order: Order; order: Order;
}; };
type PaymentGateway = {
id: number;
picture: string;
title: string;
type:
| "ZARINPAL"
| "SEP"
| "MELLAT"
| "IDPAY"
| "ZIBAL"
| "BAHAMTA"
| "BMI";
};
} }