222 lines
7.9 KiB
TypeScript
222 lines
7.9 KiB
TypeScript
"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;
|