public review
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"use client";
|
||||
|
||||
import ToggleButton, { ToggleButtonClassNames } from "@/components/button/ToggleButton";
|
||||
import TabContainer, { TabContainerClassNames, TabContainerRenderType } from "@/components/tab/TabContainer";
|
||||
import { TabHeader } from "@/components/tab/TabHeader";
|
||||
import { toast, toastLoginRequired } from "@/components/Toast";
|
||||
import Comment from "@/components/utils/Comment";
|
||||
import RateSelectionBar from "@/components/utils/RateSelectionBar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getToken } from "@/lib/api/func";
|
||||
import { extractErrorMessage } from "@/lib/func";
|
||||
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
|
||||
import clsx from "clsx";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetProfile } from "../../../(Profile)/profile/hooks/userProfileData";
|
||||
import { NegativePointEnum, PositivePointEnum } from "../enum/ReviewPoints";
|
||||
import { useCreateReview, useGetFoodReviews } from "../hooks/useFoodData";
|
||||
import { CreateReviewType } from "../types/ReviewTypes";
|
||||
|
||||
type Props = {
|
||||
foodId: string;
|
||||
};
|
||||
|
||||
const positivePointsArray = [
|
||||
PositivePointEnum.GREAT_TASTE,
|
||||
PositivePointEnum.FAST_DELIVERY,
|
||||
PositivePointEnum.GOOD_QUALITY,
|
||||
PositivePointEnum.GOOD_PORTION_SIZE,
|
||||
PositivePointEnum.FRIENDLY_SERVICE,
|
||||
];
|
||||
|
||||
const negativePointsArray = [
|
||||
NegativePointEnum.SMALL_PORTION,
|
||||
NegativePointEnum.SLOW_DELIVERY,
|
||||
NegativePointEnum.POOR_QUALITY,
|
||||
NegativePointEnum.OVERPRICED,
|
||||
NegativePointEnum.UNFRIENDLY_SERVICE,
|
||||
];
|
||||
|
||||
function FoodReviewsSection({ foodId }: Props) {
|
||||
const { t } = useTranslation("rating");
|
||||
const { name } = useParams();
|
||||
const router = useRouter();
|
||||
const { isSuccess } = useGetProfile();
|
||||
const { data: reviewsData, isLoading: reviewsLoading } = useGetFoodReviews(foodId);
|
||||
const { mutate: createReview, isPending } = useCreateReview(foodId);
|
||||
|
||||
const [rating, setRating] = useState<number>(5);
|
||||
const [comment, setComment] = useState<string>("");
|
||||
const [positivePoints, setPositivePoints] = useState<string[]>([]);
|
||||
const [negativePoints, setNegativePoints] = useState<string[]>([]);
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
|
||||
const reviews = (reviewsData?.data || []).filter((review) => review.isApproved);
|
||||
const sortedReviews = [...reviews].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
const getTranslationKey = (point: string, isPositive: boolean): string => {
|
||||
const basePath = isPositive ? "Tabs.Strengths.Options" : "Tabs.Weeknesses.Options";
|
||||
return `${basePath}.${point}`;
|
||||
};
|
||||
|
||||
const handleToggle = (pointValue: string, isPositive: boolean) => (checked: boolean) => {
|
||||
const setter = isPositive ? setPositivePoints : setNegativePoints;
|
||||
|
||||
if (checked) {
|
||||
setter((prev) => [...prev, pointValue]);
|
||||
} else {
|
||||
setter((prev) => prev.filter((p) => p !== pointValue));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRatingChange = (value: number[]) => {
|
||||
setRating(value[0]);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setRating(5);
|
||||
setComment("");
|
||||
setPositivePoints([]);
|
||||
setNegativePoints([]);
|
||||
setFormKey((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const token = await getToken();
|
||||
if (!token || !isSuccess) {
|
||||
toastLoginRequired(() => router.push(`/${name}/auth`), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (rating === 0) {
|
||||
toast(t("Errors.RatingRequired") || "لطفا امتیاز خود را انتخاب کنید", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const reviewData: CreateReviewType = {
|
||||
foodId,
|
||||
rating,
|
||||
comment,
|
||||
positivePoints,
|
||||
negativePoints,
|
||||
};
|
||||
|
||||
createReview(reviewData, {
|
||||
onSuccess: () => {
|
||||
toast(t("Success") || "نظر شما با موفقیت ثبت شد", "success");
|
||||
resetForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(extractErrorMessage(error, t("Errors.SubmitFailed") || "خطا در ثبت نظر"), "error");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const negativePointsView = (
|
||||
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
||||
{negativePointsArray.map((point) => (
|
||||
<ToggleButton
|
||||
className={{
|
||||
...ToggleButtonClassNames,
|
||||
wrapperActive: "border-red-400",
|
||||
contentActive: "text-red-400",
|
||||
}}
|
||||
key={point}
|
||||
name={`weekness_${point}`}
|
||||
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
|
||||
onToggle={handleToggle(point, false)}
|
||||
>
|
||||
{t(getTranslationKey(point, false))}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const positivePointsView = (
|
||||
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
||||
{positivePointsArray.map((point) => (
|
||||
<ToggleButton
|
||||
key={point}
|
||||
name={`strength_${point}`}
|
||||
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
|
||||
onToggle={handleToggle(point, true)}
|
||||
>
|
||||
{t(getTranslationKey(point, true))}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-base font-bold">ثبت نظر</h2>
|
||||
|
||||
<form key={formKey} onSubmit={handleSubmit} className="mt-4">
|
||||
<div className={glassSurfaceFlat("rounded-2xl p-4")}>
|
||||
<RateSelectionBar name="rating" defaultValue={[rating]} onValueChange={handleRatingChange} />
|
||||
|
||||
<div className="mt-8">
|
||||
<TabContainer
|
||||
defaultIndex={1}
|
||||
changeType={TabContainerRenderType.VISIBILITY}
|
||||
className={{
|
||||
...TabContainerClassNames,
|
||||
wrapper: "p-2!",
|
||||
scrollView: clsx(
|
||||
"border-none! rounded-xl! gap-0! h-full bg-[#EAECF0]/80! dark:bg-neutral-900/80! not-dark:gradient-border! grid! grid-cols-2 px-2! py-2! pb-2! w-full overflow-y-hidden justify-center items-center",
|
||||
),
|
||||
title: "text-sm2! font-normal mt-1",
|
||||
header: "rounded-lg h-7! w-full",
|
||||
headerActive: "bg-white dark:bg-neutral-800",
|
||||
titleActive: "text-primary",
|
||||
}}
|
||||
>
|
||||
<TabHeader title={t("Tabs.Weeknesses.Title")} viewRenderer={negativePointsView} />
|
||||
<TabHeader title={t("Tabs.Strengths.Title")} viewRenderer={positivePointsView} />
|
||||
</TabContainer>
|
||||
|
||||
<div className="w-full mt-6">
|
||||
<label htmlFor="foodReviewComment" className="text-sm font-medium">
|
||||
{t("InputComment.Label")}
|
||||
</label>
|
||||
<textarea
|
||||
name="foodReviewComment"
|
||||
id="foodReviewComment"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder={t("InputComment.Placeholder")}
|
||||
className={glassSurfaceFlat(
|
||||
"mt-2 w-full h-21 px-4 py-2.5 text-xs text-foreground rounded-xl border-0 focus:outline-none focus:ring-2 focus:ring-accent",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={isPending} className="mt-4 w-full rounded-xl font-normal disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{isPending ? t("ButtonSubmitting") || "در حال ارسال..." : t("ButtonSubmit")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-border/60">
|
||||
<h2 className="text-sm2 font-medium leading-5">نظرات کاربران</h2>
|
||||
<div>
|
||||
{reviewsLoading ? (
|
||||
<p className="text-sm2 text-center py-8 text-disabled-text">در حال بارگذاری نظرات...</p>
|
||||
) : sortedReviews.length > 0 ? (
|
||||
sortedReviews.map((review) => (
|
||||
<article key={review.id}>
|
||||
<Comment
|
||||
className="pt-6"
|
||||
user={`${review.user?.firstName || ""} ${review.user?.lastName || ""}`.trim() || "کاربر"}
|
||||
rating={review.rating}
|
||||
date={formatDate(review.createdAt)}
|
||||
text={review.comment}
|
||||
tags={[]}
|
||||
positivePoints={review.positivePoints}
|
||||
negativePoints={review.negativePoints}
|
||||
/>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm2 text-center py-8 text-disabled-text">هنوز نظری ثبت نشده است</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default FoodReviewsSection;
|
||||
Reference in New Issue
Block a user