Files
dmenu-plus-front/src/app/[name]/(Main)/[id]/components/FoodReviewsSection.tsx
T
hamid zarghami f2172e537d review list
2026-07-15 12:43:39 +03:30

245 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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";
import { isReviewApproved } from "../../about/types/Types";
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(isReviewApproved);
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;