This commit is contained in:
Parsa Nazer
2025-02-19 00:15:23 +03:30
15 changed files with 604 additions and 43 deletions
@@ -86,6 +86,7 @@ const closeModal = () => {
for_me: "بله",
};
}
visible.value = false;
};
const addNew = () => {
@@ -19,11 +19,11 @@ withDefaults(defineProps<Props>(), {
<div class="w-full flex flex-col gap-2">
<div class="flex items-center gap-1 ps-2">
<label :for="id" class="typo-label-sm">{{ label }}</label>
<span v-if="required" class="text-danger-600">*</span>
<span v-if="!!required && required" class="text-danger-600">*</span>
</div>
<slot />
<div
v-if="error.$error"
v-if="!!error && error.$error"
class="w-full typo-label-xs flex items-center py-1 px-3 rounded-md text-danger-600"
>
{{ error.$errors[0].$message }}
+144
View File
@@ -0,0 +1,144 @@
<script setup lang="ts">
// imports
import { useToast } from "~/composables/global/useToast";
// types
type Props = {
modelValue: File[];
};
type Emits = {
"update:modelValue": [value: any];
};
// props
const props = defineProps<Props>();
const { modelValue } = toRefs(props);
// emits
const emit = defineEmits<Emits>();
// state
const { addToast } = useToast();
const dropZoneRef = ref<HTMLDivElement>();
const fileLimit = 1024 * 1024 * 2;
// methods
const onDrop = (files: File[] | null) => {
if (modelValue.value.length < 3) {
files?.forEach((file, index) => {
if (file.size > fileLimit) {
files.splice(index, 1);
addToast({
message: "حداکثر حجم فایل 2 مگابایت می باشد",
options: {
status: "error",
},
});
}
});
if (files.length > 3) {
emit("update:modelValue", [...files.slice(0, 3)]);
} else {
if (modelValue.value.length + files.length <= 3) {
files?.forEach((item) => {
emit("update:modelValue", [...modelValue.value, item]);
});
} else {
addToast({
message: `مجاز به آپلود ${
3 - modelValue.value.length
} فایل دیگر هستید`,
options: {
status: "error",
},
});
}
}
} else {
addToast({
message: "محدودیت تعداد را رعایت کنید",
options: {
status: "error",
},
});
}
};
const { isOverDropZone } = useDropZone(dropZoneRef, {
onDrop,
dataTypes: ["image/jpeg", "image/png", "image/jpg"],
});
const { open: openDialog, onChange } = useFileDialog({
accept: "image/*",
directory: false,
});
onChange((files: any) => {
let arr: File[] = [];
Object.keys(files).forEach((item) => {
arr.push(files[item]);
});
onDrop(arr);
});
const deleteFile = (index: number) => {
const clone = [...modelValue.value];
clone.splice(index, 1);
emit("update:modelValue", clone);
};
</script>
<template>
<div class="flex flex-col w-full h-max gap-5 pt-8">
<div
ref="dropZoneRef"
@click="openDialog"
class="bg-slate-50 flex-col-center w-full transition-all text-black/50 gap-3 h-[20rem] border border-dashed rounded-xl cursor-pointer select-none"
:class="isOverDropZone ? 'border-black' : ' border-slate-300'"
>
<Icon name="bi:file-earmark-arrow-down" size="32" />
<p class="font-bold text-dynamic-primary text-sm lg:text-[1rem]">
برای آپلود کلیک کنید یا فایل خود را اینجا بیاندازید
</p>
<p class="text-xs font-semibold">فرمت مجاز : jpg, png, jpeg</p>
<p class="text-xs font-semibold">تعداد فایل مجاز : 3 عدد</p>
<p class="text-xs font-semibold">حداکثر حجم فایل : 2 مگابایت</p>
</div>
<ul
v-for="(item, index) in modelValue"
:key="index"
v-auto-animate
class="flex flex-row-reverse items-center justify-between w-full px-2 animate__animated animate__fadeIn"
>
<li class="w-full flex items-center">
<div class="flex justify-end w-9/12">
<p class="text-sm text-black">{{ item.name }}</p>
</div>
<div class="w-2/12">
<p class="text-sm text-black">
{{ (item.size / 1024).toFixed(2) }}KB
</p>
</div>
<div class="w-1/12">
<Icon
name="ci:close"
class="**:stroke-red-500 cursor-pointer pb-1"
@click="deleteFile(index)"
size="28"
/>
</div>
</li>
</ul>
</div>
</template>
+4 -2
View File
@@ -6,7 +6,7 @@ type Props = {
disabled?: boolean;
modelValue: string;
error?: boolean;
options: string[];
options?: string[];
placeholder?: string;
triggerRootClass?: string;
};
@@ -71,7 +71,9 @@ const classes = computed(() => {
:side-offset="5"
>
<SelectViewport class="p-[5px]">
<SelectGroup>
<slot v-if="!!$slots.content" name="content" />
<SelectGroup v-else>
<SelectItem
v-for="(option, index) in options"
:key="index"
@@ -184,7 +184,7 @@ watch(
<div class="flex items-center justify-between w-full gap-5">
<span class="text-black">فقط کالاهای موجود</span>
<Switch v-model="has_discount" />
<Switch v-model="in_stock" />
</div>
</div>
+19 -7
View File
@@ -3,24 +3,36 @@
type Props = {
title: string;
borderLess?: boolean;
};
// props
defineProps<Props>();
withDefaults(defineProps<Props>(), {
borderLess: false,
});
</script>
<template>
<div class="flex flex-col w-full">
<div class="flex flex-col items-start">
<div
class="w-full flex items-center justify-between h-[3rem] pb-5 px-5"
>
<div class="flex flex-col w-full gap-5">
<div
class="flex flex-col items-start"
:class="{
'border-b border-slate-200 pb-5': borderLess,
}"
>
<div class="w-full flex items-center justify-between h-[3rem] px-5">
<span class="typo-sub-h-lg">{{ title }}</span>
<slot name="button" />
</div>
</div>
<div class="w-full flex flex-col border border-slate-200 rounded-xl">
<div
class="w-full flex flex-col border rounded-xl"
:class="{
'border-none': borderLess,
'border-slate-200': !borderLess,
}"
>
<div class="w-full p-5">
<slot />
</div>
+12 -3
View File
@@ -27,9 +27,18 @@ const profileLinks = ref([
icon: "bi:ticket",
title: "تیکت ها",
path: { name: "profile-tickets" },
matchPattern: /^profile-ticket/,
},
]);
const isLinkActive = (link: any) => {
if (link.matchPattern) {
return link.matchPattern.test(route.name);
} else {
return route.name === link.path.name;
}
};
// queries
const { data: account, suspense } = useGetAccount();
@@ -66,9 +75,9 @@ await suspense();
:key="index"
:to="{ ...link.path }"
:class="
route.name == link.path.name
? 'bg-black text-slate-100 **:fill-slate-100 '
: '**:fill-black '
isLinkActive(link)
? 'bg-black text-slate-100 **:fill-slate-100'
: '**:fill-black'
"
class="flex items-center justify-between transition-all rounded-lg py-4 px-3"
>
@@ -6,14 +6,14 @@
>
<td
scope="row"
class="w-3/12 px-6 py-7 font-medium whitespace-nowrap text-black"
class="w-3/12 px-6 py-6 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">
<td class="w-3/12 px-6 py-6">Silver</td>
<td class="w-3/12 px-6 py-6">Laptop</td>
<td class="w-2/12 px-6 py-6">$2999</td>
<td class="w-1/12 px-6 py-6">
<NuxtLink :to="{ name: 'profile-tickets-id', params: { id: 1 } }">
<button
class="size-10 flex-center border border-slate-200 rounded-md"
@@ -0,0 +1,53 @@
<script setup lang="ts">
// types
type Props = {
is_user: boolean;
date: string;
username: string;
message: string;
profile: string;
files: string[];
};
// props
defineProps<Props>();
</script>
<template>
<div
class="w-full flex items-center"
:class="is_user ? 'justify-start' : 'justify-end'"
>
<div
class="w-full lg:w-1/2 bg-slate-50 border border-slate-200 rounded-xl p-5 flex items-start"
:class="is_user ? 'rounded-br-none' : 'rounded-bl-none'"
>
<div class="w-2/12 flex items-start justify-start">
<img :src="profile" class="size-16 rounded-full" />
</div>
<div class="w-10/12 flex flex-col items-start pt-8">
<div class="flex flex-col gap-0.5 w-full">
<p
class="text-[10px] font-semibold text-[#1677FF] line-clamp-1 text-right"
style="direction: ltr"
>
{{ date }}
</p>
<p
class="text-xs font-semibold text-dynamic-secondary line-clamp-1"
>
{{ username }}
</p>
</div>
<div class="py-2 text-start">
{{ message }}
</div>
</div>
</div>
</div>
</template>
<style scoped></style>
+1 -1
View File
@@ -28,7 +28,7 @@ const handleSelectAddress = (address: Address) => {
</script>
<template>
<div class="w-full flex flex-col gap-10">
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="آدرس های شما" icon="bi:map" />
<ProfileSection title="همه">
+13 -13
View File
@@ -156,7 +156,7 @@ const handleSubmit = (withValidation: boolean) => {
</script>
<template>
<div class="w-full flex flex-col gap-10">
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="پروفایل" icon="bi:person-vcard" />
<div class="flex flex-col gap-6">
@@ -243,7 +243,7 @@ const handleSubmit = (withValidation: boolean) => {
<div
class="w-full grid grid-cols-1 lg:grid-cols-2 gap-x-3 gap-y-5"
>
<PersonalDataField
<DataField
id="personal-data-name"
label="نام"
:error="formValidator$.first_name"
@@ -253,8 +253,8 @@ const handleSubmit = (withValidation: boolean) => {
variant="outlined"
:error="formValidator$.first_name.$error"
/>
</PersonalDataField>
<PersonalDataField
</DataField>
<DataField
id="personal-data-last-name"
label="نام خانوادگی"
:error="formValidator$.last_name"
@@ -264,8 +264,8 @@ const handleSubmit = (withValidation: boolean) => {
variant="outlined"
:error="formValidator$.last_name.$error"
/>
</PersonalDataField>
<PersonalDataField
</DataField>
<DataField
id="personal-data-gender"
label="جنسیت"
:error="formValidator$.gender"
@@ -276,8 +276,8 @@ const handleSubmit = (withValidation: boolean) => {
variant="outlined"
:error="formValidator$.gender.$error"
/>
</PersonalDataField>
<PersonalDataField
</DataField>
<DataField
id="personal-data-birth-date"
label="تاریخ تولد"
:error="formValidator$.birth_date"
@@ -286,8 +286,8 @@ const handleSubmit = (withValidation: boolean) => {
v-model="personalData.birth_date!"
:error="formValidator$.birth_date.$error"
/>
</PersonalDataField>
<PersonalDataField
</DataField>
<DataField
id="personal-data-phone"
label="تلفن همراه"
:error="formValidator$.phone"
@@ -297,8 +297,8 @@ const handleSubmit = (withValidation: boolean) => {
variant="outlined"
:error="formValidator$.phone.$error"
/>
</PersonalDataField>
<PersonalDataField
</DataField>
<DataField
id="personal-email"
label="حساب الکترونیکی"
:error="formValidator$.email"
@@ -308,7 +308,7 @@ const handleSubmit = (withValidation: boolean) => {
variant="outlined"
:error="formValidator$.email.$error"
/>
</PersonalDataField>
</DataField>
</div>
</ProfileSection>
</div>
@@ -8,7 +8,7 @@ definePageMeta({
</script>
<template>
<div class="w-full flex flex-col gap-10">
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="خرید ها و سفارش های شما" icon="bi:cart" />
</div>
</template>
+193 -2
View File
@@ -1,7 +1,198 @@
<script setup lang="ts"></script>
<script setup lang="ts">
// meta
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
});
// state
const route = useRoute();
const ticketData = ref({
message: "",
files: [],
});
const messages = [
{
is_user: true,
date: "2023-10-15T14:30:00Z",
username: "JohnDoe",
message: "Hey, did you get a chance to review the design files I sent?",
profile: "https://example.com/profiles/johndoe.jpg",
files: ["design-draft-1.pdf", "design-draft-2.pdf"],
},
{
is_user: false,
date: "2023-10-15T14:35:00Z",
username: "JaneSmith",
message:
"Yes, I just finished reviewing them. The second draft looks great!",
profile: "https://example.com/profiles/janesmith.jpg",
files: [],
},
{
is_user: true,
date: "2023-10-15T14:40:00Z",
username: "JohnDoe",
message:
"Awesome! Let me know if you need any changes before we finalize it.",
profile: "https://example.com/profiles/johndoe.jpg",
files: ["final-design-notes.txt"],
},
{
is_user: false,
date: "2023-10-15T14:35:00Z",
username: "JaneSmith",
message:
"Yes, I just finished reviewing them. The second draft looks great!",
profile: "https://example.com/profiles/janesmith.jpg",
files: [],
},
{
is_user: true,
date: "2023-10-15T14:40:00Z",
username: "JohnDoe",
message:
"Awesome! Let me know if you need any changes before we finalize it.",
profile: "https://example.com/profiles/johndoe.jpg",
files: ["final-design-notes.txt"],
},
{
is_user: false,
date: "2023-10-15T14:35:00Z",
username: "JaneSmith",
message:
"Yes, I just finished reviewing them. The second draft looks great!",
profile: "https://example.com/profiles/janesmith.jpg",
files: [],
},
{
is_user: true,
date: "2023-10-15T14:40:00Z",
username: "JohnDoe",
message:
"Awesome! Let me know if you need any changes before we finalize it.",
profile: "https://example.com/profiles/johndoe.jpg",
files: ["final-design-notes.txt"],
},
];
// computed
const ticketId = computed(() => route.params.id);
</script>
<template>
<div></div>
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle :title="`تیکت شماره ${ticketId}`" icon="bi:ticket" />
<div
class="flex flex-wrap items-center justify-between w-full py-4 border-b lg:px-5 border-slate-200"
>
<div
class="flex flex-col items-start w-1/2 gap-4 lg:gap-5 lg:w-full lg:items-center lg:flex-row"
>
<div
class="flex flex-col w-full gap-2 lg:py-2 lg:pe-5 lg:w-1/3 lg:border-e border-slate-200"
>
<p class="text-xs lg:text-sm text-dynamic-secondary">
شماره تیکت :
</p>
<p class="text-xs font-semibold lg:text-sm text-black">
634932
</p>
</div>
<div
class="flex flex-col w-full gap-2 lg:py-2 lg:pe-5 lg:w-1/3 lg:border-e border-slate-200"
>
<p class="text-xs lg:text-sm text-dynamic-secondary">
دسته&zwnj;بندی :
</p>
<p class="text-xs font-semibold lg:text-sm text-black">
گیفت&zwnj;کارت
</p>
</div>
<div
class="flex flex-col w-full gap-2 lg:py-2 lg:pe-5 lg:w-1/3"
>
<p class="text-xs lg:text-sm text-dynamic-secondary">
وضعیت :
</p>
<p
class="text-xs font-semibold lg:text-sm text-black text-dynamic-secondary"
>
بسته شده
</p>
</div>
<div class="flex flex-col w-full gap-2 lg:hidden border-black">
<p class="text-xs lg:text-sm text-dynamic-secondary">
موضوع :
</p>
<p class="text-xs font-semibold lg:text-sm text-black">
نمی&zwnj;دانم کد ارسال گیفت کارت خریداری شده را از کجا
باید ببینم
</p>
</div>
<div class="items-center justify-end hidden w-1/4 lg:flex">
<NuxtLink :to="{ name: 'profile-tickets' }">
<Button
class="rounded-full"
size="md"
end-icon="bi:arrow-left"
>
بازگشت به تیکت ها
</Button>
</NuxtLink>
</div>
</div>
</div>
<div
class="hidden w-full text-sm px-5 pb-8 mt-3 border-b lg:block text-md border-slate-200"
>
<span class="text-black/50"> موضوع : </span> نمی&zwnj;دانم کد ارسال
گیفت کارت خریداری شده را از کجا باید ببینم
</div>
<div class="w-full flex flex-col gap-5 h-[32rem] overflow-y-auto">
<TicketBubble
v-for="(message, index) in messages"
:key="index"
:is_user="message.is_user"
:date="message.date"
:message="message.message"
:profile="message.profile"
:username="message.username"
:files="message.files"
/>
</div>
<div
class="grid grid-cols-1 lg:grid-cols-2 gap-5 mt-5 pt-5 border-t border-slate-200"
>
<DataField id="message" :required="true" label="متن پیام">
<textarea
v-model="ticketData.message"
class="w-full bg-white border-[1.5px] border-slate-200 rounded-xl text-xs lg:text-sm p-5 text-black h-[10rem] lg:h-[20rem] transition resize-none outline-none focus:!border-black"
placeholder="متن تیکت را اینجا بنویسید ..."
/>
</DataField>
<FileInput v-model="ticketData.files" />
</div>
<div class="w-full flex-center py-5">
<Button class="rounded-full px-20" end-icon="bi:send" size="md">
<!-- <Icon
v-if="createAddressIsPending"
name="svg-spinners:3-dots-bounce"
/> -->
<span>ارسال پیام</span>
</Button>
</div>
</div>
</template>
<style scoped></style>
+2 -4
View File
@@ -1,6 +1,4 @@
<script setup lang="ts">
import TicketsTableRow from "~/components/profile/tickets/TicketsTableRow.vue";
// meta
definePageMeta({
@@ -26,10 +24,10 @@ const tableHeads = ref([
</script>
<template>
<div class="w-full flex flex-col gap-10">
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="تیکت های شما" icon="bi:ticket" />
<div class="w-full flex flex-col gap-8">
<div class="w-full flex flex-col gap-5">
<div class="w-full flex items-center justify-between px-5">
<div class="flex items-center justify-start gap-8">
<div class="flex items-center justify-start gap-3">
+153 -2
View File
@@ -1,7 +1,158 @@
<script setup lang="ts"></script>
<script setup lang="ts">
// meta
definePageMeta({
middleware: "check-is-logged-in",
layout: "profile",
});
// types
type TicketCategory = {
title: string;
value: string;
};
// state
const ticketCategories: TicketCategory[] = [
{
title: "مالی و حسابداری",
value: "finance_and_accounting",
},
{
title: "پروفایل کاربری",
value: "user_profile",
},
{
title: "پیگیری سفارش",
value: "order_tracking",
},
{
title: "احراز هویت",
value: "authentication",
},
{
title: "محصول",
value: "product",
},
{
title: "اعلام باگ و خطا در وبسایت",
value: "bug_and_error_reporting",
},
{
title: "سایر",
value: "other",
},
];
const ticketData = ref({
category: undefined,
order: undefined,
message: "",
files: [],
});
</script>
<template>
<div></div>
<div class="w-full flex flex-col gap-5">
<ProfilePageTitle title="تیکت جدید" icon="bi:ticket" />
<ProfileSection title="ارتباط با پشتیبانی">
<template #button>
<NuxtLink :to="{ name: 'profile-tickets' }">
<Button
class="rounded-full"
size="md"
end-icon="bi:arrow-left"
>
بازگشت به تیکت ها
</Button>
</NuxtLink>
</template>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<DataField id="category" :required="true" label="دسته بندی">
<Select
placeholder="انتخاب کنید"
variant="outlined"
v-model="ticketData.category"
>
<template #content>
<SelectGroup>
<SelectItem
v-for="(
category, index
) in ticketCategories"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
:value="category.value"
>
<SelectItemIndicator
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
>
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
>
{{ category.title }}
</SelectItemText>
</SelectItem>
</SelectGroup>
</template>
</Select>
</DataField>
<DataField id="orders" :required="true" label="خرید یا سفارش">
<Select
placeholder="انتخاب کنید"
variant="outlined"
v-model="ticketData.order"
>
<template #content>
<SelectGroup>
<SelectItem
v-for="(
category, index
) in ticketCategories"
:key="index"
class="text-xs leading-none w-full rounded-sm py-5 flex items-center justify-between h-[25px] pr-[12px] relative select-none data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-slate-300 data-[highlighted]:text-black"
:value="category.value"
>
<SelectItemIndicator
class="absolute left-0 w-[25px] inline-flex items-center justify-center"
>
<Icon name="bi:check" size="20" />
</SelectItemIndicator>
<SelectItemText
class="text-end font-iran-yekan-x text-sm"
>
{{ category.title }}
</SelectItemText>
</SelectItem>
</SelectGroup>
</template>
</Select>
</DataField>
<DataField id="message" :required="true" label="متن تیکت">
<textarea
v-model="ticketData.message"
class="w-full bg-white border-[1.5px] border-slate-200 rounded-xl text-xs lg:text-sm p-5 text-black h-[10rem] lg:h-[20rem] transition resize-none outline-none focus:!border-black"
placeholder="متن تیکت را اینجا بنویسید ..."
/>
</DataField>
<FileInput v-model="ticketData.files" />
</div>
</ProfileSection>
<div class="w-full flex-center py-5">
<Button class="rounded-full px-20" end-icon="bi:send" size="md">
<!-- <Icon
v-if="createAddressIsPending"
name="svg-spinners:3-dots-bounce"
/> -->
<span>ارسال تیکت</span>
</Button>
</div>
</div>
</template>
<style scoped></style>