merage with front
add address object to cart serializer and signal for the default address
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"semi": true
|
||||
}
|
||||
"singleAttributePerLine": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"semi": true
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const closeModal = () => {
|
||||
<template>
|
||||
<div>
|
||||
<LoadingIndicator />
|
||||
|
||||
<NuxtPwaManifest />
|
||||
|
||||
<UpdatePwaModal
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))',
|
||||
}"
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -78,6 +78,7 @@ export const QUERY_KEYS = {
|
||||
ticket: "ticket",
|
||||
orders: "orders",
|
||||
cart: "cart",
|
||||
transaction: "transaction",
|
||||
};
|
||||
|
||||
export const MUTATION_KEYS = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -64,6 +64,7 @@ const handleSelectAddress = (address: Address) => {
|
||||
:key="index"
|
||||
:address="address"
|
||||
@select="handleSelectAddress"
|
||||
:selectable="false"
|
||||
:isSelected="address.id == selectedAddress?.id"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
@@ -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%]"
|
||||
>
|
||||
با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک
|
||||
بایوگرافی درست میکنه :)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 745 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 399 KiB After Width: | Height: | Size: 755 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 1012 KiB After Width: | Height: | Size: 1012 KiB |
|
Before Width: | Height: | Size: 944 KiB After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||