Merge branch 'main' of https://github.com/Byeto-Company/hossein_por_shop
This commit is contained in:
@@ -96,11 +96,7 @@
|
|||||||
<a href="tel:02193111026"> 93111026-021 </a>
|
<a href="tel:02193111026"> 93111026-021 </a>
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span>
|
<span> ارتباط با پشتیبانی: داخلی ۱ </span>
|
||||||
برای پشتیبانی : داخلی ۱
|
|
||||||
<br />
|
|
||||||
برای مدیریت : داخلی ۴
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
<li>ایمیل: npsayna@gmail.com</li>
|
<li>ایمیل: npsayna@gmail.com</li>
|
||||||
<li><NuxtLink to="contact-us">تیکت</NuxtLink></li>
|
<li><NuxtLink to="contact-us">تیکت</NuxtLink></li>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defineProps<Props>();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="typo-p-sm">
|
<span class="typo-p-sm font-normal translate-y-px">
|
||||||
{{ rate }}
|
{{ rate }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type Props = {
|
|||||||
date: string,
|
date: string,
|
||||||
username: string,
|
username: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
rate : number
|
||||||
}
|
}
|
||||||
|
|
||||||
// props
|
// props
|
||||||
@@ -25,7 +26,7 @@ const formattedDate = useDateFormat(date.value, "YYYY/MM/DD");
|
|||||||
<div class="flex justify-between items-start w-full max-sm:flex-col gap-4">
|
<div class="flex justify-between items-start w-full max-sm:flex-col gap-4">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<span class="text-lg font-semibold sm:typo-h-6 text-black">
|
<span class="text-lg font-semibold sm:typo-h-6 text-black">
|
||||||
خیلی محصول خوبی بودددد
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
<span class="typo-p-xs sm:typo-p-sm text-slate-500">
|
<span class="typo-p-xs sm:typo-p-sm text-slate-500">
|
||||||
{{ username }}
|
{{ username }}
|
||||||
@@ -33,7 +34,7 @@ const formattedDate = useDateFormat(date.value, "YYYY/MM/DD");
|
|||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Rating :rate="2"/>
|
<Rating :rate="rate"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="typo-p-md">
|
<div class="typo-p-md">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
|||||||
@@ -3,8 +3,17 @@
|
|||||||
|
|
||||||
import useGetComments from "~/composables/api/product/useGetComments";
|
import useGetComments from "~/composables/api/product/useGetComments";
|
||||||
import useCreateComment from "~/composables/api/product/useCreateComment";
|
import useCreateComment from "~/composables/api/product/useCreateComment";
|
||||||
|
import useRateProduct from "~/composables/api/product/useRateProduct";
|
||||||
import { useAuth } from "~/composables/api/auth/useAuth";
|
import { useAuth } from "~/composables/api/auth/useAuth";
|
||||||
|
|
||||||
|
// props
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
product: Product;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -13,25 +22,66 @@ const id = route.params.id as string | undefined;
|
|||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
|
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
|
const userTitle = ref("");
|
||||||
const userComment = ref("");
|
const userComment = ref("");
|
||||||
|
const selectedRating = ref(5);
|
||||||
|
|
||||||
const showMoreComments = ref(false);
|
const showMoreComments = ref(false);
|
||||||
|
|
||||||
const { data: comments, refetch: refetchComments } = useGetComments(id, page);
|
const { data: comments, refetch: refetchComments } = useGetComments(id, page);
|
||||||
const { mutateAsync: createComment, isPending: isCreateCommentPending } = useCreateComment(id);
|
const { mutateAsync: createComment, isPending: isCreateCommentPending } = useCreateComment(id);
|
||||||
|
const { mutateAsync: rateProduct, isPending: isRateProductPending } = useRateProduct(props.product.slug);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.product.user_rating,
|
||||||
|
(newUserRating) => {
|
||||||
|
if (token.value && newUserRating !== null) {
|
||||||
|
selectedRating.value = newUserRating;
|
||||||
|
} else {
|
||||||
|
selectedRating.value = 5;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const canSubmitComment = computed(() => {
|
||||||
|
return userTitle.value.trim().length > 0 && userComment.value.trim().length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasUserRated = computed(() => {
|
||||||
|
return token.value && props.product.user_rating !== null;
|
||||||
|
});
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
|
|
||||||
const submitComment = async () => {
|
const submitComment = async () => {
|
||||||
if (userComment.value.length > 3) {
|
if (!canSubmitComment.value) {
|
||||||
await createComment({
|
return;
|
||||||
content: userComment.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
userComment.value = "";
|
|
||||||
|
|
||||||
await refetchComments();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = [
|
||||||
|
createComment({
|
||||||
|
title: userTitle.value,
|
||||||
|
content: userComment.value,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only submit rating if user hasn't rated before
|
||||||
|
if (!hasUserRated.value) {
|
||||||
|
promises.push(
|
||||||
|
rateProduct({
|
||||||
|
rating: selectedRating.value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
userTitle.value = "";
|
||||||
|
userComment.value = "";
|
||||||
|
selectedRating.value = 5;
|
||||||
|
|
||||||
|
await refetchComments();
|
||||||
};
|
};
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
@@ -52,20 +102,69 @@ const limitedComments = computed(() => {
|
|||||||
>
|
>
|
||||||
<div class="flex relative gap-8 my-42 container max-lg:flex-col">
|
<div class="flex relative gap-8 my-42 container max-lg:flex-col">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 flex flex-col gap-6 lg:min-w-[400px] h-fit bg-white p-8 rounded-xl border-[0.5px] border-slate-200"
|
class="sticky top-0 flex flex-col gap-6 lg:min-w-100 h-fit bg-white p-8 rounded-xl border-[0.5px] border-slate-200"
|
||||||
>
|
>
|
||||||
<h3 class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">نظرات کاربران</h3>
|
<h3 class="typo-h-6 max-sm:text-xl md:typo-h-5 lg:typo-h-4">نظرات کاربران</h3>
|
||||||
<div class="flex flex-col gap-2">
|
<!-- <div class="flex flex-col gap-2">
|
||||||
<Rating :rate="2" />
|
<Rating :rate="props.product.rating" />
|
||||||
<span class="typo-p-sm"> بر اساس {{ comments?.count }} نظر </span>
|
<span class="typo-p-sm"> بر اساس {{ comments?.count }} نظر </span>
|
||||||
</div>
|
</div> -->
|
||||||
<form
|
<form
|
||||||
@submit.prevent="submitComment"
|
@submit.prevent="submitComment"
|
||||||
class="flex flex-col gap-6"
|
class="flex flex-col gap-6"
|
||||||
>
|
>
|
||||||
|
<div v-if="hasUserRated" class="flex flex-col gap-3 px-4 py-3 bg-white rounded-lg border border-slate-300">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="typo-p-xs text-slate-700 font-semibold">امتیاز قبلی شما</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
v-for="star in 5"
|
||||||
|
:key="`prev-${star}`"
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
class="size-9 rounded-full flex-center cursor-not-allowed opacity-75"
|
||||||
|
:class="star <= (props.product.user_rating || 0) ? 'bg-amber-50' : 'bg-slate-50'"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="ci:star-solid"
|
||||||
|
class="size-5"
|
||||||
|
:class="star <= (props.product.user_rating || 0) ? '**:fill-yellow-500' : '**:fill-slate-300'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span class="typo-p-sm font-semibold text-slate-600 mr-2">{{ props.product.user_rating }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<span class="typo-p-sm text-slate-500">امتیاز شما</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
v-for="star in 5"
|
||||||
|
:key="star"
|
||||||
|
type="button"
|
||||||
|
class="size-9 rounded-full flex-center transition-colors"
|
||||||
|
:class="star <= selectedRating ? 'bg-amber-50' : 'bg-slate-50'"
|
||||||
|
:disabled="!token || isCreateCommentPending || isRateProductPending"
|
||||||
|
@click="selectedRating = star"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="ci:star-solid"
|
||||||
|
class="size-5"
|
||||||
|
:class="star <= selectedRating ? '**:fill-yellow-500' : '**:fill-slate-300'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span class="typo-p-sm font-semibold text-slate-500 mr-2">{{ selectedRating }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
v-model="userTitle"
|
||||||
|
:disabled="!token"
|
||||||
|
placeholder="عنوان نظر را بنویسید..."
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
:disabled="!token"
|
:disabled="!token"
|
||||||
class="w-full min-h-[125px] resize-none sm:min-h-[200px] field-sizing-content rounded-xl bg-white p-4 border border-slate-200 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
|
class="w-full min-h-31.25 resize-none sm:min-h-50 field-sizing-content rounded-xl bg-white p-4 border border-slate-300 placeholder:text-xs lg:placeholder:text-sm placeholder:font-normal"
|
||||||
v-model="userComment"
|
v-model="userComment"
|
||||||
placeholder="نظر خود را بنویسید..."
|
placeholder="نظر خود را بنویسید..."
|
||||||
/>
|
/>
|
||||||
@@ -73,10 +172,10 @@ const limitedComments = computed(() => {
|
|||||||
v-if="token"
|
v-if="token"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="rounded-full w-full"
|
class="rounded-full w-full"
|
||||||
:loading="isCreateCommentPending"
|
:loading="isCreateCommentPending || isRateProductPending"
|
||||||
:disabled="isCreateCommentPending"
|
:disabled="isCreateCommentPending || isRateProductPending || !canSubmitComment"
|
||||||
>
|
>
|
||||||
نظر بنویسید
|
ثبت نظر
|
||||||
</Button>
|
</Button>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-else
|
v-else
|
||||||
@@ -95,10 +194,11 @@ const limitedComments = computed(() => {
|
|||||||
<Comment
|
<Comment
|
||||||
v-for="comment in limitedComments"
|
v-for="comment in limitedComments"
|
||||||
:key="comment.id"
|
:key="comment.id"
|
||||||
title=""
|
:title="comment.title"
|
||||||
:content="comment.content"
|
:content="comment.content"
|
||||||
:date="comment.timestamp"
|
:date="comment.timestamp"
|
||||||
:username="'منصور مرزبان'"
|
:username="comment.user"
|
||||||
|
:rate="product.user_rating ?? 0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -109,6 +209,7 @@ const limitedComments = computed(() => {
|
|||||||
v-if="showMoreComments"
|
v-if="showMoreComments"
|
||||||
:total="comments.count"
|
:total="comments.count"
|
||||||
:items="comments.results.map((item, i) => ({ type: 'page', value: i }))"
|
:items="comments.results.map((item, i) => ({ type: 'page', value: i }))"
|
||||||
|
:per-page="3"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-else
|
v-else
|
||||||
@@ -123,15 +224,15 @@ const limitedComments = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="h-[400px] lg:flex-grow w-full border-[0.5px] flex-col-center border-slate-200 bg-white rounded-xl"
|
class="h-100 lg:grow w-full border-[0.5px] flex-col-center border-slate-200 bg-white rounded-xl"
|
||||||
>
|
>
|
||||||
<NuxtImg
|
<NuxtImg
|
||||||
src="/img/heymlz/heymlz-contact-us.gif"
|
src="/img/heymlz/heymlz-contact-us.gif"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
fetch-priority="low"
|
fetch-priority="low"
|
||||||
class="w-[200px] lg:w-[300px] translate-y-[-25px]"
|
class="w-50 lg:w-75 -translate-y-6.25"
|
||||||
/>
|
/>
|
||||||
<span class="text-xl text-black font-semibold translate-y-[-25px]"> هیچ نظری ثبت نشده است </span>
|
<span class="text-xl text-black font-semibold -translate-y-6.25"> هیچ نظری ثبت نشده است </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { selectedVariant } = inject("productVariant") as ProductVariantProvideTyp
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="w-full container py-20 flex flex-col gap-y-[1.5rem]">
|
<section class="w-full container pt-20 flex flex-col gap-y-[1.5rem]">
|
||||||
<div class="w-full flex">
|
<div class="w-full flex">
|
||||||
<span class="text-black max-lg:hidden typo-h-4 mb-4"> جزئیات محصول </span>
|
<span class="text-black max-lg:hidden typo-h-4 mb-4"> جزئیات محصول </span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ watch(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -72,7 +72,7 @@ watch(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ watch(
|
|||||||
<span class="max-sm:hidden"> تخفیف درصد </span>
|
<span class="max-sm:hidden"> تخفیف درصد </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Rating :rate="3" />
|
<Rating :rate="product!.rating" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Slider
|
<Slider
|
||||||
@@ -238,7 +238,7 @@ watch(
|
|||||||
<div class="flex items-center gap-6 flex-wrap">
|
<div class="flex items-center gap-6 flex-wrap">
|
||||||
<ProductVariant
|
<ProductVariant
|
||||||
@click="selectedVariantId = variant.id"
|
@click="selectedVariantId = variant.id"
|
||||||
v-for="variant in product!.variants.filter(p => p.color === selectedColor)"
|
v-for="variant in product!.variants.filter((p) => p.color === selectedColor)"
|
||||||
:key="variant.id"
|
:key="variant.id"
|
||||||
:variantDetail="variant"
|
:variantDetail="variant"
|
||||||
:isSelected="selectedVariantId === variant.id"
|
:isSelected="selectedVariantId === variant.id"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { API_ENDPOINTS } from "~/constants";
|
|||||||
// types
|
// types
|
||||||
|
|
||||||
export type CreateCommentRequest = {
|
export type CreateCommentRequest = {
|
||||||
|
title : string,
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// imports
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/vue-query";
|
||||||
|
import { API_ENDPOINTS } from "~/constants";
|
||||||
|
|
||||||
|
// types
|
||||||
|
|
||||||
|
export type RateProductRequest = {
|
||||||
|
rating: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRateProduct = (slug: string | undefined) => {
|
||||||
|
// state
|
||||||
|
|
||||||
|
const { $axios: axios } = useNuxtApp();
|
||||||
|
|
||||||
|
// methods
|
||||||
|
|
||||||
|
const handleRateProduct = async (variables: RateProductRequest) => {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
`${API_ENDPOINTS.product.rate}/${slug}/rating/`,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: RateProductRequest) => handleRateProduct(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useRateProduct;
|
||||||
@@ -23,6 +23,7 @@ export const API_ENDPOINTS = {
|
|||||||
comments: "/products/comments",
|
comments: "/products/comments",
|
||||||
create_comment: "/products/comments",
|
create_comment: "/products/comments",
|
||||||
get: "/products",
|
get: "/products",
|
||||||
|
rate: "/products",
|
||||||
save: "/accounts/favorites/toggle",
|
save: "/accounts/favorites/toggle",
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ const router = useRouter();
|
|||||||
const paymentGateways = ref<PaymentGateway[]>([
|
const paymentGateways = ref<PaymentGateway[]>([
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
picture: "/img/gateways/zibal.png",
|
picture: "/img/gateways/zarinpal.png",
|
||||||
title: "زیبال",
|
title: "زرین پال",
|
||||||
type: "ZIBAL",
|
type: "ZARINPAL",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -70,13 +70,12 @@ const contactWays = ref<{ title: string; ways: { type: "text" | "link"; title: s
|
|||||||
ways: [
|
ways: [
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "link",
|
||||||
title: "09026663488",
|
title: "021-93111026",
|
||||||
path: "tell:09026663488",
|
path: "tell:02193111026",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "link",
|
type: "text",
|
||||||
title: "09022202311",
|
title: "ارتباط با پشتیبانی: داخلی ۱",
|
||||||
path: "tell:09022202311",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,12 +56,15 @@ if (productResponse.isError) {
|
|||||||
<ProductHero />
|
<ProductHero />
|
||||||
<ProductVideo v-model:showChatButton="showChatButton" />
|
<ProductVideo v-model:showChatButton="showChatButton" />
|
||||||
<ProductDetails />
|
<ProductDetails />
|
||||||
<ProductsSlider
|
<div class="py-20">
|
||||||
title="محصولات مشابه"
|
<ProductsSlider
|
||||||
:products="product!.related_products"
|
title="محصولات مشابه"
|
||||||
iconImage="/img/simulare-products-section.gif"
|
:products="product!.related_products"
|
||||||
/>
|
iconImage="/img/simulare-products-section.gif"
|
||||||
<ProductComments />
|
/>
|
||||||
|
</div>
|
||||||
|
<ProductComments :product="product!" />
|
||||||
|
|
||||||
<ChatButton :showChatButton="showChatButton" />
|
<ChatButton :showChatButton="showChatButton" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Vendored
+3
-1
@@ -101,6 +101,7 @@ declare global {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
|
user_rating : number | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
meta_description: string | null;
|
meta_description: string | null;
|
||||||
meta_keywords: string | null;
|
meta_keywords: string | null;
|
||||||
@@ -155,7 +156,8 @@ declare global {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
product: number;
|
product: number;
|
||||||
user: number;
|
user: string;
|
||||||
|
title : string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Category = {
|
type Category = {
|
||||||
|
|||||||
Reference in New Issue
Block a user