Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
+2
-1
@@ -7,6 +7,8 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
|
||||
<NuxtRouteAnnouncer />
|
||||
|
||||
<NuxtLayout>
|
||||
<LoadingIndicator />
|
||||
|
||||
<ToastProvider>
|
||||
<NuxtPage />
|
||||
<div dir="ltr">
|
||||
@@ -20,4 +22,3 @@ import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ const emit = defineEmits(["select"]);
|
||||
|
||||
const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const modalIsShow = ref(false);
|
||||
|
||||
const marqueeIsPause = ref(true);
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
// queries
|
||||
@@ -97,20 +101,25 @@ const handleDeleteAddress = (id: number) => {
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center justify-between w-full gap-4 lg:flex-row"
|
||||
class="flex flex-col items-center justify-between w-full gap-8 lg:flex-row"
|
||||
>
|
||||
<span
|
||||
class="w-full text-start text-sm lg:text-[1rem] lg:w-9/12 text-slate-700"
|
||||
>
|
||||
{{
|
||||
!!address
|
||||
? `ایران, ${address.province}, ${address.city}, ${address.address}, کدپستی ${address.postal_code}`
|
||||
: "افزودن آدرس جدید"
|
||||
}}
|
||||
</span>
|
||||
<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 lg:text-[1rem] text-slate-700"
|
||||
>
|
||||
{{
|
||||
!!address
|
||||
? `ایران, ${address.province}, ${address.city}, ${address.address}, کدپستی ${address.postal_code}`
|
||||
: "افزودن آدرس جدید"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end w-full lg:w-3/12">
|
||||
<AddressModal :address="address" />
|
||||
<AddressModal
|
||||
:address="address"
|
||||
v-model:is-show="modalIsShow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -12,13 +12,23 @@ import { useToast } from "~/composables/global/useToast";
|
||||
|
||||
type Props = {
|
||||
address?: Address;
|
||||
isShow: boolean;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
"update:address": [value: File];
|
||||
"update:isShow": [value: boolean];
|
||||
};
|
||||
|
||||
// props
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { address } = toRefs(props);
|
||||
const { address, isShow } = toRefs(props);
|
||||
|
||||
// emits
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// computed
|
||||
|
||||
@@ -30,8 +40,6 @@ const { $queryClient: queryClient } = useNuxtApp();
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
const isShow = ref(false);
|
||||
|
||||
const addressData = ref<CreateOrUpdateAddressRequest>({
|
||||
id: address.value?.id ?? undefined,
|
||||
province: address.value?.province ?? "",
|
||||
@@ -56,6 +64,13 @@ const {
|
||||
isPending: createAddressIsPending,
|
||||
} = useCreateOrUpdateAddress(isEditing);
|
||||
|
||||
// computed
|
||||
|
||||
const visible = computed({
|
||||
get: () => isShow.value ?? false,
|
||||
set: (value: boolean) => emit("update:isShow", value),
|
||||
});
|
||||
|
||||
// methods
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -71,7 +86,6 @@ const closeModal = () => {
|
||||
for_me: "بله",
|
||||
};
|
||||
}
|
||||
isShow.value = false;
|
||||
};
|
||||
|
||||
const addNew = () => {
|
||||
@@ -122,199 +136,161 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot
|
||||
v-model:open="isShow"
|
||||
@update:open="
|
||||
(state) => {
|
||||
!state ? closeModal() : null;
|
||||
}
|
||||
"
|
||||
<Modal
|
||||
v-model="visible"
|
||||
:title="!!address ? 'ویرایش آدرس' : 'افزودن آدرس'"
|
||||
:icon="!!address ? 'bi:pen' : 'ci:plus'"
|
||||
:iconSize="!!address ? '20' : '32'"
|
||||
contectClass="!w-[70vw]"
|
||||
@close="closeModal"
|
||||
>
|
||||
<DialogTrigger>
|
||||
<template #trigger>
|
||||
<Button
|
||||
:end-icon="!!address ? 'bi:pen' : 'bi:plus'"
|
||||
:end-icon="!!address ? 'bi:pen' : 'ci:plus'"
|
||||
size="md"
|
||||
class="rounded-full"
|
||||
>
|
||||
<span class="font-bold">
|
||||
<span class="font-bold whitespace-pre">
|
||||
{{ !!address ? "ویرایش آدرس" : "افزودن آدرس" }}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="bg-black/50 backdrop-blur-sm data-[state=open]:animate-overlay-show fixed inset-0 z-30"
|
||||
/>
|
||||
<DialogContent
|
||||
class="data-[state=open]:animate-content-show text-black font-iran-yekan-x fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[50rem] translate-x-[-50%] translate-y-[-50%] rounded-3xl bg-white shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none z-[100]"
|
||||
>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<div class="flex-col-center gap-6 py-10" dir="rtl">
|
||||
<div
|
||||
class="w-full flex items-center px-6 justify-between py-[1.8rem] border-b border-slate-200"
|
||||
class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
<DialogClose
|
||||
class="inline-flex size-8 items-center justify-center transition-all rounded-full bg-gray-50 border border-slate-200 hover:border-black focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon name="bi:x-lg" class="**:fill-black" />
|
||||
</DialogClose>
|
||||
<DialogTitle
|
||||
class="typo-sub-h-xl font-semibold flex items-center gap-3"
|
||||
>
|
||||
{{ !!address ? "ویرایش آدرس" : "افزودن آدرس" }}
|
||||
<Icon
|
||||
:name="!!address ? 'bi:pen' : 'bi:plus'"
|
||||
:size="!!address ? '20' : '32'"
|
||||
/>
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
<div class="flex-col-center gap-6 px-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
|
||||
>
|
||||
|
||||
<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
|
||||
>
|
||||
|
||||
<Select
|
||||
:options="['بله', 'خیر']"
|
||||
placeholder="انتخاب کنید"
|
||||
v-model="addressData.for_me"
|
||||
/>
|
||||
</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
|
||||
>
|
||||
|
||||
<Input
|
||||
id="phone"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
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
|
||||
>
|
||||
|
||||
<Input
|
||||
id="province"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
v-model="addressData.province"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
>
|
||||
<Input
|
||||
id="city"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
v-model="addressData.city"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
>
|
||||
<Input
|
||||
id="post"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
v-model="addressData.postal_code"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
for="address"
|
||||
for="name"
|
||||
class="text-xs font-semibold lg:text-sm text-gray-900"
|
||||
>آدرس کامل
|
||||
>نام پیش فرض
|
||||
<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-3.5 selection:bg-slate-100 rounded-100 outline-none flex-1 !text-sm placeholder-slate-400"
|
||||
></textarea>
|
||||
|
||||
<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
|
||||
>
|
||||
|
||||
<Select
|
||||
: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
|
||||
>
|
||||
|
||||
<Input
|
||||
id="phone"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
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>
|
||||
|
||||
<Input
|
||||
id="province"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
v-model="addressData.province!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
>
|
||||
<Input
|
||||
id="city"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
v-model="addressData.city!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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
|
||||
>
|
||||
<Input
|
||||
id="post"
|
||||
type="text"
|
||||
placeholder="اینجا وارد کنید ..."
|
||||
v-model="addressData.postal_code!"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-slate-200 flex gap-3">
|
||||
<Button
|
||||
:disabled="createAddressIsPending"
|
||||
@click="addNew"
|
||||
class="rounded-full px-10"
|
||||
<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
|
||||
>
|
||||
<Icon
|
||||
v-if="createAddressIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
/>
|
||||
<span v-else>ثبت</span>
|
||||
</Button>
|
||||
<DialogClose aria-label="Close">
|
||||
<Button variant="outlined" class="rounded-full px-10">
|
||||
انصراف
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<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-3.5 selection:bg-slate-100 rounded-100 outline-none flex-1 !text-sm placeholder-slate-400"
|
||||
></textarea>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
|
||||
<div class="py-6 border-t border-slate-200 flex gap-3">
|
||||
<Button
|
||||
:disabled="createAddressIsPending"
|
||||
@click="addNew"
|
||||
class="rounded-full px-10"
|
||||
>
|
||||
<Icon
|
||||
v-if="createAddressIsPending"
|
||||
name="svg-spinners:3-dots-bounce"
|
||||
/>
|
||||
<span v-else>ثبت</span>
|
||||
</Button>
|
||||
<DialogClose aria-label="Close">
|
||||
<Button variant="outlined" class="rounded-full px-10">
|
||||
انصراف
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -6,6 +6,7 @@ type Props = {
|
||||
icon?: string;
|
||||
title: string;
|
||||
iconSize?: string;
|
||||
contectClass?: string;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
@@ -35,7 +36,14 @@ const isShow = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-model:open="isShow">
|
||||
<DialogRoot
|
||||
v-model:open="isShow"
|
||||
@update:open="
|
||||
(state) => {
|
||||
!state ? (isShow = false) : null;
|
||||
}
|
||||
"
|
||||
>
|
||||
<DialogTrigger>
|
||||
<slot name="trigger" />
|
||||
</DialogTrigger>
|
||||
@@ -49,6 +57,7 @@ const isShow = computed({
|
||||
v-if="isShow"
|
||||
>
|
||||
<div
|
||||
:class="contectClass"
|
||||
class="overflow-y-auto max-h-svh absolute left-[50%] py-10 w-fit max-w-[50rem] translate-x-[-50%]"
|
||||
>
|
||||
<DialogContent
|
||||
|
||||
@@ -8,6 +8,7 @@ type Props = {
|
||||
error?: boolean;
|
||||
options: string[];
|
||||
placeholder?: string;
|
||||
triggerRootClass?: string;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
@@ -22,7 +23,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: "وارد نشده",
|
||||
});
|
||||
|
||||
const { modelValue, variant, error } = toRefs(props);
|
||||
const { modelValue, variant, error, triggerRootClass } = toRefs(props);
|
||||
|
||||
// emit
|
||||
|
||||
@@ -46,19 +47,19 @@ const classes = computed(() => {
|
||||
? "input-solid-error"
|
||||
: "input-outlined-error"]: error.value,
|
||||
},
|
||||
triggerRootClass.value,
|
||||
];
|
||||
});
|
||||
|
||||
// watch
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-model="selectedValue" dir="rtl">
|
||||
<SelectTrigger :class="classes" class="" aria-label="Customise options">
|
||||
<SelectTrigger :class="classes">
|
||||
<SelectValue
|
||||
:placeholder="placeholder"
|
||||
v-bind="$attrs"
|
||||
:class="selectedValue ? 'text-black' : 'text-slate-400'"
|
||||
class="font-iran-yekan-x text-sm placeholder-slate-400"
|
||||
class="font-iran-yekan-x text-sm text-start placeholder-slate-400"
|
||||
/>
|
||||
<Icon name="bi:chevron-down" size="16" />
|
||||
</SelectTrigger>
|
||||
@@ -66,7 +67,7 @@ const classes = computed(() => {
|
||||
<SelectPortal>
|
||||
<SelectContent
|
||||
data-side="bottom"
|
||||
class="min-w-[160px] w-full bg-slate-50 border-slate-200 rounded-lg border shadow-sm will-change-[opacity,transform] data-[side=top]:animate-slide-down-fade data-[side=right]:animate-slide-left-fade data-[side=bottom]:animate-slide-up-fade data-[side=left]:animate-slide-right-fade z-[100]"
|
||||
class="min-w-[160px] w-full bg-slate-50 border-slate-200 rounded-lg border shadow-sm will-change-[opacity,transform] data-[side=top]:animate-slide-down-fade data-[side=right]:animate-slide-left-fade data-[side=bottom]:animate-slide-up-fade data-[side=left]:animate-slide-right-fade z-[9999]"
|
||||
:side-offset="5"
|
||||
>
|
||||
<SelectViewport class="p-[5px]">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="relative overflow-x-auto rounded-xl border border-slate-200">
|
||||
<table class="w-full text-sm text-right text-black">
|
||||
<thead class="text-black bg-gray-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<slot name="thead" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<slot name="tbody" />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
class="odd:bg-white even:bg-gray-50 last:border-none border-b border-slate-200"
|
||||
>
|
||||
<td
|
||||
scope="row"
|
||||
class="w-3/12 px-6 py-7 font-medium whitespace-nowrap text-black"
|
||||
>
|
||||
Apple MacBook Pro 17
|
||||
</td>
|
||||
<td class="w-3/12 px-6 py-7">Silver</td>
|
||||
<td class="w-3/12 px-6 py-7">Laptop</td>
|
||||
<td class="w-2/12 px-6 py-7">$2999</td>
|
||||
<td class="w-1/12 px-6 py-7">
|
||||
<NuxtLink :to="{ name: 'profile-tickets-id', params: { id: 1 } }">
|
||||
<button
|
||||
class="size-10 flex-center border border-slate-200 rounded-md"
|
||||
>
|
||||
<Icon
|
||||
name="ci:eye-open"
|
||||
class="**:stroke-black"
|
||||
size="20"
|
||||
/>
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -5,7 +5,7 @@ import { API_ENDPOINTS } from "~/constants";
|
||||
|
||||
// types
|
||||
|
||||
export type CreateOrUpdateAddressRequest = Address;
|
||||
export type CreateOrUpdateAddressRequest = Omit<Address, "is_main">;
|
||||
|
||||
const useCreateOrUpdateAddress = (update: ComputedRef<Boolean>) => {
|
||||
// state
|
||||
|
||||
@@ -1,69 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
// import
|
||||
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
import useRefreshAuth from "~/composables/api/auth/useRefreshAuth";
|
||||
import useVerify from "~/composables/api/auth/useVerify";
|
||||
|
||||
// state
|
||||
|
||||
const { mutateAsync: refreshAuth } = useRefreshAuth();
|
||||
const { token, refreshToken, updateToken, updateRefreshToken, logout } =
|
||||
useAuth();
|
||||
const { mutateAsync: verify } = useVerify();
|
||||
|
||||
// lifecycle
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
if (!!token.value) {
|
||||
// 1.1 - token is there
|
||||
|
||||
try {
|
||||
await verify({
|
||||
token: token.value,
|
||||
});
|
||||
|
||||
// 2.1 - token is valid, finish
|
||||
} catch (e) {
|
||||
// 2.2 - token is there, but not valid, try to refresh token
|
||||
|
||||
if (!!refreshToken.value) {
|
||||
// 3.1 - refresh token is there, try to refresh
|
||||
|
||||
try {
|
||||
const refreshResponse = await refreshAuth({
|
||||
refresh: refreshToken.value,
|
||||
});
|
||||
|
||||
// 4.1 - token is refreshed successfully, finish
|
||||
|
||||
updateToken(refreshResponse.access);
|
||||
updateRefreshToken(refreshResponse.refresh);
|
||||
} catch (e) {
|
||||
// 4.2 - cant refreshing token, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
} else {
|
||||
// 3.2 - refresh token is not exist, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 1.2 - token is not exist, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full flex flex-col-center persian-number font-iran-yekan-x"
|
||||
dir="rtl"
|
||||
>
|
||||
<LoadingIndicator />
|
||||
|
||||
<Header />
|
||||
<main class="w-full overflow-x-hidden">
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||
import useRefreshAuth from "~/composables/api/auth/useRefreshAuth";
|
||||
import useVerify from "~/composables/api/auth/useVerify";
|
||||
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
// state
|
||||
|
||||
const { mutateAsync: refreshAuth } = useRefreshAuth();
|
||||
const { token, refreshToken, updateToken, updateRefreshToken, logout } =
|
||||
useAuth();
|
||||
const { mutateAsync: verify } = useVerify();
|
||||
|
||||
// lifecycle
|
||||
|
||||
onServerPrefetch(async () => {
|
||||
if (!!token.value) {
|
||||
// 1.1 - token is there
|
||||
|
||||
try {
|
||||
await verify({
|
||||
token: token.value,
|
||||
});
|
||||
|
||||
// 2.1 - token is valid, finish
|
||||
} catch (e) {
|
||||
// 2.2 - token is there, but not valid, try to refresh token
|
||||
|
||||
if (!!refreshToken.value) {
|
||||
// 3.1 - refresh token is there, try to refresh
|
||||
|
||||
try {
|
||||
const refreshResponse = await refreshAuth({
|
||||
refresh: refreshToken.value,
|
||||
});
|
||||
|
||||
// 4.1 - token is refreshed successfully, finish
|
||||
|
||||
updateToken(refreshResponse.access);
|
||||
updateRefreshToken(refreshResponse.refresh);
|
||||
} catch (e) {
|
||||
// 4.2 - cant refreshing token, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
} else {
|
||||
// 3.2 - refresh token is not exist, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 1.2 - token is not exist, logout
|
||||
|
||||
logout();
|
||||
}
|
||||
});
|
||||
});
|
||||
Generated
+13
@@ -30,6 +30,7 @@
|
||||
"vue-router": "latest",
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"vue-skeletor": "^1.0.6",
|
||||
"vue3-marquee": "^4.2.2",
|
||||
"vue3-persian-datetime-picker": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -12744,6 +12745,18 @@
|
||||
"integrity": "sha512-ER4vHlFSXCW3ixK2DlczUE6CZliHsn4d2TvZ9/26C6Oq8zoyEY23BsqweMPtF8QULSz1+G5m2New1BwKNVOZhQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue3-marquee": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue3-marquee/-/vue3-marquee-4.2.2.tgz",
|
||||
"integrity": "sha512-FeFvGUVInKfFilXFcnl8sDRBJBZCZSNLlQDquJErB9db6W2xICRVqbRV/jtdzsEP0rftarLQhx9MeEAU0+TPuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vue3-persian-datetime-picker": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue3-persian-datetime-picker/-/vue3-persian-datetime-picker-1.2.2.tgz",
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"vue-router": "latest",
|
||||
"vue-scrollto": "^2.20.0",
|
||||
"vue-skeletor": "^1.0.6",
|
||||
"vue3-marquee": "^4.2.2",
|
||||
"vue3-persian-datetime-picker": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -112,14 +112,7 @@ watch(
|
||||
</ul>
|
||||
<div v-else class="w-full h-max">
|
||||
<div v-if="!products?.length" class="flex flex-grow w-full">
|
||||
<div
|
||||
class="flex-col flex-grow py-[12rem] gap-6 border-2 border-slate-200 border-dashed size-full rounded-100 flex-center"
|
||||
>
|
||||
<Icon name="bi:search" size="50" class="**:fill-gray-500" />
|
||||
<span class="text-lg text-gray-500">
|
||||
محصولی یافت نشد :(
|
||||
</span>
|
||||
</div>
|
||||
<Placeholder title="محصولی یافت نشد :(" icon="bi:search" />
|
||||
</div>
|
||||
<ul v-else class="w-full grid grid-cols-3 gap-[1.5rem]">
|
||||
<li v-for="(product, index) in products" :key="index">
|
||||
|
||||
@@ -1,15 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
// imports
|
||||
|
||||
import useGetAllAddress from "~/composables/api/account/useGetAllAddress";
|
||||
|
||||
// meta
|
||||
|
||||
definePageMeta({
|
||||
middleware: "check-is-logged-in",
|
||||
layout: "profile",
|
||||
});
|
||||
|
||||
// state
|
||||
|
||||
const selectedAddress = ref<Address | null>();
|
||||
|
||||
const modalIsShow = ref(false);
|
||||
|
||||
// queries
|
||||
|
||||
const { data: addresses, isLoading: addressesIsLoading } = useGetAllAddress();
|
||||
|
||||
// methods
|
||||
|
||||
const handleSelectAddress = (address: Address) => {
|
||||
selectedAddress.value = { ...address };
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-col gap-10">
|
||||
<ProfilePageTitle title="آدرس های شما" icon="bi:map" />
|
||||
|
||||
<ProfileSection title="همه">
|
||||
<template #button>
|
||||
<AddressModal
|
||||
:address="undefined"
|
||||
v-model:is-show="modalIsShow"
|
||||
/>
|
||||
</template>
|
||||
<ul
|
||||
v-if="addressesIsLoading"
|
||||
class="w-full grid grid-cols-1 lg:grid-cols-2 gap-5"
|
||||
>
|
||||
<Skeleton
|
||||
v-for="i in 6"
|
||||
:key="i"
|
||||
class="w-full !h-[10rem] !rounded-xl"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<div v-else class="w-full h-max">
|
||||
<div v-if="!addresses?.length" class="flex flex-grow w-full">
|
||||
<Placeholder
|
||||
title="آدرسی یافت نشد :("
|
||||
icon="bi:map"
|
||||
class="!py-[4rem] !border-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul v-else class="w-full grid grid-cols-2 gap-5">
|
||||
<AddressItem
|
||||
v-for="(address, index) in addresses"
|
||||
:key="index"
|
||||
:address="address"
|
||||
@select="handleSelectAddress"
|
||||
:isSelected="address.id == selectedAddress?.id"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</ProfileSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ definePageMeta({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="w-full flex flex-col gap-10">
|
||||
<ProfilePageTitle title="خرید ها و سفارش های شما" icon="bi:cart" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// meta
|
||||
|
||||
definePageMeta({
|
||||
middleware: "check-is-logged-in",
|
||||
layout: "profile",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import TicketsTableRow from "~/components/profile/tickets/TicketsTableRow.vue";
|
||||
|
||||
// meta
|
||||
|
||||
definePageMeta({
|
||||
middleware: "check-is-logged-in",
|
||||
layout: "profile",
|
||||
});
|
||||
|
||||
// state
|
||||
|
||||
const filters = ref({
|
||||
sort: undefined,
|
||||
status: undefined,
|
||||
search: "",
|
||||
});
|
||||
|
||||
const tableHeads = ref([
|
||||
"دسته بندی",
|
||||
"موضوع",
|
||||
"تاریخ ایجاد و بروز رسانی",
|
||||
"وضعیت",
|
||||
"عملیات",
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex flex-col gap-10">
|
||||
<ProfilePageTitle title="تیکت های شما" icon="bi:ticket" />
|
||||
|
||||
<div class="w-full flex flex-col gap-8">
|
||||
<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>
|
||||
<Select
|
||||
:options="['جدید ترین', 'قدیمی ترین']"
|
||||
v-model="filters.sort!"
|
||||
triggerRootClass="!py-2.5"
|
||||
class="w-[5rem]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-start gap-3">
|
||||
<span class="text-sm">وضعیت پرداخت</span>
|
||||
<Select
|
||||
:options="[
|
||||
'پرداخت شده',
|
||||
'در حال پردازش',
|
||||
'لغو شده',
|
||||
]"
|
||||
v-model="filters.status!"
|
||||
triggerRootClass="!py-2.5"
|
||||
class="w-[5rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NuxtLink :to="{ name: 'profile-tickets-new' }">
|
||||
<Button end-icon="bi:plus" size="md" class="rounded-full">
|
||||
<span class="font-bold whitespace-pre">
|
||||
تیکت جدید
|
||||
</span>
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<template #thead>
|
||||
<th
|
||||
v-for="(tableHead, index) in tableHeads"
|
||||
:key="index"
|
||||
scope="col"
|
||||
:class="
|
||||
[0, 1, 2].includes(index)
|
||||
? 'w-3/12'
|
||||
: tableHeads.length - 1 == index
|
||||
? 'w-1/2'
|
||||
: 'w-2/12'
|
||||
"
|
||||
class="px-6 py-5 text-sm font-normal"
|
||||
>
|
||||
{{ tableHead }}
|
||||
</th>
|
||||
</template>
|
||||
<template #tbody>
|
||||
<TicketsTableRow v-for="i in 4" />
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import Vue3Marquee from "vue3-marquee";
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(Vue3Marquee, { name: "Marquee" });
|
||||
});
|
||||
Vendored
+8
-7
@@ -95,13 +95,14 @@ declare global {
|
||||
|
||||
type Address = {
|
||||
id: number | undefined;
|
||||
province: string;
|
||||
city: string;
|
||||
postal_code: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
name: string;
|
||||
for_me: "خیر" | "بله";
|
||||
province: string | undefined;
|
||||
city: string | undefined;
|
||||
postal_code: string | undefined;
|
||||
address: string | undefined;
|
||||
phone: string | undefined;
|
||||
name: string | undefined;
|
||||
for_me: "خیر" | "بله" | boolean;
|
||||
is_main: boolean;
|
||||
};
|
||||
|
||||
type DeliveryMethod = {
|
||||
|
||||
Reference in New Issue
Block a user