This commit is contained in:
Parsa Nazer
2026-02-27 16:10:51 +03:30
5 changed files with 239 additions and 31 deletions
@@ -1,20 +1,30 @@
<script lang="ts" setup>
// imports
import useGetProduct from "~/composables/api/product/useGetProduct";
// states
const route = useRoute();
const id = route.params.id as string | undefined;
const { data: product } = useGetProduct(id);
</script>
<template>
<div class="flex items-center justify-between w-full bg-slate-100 border-[1.5px] border-slate-200 rounded-100 p-6">
<div class="flex items-start gap-3">
<div class="flex p-1 items-center justify-center rounded-full bg-success-500">
<Icon name="ci:check" class="size-4 **:stroke-white"/>
<Icon
name="ci:check"
class="size-4 **:stroke-white"
/>
</div>
<div class="flex flex-col gap-1">
<span class="typo-label-sm whitespace-nowrap">دریافت حضوری فروشگاه</span>
<span class="typo-p-sm whitespace-nowrap">معمولا طی ۲ ساعت اماده میشود</span>
<span class="typo-label-sm whitespace-nowrap">{{ product?.customer_pickup_title }}</span>
<span class="typo-p-sm whitespace-nowrap">{{ product?.customer_pickup_description }}</span>
</div>
</div>
<span class="typo-p-xs max-sm:hidden">
برسی موجودی در فروشگاه های دیگر
</span>
<span class="typo-p-xs max-sm:hidden">فروشگاه هیملز</span>
</div>
</template>
</template>
@@ -0,0 +1,33 @@
// imports
import { useMutation } from "@tanstack/vue-query";
import { API_ENDPOINTS } from "~/constants";
// types
export type CreateContactUsTicketRequest = {
full_name: string;
email: string;
phone: string;
type: "ORDER_FOLLOW_UP" | "SUGGESTION" | "COMPLAINT";
message: string;
};
const useCreateContactUsTicket = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleCreateContactUsTicket = async (params: CreateContactUsTicketRequest) => {
const { data } = await axios.post(API_ENDPOINTS.tickets.contact_us_ticket, params);
return data;
};
return useMutation({
mutationFn: (messageData: CreateContactUsTicketRequest) => handleCreateContactUsTicket(messageData),
});
};
export default useCreateContactUsTicket;
+1
View File
@@ -51,6 +51,7 @@ export const API_ENDPOINTS = {
delete_attachment: "/tickets/attachment/delete",
get_one: "/tickets",
create_message: "/tickets/message/create",
contact_us_ticket: "/tickets/contact-us/create",
},
orders: {
get_all: "/order/all",
+186 -24
View File
@@ -1,19 +1,54 @@
<script setup lang="ts">
// imports
import useVuelidate from "@vuelidate/core";
import { email, helpers, minLength, required } from "@vuelidate/validators";
import useCreateContactUsTicket, {
type CreateContactUsTicketRequest,
} from "~/composables/api/tickets/useCreateContactUsTicket";
import { useToast } from "~/composables/global/useToast";
// types
type ContactInfoForm = Omit<CreateContactUsTicketRequest, "type"> & {
type: string | undefined;
};
type RequestTypeOption = {
title: string;
value: CreateContactUsTicketRequest["type"];
};
// state
useSeoMeta({
title: "ارتباط با ما",
});
const contactInfo = ref({
name: "",
const { addToast } = useToast();
const contactInfo = ref<ContactInfoForm>({
full_name: "",
email: "",
phone: "",
requestType: undefined,
type: undefined,
message: "",
});
const requestTypes = ref(["انتقادات", "پیشنهادات", "پیگیری سفارش"]);
const requestTypes = ref<RequestTypeOption[]>([
{
title: "انتقادات",
value: "COMPLAINT",
},
{
title: "پیشنهادات",
value: "SUGGESTION",
},
{
title: "پیگیری سفارش",
value: "ORDER_FOLLOW_UP",
},
]);
const contactWays = ref<{ title: string; ways: { type: "text" | "link"; title: string; path?: string }[] }[]>([
{
@@ -56,6 +91,97 @@ const contactWays = ref<{ title: string; ways: { type: "text" | "link"; title: s
],
},
]);
const { mutateAsync: createContactUsTicket, isPending: createTicketIsPending } = useCreateContactUsTicket();
// computed
const requestTypeOptions = computed(() => requestTypes.value.map((item) => item.title));
const formRules = computed(() => {
return {
full_name: {
required: helpers.withMessage("فیلد نام و نام خانوادگی الزامی می باشد", required),
minLength: helpers.withMessage("فیلد نام و نام خانوادگی حداقل ۳ کرکتر می باشد", minLength(3)),
},
email: {
required: helpers.withMessage("فیلد پست الکترونیکی الزامی می باشد", required),
email: helpers.withMessage("پست الکترونیکی وارد شده معتبر نمی باشد", email),
},
phone: {
required: helpers.withMessage("فیلد شماره تلفن الزامی می باشد", required),
},
type: {
required: helpers.withMessage("فیلد نوع درخواست الزامی می باشد", required),
},
message: {
required: helpers.withMessage("فیلد پیغام شما الزامی می باشد", required),
minLength: helpers.withMessage("فیلد پیغام شما حداقل ۵ کرکتر می باشد", minLength(5)),
},
};
});
const formValidator$ = useVuelidate(formRules, contactInfo);
// methods
const resetForm = () => {
contactInfo.value = {
full_name: "",
email: "",
phone: "",
type: undefined,
message: "",
};
formValidator$.value.$reset();
};
const handleSubmit = async () => {
await formValidator$.value.$validate();
if (!formValidator$.value.$errors.length && contactInfo.value.type) {
const selectedType = requestTypes.value.find((item) => item.title === contactInfo.value.type)?.value;
if (!selectedType) {
addToast({
message: "نوع درخواست معتبر نیست",
options: {
status: "error",
description: "لطفا نوع درخواست را مجدد انتخاب کنید",
},
});
return;
}
try {
await createContactUsTicket({
full_name: contactInfo.value.full_name,
email: contactInfo.value.email,
phone: contactInfo.value.phone,
type: selectedType,
message: contactInfo.value.message,
});
addToast({
message: "پیغام شما با موفقیت ارسال شد",
options: {
status: "success",
description: "پس از بررسی با شما تماس می گیریم",
},
});
resetForm();
} catch (error) {
addToast({
message: "خطایی در ارسال پیغام رخ داد",
options: {
status: "error",
description: "لطفا مجدد تلاش کنید",
},
});
}
}
};
</script>
<template>
@@ -71,50 +197,86 @@ const contactWays = ref<{ title: string; ways: { type: "text" | "link"; title: s
<div class="w-full flex flex-col-reverse max-lg:-mt-14 lg:flex-row items-start justify-between">
<div class="w-full lg:w-8/12 flex flex-col items-start gap-10">
<div class="grid grid-cols-1 md:grid-cols-2 gap-[.6rem] w-full">
<div class="flex flex-col items-start w-full">
<DataField
label="نام و نام خانوادگی"
:required="true"
:error="formValidator$.full_name"
>
<Input
class="w-full"
v-model="contactInfo.full_name"
placeholder="نام و نام خانوادگی"
:error="formValidator$.full_name.$error"
/>
</div>
<div class="flex flex-col items-start w-full">
</DataField>
<DataField
label="پست الکترونیکی"
:required="true"
:error="formValidator$.email"
>
<Input
class="w-full"
v-model="contactInfo.email"
placeholder="پست الکترونیکی"
:error="formValidator$.email.$error"
/>
</div>
<div class="flex flex-col items-start w-full">
</DataField>
<DataField
label="شماره تلفن"
:required="true"
:error="formValidator$.phone"
>
<Input
class="w-full"
v-model="contactInfo.phone"
placeholder="شماره تلفن"
dir="ltr"
:error="formValidator$.phone.$error"
/>
</div>
<div class="flex flex-col items-start w-full relative">
</DataField>
<DataField
label="نوع درخواست"
:required="true"
:error="formValidator$.type"
>
<Select
v-model="contactInfo.requestType"
:options="requestTypes"
v-model="contactInfo.type"
placeholder="نوع درخواست"
:options="requestTypeOptions"
class="shrink-0 max-lg:w-[5rem] lg:w-[6.5rem] py-0.5"
triggerRootClass="!rounded-xl whitespace-nowrap max-sm:w-full shrink-0 "
/>
</div>
<div class="flex flex-col items-start col-span-1 md:col-span-2 h-[10rem] max-h-[12rem]">
</DataField>
<DataField
label="پیغام شما"
:required="true"
:error="formValidator$.message"
class="col-span-1 md:col-span-2"
>
<textarea
v-model="contactInfo.message"
placeholder="پیغام شما"
class="w-full flex items-center resize-none bg-slate-50 border-slate-200 hover:border-black focus:border-black h-[10rem] max-h-[12rem] text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 outline-none flex-1 !text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
:class="[
'w-full flex items-center resize-none bg-slate-50 h-[10rem] max-h-[12rem] text-black justify-between cursor-text transition-all border-[1.5px] gap-3 typo-label-md px-4 py-3.5 selection:bg-slate-100 rounded-100 outline-none flex-1 !text-sm placeholder-slate-400 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal',
formValidator$.message.$error
? 'border-danger-600'
: 'border-slate-200 hover:border-black focus:border-black',
]"
></textarea>
</div>
</DataField>
</div>
<div class="w-full flex-center pb-10 border-b border-slate-200">
<!-- @click="handleSubmit"
:loading="createTicketIsPending || uploadAttachmentIsPending" -->
<Button
class="rounded-full w-[14rem] h-11"
size="md"
:loading="createTicketIsPending"
@click="handleSubmit"
>
<!-- <Icon
v-if="createTicketIsPending"
:name="createTicketIsPending ? 'svg-spinners:3-dots-bounce' : 'bi:send'"
/> -->
<span>ارسال پیغام</span>
</Button>
</div>
@@ -123,7 +285,7 @@ const contactWays = ref<{ title: string; ways: { type: "text" | "link"; title: s
<div
v-for="(way, index) in contactWays"
:key="index"
class="flex flex-col gap-3"
class="flex flex-col gap-3"
>
<span class="text-slate-500 max-lg:text-sm">
{{ way.title }}
+2
View File
@@ -112,6 +112,8 @@ declare global {
best_deal_price_before_discount: string;
best_deal_price_after_discount: string;
best_deal_discount: number;
customer_pickup_title: null | string;
customer_pickup_description: null | string;
};
type ProductListItem = Pick<