public review

This commit is contained in:
hamid zarghami
2026-07-15 12:30:13 +03:30
parent cb2a0e85c4
commit f8bddfb428
12 changed files with 301 additions and 261 deletions
@@ -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;