merage with front and debug multipule product

This commit is contained in:
Parsa Nazer
2025-04-21 17:22:39 +03:30
20 changed files with 349 additions and 483 deletions
+7 -5
View File
@@ -5,8 +5,7 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
// state
const { $updateAvailable: updateAvailable, $handleUpdate: handleUpdate } =
useNuxtApp();
const { $updateAvailable: updateAvailable, $handleUpdate: handleUpdate } = useNuxtApp();
const closeModal = () => {
updateAvailable.value = false;
@@ -16,7 +15,7 @@ const closeModal = () => {
<template>
<div>
<LoadingIndicator />
<NuxtPwaManifest />
<UpdatePwaModal
@@ -32,10 +31,13 @@ const closeModal = () => {
<ToastProvider>
<ToastContainer />
<ToastViewport
class="[--viewport-padding:_25px] fixed bottom-0 left-0 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[9999999999999999] outline-none"
class="[--viewport-padding:_25px] fixed bottom-0 left-1/2 -translate-x-1/2 flex flex-col p-[var(--viewport-padding)] gap-[10px] w-[390px] max-w-[100vw] m-0 list-none z-[9999999999999999] outline-none"
/>
</ToastProvider>
<VueQueryDevtools dir="ltr" buttonPosition="top-right"/>
<VueQueryDevtools
dir="ltr"
buttonPosition="top-right"
/>
</div>
</template>
+8 -4
View File
@@ -215,22 +215,22 @@
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translateX(calc(100% + var(--viewport-padding)));
transform: translateY(calc(100% + var(--viewport-padding)));
}
to {
opacity: 1;
transform: translateX(0);
transform: translateY(0);
}
}
@keyframes toastSlideOut {
from {
opacity: 1;
transform: translateX(var(--reka-toast-swipe-end-x));
transform: translateY(var(--reka-toast-swipe-end-x));
}
to {
opacity: 0;
transform: translateX(calc(100% + var(--viewport-padding)));
transform: translateY(calc(100% + var(--viewport-padding)));
}
}
@@ -281,6 +281,10 @@
/* CONTAINER */
* {
scroll-behavior: smooth !important;
}
@utility container {
@apply mx-auto px-[var(--app-container-padding)] w-full max-sm:max-w-[var(--breakpoint-xs)] max-md:max-w-[var(--breakpoint-sm)] max-lg:max-w-[var(--breakpoint-md)] max-xl:max-w-[var(--breakpoint-lg)] max-w-[var(--breakpoint-2xl)];
}
@@ -2,6 +2,7 @@
// imports
import useDeleteAddress from "~/composables/api/account/useDeleteAddress";
import useSetOrderAddress from "~/composables/api/orders/useSetOrderAddress";
import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants";
@@ -15,13 +16,11 @@ type Props = {
// props
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
selectable: true,
});
// emit
const emit = defineEmits(["select"]);
const { address } = toRefs(props);
// state
@@ -31,19 +30,48 @@ const { addToast } = useToast();
// queries
const { mutateAsync: deleteAddress, isPending: deleteAddressIsPending } =
useDeleteAddress();
const { mutateAsync: deleteAddress, isPending: deleteAddressIsPending } = useDeleteAddress();
const { mutateAsync: setOrderAddress, isPending: setOrderAddressIsPending } = useSetOrderAddress();
// methods
const handleSelectAddress = () => {
setOrderAddress(
{ address_id: address.value?.id! },
{
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.cart],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
},
onError: () => {
addToast({
message: "در انتخاب آدرس خطایی رخ داد",
options: {
description: "لطفا مجدد تلاش کنید",
},
});
},
}
);
};
const handleDeleteAddress = (id: number) => {
deleteAddress(
{ id },
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.cart],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
addToast({
message: "آدرس با موفقیت حذف شد",
options: {
@@ -66,29 +94,30 @@ const handleDeleteAddress = (id: number) => {
<template>
<button
@click.prevent="
!!address && selectable ? emit('select', address) : null
"
:class="
isSelected
? 'border-transparent ring-2 ring-offset-2 ring-blue-500'
: 'border-slate-200'
"
@click.prevent="!!address && selectable ? handleSelectAddress() : null"
:class="isSelected ? 'border-transparent ring-2 ring-offset-2 ring-blue-500' : 'border-slate-200'"
class="flex flex-col items-center transition-all relative cursor-pointer w-full group gap-2 lg:gap-4 p-4 border rounded-xl bg-slate-50 overflow-hidden"
>
<div v-if="deleteAddressIsPending" class="absolute inset-0">
<div
v-if="deleteAddressIsPending"
class="absolute inset-0"
>
<Skeleton class="!size-full !rounded-xl" />
</div>
<span class="flex items-center justify-between w-full gap-3">
<div
class="flex items-center gap-3 max-lg:text-sm font-semibold text-slate-900"
>
<div class="flex items-center gap-3 max-lg:text-sm font-semibold text-slate-900">
{{ !!address ? address.name : "آدرس" }}
<span
v-if="isSelected"
v-if="isSelected || setOrderAddressIsPending"
class="bg-blue-500 rounded-lg px-3 py-2 text-slate-200 text-[10px] lg:text-xs"
>
انتخاب شده
<span v-if="setOrderAddressIsPending">
<Icon
name="svg-spinners:3-dots-bounce"
class="**:fill-white"
/>
</span>
<span v-else-if="isSelected && !setOrderAddressIsPending"> انتخاب شده </span>
</span>
</div>
@@ -97,13 +126,14 @@ const handleDeleteAddress = (id: number) => {
@click.stop="handleDeleteAddress(address.id!)"
class="size-8 bg-slate-200/50 rounded-sm flex-center me-2 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Icon name="bi:trash" class="**:fill-red-500" />
<Icon
name="bi:trash"
class="**:fill-red-500"
/>
</button>
</span>
<div
class="flex flex-col items-center justify-between w-full gap-3 lg:gap-8 lg:flex-row"
>
<div class="flex flex-col items-center justify-between w-full gap-3 lg:gap-8 lg:flex-row">
<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-xs lg:text-sm text-slate-700"
@@ -5,6 +5,8 @@ import useCreateOrUpdateAddress from "~/composables/api/account/useCreateOrUpdat
import useGetAccount from "~/composables/api/account/useGetAccount";
import { QUERY_KEYS } from "~/constants";
import { useToast } from "~/composables/global/useToast";
import useVuelidate from "@vuelidate/core";
import { helpers, required, minLength } from "@vuelidate/validators";
// types
@@ -42,6 +44,35 @@ const addressData = ref({
is_main: address.value?.is_main ?? false,
});
// computed
const formRules = computed(() => {
return {
province: {
required: helpers.withMessage("فیلد استان سکونت الزامی می باشد", required),
minLength: helpers.withMessage("فیلد استان سکونت حداقل 2 کرکتر می باشد", minLength(2)),
},
city: {
required: helpers.withMessage("فیلد شهر سکونت الزامی می باشد", required),
minLength: helpers.withMessage("فیلد شهر سکونت حداقل 2 کرکتر می باشد", minLength(2)),
},
postal_code: {
required: helpers.withMessage("فیلد کد پستی الزامی می باشد", required),
minLength: helpers.withMessage("فیلد کد پستی حداقل 10 کرکتر می باشد", minLength(10)),
},
address: {
required: helpers.withMessage("فیلد آدرس کامل الزامی می باشد", required),
minLength: helpers.withMessage("فیلد آدرس کامل حداقل 2 کرکتر می باشد", minLength(2)),
},
phone: {
required: helpers.withMessage("فیلد تلفن همراه الزامی می باشد", required),
minLength: helpers.withMessage("فیلد تلفن همراه حداقل 10 کرکتر می باشد", minLength(10)),
},
};
});
const formValidator$ = useVuelidate(formRules, addressData);
// queries
const { data: account } = useGetAccount();
@@ -64,35 +95,42 @@ const closeModal = () => {
is_main: false,
};
}
formValidator$.value.$reset();
isShow.value = false;
};
const addNew = () => {
createOrUpdateAddress(
{ ...addressData.value },
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
closeModal();
addToast({
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "آدرس با موفقیت اضافه شد",
options: {
status: "success",
},
});
},
onError: () => {
addToast({
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "مشکلی در افزودن آدرس رخ داد",
options: {
status: "error",
},
});
},
}
);
const handleSubmit = async () => {
await formValidator$.value.$validate();
if (!formValidator$.value.$errors.length) {
createOrUpdateAddress(
{ ...addressData.value },
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.cart],
});
closeModal();
addToast({
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "آدرس با موفقیت اضافه شد",
options: {
status: "success",
},
});
},
onError: () => {
addToast({
message: isEditing.value ? "آدرس با موفقیت ویرایش شد" : "مشکلی در افزودن آدرس رخ داد",
options: {
status: "error",
},
});
},
}
);
}
};
watch(
@@ -122,9 +160,9 @@ watch(
<Button
:end-icon="!!address ? 'bi:pen' : 'ci:plus'"
size="md"
class="rounded-full"
class="rounded-full transition-all"
:variant="!!address ? 'ghost' : 'solid'"
:class="!!address ? '!bg-transparent !underline' : ''"
:class="!!address ? '!bg-transparent !underline underline-offset-4' : ''"
>
<span class="whitespace-pre max-lg:text-xs">
{{ !!address ? "ویرایش" : "افزودن آدرس" }}
@@ -138,115 +176,111 @@ watch(
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
>
<DataField
id="name"
label="نام پیش فرض"
>
<Input
id="name"
type="text"
placeholder="اینجا وارد کنید ..."
v-model="addressData.name!"
/>
</div>
<div class="flex flex-col gap-2">
<label
for="province"
class="text-xs font-semibold lg:text-sm text-gray-900"
>
آدرس شما؟
<span class="text-sm text-red-500"> * </span>
</label>
</DataField>
<DataField
id="for_me"
label="آدرس شما؟"
:required="true"
>
<Select
id="for_me"
:options="['بله', 'خیر']"
placeholder="انتخاب کنید"
v-model="addressData.for_me as string"
/>
</div>
<div class="flex flex-col gap-2">
<label
for="phone"
class="text-xs font-semibold lg:text-sm text-gray-900"
>شماره تلفن <span class="text-sm text-red-500">*</span></label
>
</DataField>
<DataField
id="phone"
label="تلفن همراه"
:required="true"
:error="formValidator$.phone"
>
<Input
id="phone"
type="text"
placeholder="اینجا وارد کنید ..."
:error="formValidator$.phone.$error"
v-model="addressData.phone!"
/>
</div>
<div class="flex flex-col gap-2">
<label
for="province"
class="text-xs font-semibold lg:text-sm text-gray-900"
>استان
<span class="text-sm text-red-500">*</span>
</label>
</DataField>
<DataField
id="province"
label="استان"
:required="true"
:error="formValidator$.province"
>
<Input
id="province"
type="text"
placeholder="اینجا وارد کنید ..."
:error="formValidator$.province.$error"
v-model="addressData.province!"
/>
</div>
</DataField>
<div class="flex flex-col gap-2">
<label
for="city"
class="text-xs font-semibold lg:text-sm text-gray-900"
>شهر <span class="text-sm text-red-500">*</span></label
>
<DataField
id="city"
label="شهر"
:required="true"
:error="formValidator$.city"
>
<Input
id="city"
type="text"
placeholder="اینجا وارد کنید ..."
:error="formValidator$.city.$error"
v-model="addressData.city!"
/>
</div>
</DataField>
<div class="flex flex-col gap-2">
<label
for="post"
class="text-xs font-semibold lg:text-sm text-gray-900"
>کد پستی <span class="text-sm text-red-500">*</span></label
>
<DataField
id="postal_code"
label="کد پستی"
:required="true"
:error="formValidator$.postal_code"
>
<Input
id="post"
id="postal_code"
type="text"
placeholder="اینجا وارد کنید ..."
:error="formValidator$.postal_code.$error"
v-model="addressData.postal_code!"
/>
</div>
</DataField>
</div>
<div class="flex flex-col w-full gap-2">
<label
for="address"
class="text-xs font-semibold lg:text-sm text-gray-900"
>آدرس کامل <span class="text-sm text-red-500">*</span></label
>
<textarea
<DataField
id="address"
label="آدرس کامل"
:required="true"
:error="formValidator$.address"
>
<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 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
></textarea>
</div>
:error="formValidator$.address.$error"
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-2.5 lg:py-3 leading-[175%] selection:bg-slate-100 rounded-md lg:rounded-100 outline-none max-lg:h-[5rem] lg:flex-1 text-xs lg:!text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
></Textarea>
</DataField>
<div class="flex items-center justify-between w-full gap-2">
<label
for="is_main"
class="text-xs font-semibold lg:text-sm text-gray-900"
class="text-xs font-medium lg:text-sm text-gray-900"
>
به عنوان آدرس پیش فرض ثبت شود؟
</label>
@@ -260,7 +294,7 @@ watch(
<div class="py-6 border-t border-slate-200 flex gap-3">
<Button
:disabled="createAddressIsPending"
@click="addNew"
@click="handleSubmit"
class="rounded-full px-10"
size="md"
>
@@ -13,12 +13,8 @@ const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
</script>
<template>
<div
class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-gray-300 rounded-xl bg-gray-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900"
>
<div class="flex flex-col items-center w-full gap-4 p-4 border lg:gap-6 border-slate-200 rounded-xl bg-gray-50">
<span class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-gray-900">
خلاصه سفارش
</span>
@@ -52,7 +48,10 @@ const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
class="gap-2 flex-center"
>
<span class="text-sm text-black"> مشاهده بیشتر </span>
<Icon name="bi:chevron-down" class="**:stroke-black" />
<Icon
name="bi:chevron-down"
class="**:stroke-black"
/>
</button>
</div>
</template>
+31 -47
View File
@@ -26,7 +26,7 @@ const { $queryClient: queryClient } = useNuxtApp();
const { addToast } = useToast();
const counter = ref(data.value.quantity);
const debouncedCounter = refDebounced(counter, 700);
const debouncedCounter = refDebounced(counter, 500);
const { isLoading: cartImageIsLoading } = useImage({
src: data.value.product.image,
@@ -34,8 +34,7 @@ const { isLoading: cartImageIsLoading } = useImage({
// queries
const { mutateAsync: deleteCartItem, isPending: deleteCartItemIsPending } =
useDeleteCartItem();
const { mutateAsync: deleteCartItem, isPending: deleteCartItemIsPending } = useDeleteCartItem();
const { mutateAsync: addCartItem } = useAddCartItem();
@@ -96,6 +95,7 @@ watch(
{
onSuccess: () => {
invalidateCart();
queryClient.refetchQueries({ queryKey: [QUERY_KEYS.product, data.value.product.id] });
},
onError: () => {
invalidateCart();
@@ -134,24 +134,23 @@ watch(
<div class="flex flex-col w-full gap-3 lg:gap-4">
<div class="flex items-center justify-between gap-3">
<span
class="font-semibold typo-sub-h-xs lg:typo-sub-h-sm text-slate-600"
>
<span class="font-semibold typo-sub-h-xs lg:typo-sub-h-sm text-slate-600">
{{ data.product.category }}
</span>
<div
v-if="data.discount > 0"
class="text-white bg-blue-500 px-3 lg:px-4 py-1.5 lg:py-2 text-[10px] lg:text-xs rounded-full flex items-center gap-1"
>
<Icon name="bi:percent" class="size-4" />
<Icon
name="bi:percent"
class="size-4"
/>
{{ data.discount }}
تخفیف
</div>
</div>
<span
class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black"
>
<span class="font-semibold typo-sub-h-sm lg:typo-sub-h-xl text-black">
{{ data.product.title }}
</span>
@@ -171,8 +170,7 @@ watch(
</div>
<span
v-if="data.product.product_attributes.length > 0"
v-for="(variant, index) in data.product
.product_attributes"
v-for="(variant, index) in data.product.product_attributes"
:index="index"
class="px-3 py-1 rounded-full border border-slate-200 text-xs lg:text-sm"
>
@@ -180,20 +178,17 @@ watch(
</span>
</div>
<div
class="items-center justify-between hidden w-full lg:flex -mt-1"
>
<div class="items-center justify-between hidden w-full lg:flex -mt-1">
<div class="flex items-center">
<button
@click="handleIncreaseQuantity"
class="border size-10 flex-center rounded-100 border-slate-300"
:class="
deleteCartItemIsPending
? 'pointer-events-none'
: ''
"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon name="bi:plus" class="**:stroke-slate-800" />
<Icon
name="bi:plus"
class="**:stroke-slate-800"
/>
</button>
<div class="size-10 flex-center">{{ counter }}</div>
@@ -201,19 +196,11 @@ watch(
<button
@click="handleDecreaseQuantity"
class="border size-10 flex-center rounded-100 border-slate-300"
:class="
deleteCartItemIsPending
? 'pointer-events-none'
: ''
"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon
v-if="counter == 1"
:name="
deleteCartItemIsPending
? 'svg-spinners:3-dots-bounce'
: 'bi:trash'
"
:name="deleteCartItemIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:trash'"
class="**:fill-red-700"
/>
<Icon
@@ -232,9 +219,7 @@ watch(
>
{{ data.price }}
</span>
<span
class="typo-p-xl relative flex-center w-fit font-medium"
>
<span class="typo-p-xl relative flex-center w-fit font-medium">
{{ data.final_price }}
</span>
</div>
@@ -248,11 +233,12 @@ watch(
<button
@click="handleIncreaseQuantity"
class="border size-7 p-1 lg:size-10 flex-center rounded-50 border-slate-300"
:class="
deleteCartItemIsPending ? 'pointer-events-none' : ''
"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon name="bi:plus" class="**:stroke-slate-800" />
<Icon
name="bi:plus"
class="**:stroke-slate-800"
/>
</button>
<div class="size-10 text-sm flex-center">
@@ -262,20 +248,18 @@ watch(
<button
@click="handleDecreaseQuantity"
class="border size-7 lg:size-10 p-1 flex-center rounded-50 border-slate-300"
:class="
deleteCartItemIsPending ? 'pointer-events-none' : ''
"
:class="deleteCartItemIsPending ? 'pointer-events-none' : ''"
>
<Icon
v-if="counter == 1"
:name="
deleteCartItemIsPending
? 'svg-spinners:3-dots-bounce'
: 'bi:trash'
"
:name="deleteCartItemIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:trash'"
class="**:fill-red-700"
/>
<Icon v-else name="bi:dash" class="**:stroke-slate-800" />
<Icon
v-else
name="bi:dash"
class="**:stroke-slate-800"
/>
</button>
</div>
+28 -37
View File
@@ -9,19 +9,13 @@
}"
/>
<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:pt-[150px] lg:pb-[50px] justify-center"
>
<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:pt-[100px] lg:pb-[50px] justify-center">
<img
src="/img/heymlz/heymlz-small-idle.gif"
class="size-[150px] lg:size-[220px] rounded-full drop-shadow-2xl"
/>
<span
class="font-bold text-2xl lg:text-5xl text-gradient bg-gradient-to-l from-blue-500 to-blue-700"
>
<span class="font-bold text-2xl lg:text-5xl text-gradient bg-gradient-to-l from-blue-500 to-blue-700">
فروشگاه هی ملز
</span>
</div>
@@ -30,44 +24,49 @@
class="w-full flex max-lg:flex-col justify-between py-[64px] max-lg:gap-16 container items-center lg:items-start relative z-20"
>
<div class="flex flex-col gap-4 max-w-[300px]">
<h3
class="font-bold text-lg xl:text-3xl max-lg:text-center text-white"
>
<h3 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 max-lg:text-xs"
>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنگی با
تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان
گرافیک است. چاپگرها
<p class="text-md font-thin leading-[175%] mt-4 max-lg:text-center text-slate-300 max-lg:text-xs">
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنگی با تولید سادگی نامفهوم از صنعت چاپ و با
استفاده از طراحان گرافیک است. چاپگرها
</p>
<div
class="flex items-center gap-4 mt-6 max-lg:justify-center"
>
<NuxtLink to="#" class="flex-center size-[1.5rem]">
<div class="flex items-center gap-4 mt-6 max-lg:justify-center">
<NuxtLink
to="#"
class="flex-center size-[1.5rem]"
>
<Icon
name="ci:instagram"
class="**:fill-white"
size="24"
/>
</NuxtLink>
<NuxtLink to="#" class="flex-center size-[1.5rem]">
<NuxtLink
to="#"
class="flex-center size-[1.5rem]"
>
<Icon
name="ci:facebook"
class="**:fill-white **:stroke-white"
size="20"
/>
</NuxtLink>
<NuxtLink to="#" class="flex-center size-[1.5rem]">
<NuxtLink
to="#"
class="flex-center size-[1.5rem]"
>
<Icon
name="ci:tiktok"
class="**:fill-white **:stroke-white"
size="20"
/>
</NuxtLink>
<NuxtLink to="#" class="flex-center size-[1.5rem]">
<NuxtLink
to="#"
class="flex-center size-[1.5rem]"
>
<Icon
name="ci:youtube"
class="**:fill-white"
@@ -79,9 +78,7 @@
<div class="flex justify-center lg:justify-end flex-1">
<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 max-lg:text-xs"
>
<ul class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs">
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li>
@@ -92,9 +89,7 @@
<div class="flex justify-end flex-1">
<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 max-lg:text-xs"
>
<ul class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs">
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li>
@@ -104,12 +99,8 @@
</div>
<div class="flex justify-end flex-1">
<div class="flex flex-col gap-6 max-lg:text-center">
<h3 class="font-bold w-full text-white">
لینک های مفید
</h3>
<ul
class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs"
>
<h3 class="font-bold w-full text-white">لینک های مفید</h3>
<ul class="flex flex-col gap-4 font-thin text-slate-300 max-lg:text-xs">
<li>از طراحان گرافیک است</li>
<li>تولید نامفهوم</li>
<li>ستون و سطرآنچنان که لازم</li>
+1 -1
View File
@@ -39,7 +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,
},
];
});
@@ -40,17 +40,17 @@ const statusIcon = computed(() => {
case "success":
return {
name: "duo-icons:check-circle",
class: "**:fill-success-500 [filter:drop-shadow(0_0_10px_var(--color-success-500))]",
class: "**:fill-success-500 [filter:drop-shadow(0_0_20px_var(--color-success-500))]",
};
case "error":
return {
name: "duo-icons:alert-triangle",
class: "**:fill-danger-500 [filter:drop-shadow(0_0_10px_var(--color-danger-500))]",
class: "**:fill-danger-500 [filter:drop-shadow(0_0_20px_var(--color-danger-500))]",
};
case "info":
return {
name: "duo-icons:info",
class: "**:fill-cyan-500 [filter:drop-shadow(0_0_10px_var(--color-cyan-500))]",
class: "**:fill-cyan-500 [filter:drop-shadow(0_0_20px_var(--color-cyan-500))]",
};
case "warning":
return {
@@ -88,20 +88,25 @@ onMounted(() => {
:duration="options.duration ?? 4000"
@swipeEnd="onSwipeEnd"
v-model:open="open"
class="w-full bg-white shadow-md justify-items-start shadow-black/3 border-t-[0.5px] border-slate-200 p-4 grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center data-[state=open]:animate-toast-in data-[state=closed]:animate-toast-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-toast-out"
class="w-full bg-white shadow-md justify-items-start shadow-black/3 border-[0.5px] flex flex-col border-slate-300 p-4 gap-x-[15px] items-center data-[state=open]:animate-toast-in data-[state=closed]:animate-toast-hide data-[swipe=move]:translate-x-[var(--reka-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=cancel]:transition-[transform_200ms_ease-out] data-[swipe=end]:animate-toast-out"
:class="options.description ? 'rounded-150' : 'rounded-full'"
>
<ToastTitle
:class="[{ 'mb-1.5': options.description }]"
class="w-full justify-items-start [grid-area:_title] font-medium text-slate-600 text-sm flex items-center justify-between gap-2"
:class="[{ 'mb-1': options.description }]"
class="w-full justify-items-start font-medium text-slate-600 text-sm flex items-center justify-between gap-2"
>
<Icon :name="statusIcon.name" :class="statusIcon.class" size="24" />
<span class="text-start -me-2">{{ message }}</span>
<Icon
:name="statusIcon.name"
:class="statusIcon.class"
size="24"
/>
<span class="text-start">{{ message }}</span>
</ToastTitle>
<ToastDescription v-if="options.description" as-child>
<div
class="[grid-area:_description] m-0 mr-8 text-slate-500 typo-p-sm text-start"
>
<ToastDescription
v-if="options.description"
as-child
>
<div class="text-slate-400 typo-p-xs font-medium flex items-center justify-end w-full">
{{ options.description }}
</div>
</ToastDescription>
+6 -16
View File
@@ -8,9 +8,7 @@ const route = useRoute();
// computed
const pageTitle = computed(() => route.meta.pageTitle);
const prevPage = computed(
() => route.meta.prevPage as { name: string; label: string } | undefined
);
const prevPage = computed(() => route.meta.prevPage as { name: string; label: string } | undefined);
// queries
@@ -20,9 +18,7 @@ await suspense();
// computed
const hasCartItem = computed(
() => !!cart.value && cart.value.items.length! > 0
);
const hasCartItem = computed(() => !!cart.value && cart.value.items.length! > 0);
</script>
<template>
@@ -32,20 +28,16 @@ const hasCartItem = computed(
>
<Header />
<main
class="w-full overflow-x-hidden flex flex-col gap-[5rem] lg:max-w-[85vw]"
>
<main class="w-full overflow-x-hidden flex flex-col gap-[5rem] lg:max-w-[85vw]">
<div class="w-full flex flex-col container">
<div
class="flex flex-col items-center justify-center py-[3.5rem] lg:py-[5rem] gap-5 lg:gap-0 lg:flex-row"
>
<div
class="flex items-center justify-start w-full lg:w-3/12"
>
<div class="flex items-center justify-start w-full lg:w-3/12">
<NuxtLink
v-if="prevPage"
:to="{ name: prevPage?.name }"
class="flex items-center gap-2 text-sm lg:text-[1rem]"
class="flex items-center gap-2 text-sm lg:text-[1rem] font-medium"
>
<Icon
name="bi:arrow-right"
@@ -57,9 +49,7 @@ const hasCartItem = computed(
</NuxtLink>
</div>
<h1
class="w-full text-center lg:w-6/12 typo-h-5 lg:typo-h-4"
>
<h1 class="w-full text-center lg:w-6/12 typo-h-5 lg:typo-h-4">
{{ pageTitle }}
</h1>
+5
View File
@@ -0,0 +1,5 @@
export default defineNuxtRouteMiddleware((to, from) => {
if (to.path !== from.path && process.client) {
window.scrollTo(0, 0);
}
});
+3 -9
View File
@@ -80,18 +80,12 @@ onMounted(() => {
<template>
<div class="flex flex-col w-full gap-5">
<div
class="flex flex-col items-center w-full gap-4 p-4 border border-gray-300 rounded-xl bg-gray-50"
>
<span
class="flex items-center justify-start w-full text-[1.125rem] font-semibold text-gray-900"
>
<div class="flex flex-col items-center w-full gap-4 p-4 border border-slate-200 rounded-xl bg-gray-50">
<span class="flex items-center justify-start w-full text-[1.125rem] font-semibold text-gray-900">
روش پرداخت
</span>
<div
class="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"
>
<div class="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
<Gateway
v-for="(gateway, index) in paymentGateways"
:index="index"
+27 -76
View File
@@ -2,9 +2,8 @@
// imports
import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
import useSetOrderAddress from "~/composables/api/orders/useSetOrderAddress";
import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants";
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
// meta
@@ -29,10 +28,9 @@ type DeliveryData = {
// queries
const { data: addresses, isLoading: addressesIsLoading } = useGetAllAddress();
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
const { mutateAsync: setOrderAddress, isPending: setOrderAddressIsPending } =
useSetOrderAddress();
const { data: addresses, isLoading: addressesIsLoading } = useGetAllAddress();
// computed
@@ -53,41 +51,6 @@ const deliveryData = ref<DeliveryData>({
tipax: false,
},
});
// methods
const handleSelectAddress = (address: Address) => {
deliveryData.value.address = { ...address };
};
// watch
whenever(
() => deliveryData.value.address,
(nv) => {
setOrderAddress(
{ address_id: nv.id! },
{
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.addresses],
});
},
onError: () => {
addToast({
message: "در انتخاب آدرس خطایی رخ داد",
options: {
description: "لطفا مجدد تلاش کنید",
},
});
},
}
);
},
{
deep: true,
}
);
</script>
<template>
@@ -95,17 +58,20 @@ whenever(
<AddressItem />
<div class="flex flex-col w-full gap-6">
<div class="flex items-center gap-3 py-3">
<NuxtImg src="/img/location.gif" class="size-12 pb-1 -mr-3" />
<span class="typo-sub-h-xl -mr-3"> آدرس های شما </span>
<Icon
name="svg-spinners:180-ring-with-bg"
size="20"
v-if="setOrderAddressIsPending"
class="pb-0.5"
<NuxtImg
src="/img/location.gif"
class="size-12 pb-1 -mr-3"
/>
<span class="typo-sub-h-xl -mr-3"> آدرس های شما </span>
</div>
<div v-if="addressesIsLoading" class="flex flex-col gap-6 w-full">
<Skeleton v-for="i in 3" class="w-full !h-[8rem] !rounded-xl" />
<div
v-if="addressesIsLoading"
class="flex flex-col gap-6 w-full"
>
<Skeleton
v-for="i in 3"
class="w-full !h-[8rem] !rounded-xl"
/>
</div>
<template v-else>
<div
@@ -120,34 +86,28 @@ whenever(
/>
</div>
<div v-else class="flex flex-col gap-6 w-full">
<div
v-else
class="flex flex-col gap-6 w-full"
>
<AddressItem
v-for="(address, index) in addresses"
:key="index"
:isSelected="address.id === cart?.address.id"
:address="address"
@select="handleSelectAddress"
:isSelected="address.id == deliveryData.address?.id"
/>
</div>
</template>
</div>
<div
class="flex flex-col items-center w-full gap-4 my-3 p-4 border border-slate-200 rounded-xl bg-slate-50"
>
<span
class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900"
>
<div class="flex flex-col items-center w-full gap-4 my-3 p-4 border border-slate-200 rounded-xl bg-slate-50">
<span class="flex items-center justify-start w-full lg:text-[1.125rem] font-semibold text-slate-900">
شیوه ارسال
</span>
<label
@click="deliveryData.deliveryMethod.pishtaz = true"
:class="
deliveryData.deliveryMethod.pishtaz
? 'ring-black ring-offset-2 ring-2'
: ''
"
:class="deliveryData.deliveryMethod.pishtaz ? 'ring-black ring-offset-2 ring-2' : ''"
class="flex flex-col select-none w-full gap-2 p-3 transition-all border cursor-pointer delivery-option focus-within:ring-2 ring-blue-500 ring-offset-2 focus-within:border-blue-500 rounded-100 border-slate-200 bg-slate-50"
>
<div class="flex items-center justify-between w-full">
@@ -161,15 +121,10 @@ whenever(
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span
class="w-full text-slate-800 text-sm lg:text-[1rem]"
>پست پیشتاز</span
>
<span class="w-full text-slate-800 text-sm lg:text-[1rem]">پست پیشتاز</span>
</div>
<span class="text-slate-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
<span class="text-slate-800 text-sm lg:text-[1rem]"> ۱۵۰٬۰۰۰ تومان </span>
</div>
</label>
@@ -185,14 +140,10 @@ whenever(
class="size-6 my-auto bg-white text-sm ms-1 flex items-center justify-center shadow-xl rounded-full transition-transform translate-x-0.5 will-change-transform data-[state=checked]:-translate-x-[68%]"
/>
</SwitchRoot>
<span class="w-full text-slate-800 text-sm lg:text-[1rem]"
>تیپاکس</span
>
<span class="w-full text-slate-800 text-sm lg:text-[1rem]">تیپاکس</span>
</div>
<span class="text-slate-800 text-sm lg:text-[1rem]">
۱۵۰٬۰۰۰ تومان
</span>
<span class="text-slate-800 text-sm lg:text-[1rem]"> ۱۵۰٬۰۰۰ تومان </span>
</label>
</div>
+22 -73
View File
@@ -4,9 +4,7 @@
import useVuelidate from "@vuelidate/core";
import { helpers, required, minLength, email } from "@vuelidate/validators";
import useGetAccount from "~/composables/api/account/useGetAccount";
import useUpdateAccount, {
type UpdateAccountRequest,
} from "~/composables/api/account/useUpdateAccount";
import useUpdateAccount, { type UpdateAccountRequest } from "~/composables/api/account/useUpdateAccount";
import { useObjectTrack } from "~/composables/global/useObjectTrack";
import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants";
@@ -40,18 +38,11 @@ const profilePictureModalIsShow = ref(false);
const { isNotEqual, clear: clearObjectTracker } = useObjectTrack(personalData);
const alises = ref([
"شکارچی",
"آیفون باز",
"خوش سلیقه",
"دست و دلباز",
"چرم باز",
]);
const alises = ref(["شکارچی", "آیفون باز", "خوش سلیقه", "دست و دلباز", "چرم باز"]);
// queries
const { mutateAsync: updateAccount, isPending: updateAccountIsPending } =
useUpdateAccount();
const { mutateAsync: updateAccount, isPending: updateAccountIsPending } = useUpdateAccount();
// computed
@@ -59,52 +50,28 @@ const formRules = computed(() => {
return {
first_name: {
required: helpers.withMessage("فیلد نام الزامی می باشد", required),
minLength: helpers.withMessage(
"فیلد نام حداقل ۳ کرکتر می باشد",
minLength(3)
),
minLength: helpers.withMessage("فیلد نام حداقل ۳ کرکتر می باشد", minLength(3)),
},
last_name: {
required: helpers.withMessage(
"فیلد نام خانوادگی الزامی می باشد",
required
),
minLength: helpers.withMessage(
"فیلد نام خانوادگی حداقل ۳ کرکتر می باشد",
minLength(3)
),
required: helpers.withMessage("فیلد نام خانوادگی الزامی می باشد", required),
minLength: helpers.withMessage("فیلد نام خانوادگی حداقل ۳ کرکتر می باشد", minLength(3)),
},
phone: {
required: helpers.withMessage(
"فیلد شماره تلفن الزامی می باشد",
required
),
required: helpers.withMessage("فیلد شماره تلفن الزامی می باشد", required),
phoneValidator: helpers.withMessage(
"شماره تلفن وارد شده معتبر نمی باشد",
helpers.regex(/^0?[1-9][0-9]{9}$/)
),
},
gender: {
required: helpers.withMessage(
"فیلد جنسیت الزامی می باشد",
required
),
required: helpers.withMessage("فیلد جنسیت الزامی می باشد", required),
},
email: {
required: helpers.withMessage(
"فیلد حساب الکترونیکی الزامی می باشد",
required
),
email: helpers.withMessage(
"حساب الکترونیکی وارد شده معتبر نمی باشد",
email
),
required: helpers.withMessage("فیلد حساب الکترونیکی الزامی می باشد", required),
email: helpers.withMessage("حساب الکترونیکی وارد شده معتبر نمی باشد", email),
},
birth_date: {
required: helpers.withMessage(
"فیلد تاریخ تولد الزامی می باشد",
required
),
required: helpers.withMessage("فیلد تاریخ تولد الزامی می باشد", required),
},
};
});
@@ -167,18 +134,14 @@ const handleSubmit = (withValidation: boolean) => {
<div
class="w-full flex flex-col lg:flex-row items-center max-lg:gap-5 lg:justify-between border p-6 rounded-xl border-slate-200"
>
<div
class="flex items-center justify-start gap-5 w-full lg:w-8/12"
>
<div class="flex items-center justify-start gap-5 w-full lg:w-8/12">
<div class="relative shrink-0 rounded-full flex-center">
<Avatar
class="!size-20 lg:!size-32"
: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)}`
: 'بدون نام کاربری'
"
/>
@@ -192,18 +155,12 @@ const handleSubmit = (withValidation: boolean) => {
<div class="flex flex-col gap-2 lg:gap-3">
<span class="typo-sub-h-md lg:typo-sub-h-lg"
>{{ account?.first_name }}
{{ account?.last_name }}</span
>{{ account?.first_name }} {{ account?.last_name }}</span
>
<span
class="typo-sub-h-xs lg:typo-sub-h-sm !font-light text-slate-600 leading-[200%]"
>
با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک
بایوگرافی درست میکنه :)
<span class="typo-sub-h-xs lg:typo-sub-h-sm !font-light text-slate-600 leading-[200%]">
با اولین خریدتون هوش مصنوعی وبسایتمون واستون یک بایوگرافی درست میکنه :)
</span>
<div
class="flex-center border border-yellow-500 pe-3.5 ps-1 w-max rounded-full"
>
<div class="flex-center border border-yellow-500 pe-3.5 ps-1 w-max rounded-full">
<div class="rounded-full p-1.5 lg:p-2">
<Icon
name="bi:patch-check"
@@ -211,26 +168,20 @@ const handleSubmit = (withValidation: boolean) => {
size="20"
/>
</div>
<span class="text-[10px] lg:text-xs text-yellow-500"
>جزو ۳ مشتری برتر</span
>
<span class="text-[10px] lg:text-xs text-yellow-500">جزو ۳ مشتری برتر</span>
</div>
</div>
</div>
<div class="flex flex-col items-start gap-3 w-full lg:w-4/12">
<span class="typo-sub-h-md lg:typo-sub-h-lg"
>لقب های شما</span
>
<span class="typo-sub-h-md lg:typo-sub-h-lg">لقب های شما</span>
<span class="flex w-full flex-wrap gap-2">
<span
v-for="(alise, index) in alises"
:key="index"
class="flex-center bg-slate-50 border border-slate-200 py-1.5 lg:py-2 px-3 w-max rounded-full"
>
<span class="text-[10px] lg:text-xs text-black">{{
alise
}}</span>
<span class="text-[10px] lg:text-xs text-black">{{ alise }}</span>
</span>
</span>
</div>
@@ -251,9 +202,7 @@ const handleSubmit = (withValidation: boolean) => {
<span v-else> ثبت تغییرات </span>
</Button>
</template>
<div
class="w-full grid grid-cols-1 lg:grid-cols-2 gap-x-3 gap-y-5"
>
<div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-x-3 gap-y-5">
<DataField
id="personal-data-name"
label="نام"
@@ -290,7 +239,7 @@ const handleSubmit = (withValidation: boolean) => {
</DataField>
<DataField
id="personal-data-birth-date"
label="تاریخ تولد"
label="تاریsخ تولد"
:error="formValidator$.birth_date"
>
<Datepicker
+6 -17
View File
@@ -1,24 +1,18 @@
import axiosOriginal from "axios";
import { useAuth } from "~/composables/api/auth/useAuth";
import { API_ENDPOINTS } from "~/constants";
import Logger from "~/tools/logger";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
const { token } = useAuth();
const axios = axiosOriginal.create({
baseURL: config.public.API_BASE_URL
baseURL: config.public.API_BASE_URL,
});
axios.interceptors.request.use((config) => {
if (
!config.url?.includes(API_ENDPOINTS.auth.signin) &&
!config.url?.includes(API_ENDPOINTS.account.send_otp)
) {
config.headers.Authorization = token.value
? `Bearer ${token.value}`
: undefined;
if (!config.url?.includes(API_ENDPOINTS.auth.signin) && !config.url?.includes(API_ENDPOINTS.account.send_otp)) {
config.headers.Authorization = token.value ? `Bearer ${token.value}` : undefined;
}
return config;
@@ -28,19 +22,14 @@ export default defineNuxtPlugin(() => {
(response) => {
return response;
},
async function(error) {
if (config.public.DEBUG === "true" && import.meta.server) {
await Logger.axiosErrorLog(error);
}
async function (error) {
return Promise.reject(error);
}
);
return {
provide: {
axios
}
axios,
},
};
});
-6
View File
@@ -1,6 +0,0 @@
export default defineNuxtPlugin((nuxtApp) => {
// Also possible
nuxtApp.hook('vue:error', (error, instance, info) => {
// handle error, e.g. report to a service
})
})
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

-40
View File
@@ -1,40 +0,0 @@
import fs from "fs/promises";
import { ensureFileExists } from "~/utils";
class Logger {
public static async axiosErrorLog(error: any) {
const errorJson = error.toJSON();
const nowDate = new Date();
const logData: AxiosLogType = {
url: errorJson.config.url,
code: errorJson.code!,
status: errorJson.status!,
method: errorJson.config.method,
response: error?.response?.data,
requestHeaders: errorJson.config.headers,
// responseHeaders: error.response.headers,
payload: errorJson.config.data ? JSON.parse(errorJson.config.data) : undefined,
params: errorJson.config.params ?? undefined,
date: nowDate.toString()
};
const logFilePath = ".logs/log.json";
try {
await ensureFileExists(logFilePath, "[]");
const oldLogs = await fs.readFile(logFilePath, "utf-8");
const oldLogsJson = JSON.parse(oldLogs) as Record<any, any>[];
oldLogsJson.push(logData);
await fs.writeFile(logFilePath, JSON.stringify(oldLogsJson, null, 2));
} catch (e) {
console.error(e);
}
}
}
export default Logger;
+4 -19
View File
@@ -109,10 +109,7 @@ declare global {
colors: string[];
};
type ProductListItem = Pick<
Product,
"id" | "variants" | "name" | "rating" | "slug" | "category" | "colors"
>;
type ProductListItem = Pick<Product, "id" | "variants" | "name" | "rating" | "slug" | "category" | "colors">;
type Article = {
id: number;
@@ -193,13 +190,7 @@ declare global {
id: number;
count: number;
images: string[];
status:
| "ADMIN_PENDING"
| "PENDING"
| "POSTED"
| "RECEIVED"
| "CANCELED"
| "REFUND";
status: "ADMIN_PENDING" | "PENDING" | "POSTED" | "RECEIVED" | "CANCELED" | "REFUND";
verbose_status: string;
is_paid: boolean;
created_at: string;
@@ -248,6 +239,7 @@ declare global {
cart_total: string;
tax: string;
final_price: string;
address: Address;
};
type ServerFile = {
@@ -281,14 +273,7 @@ declare global {
id: number;
picture: string;
title: string;
type:
| "ZARINPAL"
| "SEP"
| "MELLAT"
| "IDPAY"
| "ZIBAL"
| "BAHAMTA"
| "BMI";
type: "ZARINPAL" | "SEP" | "MELLAT" | "IDPAY" | "ZIBAL" | "BAHAMTA" | "BMI";
};
type Transaction = {