Files
dmenu-plus-front/src/app/[name]/(Main)/[id]/components/FoodReviewsSection.tsx
T
morteza 234729329c
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
review
2026-07-17 10:54:14 +03:30

222 lines
7.9 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 RateSelectionBar from "@/components/utils/RateSelectionBar";
import ReviewChatThread from "./ReviewChatThread";
import { buildReviewThreads } from "../utils/reviewHelpers";
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 reviewThreads = buildReviewThreads(reviewsData?.data || []);
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 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>
) : reviewThreads.length > 0 ? (
reviewThreads.map((thread) => <ReviewChatThread key={thread.root.id} thread={thread} />)
) : (
<p className="text-sm2 text-center py-8 text-disabled-text">هنوز نظری ثبت نشده است</p>
)}
</div>
</div>
</section>
);
}
export default FoodReviewsSection;