merage with front

add address object to cart serializer and signal for the default address
This commit is contained in:
Parsa Nazer
2025-04-18 17:30:23 +03:30
82 changed files with 1322 additions and 870 deletions
+5 -3
View File
@@ -1,4 +1,6 @@
{
"tabWidth": 4,
"semi": true
}
"singleAttributePerLine": true,
"printWidth": 120,
"tabWidth": 4,
"semi": true
}
+1
View File
@@ -16,6 +16,7 @@ const closeModal = () => {
<template>
<div>
<LoadingIndicator />
<NuxtPwaManifest />
<UpdatePwaModal
+32
View File
@@ -325,4 +325,36 @@
.vpd-controls button {
@apply flex justify-center items-center;
}
.pattern {
width: 100%;
height: 100%;
--color: #191a1a10;
background-color: white;
background-image: linear-gradient(
0deg,
transparent 24%,
var(--color) 25%,
var(--color) 26%,
transparent 27%,
transparent 74%,
var(--color) 75%,
var(--color) 76%,
transparent 77%,
transparent
),
linear-gradient(
90deg,
transparent 24%,
var(--color) 25%,
var(--color) 26%,
transparent 27%,
transparent 74%,
var(--color) 75%,
var(--color) 76%,
transparent 77%,
transparent
);
background-size: 55px 55px;
}
}
@@ -10,11 +10,14 @@ import { QUERY_KEYS } from "~/constants";
type Props = {
address?: Address;
isSelected?: boolean;
selectable?: boolean;
};
// props
defineProps<Props>();
withDefaults(defineProps<Props>(), {
selectable: true,
});
// emit
@@ -63,7 +66,9 @@ const handleDeleteAddress = (id: number) => {
<template>
<button
@click.prevent="!!address ? emit('select', address) : null"
@click.prevent="
!!address && selectable ? emit('select', address) : null
"
:class="
isSelected
? 'border-transparent ring-2 ring-offset-2 ring-blue-500'
@@ -76,7 +81,7 @@ const handleDeleteAddress = (id: number) => {
</div>
<span class="flex items-center justify-between w-full gap-3">
<div
class="flex items-center gap-3 lg:text-[1.125rem] font-semibold text-slate-900"
class="flex items-center gap-3 max-lg:text-sm font-semibold text-slate-900"
>
{{ !!address ? address.name : "آدرس" }}
<span
@@ -101,7 +106,7 @@ const handleDeleteAddress = (id: number) => {
>
<div class="w-full lg:w-9/12 overflow-hidden">
<div
class="w-full overflow-hidden overflow-ellipsis gap-5 text-start whitespace-pre text-sm text-slate-700"
class="w-full overflow-hidden overflow-ellipsis gap-5 text-start whitespace-pre text-xs lg:text-sm text-slate-700"
>
{{
!!address
@@ -38,11 +38,7 @@ const addressData = ref({
address: address.value?.address ?? "",
name: address.value?.name ?? "",
phone: address.value?.phone ?? "",
for_me: !isEditing.value
? address.value?.for_me ?? "بله"
: address.value?.for_me == true
? "بله"
: "خیر",
for_me: !isEditing.value ? address.value?.for_me ?? "بله" : address.value?.for_me == true ? "بله" : "خیر",
is_main: address.value?.is_main ?? false,
});
@@ -50,10 +46,7 @@ const addressData = ref({
const { data: account } = useGetAccount();
const {
mutateAsync: createOrUpdateAddress,
isPending: createAddressIsPending,
} = useCreateOrUpdateAddress(isEditing);
const { mutateAsync: createOrUpdateAddress, isPending: createAddressIsPending } = useCreateOrUpdateAddress(isEditing);
// methods
@@ -84,9 +77,7 @@ const addNew = () => {
});
closeModal();
addToast({
message: isEditing.value
? "آدرس با موفقیت ویرایش شد"
: "آدرس با موفقیت اضافه شد",
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "آدرس با موفقیت اضافه شد",
options: {
status: "success",
},
@@ -94,9 +85,7 @@ const addNew = () => {
},
onError: () => {
addToast({
message: isEditing.value
? "آدرس با موفقیت ویرایش شد"
: "مشکلی در افزودن آدرس رخ داد",
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "مشکلی در افزودن آدرس رخ داد",
options: {
status: "error",
},
@@ -110,8 +99,7 @@ watch(
() => addressData.value.for_me,
(newValue) => {
if (!isEditing.value) {
addressData.value.phone =
newValue == "بله" ? account.value!.phone : "";
addressData.value.phone = newValue == "بله" ? account.value!.phone : "";
}
},
{
@@ -138,23 +126,23 @@ watch(
:variant="!!address ? 'ghost' : 'solid'"
:class="!!address ? '!bg-transparent !underline' : ''"
>
<span class="whitespace-pre">
<span class="whitespace-pre max-lg:text-xs">
{{ !!address ? "ویرایش" : "افزودن آدرس" }}
</span>
</Button>
</template>
<template #content>
<div class="flex-col-center gap-6 py-10" dir="rtl">
<div
class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"
>
<div
class="flex-col-center gap-6 py-10"
dir="rtl"
>
<div class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<div class="flex flex-col gap-2">
<label
for="name"
class="text-xs font-semibold lg:text-sm text-gray-900"
>نام پیش فرض
<span class="text-sm text-red-500">*</span></label
>نام پیش فرض <span class="text-sm text-red-500">*</span></label
>
<Input
@@ -185,8 +173,7 @@ watch(
<label
for="phone"
class="text-xs font-semibold lg:text-sm text-gray-900"
>شماره تلفن
<span class="text-sm text-red-500">*</span></label
>شماره تلفن <span class="text-sm text-red-500">*</span></label
>
<Input
@@ -217,8 +204,7 @@ watch(
<label
for="city"
class="text-xs font-semibold lg:text-sm text-gray-900"
>شهر
<span class="text-sm text-red-500">*</span></label
>شهر <span class="text-sm text-red-500">*</span></label
>
<Input
id="city"
@@ -232,8 +218,7 @@ watch(
<label
for="post"
class="text-xs font-semibold lg:text-sm text-gray-900"
>کد پستی
<span class="text-sm text-red-500">*</span></label
>کد پستی <span class="text-sm text-red-500">*</span></label
>
<Input
id="post"
@@ -248,14 +233,13 @@ watch(
<label
for="address"
class="text-xs font-semibold lg:text-sm text-gray-900"
>آدرس کامل
<span class="text-sm text-red-500">*</span></label
>آدرس کامل <span class="text-sm text-red-500">*</span></label
>
<textarea
id="address"
placeholder="آدرس خود را بنویسید"
v-model="addressData.address"
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-1.5 lg:py-3.5 selection:bg-slate-100 rounded-md lg:rounded-100 outline-none flex-1 text-xs lg:!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-1.5 lg:py-3.5 selection:bg-slate-100 rounded-md lg:rounded-100 outline-none flex-1 text-xs lg:!text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
></textarea>
</div>
@@ -266,7 +250,10 @@ watch(
>
به عنوان آدرس پیش فرض ثبت شود؟
</label>
<Switch id="is_main" v-model="addressData.is_main" />
<Switch
id="is_main"
v-model="addressData.is_main"
/>
</div>
</div>
+28 -48
View File
@@ -20,26 +20,15 @@ const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
const discountCode = ref(cart.value?.discount_code?.code || "");
const {
mutateAsync: submitDiscountCode,
isPending: submitDiscountCodeIsPending,
} = useSubmitDiscountCode();
const { mutateAsync: submitDiscountCode, isPending: submitDiscountCodeIsPending } = useSubmitDiscountCode();
const {
mutateAsync: deleteDiscountCode,
isPending: deleteDiscountCodeIsPending,
} = useDeleteDiscountCode();
const { mutateAsync: deleteDiscountCode, isPending: deleteDiscountCodeIsPending } = useDeleteDiscountCode();
const { mutateAsync: pay, isPending: paymentIsPending } = usePayOrder();
// computed
const nextPage = computed(
() =>
route.meta.nextPage as
| { name: string; label: string; query?: string }
| undefined
);
const nextPage = computed(() => route.meta.nextPage as { name: string; label: string; query?: string } | undefined);
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
@@ -90,7 +79,9 @@ const handlePayment = () => {
},
{
onSuccess: (data) => {
window.location.href = data.url;
setTimeout(() => {
window.location.href = data.url;
}, 2000);
},
onError: () => {
addToast({
@@ -107,17 +98,20 @@ const handlePayment = () => {
</script>
<template>
<div
class="flex flex-col bg-slate-50 w-full lg:w-3/12 transition-all border border-slate-200 rounded-xl"
>
<div
class="w-full flex items-center justify-between py-5 px-4 border-b border-slate-200"
>
<div class="flex flex-col bg-slate-50 w-full lg:w-3/12 transition-all border border-slate-200 rounded-xl">
<div class="w-full flex items-center justify-between py-5 px-4 border-b border-slate-200">
<span class="typo-sub-h-xl text-black">فاکتور خرید</span>
<Icon name="ci:cart" class="**:stroke-black" size="24" />
<Icon
name="ci:cart"
class="**:stroke-black"
size="24"
/>
</div>
<div v-if="cartIsLoading" class="flex flex-col p-4 gap-4 !rounded-lg">
<div
v-if="cartIsLoading"
class="flex flex-col p-4 gap-4 !rounded-lg"
>
<Skeleton
v-for="i in 5"
:key="i"
@@ -128,10 +122,11 @@ const handlePayment = () => {
/>
</div>
<div v-else class="flex flex-col p-4 gap-4">
<div
class="flex items-center justify-between w-full text-slate-800"
>
<div
v-else
class="flex flex-col p-4 gap-4"
>
<div class="flex items-center justify-between w-full text-slate-800">
<span class="max-w-1/2 text-sm"> جمع سبد خرید: </span>
<span class="max-w-1/2 text-sm">
@@ -139,9 +134,7 @@ const handlePayment = () => {
</span>
</div>
<div
class="flex items-center justify-between w-full text-slate-800"
>
<div class="flex items-center justify-between w-full text-slate-800">
<span class="max-w-1/2 text-sm"> مالیات ارزش افزوده: </span>
<span class="max-w-1/2 text-sm"> {{ cart?.tax }} </span>
@@ -158,9 +151,7 @@ const handlePayment = () => {
</span>
</div>
<div
class="flex items-center justify-between w-full text-slate-900"
>
<div class="flex items-center justify-between w-full text-slate-900">
<span class="max-w-1/2 text-sm"> جمع کل: </span>
<span class="max-w-1/2 text-sm">
@@ -176,23 +167,12 @@ const handlePayment = () => {
:disabled="hasSubmittedDiscountCode"
/>
<button
@click="
hasSubmittedDiscountCode
? handleDeleteDiscountCode()
: handleSubmitDiscountCode()
"
class="text-xs px-5 rounded-e-100 py-1.5 text-white bg-blue-500 hover:bg-transparent hover:text-blue-500 border-[1.5px] border-blue-500 hover:border-white transition-all disabled:cursor-not-allowed"
:disabled="
!discountCode.length ||
submitDiscountCodeIsPending ||
deleteDiscountCodeIsPending
"
@click="hasSubmittedDiscountCode ? handleDeleteDiscountCode() : handleSubmitDiscountCode()"
class="text-xs px-5 rounded-e-100 py-1.5 text-white bg-black hover:bg-transparent hover:text-black border-[1.5px] border-black hover:border-black transition-all disabled:cursor-not-allowed"
:disabled="!discountCode.length || submitDiscountCodeIsPending || deleteDiscountCodeIsPending"
>
<Icon
v-if="
submitDiscountCodeIsPending ||
deleteDiscountCodeIsPending
"
v-if="submitDiscountCodeIsPending || deleteDiscountCodeIsPending"
name="svg-spinners:180-ring-with-bg"
size="20"
class="**:fill-white"
+7 -2
View File
@@ -8,6 +8,7 @@ import { useImage } from "@vueuse/core";
type Props = {
src: string | null | undefined;
alt: string;
iconClass?: string;
};
// props
@@ -18,7 +19,7 @@ const { src } = toRefs(props);
// state
const { isLoading } = useImage({ src: src.value ?? '' });
const { isLoading } = useImage({ src: src.value ?? "" });
</script>
<template>
@@ -41,7 +42,11 @@ const { isLoading } = useImage({ src: src.value ?? '' });
:delay-ms="600"
>
<div class="size-full rounded-full flex-center">
<Icon name="ci:profile" size="16" class="**:stroke-black" />
<Icon
name="ci:profile"
class="**:stroke-black text-lg"
:class="iconClass"
/>
</div>
</AvatarFallback>
</template>
+25 -25
View File
@@ -24,7 +24,7 @@ const {} = toRefs(props);
</div>
<div class="-rotate-z-2 z-20">
<div
class="bg-blue-500 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee-reverse h-[90px] sm:h-[140px]"
class="bg-black flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee-reverse h-[90px] sm:h-[140px]"
>
<template v-for="i in 10">
<div class="text-[30px] lg:text-[40px] text-white whitespace-nowrap font-semibold opacity-85">
@@ -46,32 +46,32 @@ const {} = toRefs(props);
class="bg-slate-100/70 flex items-center pr-20 gap-12 sm:gap-20 w-max animate-marquee h-[90px] sm:h-[140px]"
>
<template v-for="i in 1">
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<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>
<template v-for="i in 1">
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-1.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-2.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-3.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-4.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-5.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<NuxtImg src="/img/brands/brand-6.png" class="h-[25px] sm:h-[45px] grayscale opacity-40" />
<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>
+1 -1
View File
@@ -40,7 +40,7 @@ const value = computed({
placeholder="وارد نشده"
altFormat="jYYYY-jMM-jDD"
color="#000000"
inputClass="!py-1 lg:!py-3.5 placeholder-slate-400 !rounded-s-md lg:!rounded-s-100 !border-[1.5px] hover:!border-black !transition-all !border-slate-200 text-xs lg:text-sm"
inputClass="!py-1 lg:!py-[9px] placeholder-slate-400 !rounded-s-md lg:!rounded-s-100 !border-[1.5px] hover:!border-black !transition-all !border-slate-200 text-xs lg:text-sm"
:autoSubmit="false"
v-model="value"
/>
+9 -6
View File
@@ -4,13 +4,16 @@
src="/img/footer-bg.jpg"
alt=""
class="absolute z-10 object-cover opacity-45"
:style="{
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0) 100%',
}"
/>
<div
class="flex flex-col gap-4 items-center justify-center relative z-20"
>
<div
class="flex items-center flex-col gap-8 pb-[10px] pt-[80px] lg:py-[150px] justify-center"
class="flex items-center flex-col gap-8 pb-[10px] pt-[80px] lg:pt-[150px] lg:pb-[50px] justify-center"
>
<img
src="/img/heymlz/heymlz-small-idle.gif"
@@ -28,13 +31,13 @@
>
<div class="flex flex-col gap-4 max-w-[300px]">
<h3
class="font-bold text-xl xl:text-3xl max-lg:text-center text-white"
class="font-bold text-lg xl:text-3xl max-lg:text-center text-white"
>
با ما در ارتباط باشید...
</h3>
<p
class="text-md font-thin leading-[175%] mt-4 max-lg:text-center text-slate-300"
class="text-md font-thin leading-[175%] mt-4 max-lg:text-center text-slate-300 max-lg:text-xs"
>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنگی با
تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان
@@ -77,7 +80,7 @@
<div class="flex flex-col gap-6 max-lg:text-center">
<h3 class="font-bold text-white">لینک های مفید</h3>
<ul
class="flex flex-col gap-4 font-thin text-slate-300"
class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs"
>
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
@@ -90,7 +93,7 @@
<div class="flex flex-col gap-6 max-lg:text-center">
<h3 class="font-bold text-white">لینک های مفید</h3>
<ul
class="flex flex-col gap-4 font-thin text-slate-300"
class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs"
>
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
@@ -105,7 +108,7 @@
لینک های مفید
</h3>
<ul
class="flex flex-col gap-4 font-thin text-slate-300"
class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs"
>
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
+40 -33
View File
@@ -29,44 +29,57 @@ const isHomePage = computed(() => route.path === "/");
class="w-full flex-center"
:class="isHomePage ? 'fixed top-0 left-0 z-999' : 'z-999'"
>
<div
class="size-full flex items-center justify-between container h-[65px] lg:h-[85px] shrink-0 grow-0"
>
<button class="md:hidden header-navbar-item" @click="isSideDrawerOpen = true">
<Icon name="humbleicons:bars" size="28" />
<div class="size-full flex items-center justify-between container h-[65px] lg:h-[85px] shrink-0 grow-0">
<button
class="md:hidden header-navbar-item"
@click="isSideDrawerOpen = true"
>
<Icon
name="humbleicons:bars"
size="28"
/>
</button>
<div class="max-md:hidden flex items-center gap-8 lg:gap-16">
<div
class="flex items-center justify-end gap-4 lg:gap-[1.5rem]"
>
<Tooltip v-if="!!account && !!token" title="حساب کاربری">
<div class="flex items-center justify-end gap-4 lg:gap-[1.5rem]">
<Tooltip
v-if="!!account && !!token"
title="حساب کاربری"
>
<NuxtLink
:to="{ name: 'profile' }"
class="flex items-center justify-center"
>
<Avatar
class="!size-7"
iconClass="header-navbar-item"
:src="account.profile_photo"
:alt="
account.first_name && account.last_name
? `${account.first_name.charAt(
0
)} ${account.last_name.charAt(0)}`
? `${account.first_name.charAt(0)} ${account.last_name.charAt(0)}`
: 'بدون نام کاربری'
"
/>
</NuxtLink>
</Tooltip>
<Tooltip v-else title="ورود">
<NuxtLink to="/signin" class="flex-center">
<Tooltip
v-else
title="ورود"
>
<NuxtLink
to="/signin"
class="flex-center"
>
<Icon
name="ci:profile"
class="**:stroke-black size-5 lg:size-6"
class="**:stroke-black size-5 lg:size-6 header-navbar-item"
/>
</NuxtLink>
</Tooltip>
<Tooltip title="محصولات">
<NuxtLink to="/products" class="flex-center header-navbar-item">
<NuxtLink
to="/products"
class="flex-center header-navbar-item"
>
<Icon
name="ci:search"
class="**:stroke-black size-4.5 lg:size-[21px]"
@@ -74,7 +87,10 @@ const isHomePage = computed(() => route.path === "/");
</NuxtLink>
</Tooltip>
<Tooltip title="سبد خرید">
<NuxtLink to="/cart" class="flex-center">
<NuxtLink
to="/cart"
class="flex-center"
>
<button class="relative">
<Icon
name="ci:cart"
@@ -97,28 +113,19 @@ const isHomePage = computed(() => route.path === "/");
v-for="(link, index) in NAV_LINKS"
:key="index"
:to="link.path"
class="underline-offset-[10px]"
activeClass="underline"
>
{{ link.title }}
</NuxtLink>
</nav>
</div>
<div class="header-navbar-item">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 lg:h-6 "
fill="none"
viewBox="0 0 220 40"
>
<path
fill="black"
d="M20 40c11.046 0 20-8.954 20-20V6a6 6 0 0 0-6-6H21v8.774c0 2.002.122 4.076 1.172 5.78a9.999 9.999 0 0 0 6.904 4.627l.383.062a.8.8 0 0 1 0 1.514l-.383.062a10 10 0 0 0-8.257 8.257l-.062.383a.8.8 0 0 1-1.514 0l-.062-.383a10 10 0 0 0-4.627-6.904C12.85 21.122 10.776 21 8.774 21H.024C.547 31.581 9.29 40 20 40Z"
></path>
<path
fill="black"
d="M0 19h8.774c2.002 0 4.076-.122 5.78-1.172a10.018 10.018 0 0 0 3.274-3.274C18.878 12.85 19 10.776 19 8.774V0H6a6 6 0 0 0-6 6v13ZM46.455 2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM211.711 12.104c5.591 0 8.289 3.905 8.289 8.428v8.495h-5.851V21.54c0-2.05-.748-3.742-2.893-3.742-2.145 0-2.86 1.692-2.86 3.742v7.486h-5.851V21.54c0-2.05-.715-3.742-2.861-3.742-2.145 0-2.893 1.692-2.893 3.742v7.486h-5.85v-8.495c0-4.523 2.697-8.428 8.288-8.428 3.056 0 5.266 1.204 6.274 3.189 1.072-1.985 3.413-3.19 6.208-3.19ZM180.427 23.82c1.885 0 2.698-1.725 2.698-3.776v-7.29h5.85v8.006c0 4.784-2.795 8.755-8.548 8.755-5.754 0-8.549-3.97-8.549-8.755v-8.006h5.851v7.29c0 2.05.812 3.776 2.698 3.776ZM163.275 29.547c-3.673 0-6.046-1.269-7.444-3.742l4.226-2.376c.585 1.041 1.462 1.562 2.925 1.562 1.203 0 1.755-.423 1.755-.944 0-1.985-8.581.033-8.581-6.28 0-3.06 2.6-5.533 7.021-5.533 3.868 0 5.981 1.887 6.924 3.71l-4.226 2.408c-.357-.976-1.463-1.562-2.568-1.562-.845 0-1.3.358-1.3.846 0 2.018 8.581.163 8.581 6.281 0 3.417-3.348 5.63-7.313 5.63ZM142.833 36.512h-5.851V20.858c0-4.98 3.738-8.592 8.939-8.592 5.071 0 8.939 3.873 8.939 8.592 0 5.207-3.446 8.657-8.614 8.657-1.203 0-2.405-.358-3.413-.912v7.909Zm3.088-12.497c1.853 0 3.088-1.432 3.088-3.125 0-1.724-1.235-3.124-3.088-3.124s-3.088 1.4-3.088 3.125c0 1.692 1.235 3.124 3.088 3.124ZM131.121 11.03c-1.918 0-3.51-1.595-3.51-3.515 0-1.92 1.592-3.515 3.51-3.515 1.918 0 3.511 1.595 3.511 3.515 0 1.92-1.593 3.515-3.511 3.515Zm-2.925 1.724h5.851v16.273h-5.851V12.754ZM116.97 29.515c-5.071 0-8.939-3.905-8.939-8.657 0-4.719 3.868-8.624 8.939-8.624s8.939 3.905 8.939 8.624c0 4.752-3.868 8.657-8.939 8.657Zm0-5.5c1.853 0 3.088-1.432 3.088-3.125 0-1.724-1.235-3.156-3.088-3.156s-3.088 1.432-3.088 3.156c0 1.693 1.235 3.125 3.088 3.125ZM96.983 37c-4.03 0-6.956-1.79-8.451-4.98l4.843-2.603c.52 1.107 1.495 2.246 3.51 2.246 2.114 0 3.511-1.335 3.674-3.678-.78.684-2.016 1.204-3.868 1.204-4.519 0-8.16-3.482-8.16-8.364 0-4.718 3.869-8.559 8.94-8.559 5.201 0 8.939 3.613 8.939 8.592v6.444c0 5.858-4.064 9.698-9.427 9.698Zm.39-13.31c1.755 0 3.088-1.205 3.088-2.995 0-1.757-1.332-2.929-3.088-2.929-1.723 0-3.088 1.172-3.088 2.93 0 1.79 1.365 2.993 3.088 2.993ZM78.607 29.515c-5.071 0-8.94-3.905-8.94-8.657 0-4.719 3.869-8.624 8.94-8.624 5.07 0 8.939 3.905 8.939 8.624 0 4.752-3.869 8.657-8.94 8.657Zm0-5.5c1.853 0 3.088-1.432 3.088-3.125 0-1.724-1.235-3.156-3.088-3.156s-3.088 1.432-3.088 3.156c0 1.693 1.235 3.125 3.088 3.125ZM59.013 7.06v16.434H68.7v5.533H58.2c-3.705 0-5.2-1.953-5.2-5.045V7.06h6.013Z"
></path>
</svg>
<div class="header-navbar-item flex items-center justify-end h-full">
<NuxtImg
src="/logo/logo-row.png"
class="h-2/3"
/>
</div>
</div>
</header>
+7 -5
View File
@@ -40,22 +40,24 @@ const classes = computed(() => {
"input-outlined": variant.value === "outlined",
"pointer-events-none opacity-80 text-slate-500": disabled.value,
"input-effects": !error.value,
[variant.value === "solid"
? "input-solid-error"
: "input-outlined-error"]: error.value,
[variant.value === "solid" ? "input-solid-error" : "input-outlined-error"]: error.value,
},
];
});
</script>
<template>
<div v-bind="$attrs" :class="classes" @click="inputRef?.focus()">
<div
v-bind="$attrs"
:class="classes"
@click="inputRef?.focus()"
>
<slot name="startItem" />
<input
v-model="value"
ref="inputRef"
class="outline-none flex-1 text-xs lg:text-sm placeholder-slate-400"
class="outline-none flex-1 text-xs lg:text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
:placeholder="placeholder"
/>
+91 -88
View File
@@ -1,116 +1,119 @@
<script lang="ts" setup>
// state
// states
const { $gsap: gsap } = useNuxtApp();
const disableLoadingOverlay = useState("disableLoadingOverlay");
const isSiteLoadingDisabled = useCookie("is-site-loading-disabled", {
default: () => false,
expires: new Date(Date.now() + 15 * 60 * 1000),
});
const shouldRenderLoadingOverlay = ref(true);
const isAssetLoaded = ref(false);
const criticalLoad = ref(true);
const progressInterval = ref<NodeJS.Timeout | null>(null);
const assetLoadingProgress = ref(10);
const isWindowScrollLocked = useScrollLock(window);
// lifecycle
// computed
onMounted(async () => {
const timeline = gsap.timeline();
timeline.to("#loading-overlay", {
opacity: 1,
});
isWindowScrollLocked.value = true;
const preload = (url: string) => {
return new Promise((resolve) => {
const image = new Image();
image.src = url;
image.onload = () => resolve(true);
});
const progressStyle = computed(() => {
return {
width: `${assetLoadingProgress.value}%`,
};
});
await Promise.all([
preload("/img/heymlz/heymlz-fast-loading.gif"),
preload("/img/heymlz/heymlz-pulling.gif"),
preload("/img/heymlz/heymlz-falling.gif"),
preload("/img/heymlz/heymlz-seat.gif"),
]);
// methods
timeline.to("#loading-overlay", {
const onAssetLoaded = () => {
clearInterval(progressInterval.value!);
criticalLoad.value = false;
assetLoadingProgress.value = 100;
isAssetLoaded.value = true;
};
const onAssetFinished = () => {
gsap.to("#loading-overlay", {
opacity: 0,
delay: 5.5,
onComplete: () => {
shouldRenderLoadingOverlay.value = false;
isWindowScrollLocked.value = false;
disableLoadingOverlay.value = true;
isSiteLoadingDisabled.value = true;
},
});
};
// watch
watch([assetLoadingProgress, criticalLoad], ([assetLoadingProgress, criticalLoad]) => {
if (criticalLoad && assetLoadingProgress >= 100) {
onAssetFinished();
}
});
// lifecycle
onMounted(() => {
if (!isSiteLoadingDisabled.value) {
isWindowScrollLocked.value = true;
const heymlzLoadingAnimation = document.querySelector("#heymlz-loading-animation") as HTMLVideoElement;
if (heymlzLoadingAnimation?.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
onAssetLoaded();
}
progressInterval.value = setInterval(() => {
assetLoadingProgress.value += Math.random() * 10;
}, 250);
gsap.to("#loading-overlay", {
opacity: 1,
});
} else {
shouldRenderLoadingOverlay.value = false;
}
});
</script>
<template>
<div
v-if="shouldRenderLoadingOverlay"
v-if="shouldRenderLoadingOverlay && !isSiteLoadingDisabled"
id="loading-overlay"
class="fixed inset-0 size-full z-9999 flex-center bg-black"
>
<NuxtImg
id="loading-overlay-image"
src="/img/heymlz/heymlz-fast-loading.gif"
class="absolute z-20 w-[700px] brightness-75"
alt=""
<div
class="flex-col-center gap-6 transition-all duration-450 ease-in-out"
:class="isAssetLoaded ? 'opacity-0 scale-75' : 'opacity-100 scale-100'"
>
<NuxtImg
src="/img/heymlz/heymlz-text-logo.png"
class="invert w-[250px]"
/>
<div class="bg-slate-800 w-[400px] h-1 rounded-full overflow-hidden">
<div
class="bg-slate-100 h-full w-full transition-all duration-250"
:style="progressStyle"
/>
</div>
</div>
<video
id="heymlz-loading-animation"
muted
autoplay
playsinline
webkit-playsinline
@canplay="onAssetLoaded"
@ended="onAssetFinished"
src="/video/heymlz/heymlz-fast-loading.mp4"
class="absolute z-20 w-[700px] brightness-75 transition-all duration-[1s]"
:class="isAssetLoaded ? 'opacity-100' : 'opacity-0'"
:style="{
mask: 'linear-gradient(to bottom, rgba(0,0,0,0) 0%, black 20%, black 80%, rgba(0,0,0,0) 100%)',
}"
/>
<!-- <div
id="loading-overlay-gradient"
class="opacity-0 scale-x-0 w-[1000px] h-[70px] bg-linear-to-r from-blue-500 via-violet-500 to-purple-500 blur-[150px] rounded-[100px]"
/> -->
</div>
</template>
<style>
#loading-overlay-image {
animation-name: loading-overlay-image-animation;
animation-duration: 1s;
animation-delay: 0.35s;
animation-fill-mode: forwards;
}
#loading-overlay-gradient {
animation: 1.5s normal 0.5s 1 forwards loading-overlay-gradient-animation,
1s ease-in-out 2s infinite alternate-reverse
loading-overlay-gradient-pules-animation;
}
@keyframes loading-overlay-image-animation {
from {
opacity: 0;
scale: 0.7;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes loading-overlay-gradient-animation {
from {
opacity: 0;
scale: 0 1 1;
}
to {
opacity: 0.9;
scale: 1 1 1;
}
}
@keyframes loading-overlay-gradient-pules-animation {
from {
opacity: 0.8;
scale: 0.8 1 1;
}
to {
opacity: 0.9;
scale: 1 1 1;
}
}
</style>
+1 -1
View File
@@ -77,7 +77,7 @@ const isShow = computed({
/>
</DialogClose>
<DialogTitle
class="typo-sub-h-xl font-semibold flex items-center gap-3"
class="typo-sub-h-md lg:typo-sub-h-xl font-semibold flex items-center gap-3"
>
{{ title }}
<Icon
+1 -1
View File
@@ -131,7 +131,7 @@ watch(() => status.value, (value) => {
:key="id"
:index="index"
:autofocus="autofocus ? index === 0 ? true : 'off' : 'off'"
class="disabled:text-slate-400 focus-within:border-black transition-all size-12 sm:size-16 bg-slate-50 typo-label-lg rounded-md sm:rounded-lg text-center border-[1.5px] border-slate-200 outline-none"
class="disabled:text-slate-400 persian-number focus-within:border-black transition-all size-10 sm:size-16 bg-slate-50 typo-label-lg rounded-sm sm:rounded-lg text-center border-[1.5px] border-slate-200 outline-none"
/>
</PinInputRoot>
</div>
+16 -11
View File
@@ -45,9 +45,7 @@ const classes = computed(() => {
"input-solid": variant.value === "solid",
"input-outlined": variant.value === "outlined",
"input-effects": !error.value,
[variant.value === "solid"
? "input-solid-error"
: "input-outlined-error"]: error.value,
[variant.value === "solid" ? "input-solid-error" : "input-outlined-error"]: error.value,
},
triggerRootClass.value,
];
@@ -61,14 +59,17 @@ const classes = computed(() => {
:disabled="disabled || loading"
>
<SelectTrigger :class="classes">
<slot v-if="!!$slots.trigger" name="trigger" />
<slot
v-if="!!$slots.trigger"
name="trigger"
/>
<SelectValue
v-else
:placeholder="placeholder"
v-bind="$attrs"
:class="selectedValue ? '!text-black' : 'text-slate-400'"
class="font-iran-yekan-x text-xs lg:text-sm text-start placeholder-slate-400"
:class="selectedValue ? '!text-black' : 'text-slate-400 font-normal'"
class="font-iran-yekan-x text-xs lg:text-sm text-start placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm"
/>
<Icon
:name="loading ? 'svg-spinners:3-dots-fade' : 'bi:chevron-down'"
@@ -83,7 +84,10 @@ const classes = computed(() => {
:side-offset="5"
>
<SelectViewport class="p-[5px]">
<slot v-if="!!$slots.content" name="content" />
<slot
v-if="!!$slots.content"
name="content"
/>
<SelectGroup v-else>
<SelectItem
@@ -95,11 +99,12 @@ const classes = computed(() => {
<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">
{{ option }}
</SelectItemText>
</SelectItem>
@@ -36,15 +36,12 @@ const highlights = ref<Highlight[]>([
<template>
<section class="w-full border-t-[0.5px] border-slate-200">
<div
class="w-full py-[5rem] gap-8 sm:gap-12 xl:gap-0 container grid grid-cols-2 lg:grid-cols-4"
class="w-full py-[3rem] lg:py-[5rem] gap-8 sm:gap-12 xl:gap-0 container grid grid-cols-2 lg:grid-cols-4"
>
<template v-for="(highlight, index) in highlights" :key="index">
<div class="flex flex-col-center gap-[.75rem] px-5">
<div class="size-[70px] md:size-[100px] flex-center">
<NuxtImg
:src="highlight.icon"
class="w-full"
/>
<NuxtImg :src="highlight.icon" class="w-full" />
</div>
<div class="w-full flex-col-center gap-[.25rem]">
+2 -4
View File
@@ -39,9 +39,7 @@ const classes = computed(() => {
"input-solid": variant.value === "solid",
"input-outlined": variant.value === "outlined",
"input-effects": !error.value,
[variant.value === "solid"
? "input-solid-error"
: "input-outlined-error"]: error.value,
[variant.value === "solid" ? "input-solid-error" : "input-outlined-error"]: error.value,
},
];
});
@@ -53,7 +51,7 @@ const classes = computed(() => {
ref="inputRef"
v-bind="$attrs"
:class="classes"
class="size-full outline-none placeholder-slate-400"
class="size-full outline-none placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
:placeholder="placeholder"
/>
</template>
@@ -13,7 +13,7 @@
<span class="typo-p-sm whitespace-nowrap">معمولا طی ۲ ساعت اماده میشود</span>
</div>
</div>
<span class="typo-p-xs">
<span class="typo-p-xs max-sm:hidden">
برسی موجودی در فروشگاه های دیگر
</span>
</div>
@@ -50,7 +50,7 @@ const onInput = (e: any) => {
<template>
<NumberFieldRoot
:disabled="disable"
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4"
class="rounded-full border-slate-200 border-[1.5px] flex items-center bg-white gap-4 p-4 max-sm:h-[48px]"
v-model="currentQuantity"
:min="1"
:max="max"
@@ -1,32 +1,31 @@
<script lang="ts" setup>
// types
type Props = {
maxQuantity: number;
quantity: number;
}
showSlider: boolean;
};
// props
defineProps<Props>();
</script>
<template>
<div class="flex flex-col gap-2 w-full">
<p class="typo-p-md text-slate-500">
<p class="typo-p-sm sm:typo-p-md text-slate-500">
تعداد
<span class="text-black font-bold">
{{ maxQuantity }}
</span>
عدد از این محصول موجود است
</p>
<div class="h-2 rounded-full relative bg-slate-200 w-full">
<div v-if="showSlider" class="h-2 rounded-full relative bg-slate-200 w-full">
<div
:style="{ width: `${quantity * (100 / maxQuantity)}%` }"
class="h-full absolute right-0 rounded-full bg-black transition-all ease-out"
/>
</div>
</div>
</template>
</template>
@@ -7,9 +7,7 @@ import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
// provide / inject
const { selectedVariant } = inject(
"productVariant"
) as ProductVariantProvideType;
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
// state
@@ -46,15 +44,14 @@ watch(
);
watch(selectedVariant, (newValue) => {
quantity.value = newValue!.cart_quantity;
quantity.value = newValue!.cart_quantity === 0 ? 1 : newValue!.cart_quantity;
});
// lifecycle
onMounted(() => {
quantity.value = selectedVariant.value!.cart_quantity;
quantity.value = selectedVariant.value!.cart_quantity === 0 ? 1 : selectedVariant.value!.cart_quantity;
});
</script>
<template>
@@ -65,14 +62,20 @@ onMounted(() => {
:max="selectedVariant!.in_stock"
>
<NumberFieldIncrement class="cursor-pointer">
<Icon name="ci:plus" class="**:stroke-slate-500 size-5" />
<Icon
name="ci:plus"
class="**:stroke-slate-500 size-5"
/>
</NumberFieldIncrement>
<div class="relative">
<div
:class="isAddCartItemPending ? 'opacity-100' : 'opacity-0'"
class="w-[40px] h-[25px] flex-center transition-all absolute bg-white"
>
<Icon :name="'svg-spinners:180-ring-with-bg'" class="size-[25px]" />
<Icon
:name="'svg-spinners:180-ring-with-bg'"
class="size-[25px]"
/>
</div>
<NumberFieldInput
@input="onInput"
@@ -81,7 +84,10 @@ onMounted(() => {
/>
</div>
<NumberFieldDecrement class="cursor-pointer">
<Icon name="ci:minus" class="**:stroke-slate-500 size-5" />
<Icon
name="ci:minus"
class="**:stroke-slate-500 size-5"
/>
</NumberFieldDecrement>
</NumberFieldRoot>
</template>
@@ -1,5 +1,4 @@
<script lang="ts" setup>
// types
type Props = {
@@ -7,33 +6,49 @@ type Props = {
title: string;
color: string;
price: string;
}
};
// props
const props = defineProps<Props>();
const { picture, price, title, color } = toRefs(props);
// methods
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
</script>
<template>
<div class="max-w-[500px] w-full h-[116px] flex items-center justify-between py-2 pe-6 ps-2 bg-white rounded-150">
<div
class="max-w-[500px] w-full h-[116px] flex items-center justify-between py-2 pe-6 ps-2 gap-4 bg-white rounded-150"
>
<div class="flex items-center gap-4">
<div class="relative size-[100px] rounded-100 overflow-hidden border-[0.5px] border-slate-200">
<NuxtImg :src="picture" :alt="title" class="object-cover absolute" />
<NuxtImg
:src="picture"
:alt="title"
class="object-cover absolute"
/>
</div>
<div class="flex flex-col gap-1.5">
<span class="typo-sub-h-md text-black">{{ title }}</span>
<div class="flex items-center gap-2">
<span class="typo-p-sm text-slate-500">رنگ</span>
<ColorCircle class="!size-5" :style="{backgroundColor: color}" />
<ColorCircle
class="!size-5"
:style="{ backgroundColor: color }"
/>
</div>
<span class="typo-p-md text-black">{{ price }}</span>
</div>
</div>
<Button class="rounded-full">
افزودن
<span class="max-sm:hidden">به سبد</span>
<Button
@click="scrollToTop"
class="rounded-full max-sm:h-[45px]"
>
مشاهده
</Button>
</div>
</template>
</template>
@@ -27,7 +27,9 @@ 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
@@ -137,7 +139,7 @@ watch(
? 'bg-black text-white'
: 'bg-slate-100'
"
class="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-full text-sm"
class="py-1 px-3 cursor-pointer text-nowrap transition-all rounded-md text-sm"
>
{{ sort.title }}
</button>
+3 -14
View File
@@ -22,24 +22,13 @@ const onSwiper = (swiper: SwiperClass) => {
ref="sectionTarget"
class="flex flex-col justify-center gap-4 bg-black h-[110svh] sm:h-[150svh] relative overflow-hidden"
>
<div class="w-full relative translate-y-[-90px] sm:translate-y-[-200px] z-10 container">
<div class="flex-col-center gap-6">
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4">
دسته بندی ها
</span>
<p
class="text-slate-300 text-center max-w-[750px] typo-p-sm md:typo-p-lg xl:typo-p-xl"
>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و
با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه
و مجله در ستون و سطرآنچنان که
</p>
</div>
<div class="w-full relative translate-y-[-55px] sm:translate-y-[-130px] flex-center z-10 container">
<span class="text-white typo-h-6 md:typo-h-5 lg:typo-h-4"> دسته بندی ها </span>
</div>
<div class="w-full my-20 relative">
<NuxtImg
class="aspect-square w-[240px] md:w-[300px] lg:w-[350px] translate-[-164px] md:translate-[-206px] lg:translate-[-240px] absolute left-1/2 -translate-x-1/2 z-10"
class="aspect-square 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"
:style="{
filter: 'drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.4))',
}"
+119 -67
View File
@@ -1,5 +1,4 @@
<script setup lang="ts">
// import
import { Swiper, SwiperSlide } from "swiper/vue";
@@ -15,12 +14,9 @@ const swiper_instance = ref<SwiperClass | null>(null);
const observerTarget = ref(null);
const shouldPauseVideos = ref(false);
useIntersectionObserver(
observerTarget,
([entry], observerElement) => {
shouldPauseVideos.value = entry.rootBounds ? !entry.isIntersecting : false;
}
);
useIntersectionObserver(observerTarget, ([entry], observerElement) => {
shouldPauseVideos.value = entry.rootBounds ? !entry.isIntersecting : false;
});
const isMuted = ref(true);
const slidesPerView = ref(1);
@@ -37,7 +33,7 @@ const onSwiper = (swiper: SwiperClass) => {
const updateVideoStates = () => {
const activeIndex = swiper_instance.value?.realIndex || 0;
const videosElements = document.querySelectorAll(`.slide-video`) as NodeListOf<HTMLVideoElement>;
videosElements.forEach(videoElement => {
videosElements.forEach((videoElement) => {
if (videoElement.id === `slide-video-${activeIndex}` && !shouldPauseVideos.value) {
videoElement.play();
} else {
@@ -48,58 +44,84 @@ const updateVideoStates = () => {
// watch
watch(() => [shouldPauseVideos.value, swiper_instance.value?.realIndex], () => {
updateVideoStates();
});
watch(
() => [shouldPauseVideos.value, swiper_instance.value?.realIndex],
() => {
updateVideoStates();
}
);
// lifecycle
const initializeGsapAnimation = () => {
gsapTimeline
.fromTo(".header-slider-item", {
borderRadius: 0,
height: "100svh"
}, {
height: "80svh",
borderRadius: "20px"
})
.fromTo(slidesPerView, {
value: 1
}, {
value: 1.2
}, "=")
.fromTo(".header-navbar-item", {
filter: "invert(100%)"
}, {
filter: "invert(0%)"
}, "=")
.fromTo("#header-navbar", {
background: "transparent"
}, {
background: "white"
})
.fromTo("#header-slider-wrapper", {
marginTop: "0px",
scale: 1.025
}, {
marginTop: () => {
const navbarEl = document.querySelector("#header-navbar") as HTMLDivElement;
return `${navbarEl.clientHeight}px`;
.fromTo(
".header-slider-item",
{
borderRadius: 0,
height: "100svh",
},
scale: 1
}, "=");
{
height: "80svh",
borderRadius: "20px",
}
)
.fromTo(
slidesPerView,
{
value: 1,
},
{
value: 1.2,
},
"="
)
.fromTo(
".header-navbar-item",
{
filter: "invert(100%)",
},
{
filter: "invert(0%)",
},
"="
)
.fromTo(
"#header-navbar",
{
background: "transparent",
},
{
background: "white",
}
)
.fromTo(
"#header-slider-wrapper",
{
marginTop: "0px",
scale: 1.025,
},
{
marginTop: () => {
const navbarEl = document.querySelector("#header-navbar") as HTMLDivElement;
return `${navbarEl.clientHeight}px`;
},
scale: 1,
},
"="
);
};
const resetTimelineForMobile = () => {
gsap.to("#header-navbar", {
background: "white"
background: "white",
});
gsap.to(".header-navbar-item", {
filter: "invert(0%)"
filter: "invert(0%)",
});
gsap.set(".header-slider-item", {
borderRadius: "20px",
height: "450px"
height: "450px",
});
};
@@ -118,7 +140,7 @@ onMounted(() => {
pin: true,
start: "top top",
// markers: true,
end: "bottom top"
end: "bottom top",
});
const calculateOnResize = () => {
@@ -141,14 +163,12 @@ onMounted(() => {
window.addEventListener("resize", () => {
calculateOnResize();
});
});
onUnmounted(() => {
resetTimelineForMobile();
scrollTrigger.disable();
});
</script>
<template>
@@ -156,7 +176,10 @@ onUnmounted(() => {
id="header-slider-container"
class="w-full z-50"
>
<div id="header-slider-wrapper" class="relative">
<div
id="header-slider-wrapper"
class="relative"
>
<Swiper
ref="observerTarget"
:slides-per-view="slidesPerView"
@@ -164,8 +187,8 @@ onUnmounted(() => {
:centered-slides="true"
:breakpoints="{
768: {
spaceBetween : 40
}
spaceBetween: 40,
},
}"
@swiper="onSwiper"
>
@@ -174,9 +197,7 @@ onUnmounted(() => {
:key="slide.id"
>
<div class="max-md:container">
<div
class="header-slider-item relative w-full overflow-hidden max-md:rounded-[20px]"
>
<div class="header-slider-item relative w-full overflow-hidden max-md:rounded-[20px]">
<template v-if="!!slide.video">
<video
:id="`slide-video-${index}`"
@@ -202,14 +223,15 @@ onUnmounted(() => {
:class="swiper_instance?.realIndex !== index ? 'opacity-0' : ''"
class="w-full transition-opacity pb-6 xs:pb-10 lg:pb-16 px-6 xs:px-10 lg:px-16 gap-6 xs:gap-10 lg:gap-12 container flex flex-col h-full justify-end relative z-10"
>
<div class="header-slider-item-child w-full">
<div class="border-b border-white/10 pb-6 flex flex-col gap-2 md:gap-4">
<div class="flex items-center gap-4 lg:gap-8">
<div
class="hover:scale-110 size-[36px] md:size-[42px] lg:size-[50px] relative flex items-center justify-center">
class="hover:scale-110 size-[36px] md:size-[42px] lg:size-[50px] relative flex items-center justify-center"
>
<div
class="size-full scale-75 bg-white absolute rounded-full animate-ping" />
class="size-full scale-75 bg-white absolute rounded-full animate-ping"
/>
<button
@click="isMuted = !isMuted"
class="transition-all cursor-pointer flex-center bg-white z-10 size-full rounded-full"
@@ -244,7 +266,11 @@ onUnmounted(() => {
id="header-slider-pagination-child"
class="flex items-center justify-between"
>
<button @click="swiper_instance?.slidePrev()">
<button
@click="swiper_instance?.slidePrev()"
class="relative"
>
<div class="size-8 blur-xl bg-white absolute ping-animation max-sm:hidden"></div>
<Icon
class="**:stroke-white cursor-pointer size-6 md:size-8"
name="ci:arrow-right"
@@ -253,27 +279,53 @@ onUnmounted(() => {
<div class="flex items-center justify-center gap-3 text-white">
<div
v-for="(_slide, index) in homeData!.sliders"
:class="swiper_instance?.realIndex === index ? 'bg-white' : 'bg-transparent'"
:class="
swiper_instance?.realIndex === index ? 'bg-white' : 'bg-transparent'
"
class="border border-white size-2 md:size-3 rounded-full transition-all duration-200"
>
</div>
></div>
</div>
<button>
<button
@click="swiper_instance?.slideNext()"
class="relative"
>
<div class="size-8 blur-xl bg-white absolute ping-animation max-sm:hidden"></div>
<Icon
@click="swiper_instance?.slideNext()"
class="**:stroke-white cursor-pointer size-6 md:size-8"
name="ci:arrow-left"
/>
</button>
</div>
</div>
</div>
</div>
</SwiperSlide>
</Swiper>
</div>
</div>
</template>
</template>
<style>
@keyframes ping-anime {
0% {
scale: 0.7;
opacity: 1;
}
10% {
scale: 2;
opacity: 0;
}
100% {
scale: 2;
opacity: 0;
}
}
.ping-animation {
animation-name: ping-anime;
animation-iteration-count: infinite;
animation-duration: 6s;
}
</style>
+32 -52
View File
@@ -32,10 +32,12 @@ watch(
heymlzElementIsVisible,
(newValue) => {
if (newValue) {
showHeymlzAnimation.value = true;
setTimeout(() => {
showHeymlzAnimation.value = false;
}, 3200);
showHeymlzAnimation.value = true;
setTimeout(() => {
showHeymlzAnimation.value = false;
}, 3200);
}, 400);
}
},
{
@@ -61,9 +63,7 @@ watch(
(newValue) => {
const clientRect = previewContainerEl.value?.getBoundingClientRect()!;
const percent = clientRect.width / 100;
const clipPercent =
(newValue + draggableEl.value!.clientWidth / 2 - clientRect.x - 8) /
percent;
const clipPercent = (newValue + draggableEl.value!.clientWidth / 2 - clientRect.x - 8) / percent;
if (clipPercent >= 5 && clipPercent <= 95) {
clipPathPercent.value = clipPercent;
}
@@ -75,12 +75,8 @@ watch(
<div class="container mb-40 lg:mb-80 mt-20">
<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
>
<span class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-black">
تفاوت محصلات ما را ببینید
</span>
<span class="typo-p-sm md:typo-p-md text-slate-500"> مقایسه محصولات </span>
<span class="typo-h-6 md:typo-h-5 lg:typo-h-3 text-black"> تفاوت محصلات ما را ببینید </span>
</div>
<div
ref="previewContainerEl"
@@ -90,11 +86,7 @@ watch(
<NuxtImg
v-if="activeSlideVideo !== 'right'"
:src="homeData!.difreance_section.image1"
:class="
showHeymlzAnimation
? 'brightness-35'
: 'brightness-[95%]'
"
:class="showHeymlzAnimation ? 'brightness-25 blur-sm' : 'brightness-[95%] blur-[0px]'"
class="select-none absolute size-full object-cover transition-[filter] duration-250"
:alt="homeData!.difreance_section.title1"
/>
@@ -117,11 +109,7 @@ watch(
<NuxtImg
v-if="activeSlideVideo !== 'left'"
:src="homeData!.difreance_section.image2"
:class="
showHeymlzAnimation
? 'brightness-35'
: 'brightness-[95%]'
"
:class="showHeymlzAnimation ? 'brightness-25 blur-sm' : 'brightness-[95%] blur-[0px]'"
class="overlay-image select-none absolute object-cover size-full transition-[filter] duration-250"
:alt="homeData!.difreance_section.title2"
/>
@@ -136,15 +124,20 @@ watch(
/>
</Transition>
<NuxtImg
v-if="showHeymlzAnimation"
src="/img/heymlz/heymlz-pulling.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
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="{
@@ -154,33 +147,25 @@ watch(
activeSlideVideo !== 'none' ? 'opacity-10' : '',
showHeymlzAnimation ? 'bg-neutral-200' : 'bg-black',
]"
class="select-none w-2 h-full absolute left-0 flex items-center justify-center transition-opacity duration-250"
class="select-none w-[5px] sm:w-2 h-full absolute left-0 flex items-center justify-center transition-opacity duration-250"
>
<div
ref="draggableEl"
: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"
: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"
>
<Icon
name="ci:arrows"
size="24"
class="transition-all"
:class="
showHeymlzAnimation
? '**:stroke-black'
: '**:stroke-white'
"
class="transition-all size-5 sm:size-6"
:class="showHeymlzAnimation ? '**:stroke-black' : '**:stroke-white'"
/>
</div>
</div>
</div>
<div
class="absolute bottom-0 p-6 md:p-10 w-full flex justify-between items-end"
: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
class="flex flex-col gap-2 text-black transition-opacity"
@@ -218,11 +203,6 @@ watch(
<style>
.overlay-image {
clip-path: polygon(
v-bind('clipPathPercent + "%"') 0,
100% 0,
100% 100%,
v-bind('clipPathPercent + "%"') 100%
);
clip-path: polygon(v-bind('clipPathPercent + "%"') 0, 100% 0, 100% 100%, v-bind('clipPathPercent + "%"') 100%);
}
</style>
@@ -1,40 +0,0 @@
<script setup lang="ts">
// types
type Props = {
circle?: boolean,
size?: number
}
// props
const props = withDefaults(defineProps<Props>(), {
size: 200
});
const { circle } = toRefs(props);
</script>
<template>
<div
:style="{
height: `${size}px`,
width: circle ? `${size}px` : '100%'
}"
class="relative flex items-center w-full justify-center shrink-0"
:class="{
'rounded-full overflow-hidden': circle,
}"
>
<NuxtImg
:style="{
maskImage: 'radial-gradient(black, transparent 70%)'
}"
src="/img/heymlz/heymlz-idle.gif"
class="size-full object-cover absolute pt-2"
alt="ai-loading"
/>
</div>
</template>
@@ -1,7 +1,6 @@
<script setup lang="ts">
// import
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
import useGetChat from "~/composables/api/chat/useGetChat";
import ChatInput from "~/components/product/ChatBox/ChatInput.vue";
import { useIsMutating } from "@tanstack/vue-query";
@@ -20,6 +19,8 @@ const { isLoggedIn } = useAuth();
const route = useRoute();
const id = route.params.id as string | number;
const isMobile = useMediaQuery("(max-width: 480px)");
const scrollToBottomTimer = ref<NodeJS.Timeout | null>(null);
const chatContainerEl = ref<HTMLElement | null>(null);
@@ -31,26 +32,24 @@ const {
isPending: isChatPending,
isFetchingNextPage: isNextChatPagePending,
hasNextPage: hasMoreChat,
fetchNextPage: loadMoreChat
fetchNextPage: loadMoreChat,
} = useGetChat(id, isOpen);
const isCreateMessagePending = useIsMutating({
mutationKey: [MUTATION_KEYS.create_chat]
mutationKey: [MUTATION_KEYS.create_chat],
});
const canLoadMoreChat = ref(false);
const isChatScrollLocked = useScrollLock(chatContainerEl);
const { y: chatContainerScrollY } = useScroll(chatContainerEl, {
behavior: "smooth"
behavior: "smooth",
});
useInfiniteScroll(
chatContainerEl,
async () => {
if (hasMoreChat.value && !isChatPending.value) {
lastMessageBeforeUpdate.value = chatMessages.value
? chatMessages.value[0].id
: 0;
lastMessageBeforeUpdate.value = chatMessages.value ? chatMessages.value[0].id : 0;
await loadMoreChat();
}
},
@@ -58,7 +57,7 @@ useInfiniteScroll(
distance: 10,
direction: "top",
throttle: 1000,
canLoadMore: () => canLoadMoreChat.value
canLoadMore: () => canLoadMoreChat.value,
}
);
@@ -103,8 +102,7 @@ watch(
`#message-container-${lastMessageBeforeUpdate.value}`
) as HTMLElement;
lastChatMessageEl?.scrollIntoView();
chatContainerEl.value!.scrollTop =
chatContainerEl.value!.scrollTop + scrollTopOld;
chatContainerEl.value!.scrollTop = chatContainerEl.value!.scrollTop + scrollTopOld;
});
}
}
@@ -121,28 +119,31 @@ whenever(
}, 2000);
},
{
once: true
once: true,
}
);
</script>
<template>
<Transition name="fade-right-to-left">
<Transition :name="isMobile ? 'fade-down' : 'fade-right-to-left'">
<div
v-if="isOpen"
class="fixed z-50 right-8 bottom-8 w-[450px] transition-all duration-500 overflow-hidden h-[700px] rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
class="fixed z-50 max-xs:inset-0 xs:right-8 xs:bottom-8 w-full xs:w-[450px] transition-all duration-500 overflow-hidden h-svh xs:h-[700px] xs:rounded-250 shadow-2xl shadow-black/30 pt-[40px] bg-white"
>
<CloseButton :disabled="!!isCreateMessagePending" />
<template v-if="isLoggedIn">
<Transition name="zoom" mode="out-in">
<Transition
name="zoom"
mode="out-in"
>
<div
v-if="!isChatPending"
class="p-4.5 h-full flex flex-col justify-between gap-4"
>
<div
:style="{
maskImage: 'linear-gradient(to top, transparent, black 5%, black, black)'
maskImage: 'linear-gradient(to top, transparent, black 5%, black, black)',
}"
class="hide-scrollbar flex flex-col py-7 gap-6 h-full overflow-y-auto"
ref="chatContainerEl"
@@ -183,7 +184,11 @@ whenever(
v-else
class="w-full h-full flex items-center justify-center absolute inset-0"
>
<AiLoading />
<NuxtImg
class="size-[250px] drop-shadow-2xl"
src="/img/heymlz/heymlz-small-idle.gif"
alt=""
/>
</div>
</Transition>
</template>
@@ -191,12 +196,15 @@ whenever(
class="text-black p-6 size-full flex justify-center items-center flex-col"
v-else
>
<NuxtImg class="size-[250px]" src="/img/heymlz/heymlz-loading-1.gif" alt="" />
<NuxtImg
class="size-[250px] drop-shadow-2xl"
src="/img/heymlz/heymlz-small-idle.gif"
alt=""
/>
<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">
@@ -1,7 +1,19 @@
<script setup lang="ts">
// types
type Props = {
showChatButton: boolean;
};
// props
defineProps<Props>();
// state
const isOpen = ref(false);
const isMobile = useMediaQuery("(max-width: 480px)");
const isWindowScrollLocked = useScrollLock(window);
// methods
@@ -14,19 +26,29 @@ provide("isOpen", {
closeChat,
});
// watches
watch([isMobile, isOpen], ([isMobile, isOpen]) => {
isWindowScrollLocked.value = isMobile && isOpen;
});
</script>
<template>
<button
v-if="!isOpen"
@click="isOpen = !isOpen"
class="cursor-pointer z-50 fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-black size-[70px] flex justify-center items-center rounded-full"
<Transition
name="fade"
:duration="150"
>
<Icon
name="streamline:artificial-intelligence-spark"
class="**:stroke-white size-[26px]"
/>
</button>
<button
v-if="showChatButton && !isOpen"
@click="isOpen = !isOpen"
class="cursor-pointer z-50 fixed shadow-xl shadow-black/30 right-8 bottom-8 bg-blue-500 size-[70px] flex justify-center items-center rounded-full"
>
<Icon
name="streamline:artificial-intelligence-spark"
class="**:stroke-white size-[26px]"
/>
</button>
</Transition>
<ChatBoxContainer :isOpen="isOpen" />
</template>
@@ -1,7 +1,6 @@
<script setup lang="ts">
// types
import AiLoading from "~/components/product/ChatBox/AiLoading.vue";
import useCreateChatMessage from "~/composables/api/chat/useCreateChatMessage";
import { useToast } from "~/composables/global/useToast";
@@ -14,8 +13,7 @@ const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast();
const { mutateAsync: createMessage, isPending: isCreatingMessage } =
useCreateChatMessage(queryClient);
const { mutateAsync: createMessage, isPending: isCreatingMessage } = useCreateChatMessage(queryClient);
const chatInputEl = ref<HTMLInputElement | null>(null);
@@ -46,7 +44,10 @@ const sendMessage = async () => {
<template>
<div class="relative">
<div id="poda" class="poda-rotate">
<div
id="poda"
class="poda-rotate"
>
<div
class="glow w-full"
:class="isCreatingMessage ? '' : '!opacity-0'"
@@ -80,11 +81,7 @@ const sendMessage = async () => {
<input
ref="chatInputEl"
:disabled="isCreatingMessage"
:placeholder="
isCreatingMessage
? 'دارم فکر میکنم...'
: 'سوال خود را بپرسید'
"
:placeholder="isCreatingMessage ? 'دارم فکر میکنم...' : 'سوال خود را بپرسید'"
type="text"
name="text"
class="focus:outline-none h-full typo-p-sm w-full border-none"
@@ -97,11 +94,11 @@ const sendMessage = async () => {
:class="isCreatingMessage ? 'bg-transparent' : 'bg-black'"
>
<TransitionGroup name="fade-down">
<AiLoading
<Icon
v-if="isCreatingMessage"
circle
:size="75"
class="mb-1"
name="svg-spinners:wind-toy"
size="24"
class="text-black"
/>
<Icon
v-else
@@ -188,14 +185,7 @@ const sendMessage = async () => {
height: 600px;
background-repeat: no-repeat;
background-position: 0 0;
background-image: conic-gradient(
transparent,
#998fdc,
transparent 10%,
transparent 50%,
#cf7bba,
transparent 60%
);
background-image: conic-gradient(transparent, #998fdc, transparent 10%, transparent 50%, #cf7bba, transparent 60%);
transition: all 2s;
}
@@ -1,4 +1,6 @@
<script setup lang="ts">
import useGetAccount from '~/composables/api/account/useGetAccount';
// types
type Props = {
@@ -21,6 +23,7 @@ const emit = defineEmits(["textUpdate"]);
// state
const { $gsap: gsap } = useNuxtApp();
const { data: account } = useGetAccount();
// methods
@@ -79,26 +82,32 @@ onMounted(() => {
>
<NuxtImg
v-if="reverse"
src="/img/heymlz/footer-share.svg"
src="/img/heymlz/heymlz-full-body.jpg"
class="size-full object-cover absolute"
alt="profile"
/>
<NuxtImg
v-else
:src="account?.profile_photo ?? ''"
class="size-full object-cover absolute"
alt="profile"
/>
</div>
<div
class="rounded-150 px-4 py-3"
class="rounded-150 px-4 py-3 whitespace-pre-wrap overflow-hidden"
:class="
reverse
? 'bg-slate-100 text-slate-600'
: 'bg-black text-white'
"
>
<p
<div
v-if="!loadingContent"
:id="`chat-message-content-${id}`"
class="typo-p-sm font-normal whitespace-pre-wrap"
v-html="content"
>
{{ content }}
</p>
</div>
<Icon v-else name="svg-spinners:3-dots-bounce" size="20" />
</div>
+37 -10
View File
@@ -15,16 +15,17 @@ const page = ref(1);
const { token } = useAuth();
const userComment = ref("");
const showMoreComments = ref(false);
const { data: comments, refetch: refetchComments } = useGetComments(id, page);
const { mutateAsync: createComment, isPending: isCreateCommentPending } =
useCreateComment(id);
const { mutateAsync: createComment, isPending: isCreateCommentPending } = useCreateComment(id);
// methods
const submitComment = async () => {
if (userComment.value.length > 3) {
await createComment({
content: userComment.value
content: userComment.value,
});
userComment.value = "";
@@ -32,6 +33,16 @@ const submitComment = async () => {
await refetchComments();
}
};
// computed
const limitedComments = computed(() => {
if (showMoreComments.value) {
return comments.value!.results;
}
return comments.value!.results.slice(0, 3);
});
</script>
<template>
@@ -43,9 +54,7 @@ const submitComment = async () => {
<h3 class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">نظرات کاربران</h3>
<div class="flex flex-col gap-2">
<Rating :rate="2" />
<span class="typo-p-sm">
بر اساس {{ comments?.count }} نظر
</span>
<span class="typo-p-sm"> بر اساس {{ comments?.count }} نظر </span>
</div>
<form
@submit.prevent="submitComment"
@@ -53,7 +62,7 @@ const submitComment = async () => {
>
<textarea
:disabled="!token"
class="w-full min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200"
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"
v-model="userComment"
placeholder="نظر خود را بنویسید..."
/>
@@ -66,8 +75,14 @@ const submitComment = async () => {
>
نظر بنویسید
</Button>
<NuxtLink v-else to="/signin">
<Button type="button" class="rounded-full w-full">
<NuxtLink
v-else
to="/signin"
>
<Button
type="button"
class="rounded-full w-full"
>
وارد شوید
</Button>
</NuxtLink>
@@ -75,18 +90,30 @@ const submitComment = async () => {
</div>
<div class="flex flex-col gap-8 w-full">
<Comment
v-for="comment in comments!.results"
v-for="comment in limitedComments"
:key="comment.id"
title=""
:content="comment.content"
:date="comment.timestamp"
:username="'منصور مرزبان'"
/>
<div class="flex items-center justify-center w-full">
<Pagination
v-if="showMoreComments"
:total="comments!.count"
:items="comments!.results.map((item, i) => ({ type: 'page', value: i }))"
/>
<Button
v-else
type="button"
variant="primary"
@click="showMoreComments = !showMoreComments"
class="rounded-full px-8"
end-icon="bi:plus"
>
نمایش همه
</Button>
</div>
</div>
</div>
+73 -50
View File
@@ -1,5 +1,4 @@
<script lang="ts" setup>
// import
import useGetProduct from "~/composables/api/product/useGetProduct";
@@ -31,61 +30,75 @@ const { selectedVariant, changeSelectedVariant } = inject("productVariant") as P
const addItemToCart = async () => {
await addCartItem({
id: selectedVariant.value!.id,
quantity: selectedQuantity.value
quantity: selectedQuantity.value,
});
await refetchProduct();
};
// watch
watch(() => selectedVariantId.value, (newId) => {
const newVariant = product.value!.variants.find(variant => variant.id === newId)!;
watch([selectedVariantId, product], ([selectedVariantId, product]) => {
const newVariant = product!.variants.find((variant) => variant.id === selectedVariantId)!;
changeSelectedVariant(newVariant);
});
watch(() => selectedColor.value, (newValue) => {
const filteredVariants = product.value!.variants.filter(v => v.color === newValue);
selectedVariantId.value = filteredVariants[0].id;
selectedVariant.value = filteredVariants[0];
}, {
immediate: true
});
watch(() => selectedVariant.value!, (newValue) => {
selectedQuantity.value = 1;
selectedSlide.value = newValue.images[0].id;
});
watch(
() => selectedColor.value,
(newValue) => {
const filteredVariants = product.value!.variants.filter((v) => v.color === newValue);
selectedVariantId.value = filteredVariants[0].id;
selectedVariant.value = filteredVariants[0];
},
{
immediate: true,
}
);
watch(
() => selectedVariant.value!,
(newValue) => {
selectedQuantity.value = 1;
selectedSlide.value = newValue.images[0].id;
}
);
</script>
<template>
<div class="flex max-lg:flex-col gap-12 xl:gap-16 container pt-[5rem] pb-28">
<div class="flex flex-col gap-3 lg:hidden">
<NuxtLink to="#" class="typo-label-sm"> {{ product!.category.name }}</NuxtLink>
<h1 class="typo-h-6 xs:typo-h-5 sm:typo-h-4 lg:typo-h-2"> {{ product!.name }} </h1>
<NuxtLink
to="#"
class="typo-label-sm"
>
{{ product!.category.name }}</NuxtLink
>
<h1 class="typo-h-6 xs:typo-h-5 sm:typo-h-4 lg:typo-h-2">
{{ product!.name }}
</h1>
<div class="flex w-full items-center justify-between h-[85px]">
<div class="flex items-end gap-4">
<div class="flex flex-col gap-2">
<span
v-if="selectedVariant!.discount > 0"
class="typo-p-lg relative flex-center w-fit"
class="typo-p-md sm:typo-p-lg relative flex-center w-fit"
:class="'after:w-full after:h-[2px] after:bg-black after:absolute'"
>
{{ selectedVariant!.price }}
</span>
<span
class="typo-p-2xl relative flex-center w-fit font-medium"
>
<span class="typo-p-md sm:typo-p-2xl relative flex-center w-fit font-medium">
{{ selectedVariant!.discount > 0 ? selectedVariant!.price : selectedVariant!.price }}
</span>
</div>
<div
v-if="selectedVariant!.discount > 0"
class="text-white bg-blue-500 mb-1 px-4 py-2 text-xs rounded-full flex items-center gap-1"
class="text-white bg-blue-500 mb-1 px-3 sm:px-4 py-1.5 sm:py-2 text-xs rounded-full flex items-center gap-1"
>
<Icon name="material-symbols:percent" class="size-4" />
<Icon
name="material-symbols:percent"
class="size-3.5 sm:size-4"
/>
{{ selectedVariant!.discount }}
درصد تخفیف
<span class="max-sm:hidden"> تخفیف درصد </span>
</div>
</div>
<Rating :rate="3" />
@@ -103,7 +116,9 @@ watch(() => selectedVariant.value!, (newValue) => {
>
{{ product!.category.name }}
</NuxtLink>
<h1 class="typo-h-4 xl:typo-h-3 max-lg:hidden"> {{ product!.name }} </h1>
<h1 class="typo-h-4 xl:typo-h-3 max-lg:hidden">
{{ product!.name }}
</h1>
<div class="flex w-full items-center justify-between h-[85px] max-lg:hidden">
<div class="flex items-end gap-4">
<div class="flex flex-col gap-2">
@@ -114,9 +129,7 @@ watch(() => selectedVariant.value!, (newValue) => {
>
{{ selectedVariant!.price }}
</span>
<span
class="typo-p-2xl relative flex-center w-fit font-medium"
>
<span class="typo-p-2xl relative flex-center w-fit font-medium">
{{ selectedVariant!.discount > 0 ? selectedVariant!.price : selectedVariant!.price }}
</span>
</div>
@@ -124,37 +137,42 @@ watch(() => selectedVariant.value!, (newValue) => {
v-if="selectedVariant!.discount > 0"
class="text-white bg-blue-500 mb-1 px-4 py-2 text-xs rounded-full flex items-center gap-1"
>
<Icon name="material-symbols:percent" class="size-4" />
<Icon
name="material-symbols:percent"
class="size-4"
/>
{{ selectedVariant!.discount }}
<span class="max-sm:hidden">درصد</span>
تخفیف
</div>
<Rating :rate="3" class="sm:hidden" />
<Rating
:rate="3"
class="sm:hidden"
/>
</div>
<Rating :rate="3" class="max-sm:hidden" />
<Rating
:rate="3"
class="max-sm:hidden"
/>
</div>
<div
class="py-8 typo-p-md text-slate-500 text-justify [&_a]:text-blue-400 [&_strong]:font-bold [&_u]:text-red-400"
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"
v-html="product!.description"
/>
<div class="flex items-center gap-4">
<span class="typo-p-lg">
تنوع رنگی :
</span>
<span class="typo-md sm:typo-p-lg"> تنوع رنگی : </span>
<div class="flex items-center gap-4 py-4">
<ColorCircle
v-for="color in product!.colors"
:key="color"
@click="selectedColor = color"
selectable
:selected="selectedColor === color "
:style="{backgroundColor: color}"
:selected="selectedColor === color"
:style="{ backgroundColor: color }"
class="cursor-pointer"
/>
</div>
@@ -162,7 +180,7 @@ watch(() => selectedVariant.value!, (newValue) => {
<div class="flex items-center gap-6 flex-wrap">
<ProductVariant
@click="variant.in_stock > 0 ? selectedVariantId = variant.id : undefined"
@click="variant.in_stock > 0 ? (selectedVariantId = variant.id) : undefined"
v-for="variant in product!.variants.filter(p => p.color === selectedColor)"
:key="variant.id"
:variantDetail="variant"
@@ -171,8 +189,8 @@ watch(() => selectedVariant.value!, (newValue) => {
</div>
<div class="w-full flex flex-col gap-6 mt-10">
<RemainQuantity
:showSlider="selectedVariant!.cart_quantity === 0"
:maxQuantity="selectedVariant!.in_stock"
:quantity="selectedQuantity"
/>
@@ -184,24 +202,31 @@ watch(() => selectedVariant.value!, (newValue) => {
@click="addItemToCart"
:loading="isAddCartItemPending"
:disabled="isAddCartItemPending"
class="w-full rounded-full"
class="w-full rounded-full max-sm:h-[48px]"
end-icon="ci:plus"
>
افزودن به سبد خرید
</Button>
<NuxtLink v-else to="/cart" class="w-full">
<NuxtLink
v-else
to="/cart"
class="w-full"
>
<Button
class="w-full rounded-full h-full"
class="w-full rounded-full h-full max-sm:h-[48px]"
end-icon="ci:cart"
>
مشاهده در سبد خرید
</Button>
</NuxtLink>
</template>
<NuxtLink v-else to="/signin" class="w-full">
<NuxtLink
v-else
to="/signin"
class="w-full"
>
<Button
class="w-full rounded-full h-full"
class="w-full rounded-full h-full max-sm:h-[48px]"
end-icon="ci:user"
>
ابتدا وارد شوید
@@ -216,13 +241,11 @@ watch(() => selectedVariant.value!, (newValue) => {
/>
<UpdateQuantity v-else />
</div>
<InfoCard />
<Share />
</div>
</div>
</div>
@@ -34,7 +34,7 @@ defineProps<Props>();
</div>
<div class="w-full">
<div class="w-full flex justify-between items-center gap-2">
<span class="text-xl font-medium">
<span class="text-md sm:text-xl font-medium">
{{ variantDetail.price }}
</span>
<div
@@ -68,7 +68,7 @@ defineProps<Props>();
<div
v-for="attribute in variantDetail.product_attributes"
class="flex items-center gap-2 text-sm rounded-full border border-slate-400 px-4 h-[40px]"
class="flex items-center gap-2 text-xs sm:text-sm rounded-full border border-slate-400 px-3 sm:px-4 h-[35px] sm:h-[40px]"
>
<span>{{ attribute.attribute_type.name }}</span>
<span>{{ attribute.value }}</span>
+57 -12
View File
@@ -1,10 +1,27 @@
<script lang="ts" setup>
// import
import useGetProduct from "~/composables/api/product/useGetProduct";
import type { ProductVariantProvideType } from "~/pages/product/[id].vue";
// types
type Props = {
showChatButton: boolean;
};
// props
defineProps<Props>();
// emits
const emit = defineEmits(["update:showChatButton"]);
// provide / inject
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
// state
const route = useRoute();
@@ -12,12 +29,32 @@ const id = route.params.id as string | undefined;
const { data: product } = useGetProduct(id);
const { selectedVariant } = inject("productVariant") as ProductVariantProvideType;
const videoSectionEl = useTemplateRef<HTMLDivElement>("videoSectionEl");
const { bottom: videoSectionBottomBounding, height: videoSectionHeightBounding } = useElementBounding(videoSectionEl);
const isVideoSectionVisible = useElementVisibility(videoSectionEl, {
rootMargin: "0px 0px -20% 0px",
});
// computed
const isVideoSectionHittingBottom = computed(() => {
return videoSectionBottomBounding.value < videoSectionHeightBounding.value - 100;
});
// watch
watch([isVideoSectionVisible, isVideoSectionHittingBottom], ([isVideoSectionVisible, isVideoSectionHittingBottom]) => {
emit("update:showChatButton", !(isVideoSectionVisible && !isVideoSectionHittingBottom));
});
</script>
<template>
<section v-if="selectedVariant?.video" class="h-[110svh] w-full relative bg-black mt-[5rem]">
<section
v-if="selectedVariant?.video"
ref="videoSectionEl"
class="h-[110svh] w-full relative bg-black mt-[5rem]"
>
<video
:src="selectedVariant.video"
class="object-cover absolute size-full"
@@ -28,14 +65,22 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
loop
/>
<div class="size-full absolute inset-0 bg-black/20" />
<div class="absolute max-sm:flex items-center justify-center max-sm:px-5 sm:right-10 bottom-10 w-full">
<StickyCard
:color="selectedVariant.color!"
:price="selectedVariant.price"
:picture="selectedVariant.images[0].image"
:title="product!.name"
class="max-sm:!w-full"
/>
</div>
<Transition
name="fade"
:duration="150"
>
<div
v-if="isVideoSectionVisible && !isVideoSectionHittingBottom"
class="fixed max-sm:flex max-sm:w-full items-center justify-center max-sm:px-5 sm:right-10 bottom-10 h-fit w-fit"
>
<StickyCard
:color="selectedVariant.color!"
:price="selectedVariant.price"
:picture="selectedVariant.images[0].image"
:title="product!.name"
class="max-sm:!w-full"
/>
</div>
</Transition>
</section>
</template>
@@ -6,7 +6,7 @@
<Button
end-icon="ci:filter"
variant="outlined"
class="max-lg:size-11 rounded-full max-lg:aspect-square"
class="max-lg:size-11 rounded-xl max-lg:aspect-square lg:py-3.5"
>
<span class="hidden lg:block"> فیلتر محصولات </span>
</Button>
@@ -53,18 +53,20 @@ const handleSubmit = () => {
<template #content>
<div class="w-full flex flex-col text-start gap-3 py-5" dir="rtl">
<p class="leading-[175%]">
<p class="leading-[175%] text-xs lg:text-sm">
با خارج شدن از حساب کاربری، دسترسی شما به برخی از امکانات
محدود خواهد شد. اگر قصد دارید دوباره وارد شوید، میتوانید از
همان اطلاعات حساب خود استفاده کنید.
</p>
<p>ما همیشه منتظر بازگشت شما هستیم! 😊</p>
<p class="text-xs lg:text-sm">
ما همیشه منتظر بازگشت شما هستیم! 😊
</p>
</div>
<div class="py-6 border-t border-slate-200 flex gap-3">
<Button
@click="handleSubmit"
class="rounded-full px-10"
class="rounded-full px-5 lg:px-10"
size="md"
>
<Icon
@@ -76,7 +78,7 @@ const handleSubmit = () => {
<DialogClose aria-label="Close">
<Button
variant="outlined"
class="rounded-full px-10"
class="rounded-full px-5 lg:px-10"
size="md"
>
نه فعلا هستم
@@ -23,7 +23,7 @@ const toggleSidebar = inject("toggleSidebar");
<button class="flex-center lg:hidden" @click="toggleSidebar">
<Icon name="bi:chevron-right" size="18" class="**:fill-black" />
</button>
<p class="font-semibold lg:text-lg text-black">
<p class="font-semibold text-sm lg:text-lg text-black">
{{ title }}
</p>
</div>
@@ -22,9 +22,9 @@ withDefaults(defineProps<Props>(), {
}"
>
<div
class="w-full flex items-center justify-between h-[2rem] lg:h-[3rem] px-5"
class="w-full flex items-center justify-between h-[2rem] lg:h-[3rem] lg:px-5"
>
<span class="typo-sub-h-md lg:typo-sub-h-lg">{{ title }}</span>
<span class="text-sm lg:text-lg">{{ title }}</span>
<slot name="button" />
</div>
</div>
@@ -116,14 +116,14 @@ const toggleSidebar = inject("toggleSidebar");
? 'bg-black text-slate-100 **:fill-slate-100'
: '**:fill-black hover:bg-gray-200'
"
class="flex items-center justify-between transition-all rounded-lg py-4 px-3"
class="flex items-center justify-between transition-all rounded-lg py-3.5 lg:py-4 px-3"
@click="toggleSidebar"
>
<span class="flex-center gap-3">
<div class="size-5 flex-center">
<Icon :name="link.icon" />
</div>
<span class="text-sm">{{ link.title }}</span>
<span class="text-xs lg:text-sm">{{ link.title }}</span>
</span>
<Icon name="bi:chevron-left" class="transition-all" />
@@ -141,7 +141,9 @@ const toggleSidebar = inject("toggleSidebar");
class="**:fill-danger-500"
/>
</div>
<span class="text-sm"> خروج از حساب </span>
<span class="text-xs lg:text-sm">
خروج از حساب
</span>
</span>
<Icon
@@ -84,7 +84,13 @@ onFileDialogChange((files: any) => {
</script>
<template>
<Modal v-model="visible" title="عکس پروفایل" icon="bi:image" iconSize="20">
<Modal
v-model="visible"
title="عکس پروفایل"
icon="bi:image"
iconSize="20"
contectClass="w-full max-lg:container lg:!w-[30vw]"
>
<template #trigger>
<button
class="bg-black text-slate-100 rounded-full p-2 flex-center absolute -bottom-0 -right-0"
@@ -93,61 +99,62 @@ onFileDialogChange((files: any) => {
</button>
</template>
<template #content>
<div class="w-max">
<div
class="w-full flex flex-col-reverse items-center justify-between py-10 gap-10 px-4"
>
<div
class="w-full flex flex-col-reverse items-center justify-between py-10 gap-10 px-4"
class="flex items-center justify-center w-full flex-wrap gap-4 max-w-[500px]"
>
<div
class="flex items-center justify-between w-full flex-wrap gap-4 max-w-[500px]"
<button
v-for="(avatar, index) in avatars"
:key="index"
class="size-16 lg:size-20 rounded-full focus:ring-2 focus:ring-offset-1 focus:ring-black transition-all"
>
<button
v-for="(avatar, index) in avatars"
:key="index"
class="size-20 rounded-full focus:ring-2 focus:ring-offset-1 focus:ring-black transition-all"
>
<Avatar
:src="avatar"
:alt="`avatar-${index}`"
class="size-full"
/>
</button>
</div>
<Avatar
:src="avatar"
:alt="`avatar-${index}`"
class="size-full"
/>
</button>
</div>
<div
class="w-full flex-center gap-4 max-w-[500px] flex-wrap"
<div class="w-full flex-center gap-4 max-w-[500px] flex-wrap">
<button
class="size-6 lg:size-8 rounded-full bg-orange-100 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
<button
class="size-6 lg:size-8 rounded-full bg-orange-200 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
<button
class="size-6 lg:size-8 rounded-full bg-amber-600 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
<button
class="size-6 lg:size-8 rounded-full bg-amber-700 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
<button
class="size-6 lg:size-8 rounded-full bg-amber-800 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
</div>
<div class="w-full flex-col-center gap-5">
<Avatar
:src="currentProfile"
alt=""
class="!size-24 lg:!size-32"
iconClass="!text-xl lg:!text-2xl"
/>
<Button
class="rounded-full w-[8rem]"
@click="openFileDialog"
:loading="updateAccountIsPending"
size="md"
>
<button
class="size-8 rounded-full bg-orange-100 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
<Icon
v-if="updateAccountIsPending"
name="svg-spinners:3-dots-bounce"
/>
<button
class="size-8 rounded-full bg-orange-200 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
<button
class="size-8 rounded-full bg-amber-600 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
<button
class="size-8 rounded-full bg-amber-700 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
<button
class="size-8 rounded-full bg-amber-800 whitespace-nowrap ring-2 hover:ring-black ring-slate-200 ring-offset-3"
/>
</div>
<div class="w-full flex-col-center gap-5">
<Avatar :src="currentProfile" alt="" class="!size-32" />
<Button
class="rounded-full w-[8rem]"
@click="openFileDialog"
:loading="updateAccountIsPending"
size="md"
>
<Icon
v-if="updateAccountIsPending"
name="svg-spinners:3-dots-bounce"
/>
<span v-else>آپلود عکس شما</span>
</Button>
</div>
<span v-else>آپلود عکس شما</span>
</Button>
</div>
</div>
</template>
@@ -6,14 +6,16 @@
>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<Icon name="bi:info-circle" size="20" />
<h3 class="typo-sub-h-md font-semibold">تغییر وضعیت سفارش</h3>
<Icon name="bi:info-circle" class="lg:text-lg" />
<h3 class="max-lg:text-sm font-semibold">تغییر وضعیت سفارش</h3>
|
<span class="typo-p-xs text-cyan-500 font-semibold">
<span class="text-xs text-cyan-500 font-semibold">
۲۳ تیر
</span>
</div>
<p class="typo-p-sm text-slate-700 text-justify">
<p
class="text-xs lg:text-sm leading-[175%] text-slate-700 text-justify"
>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با
استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله
در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد
@@ -26,21 +26,23 @@ const createdTimeAgo = usePersianTimeAgo(new Date(data.value.created_at));
>
<td
scope="row"
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap"
class="w-3/12 px-6 py-6 text-xs lg:text-sm font-medium whitespace-pre shrink-0"
:class="data.order_id ? 'text-cyan-500' : 'text-black'"
>
{{ data.order_id ? `${data.order_id}#` : "--" }}
</td>
<td class="w-3/12 px-6 py-6">
<td
class="w-3/12 px-6 py-6 text-xs lg:text-sm font-medium whitespace-pre shrink-0"
>
{{ data.created_at ? createdTimeAgo : "--" }}
</td>
<td class="w-2/12 px-6 py-6">
<td class="w-2/12 px-6 py-6 text-xs lg:text-sm whitespace-pre shrink-0">
{{ data.count ? data.count : "--" }}
</td>
<td class="w-2/12 px-6 py-6">
<td class="w-2/12 px-6 py-6 text-xs lg:text-sm whitespace-pre shrink-0">
{{ data.final_price ? data.final_price : "--" }}
</td>
<td class="w-2/12 px-6 py-6">
<td class="w-2/12 px-6 py-6 whitespace-pre shrink-0">
<div
class="w-max rounded-full py-1.5 px-3 text-xs border"
:class="{
@@ -61,7 +63,7 @@ const createdTimeAgo = usePersianTimeAgo(new Date(data.value.created_at));
{{ data.verbose_status ? data.verbose_status : "--" }}
</div>
</td>
<td class="w-1/12 px-6 py-6">
<td class="w-1/12 px-6 py-6 shrink-0">
<NuxtLink
:to="{
name: 'profile-purchases-and-orders-id',
@@ -69,7 +71,7 @@ const createdTimeAgo = usePersianTimeAgo(new Date(data.value.created_at));
}"
>
<button
class="size-10 flex-center border border-slate-200 rounded-md"
class="size-9 lg:size-10 flex-center border border-slate-200 rounded-md"
>
<Icon
name="ci:eye-open"
@@ -6,23 +6,23 @@
>
<td
scope="row"
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap text-black"
class="w-3/12 px-6 py-6 shrink-0 font-medium whitespace-nowrap text-black"
>
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-3/12 px-6 py-6">
<td class="w-3/12 px-6 py-6 shrink-0">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-2/12 px-6 py-6">
<td class="w-2/12 px-6 py-6 shrink-0">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-2/12 px-6 py-6">
<td class="w-2/12 px-6 py-6 shrink-0">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-2/12 px-6 py-6">
<td class="w-2/12 px-6 py-6 shrink-0">
<Skeleton class="w-full !h-10 !rounded-sm" />
</td>
<td class="w-1/12 px-6 py-6">
<td class="w-1/12 px-6 py-6 shrink-0">
<Skeleton class="!size-10 !rounded-sm" />
</td>
</tr>
@@ -27,14 +27,16 @@ const updatedTimeAgo = usePersianTimeAgo(new Date(data.value.updated_at));
>
<td
scope="row"
class="w-3/12 px-6 py-6 font-medium whitespace-nowrap text-black"
class="w-3/12 px-6 py-6 text-xs lg:text-sm font-medium whitespace-pre shrink-0"
>
{{ data.ticket_category ? data.ticket_category : "--" }}
</td>
<td class="w-3/12 px-6 py-6">
{{ data.subject ? data.subject : "--" }}
</td>
<td class="w-3/12 px-6 py-6 flex flex-col gap-3 text-sm">
<td
class="w-3/12 px-6 py-6 flex flex-col gap-3 text-xs lg:text-sm font-medium whitespace-pre shrink-0"
>
<span class="w-full whitespace-pre">
ایجاد : {{ data.created_at ? createdTimeAgo : "--" }}
</span>
@@ -42,7 +44,7 @@ const updatedTimeAgo = usePersianTimeAgo(new Date(data.value.updated_at));
بروزرسانی : {{ data.updated_at ? updatedTimeAgo : "--" }}
</span>
</td>
<td class="w-2/12 px-6 py-6">
<td class="w-2/12 px-6 py-6 text-xs lg:text-sm whitespace-pre shrink-0">
<div
class="w-max rounded-full py-1.5 px-3 text-xs border"
:class="{
@@ -57,12 +59,12 @@ const updatedTimeAgo = usePersianTimeAgo(new Date(data.value.updated_at));
{{ data.status ? data.status : "--" }}
</div>
</td>
<td class="w-1/12 px-6 py-6">
<td class="w-1/12 px-6 py-6 shrink-0">
<NuxtLink
:to="{ name: 'profile-tickets-id', params: { id: data.id } }"
>
<button
class="size-10 flex-center border border-slate-200 rounded-md"
class="size-9 lg:size-10 flex-center border border-slate-200 rounded-md"
>
<Icon
name="ci:eye-open"
@@ -0,0 +1,32 @@
// imports
import { useQuery } from "@tanstack/vue-query";
import { API_ENDPOINTS, QUERY_KEYS } from "~/constants";
// types
export type GetTransactionResponse = Transaction;
export type GetTransactionRequest = string;
const useGetTransaction = (params: ComputedRef<GetTransactionRequest>) => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleGetTransaction = async (tc: GetTransactionRequest) => {
const { data } = await axios.get<GetTransactionResponse>(
`${API_ENDPOINTS.orders.checkout.transaction}/${tc}`
);
return data;
};
return useQuery({
queryKey: [QUERY_KEYS.transaction, params],
queryFn: () => handleGetTransaction(params.value),
});
};
export default useGetTransaction;
@@ -0,0 +1,23 @@
// composables/usePersianDate.ts
import { format, toDate } from "date-fns-jalali";
import { faIR } from "date-fns-jalali/locale";
export default function usePersianDate() {
const formatToPersian = (isoDate: string): string => {
try {
const date = toDate(new Date(isoDate));
const persianDate = format(date, "yyyy/MM/dd", { locale: faIR });
const persianTime = format(date, "HH:mm", { locale: faIR });
return `${persianDate} | ${persianTime}`;
} catch (error) {
return "Invalid date";
}
};
return {
formatToPersian,
};
}
+1
View File
@@ -78,6 +78,7 @@ export const QUERY_KEYS = {
ticket: "ticket",
orders: "orders",
cart: "cart",
transaction: "transaction",
};
export const MUTATION_KEYS = {
+3 -3
View File
@@ -35,9 +35,9 @@ provide("toggleSidebar", toggleSidebar);
watch(
() => isSidebarShow.value,
(nv) => {
console.log(nv);
isScrollLocked.value = nv ? true : false;
if (isMobile.value) {
isScrollLocked.value = nv ? true : false;
}
},
{
immediate: true,
+5 -11
View File
@@ -20,19 +20,13 @@ export default defineNuxtConfig({
title: "فروشگاه هی ملز",
},
pageTransition: {
enterActiveClass:
"animate__animated animate__fadeIn animate__faster",
leaveActiveClass:
"animate__animated animate__fadeOut animate__faster",
mode: "out-in",
},
layoutTransition: {
enterActiveClass:
"animate__animated animate__fadeIn animate__faster",
leaveActiveClass:
"animate__animated animate__fadeOut animate__faster",
name: "fade",
mode: "out-in",
},
// layoutTransition: {
// name: "fade",
// mode: "out-in",
// },
},
postcss: {
+3 -3
View File
@@ -37,7 +37,7 @@
"nuxt": "^3.15.4",
"reka-ui": "^1.0.0-alpha.6",
"sanitize-html": "^2.15.0",
"swiper": "^11.2.4",
"swiper": "^11.2.6",
"universal-cookie": "^7.2.2",
"vue": "latest",
"vue-router": "latest",
@@ -49,13 +49,13 @@
"workbox-window": "^7.3.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.9",
"@tailwindcss/postcss": "^4.1.4",
"@types/node": "^22.13.11",
"@types/sanitize-html": "^2.13.0",
"@types/web-push": "^3.6.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.9",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.2",
"vue-tsc": "^2.2.8"
}
+1 -1
View File
@@ -100,7 +100,7 @@ whenever(
<Icon
name="svg-spinners:180-ring-with-bg"
size="20"
v-if="!setOrderAddressIsPending"
v-if="setOrderAddressIsPending"
class="pb-0.5"
/>
</div>
+20 -10
View File
@@ -55,27 +55,34 @@ if (response.isError) {
<template>
<div class="container">
<div class="pt-20 pb-8 sm:py-20 flex gap-12 sm:gap-6 justify-between items-center max-sm:flex-col max-sm:items-start">
<span class="typo-h-5 lg:typo-h-4 text-black">دسته بندی ها</span>
<div class="flex items-center gap-4 sm:gap-8 max-sm:w-full">
<div
class="w-full flex flex-col lg:flex-row justify-end items-end py-[3.5rem] lg:py-[5rem] gap-10 lg:gap-5"
>
<div class="flex flex-col items-center lg:items-start w-full">
<h1 class="typo-h-5 lg:typo-h-4 text-black">دسته بندی ها</h1>
</div>
<div
class="w-full flex items-center justify-between lg:justify-end gap-4"
>
<Input
class="max-w-[400px] w-full rounded-full h-[38px] lg:h-[50px]"
variant="outlined"
placeholder="جستجو..."
placeholder="جست و جو محصول ..."
v-model="search"
variant="outlined"
class="!rounded-xl w-full lg:w-8/12"
>
<template #endItem>
<div class="flex items-center gap-1">
<Icon
class="translate-y-[-1px] size-[18px] lg:size-[24px]"
class="translate-y-[-1px] text-[20px] lg:text-[24px]"
name="ci:search"
/>
</div>
</template>
</Input>
<Select
class="whitespace-nowrap max-sm:w-full"
class="shrink-0 max-lg:w-[5rem] lg:w-[6.5rem] py-0.5"
triggerRootClass="!rounded-xl whitespace-nowrap max-sm:w-full shrink-0 "
:options="mainCategoriesMenu"
variant="outlined"
placeholder="انتخاب کنید"
@@ -106,7 +113,10 @@ if (response.isError) {
<div
class="flex-col flex-grow py-32 sm:py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-100 flex-center"
>
<Icon name="bi:search" class="**:fill-gray-500 size-[40px] sm:size-[50px]" />
<Icon
name="bi:search"
class="**:fill-gray-500 size-[40px] sm:size-[50px]"
/>
<span class="text-lg text-gray-500">
دسته بندی یافت نشد :(
</span>
+72 -35
View File
@@ -53,18 +53,15 @@ const contactWays = ref([
<template>
<div class="w-full container flex flex-col">
<div class="w-full flex-center py-[5rem]">
<div
class="flex flex-col items-center gap-[1.5rem] text-black w-full"
>
<h1 class="typo-h-3">ارتباط با ما</h1>
<p>
ما اینجا هستیم تا کمک کنیم. برای پشتیبانی، بازخورد یا هر
سوالی که ممکن است داشته باشید تماس بگیرید.
<div class="flex flex-col items-center gap-3 text-black w-full">
<h1 class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">ارتباط با ما</h1>
<p class="text-slate-500 text-center max-w-[750px] max-lg:text-sm leading-[175%]">
ما اینجا هستیم تا کمک کنیم. برای پشتیبانی، بازخورد یا هر سوالی که ممکن است داشته باشید تماس بگیرید.
</p>
</div>
</div>
<div class="w-full flex items-start justify-between">
<div class="w-8/12 flex flex-col items-start gap-16">
<div class="w-full flex flex-col-reverse max-lg:-mt-14 lg:flex-row items-start justify-between">
<div class="w-full lg:w-8/12 flex flex-col items-start gap-10">
<div class="grid grid-cols-1 md:grid-cols-2 gap-[.6rem] w-full">
<div class="flex flex-col items-start w-full">
<Input
@@ -73,10 +70,16 @@ const contactWays = ref([
/>
</div>
<div class="flex flex-col items-start w-full">
<Input class="w-full" placeholder="پست الکترونیکی" />
<Input
class="w-full"
placeholder="پست الکترونیکی"
/>
</div>
<div class="flex flex-col items-start w-full">
<Input class="w-full" placeholder="شماره تلفن" />
<Input
class="w-full"
placeholder="شماره تلفن"
/>
</div>
<div class="flex flex-col items-start w-full relative">
<Select
@@ -85,55 +88,89 @@ const contactWays = ref([
placeholder="نوع درخواست"
/>
</div>
<div
class="flex flex-col items-start col-span-1 md:col-span-2 h-[10rem] max-h-[12rem]"
>
<div class="flex flex-col items-start col-span-1 md:col-span-2 h-[10rem] max-h-[12rem]">
<textarea
placeholder="پیغام شما"
class="w-full flex items-center resize-none bg-slate-50 border-slate-200 hover:border-black focus:border-black h-[10rem] max-h-[12rem] 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="w-full flex items-center resize-none bg-slate-50 border-slate-200 hover:border-black focus:border-black h-[10rem] max-h-[12rem] 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 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
></textarea>
</div>
</div>
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 w-full"
>
<div class="w-full flex-center pb-10 border-b border-slate-200">
<!-- @click="handleSubmit"
:loading="createTicketIsPending || uploadAttachmentIsPending" -->
<Button
class="rounded-full w-[14rem] h-11"
size="md"
>
<!-- <Icon
v-if="createTicketIsPending"
:name="createTicketIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:send'"
/> -->
<span>ارسال پیغام</span>
</Button>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 w-full max-lg:gap-10">
<div
v-for="(way, index) in contactWays"
:key="index"
class="flex flex-col gap-3"
class="flex flex-col max-lg:items-center gap-3"
>
<span class="text-slate-500 typo-p-md">
<span class="text-slate-500 max-lg:text-sm">
{{ way.title }}
</span>
<div
v-for="(link, index) in way.ways"
:key="index"
class="flex flex-col"
>
<a :href="link.path" class="text-black underline">{{
link.title
}}</a>
<a
:href="link.path"
class="text-black underline max-lg:text-xs lg:text-sm"
>{{ link.title }}</a
>
</div>
</div>
<div class="flex flex-col gap-3">
<span class="text-slate-500 typo-p-md">
شبکه های اجتماعی
</span>
<div class="flex flex-col max-lg:items-center gap-3">
<span class="text-slate-500 typo-p-md"> شبکه های اجتماعی </span>
<div class="flex items-center gap-3">
<a href="#" class="">
<Icon name="bi:telegram" size="20" />
<a
href="#"
class=""
>
<Icon
name="bi:telegram"
class="lg:text-lg"
/>
</a>
<a href="#" class="">
<Icon name="bi:instagram" size="20" />
<a
href="#"
class=""
>
<Icon
name="bi:instagram"
class="lg:text-lg"
/>
</a>
<a href="#" class="">
<Icon name="bi:twitter-x" size="20" />
<a
href="#"
class=""
>
<Icon
name="bi:twitter-x"
class="lg:text-lg"
/>
</a>
</div>
</div>
</div>
</div>
<div class="w-4/12 h-full flex-center">
<NuxtImg src="/logo.png" class="size-2/3 -mt-5" />
<div class="w-full lg:w-4/12 h-full flex-center">
<NuxtImg
src="/img/heymlz/contact-us.gif"
class="size-2/3 -mt-5 lg:scale-150 drop-shadow-2xl"
/>
</div>
</div>
</div>
+6 -9
View File
@@ -1,14 +1,14 @@
<script lang="ts" setup>
// import
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();
const disableLoadingOverlay = useState("disableLoadingOverlay", () => false);
// ssr
@@ -17,7 +17,7 @@ const response = await suspense();
if (response.isError) {
throw createError({
statusCode: 500,
statusMessage: `Landing error : ${response.error.message}`
statusMessage: `Landing error : ${response.error.message}`,
});
}
@@ -26,12 +26,11 @@ if (response.isError) {
onMounted(() => {
window.scrollTo(0, 0);
});
</script>
<template>
<div class="w-full">
<LoadingOverlay v-if="!disableLoadingOverlay" />
<LoadingOverlay />
<Hero class="mb-20 max-md:mt-[80px]" />
<Preview />
<ProductsShowcase class="mb-40" />
@@ -41,8 +40,6 @@ onMounted(() => {
/>
<Categories class="mt-40" />
<Brands />
<ClientOnly>
<LatestStories class="mb-20" />
</ClientOnly>
<LatestStories class="mb-20" />
</div>
</template>
</template>
+10 -10
View File
@@ -1,5 +1,4 @@
<script lang="ts" setup>
// import
import ChatButton from "~/components/product/ChatBox/ChatButton.vue";
@@ -18,18 +17,20 @@ const { suspense: suspenseComments } = useGetComments(id, page);
const selectedVariant = ref<ProductVariant>();
const showChatButton = ref(true);
// type
export type ProductVariantProvideType = {
selectedVariant: typeof selectedVariant,
changeSelectedVariant: (value: ProductVariant) => void
}
selectedVariant: typeof selectedVariant;
changeSelectedVariant: (value: ProductVariant) => void;
};
// provide / inject
provide("productVariant", {
selectedVariant,
changeSelectedVariant: (value: ProductVariant) => selectedVariant.value = value
changeSelectedVariant: (value: ProductVariant) => (selectedVariant.value = value),
});
// ssr
@@ -40,22 +41,21 @@ const commentsResponse = await suspenseComments();
if (productResponse.isError || commentsResponse.isError) {
throw createError({
statusCode: 404,
statusMessage: `error : product ${id} prefetch error`
statusMessage: `error : product ${id} prefetch error`,
});
}
</script>
<template>
<div class="w-full flex flex-col ">
<div class="w-full flex flex-col">
<ProductHero />
<ProductVideo />
<ProductVideo v-model:showChatButton="showChatButton" />
<ProductComments />
<ProductDetails />
<ProductsGrid
title="محصولات مشابه"
:products="product!.related_products"
/>
<ChatButton />
<ChatButton :showChatButton="showChatButton" />
</div>
</template>
+4 -4
View File
@@ -65,13 +65,13 @@ watch(
<div
class="flex flex-col items-center lg:items-start gap-[1rem] lg:gap-[1.5rem] text-black w-full"
>
<div class="flex-center gap-[.75rem]">
<!-- <div class="flex-center gap-[.75rem]">
<span class="text-xs lg:text-sm">خانه</span>
<span class="text-xs lg:text-sm">/</span>
<span class="text-xs lg:text-sm">محصولات</span>
<span class="text-xs lg:text-sm">/</span>
<span class="text-xs lg:text-sm">همه</span>
</div>
</div> -->
<h1 class="typo-h-5 lg:typo-h-4">لیست محصولات</h1>
</div>
@@ -82,7 +82,7 @@ watch(
placeholder="جست و جو محصول ..."
v-model="search"
variant="outlined"
class="!rounded-full w-full lg:w-8/12"
class="!rounded-xl w-full lg:w-8/12"
>
<template #endItem>
<div class="flex items-center gap-1">
@@ -97,7 +97,7 @@ watch(
<FilterButton />
<template #fallback>
<Skeleton
class="!size-11 lg:!w-[10.35rem] lg:!h-[3.35rem] shrink-0 !rounded-full"
class="!size-11 lg:!w-[10.35rem] lg:!h-[3.35rem] shrink-0 !rounded-xl"
/>
</template>
</Suspense>
+1
View File
@@ -64,6 +64,7 @@ const handleSelectAddress = (address: Address) => {
:key="index"
:address="address"
@select="handleSelectAddress"
:selectable="false"
:isSelected="address.id == selectedAddress?.id"
/>
</ul>
+1 -1
View File
@@ -196,7 +196,7 @@ const handleSubmit = (withValidation: boolean) => {
{{ account?.last_name }}</span
>
<span
class="typo-sub-h-xs lg:typo-sub-h-sm font-light text-slate-600 leading-[200%]"
class="typo-sub-h-xs lg:typo-sub-h-sm !font-light text-slate-600 leading-[200%]"
>
با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک
بایوگرافی درست میکنه :)
+7 -7
View File
@@ -60,13 +60,13 @@ watch(
<div
v-if="isSupported"
class="w-fill flex items-center justify-between p-5 pt-0 border-b border-slate-200"
class="w-fill flex items-center justify-between py-5 lg:p-5 pt-0 border-b border-slate-200 gap-5"
>
<div class="flex items-start justify-start gap-3">
<Icon name="bi:bell" size="20" />
<div class="flex flex-col gap-1 pb-0.5">
<span class="text-sm"> دریافت مستقیم اعلانات </span>
<span class="text-xs text-slate-500">
<span class="text-xs text-slate-500 leading-[175%]">
اعلانات حساب شما به صورت مستقیم به دستگاه شما ارسال می
شود
</span>
@@ -83,10 +83,10 @@ watch(
</div>
</div>
<div class="w-full flex items-center justify-between px-5">
<span> 1 اعلان </span>
<div class="w-full flex items-center justify-between lg:px-5">
<span class="max-lg:text-sm"> 1 اعلان </span>
<div class="flex items-center justify-start gap-3">
<span class="text-sm">فیلتر بر اساس</span>
<span class="text-xs lg:text-sm">فیلتر بر اساس</span>
<Select
v-model="params.sort!"
@@ -98,7 +98,7 @@ watch(
<SelectItem
v-for="(category, index) in sortFilters"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
class="text-xs leading-none w-full rounded-sm py-4 lg: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"
>
<SelectItemIndicator
@@ -107,7 +107,7 @@ watch(
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
class="text-end font-iran-yekan-x text-xs lg:text-sm"
>
{{ category.title }}
</SelectItemText>
@@ -118,10 +118,16 @@ const clearFilters = () => {
<ProfilePageTitle title="خرید ها و سفارش های شما" icon="bi:cart" />
<div class="w-full flex flex-col gap-5">
<div class="w-full flex items-center justify-between px-5">
<div class="flex items-center justify-start gap-8">
<div class="flex items-center justify-start gap-3">
<span class="text-sm">ترتیب بر اساس</span>
<div
class="w-full flex flex-col-reverse lg:flex-row items-center justify-between lg:px-5 gap-5"
>
<div
class="max-lg:w-full flex items-center justify-start gap-8"
>
<div
class="flex flex-col lg:flex-row items-start lg:items-center justify-start gap-3 max-lg:w-full"
>
<span class="text-xs lg:text-sm">ترتیب بر اساس</span>
<Select
v-model="params.sort!"
triggerRootClass="!py-2.5"
@@ -132,7 +138,7 @@ const clearFilters = () => {
<SelectItem
v-for="(category, index) in sortFilters"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
class="text-xs leading-none w-full rounded-sm py-4 lg: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"
>
<SelectItemIndicator
@@ -141,7 +147,7 @@ const clearFilters = () => {
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
class="text-end font-iran-yekan-x text-xs lg:text-sm"
>
{{ category.title }}
</SelectItemText>
@@ -150,8 +156,10 @@ const clearFilters = () => {
</template>
</Select>
</div>
<div class="flex items-center justify-start gap-3">
<span class="text-sm">وضعیت</span>
<div
class="flex flex-col lg:flex-row items-start lg:items-center justify-start gap-3 max-lg:w-full"
>
<span class="text-xs lg:text-sm">وضعیت</span>
<Select
v-model="params.status!"
triggerRootClass="!py-2.5"
@@ -164,7 +172,7 @@ const clearFilters = () => {
category, index
) in statusFilters"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
class="text-xs leading-none w-full rounded-sm py-4 lg: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"
>
<SelectItemIndicator
@@ -173,7 +181,7 @@ const clearFilters = () => {
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
class="text-end font-iran-yekan-x text-xs lg:text-sm"
>
{{ category.title }}
</SelectItemText>
@@ -184,7 +192,10 @@ const clearFilters = () => {
</div>
</div>
<div class="flex-center gap-4">
<div
class="flex items-center lg:justify-center gap-4 max-lg:w-full"
:class="hasFilters ? 'justify-between' : 'justify-end'"
>
<Button
v-if="hasFilters"
end-icon="bi:x"
@@ -227,7 +238,7 @@ const clearFilters = () => {
? 'w-1/2'
: 'w-2/12'
"
class="px-6 py-5 text-sm font-normal"
class="px-6 py-5 text-xs lg:text-sm font-normal shrink-0 whitespace-pre"
>
{{ tableHead }}
</th>
@@ -246,7 +257,10 @@ const clearFilters = () => {
</template>
</Table>
<div v-if="data && paginationData && data.count > 10" class="w-full flex-center py-10">
<div
v-if="data && paginationData && data.count > 10"
class="w-full flex-center py-10"
>
<Pagination :items="paginationData" :total="data.count" />
</div>
</div>
+27 -13
View File
@@ -99,10 +99,16 @@ const clearFilters = () => {
<ProfilePageTitle title="تیکت های شما" icon="bi:ticket" />
<div class="w-full flex flex-col gap-5">
<div class="w-full flex items-center justify-between px-5">
<div class="flex items-center justify-start gap-8">
<div class="flex items-center justify-start gap-3">
<span class="text-sm">ترتیب بر اساس</span>
<div
class="w-full flex flex-col-reverse lg:flex-row items-center justify-between lg:px-5 gap-5"
>
<div
class="max-lg:w-full flex items-center justify-start gap-8"
>
<div
class="flex flex-col lg:flex-row items-start lg:items-center justify-start gap-3 max-lg:w-full"
>
<span class="text-xs lg:text-sm">ترتیب بر اساس</span>
<Select
v-model="params.sort!"
triggerRootClass="!py-2.5"
@@ -113,7 +119,7 @@ const clearFilters = () => {
<SelectItem
v-for="(category, index) in sortFilters"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
class="text-xs leading-none w-full rounded-sm py-4 lg: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"
>
<SelectItemIndicator
@@ -122,7 +128,7 @@ const clearFilters = () => {
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
class="text-end font-iran-yekan-x lg:text-sm"
>
{{ category.title }}
</SelectItemText>
@@ -131,8 +137,10 @@ const clearFilters = () => {
</template>
</Select>
</div>
<div class="flex items-center justify-start gap-3">
<span class="text-sm">وضعیت</span>
<div
class="flex flex-col lg:flex-row items-start lg:items-center justify-start gap-3 max-lg:w-full"
>
<span class="text-xs lg:text-sm">وضعیت</span>
<Select
v-model="params.status!"
triggerRootClass="!py-2.5"
@@ -145,7 +153,7 @@ const clearFilters = () => {
category, index
) in statusFilters"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
class="text-xs leading-none w-full rounded-sm py-4 lg: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"
>
<SelectItemIndicator
@@ -154,7 +162,7 @@ const clearFilters = () => {
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
class="text-end font-iran-yekan-x text-xs lg:text-sm"
>
{{ category.title }}
</SelectItemText>
@@ -165,7 +173,10 @@ const clearFilters = () => {
</div>
</div>
<div class="flex-center gap-4">
<div
class="flex items-center lg:justify-center gap-4 max-lg:w-full"
:class="hasFilters ? 'justify-between' : 'justify-end'"
>
<Button
v-if="hasFilters"
end-icon="bi:x"
@@ -208,7 +219,7 @@ const clearFilters = () => {
? 'w-1/2'
: 'w-2/12'
"
class="px-6 py-5 text-sm font-normal"
class="px-6 py-5 text-xs lg:text-sm font-normal shrink-0 whitespace-pre"
>
{{ tableHead }}
</th>
@@ -227,7 +238,10 @@ const clearFilters = () => {
</template>
</Table>
<div v-if="data && paginationData && data.count > 7" class="w-full flex-center py-10">
<div
v-if="data && paginationData && data.count > 7"
class="w-full flex-center py-10"
>
<Pagination :items="paginationData" :total="data?.count" />
</div>
</div>
+20 -11
View File
@@ -10,6 +10,7 @@ import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants";
import useVuelidate from "@vuelidate/core";
import { helpers, required, minLength } from "@vuelidate/validators";
import type { GetAllOrdersRequest } from "~/composables/api/orders/useGetAllOrders";
// meta
@@ -72,9 +73,18 @@ const ticketData = ref<CreateTicketRequest>({
attachments: [],
});
const ordersFilter = computed<GetAllOrdersRequest>(() => {
return {
sort: "created_at",
status: "RECEIVED",
page: 1,
} as any;
});
// queries
const { data: orders, isLoading: ordersIsLoading } = useGetAllOrders();
const { data: orders, isLoading: ordersIsLoading } =
useGetAllOrders(ordersFilter);
const { mutateAsync: createTicket, isPending: createTicketIsPending } =
useCreateTicket();
@@ -257,7 +267,7 @@ const handleSubmit = async () => {
<template #content>
<SelectGroup>
<SelectItem
v-for="(order, index) in orders"
v-for="(order, index) in orders?.results"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] px-[12px] shrink-0 relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
:value="order.id"
@@ -267,17 +277,14 @@ const handleSubmit = async () => {
>
<div class="flex items-center gap-4">
<AvatarGroup
:items="[
'https://c262408.parspack.net/media/profile_photos/Jackie_Robinson_NPG_97_135.jpg?AWSAccessKeyId=mtiSN2JWjWgyfr2u&Signature=mlUzygzyg2gQD7B5STTlgM2N%2FUM%3D&Expires=1740517316',
'https://c262408.parspack.net/media/profile_photos/Jackie_Robinson_NPG_97_135.jpg?AWSAccessKeyId=mtiSN2JWjWgyfr2u&Signature=mlUzygzyg2gQD7B5STTlgM2N%2FUM%3D&Expires=1740517316',
'https://c262408.parspack.net/media/profile_photos/Jackie_Robinson_NPG_97_135.jpg?AWSAccessKeyId=mtiSN2JWjWgyfr2u&Signature=mlUzygzyg2gQD7B5STTlgM2N%2FUM%3D&Expires=1740517316',
]"
v-if="order.images.length > 0"
:items="order.images"
:max="2"
size="32px"
/>
<div
class="flex flex-col items-start gap-1 text-[10px]"
class="flex items-start gap-1 text-[10px]"
>
<span
>{{
@@ -285,14 +292,16 @@ const handleSubmit = async () => {
}}
محصول</span
>
|
<span>
شماره سفارش : {{ order.id }}
شماره سفارش :
{{ order.id }}#
</span>
</div>
</div>
<span>
{{ order.status }}
<span class="text-[10px]">
{{ order.verbose_status }}
</span>
</SelectItemText>
</SelectItem>
+4 -8
View File
@@ -151,13 +151,7 @@ const resetForm = () => {
<template>
<div class="w-full flex h-svh items-center relative container">
<div
class="bg-[url(/img/pattern-1.png)] -z-10 size-full fixed inset-0 opacity-70"
:style="{
backgroundSize: 150,
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0.3) 80%)',
}"
/>
<div class="pattern -z-10 size-full fixed inset-0" />
<div
class="flex items-center justify-center flex-col size-full translate-y-[-100px]"
>
@@ -172,7 +166,9 @@ const resetForm = () => {
<div
class="max-w-[600px] w-full p-6 h-[350px] sm:h-[400px] flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-3xl"
>
<h1 class="typo-h-6 sm:typo-h-5 mt-8">شماره خود را وارد کنید</h1>
<h1 class="typo-h-6 sm:typo-h-5 mt-8">
شماره خود را وارد کنید
</h1>
<form @submit.prevent class="max-w-[500px] w-full mt-12">
<Input
+187 -48
View File
@@ -1,75 +1,214 @@
<script setup lang="ts">
// imports
import useGetTransaction from "~/composables/api/orders/useGetTransaction";
import usePersianDate from "~/composables/global/usePersianDate";
// meta
definePageMeta({
layout: "none",
});
// state
const route = useRoute();
const { formatToPersian } = usePersianDate();
// computed
const tracking_code = computed(() => route.query["tc"] as string);
// queries
const {
data: transaction,
isLoading: transactionIsLoading,
suspense,
} = useGetTransaction(tracking_code);
await suspense();
// computed
const statusVariants = computed(() => {
if (transaction.value?.bank_result?.status == "succeeded") {
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",
icon: "bi:check",
title: "تراکنش موفق",
hue_deg: "[filter:_hue-rotate(260deg)] ",
};
} else if (transaction.value?.bank_result?.status == "canceled") {
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",
icon: "bi:x",
title: "تراکنش ناموفق",
hue_deg: "[filter:_hue-rotate(120deg)]",
};
} else {
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",
icon: "bi:question-circle",
title: "تراکنش معلق",
hue_deg: "[filter:_hue-rotate(0deg)]",
};
}
});
const statusTitle = computed(() => {
if (transaction.value?.bank_result?.status == "succeeded") {
return "";
} else if (transaction.value?.bank_result?.status == "canceled") {
return "";
} else {
return "";
}
});
</script>
<template>
<div class="w-full flex-col-center gap-3 h-svh relative">
<div class="w-full flex-col-center container gap-3 h-svh">
<div
class="bg-[url(/img/pattern-1.png)] -z-10 size-full fixed inset-0 opacity-70"
class="pattern -z-10 size-full fixed inset-0 opacity-90"
:style="{
backgroundSize: 150,
mask: 'linear-gradient(to bottom, black 0%, rgba(0,0,0,0.3) 80%)',
}"
/>
<NuxtImg src="/logo/logo-col.png" class="size-44 -mt-12" />
<div class="max-w-[500px] w-full relative">
<NuxtImg
class="aspect-square w-[220px] md:w-[280px] lg:w-[320px] absolute -top-[70px] md:-top-[110px] lg:-top-[138px] left-1/2 -translate-x-1/2 z-10 [filter:_drop-shadow(0px_4px_20px_rgba(0, 0, 0, 0.15))]"
src="/img/heymlz/heymlz-seat.gif"
:class="statusVariants.hue_deg"
/>
<div
class="max-w-[500px] w-full p-6 gap-6 flex flex-col items-center bg-white border shadow-black/10 justify-center border-slate-300 rounded-3xl relative overflow-hidden"
>
<div
class="w-full h-[5rem] bg-success-500 absolute left-0 top-0 flex-center gap-2 text-white"
class="w-full h-5/6 absolute rounded-3xl -z-3 -bottom-1 left-1/2 -translate-x-1/2"
:class="statusVariants.after_background_color"
/>
<div
class="w-full p-5 lg:p-6 gap-5 lg:gap-6 flex flex-col items-center bg-white border shadow-black/10 justify-center overflow-hidden border-slate-300 rounded-3xl relative mt-20 z-1"
>
<Icon name="bi:check" size="28" />
<h1 class="typo-h-6 font-normal">تراکنش موفق</h1>
</div>
<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,
]"
>
<Icon
:name="statusVariants.icon"
class="text-2xl lg:text-3xl"
/>
<h1 class="text-lg lg:text-2xl font-normal">
{{ statusVariants.title }}
</h1>
</div>
<div class="w-full flex flex-col gap-5 pt-[5.5rem] p-1">
<div
class="w-full flex flex-row-reverse items-center justify-between"
class="w-full flex flex-col gap-4 lg:gap-5 pt-[4.5rem] lg:pt-[5.5rem] p-1"
>
<span>مبلغ تراكنش </span>
<span>١٢٣ تومان</span>
</div>
<div
class="w-full flex flex-row-reverse items-center justify-between"
>
<span>شماره پيكَيرى </span>
<span>١٢٣ تومان</span>
</div>
<div
class="w-full flex flex-row-reverse items-center justify-between"
>
<span>شماره ارجاع </span>
<span>١٢٣ تومان</span>
</div>
<div
class="w-full flex flex-row-reverse items-center justify-between"
>
<span>تاريخ و ساعت </span>
<span>١٢٣ تومان</span>
</div>
</div>
<div class="w-full flex items-center justify-between gap-5">
<NuxtLink to="/" class="w-full">
<Button
class="w-full rounded-full"
start-icon="ci:left-rotation"
variant="secondary"
>بازگشت به فروشگاه</Button
<div
v-if="transaction?.bank_result?.bank_type"
class="w-full flex flex-row-reverse items-center justify-between max-lg:text-xs"
>
</NuxtLink>
<Button
class="w-full rounded-full bg-success-500 hover:text-success-500 hover:border-success-500 hover:**:!stroke-success-500"
start-icon="ci:share"
variant="primary"
>دانلود فاکتور</Button
<span class="font-medium">درگاه پرداخت</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
>
</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
>
</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>
</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>
</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>
</div>
</div>
<div
v-if="transaction?.detail"
class="w-full text-center opacity-50 text-sm lg:text-sm leading-[175%]"
>
{{ 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">
<Button
class="w-full rounded-full max-lg:py-2"
start-icon="ci:left-rotation"
variant="secondary"
>بازگشت به فروشگاه</Button
>
</NuxtLink>
<Button
v-if="transaction?.bank_result?.status == 'succeeded'"
class="w-full rounded-full max-lg:py-2 bg-success-500 hover:text-success-500 hover:border-success-500 hover:**:!stroke-success-500"
start-icon="ci:share"
variant="primary"
>دانلود فاکتور</Button
>
</div>
</div>
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

After

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 1012 KiB

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 KiB

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.
+14
View File
@@ -290,4 +290,18 @@ declare global {
| "BAHAMTA"
| "BMI";
};
type Transaction = {
detail: string;
bank_result?: {
status: "succeeded" | "canceled" | "pending";
bank_type: string;
tracking_code: string;
amount: string | null;
created_at: string;
response_result: string | null;
reference_number: string | null;
status_detail: string | null;
};
};
}