This commit is contained in:
Parsa Nazer
2025-02-15 00:08:20 +03:30
21 changed files with 499 additions and 289 deletions
+2 -1
View File
@@ -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>
+20 -11
View File
@@ -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>
+154 -178
View File
@@ -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>
+10 -1
View File
@@ -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
+7 -6
View File
@@ -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]">
+18
View File
@@ -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
-61
View File
@@ -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">
+57
View File
@@ -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();
}
});
});
+13
View File
@@ -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",
+1
View File
@@ -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": {
+1 -8
View File
@@ -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">
+59
View File
@@ -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>
-14
View File
@@ -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>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div></div>
</template>
<style scoped></style>
+94
View File
@@ -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>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div></div>
</template>
<style scoped></style>
+5
View File
@@ -0,0 +1,5 @@
import Vue3Marquee from "vue3-marquee";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Vue3Marquee, { name: "Marquee" });
});
+8 -7
View File
@@ -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 = {