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;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import * as api from "../service/Service";
|
import * as api from "../service/Service";
|
||||||
|
import { CreateReviewType } from "../types/ReviewTypes";
|
||||||
|
|
||||||
export const useGetFood = (id: string) => {
|
export const useGetFood = (id: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -13,3 +14,22 @@ export const useToggleFavorite = () => {
|
|||||||
mutationFn: api.toggleFavorite,
|
mutationFn: api.toggleFavorite,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetFoodReviews = (foodId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["food-reviews", foodId],
|
||||||
|
queryFn: () => api.getFoodReviews(foodId),
|
||||||
|
enabled: !!foodId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateReview = (foodId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: CreateReviewType) => api.createReview(params),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["food-reviews", foodId] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData";
|
import { useGetProfile } from "../../(Profile)/profile/hooks/userProfileData";
|
||||||
import { useGetAbout } from "../about/hooks/useAboutData";
|
import { useGetAbout } from "../about/hooks/useAboutData";
|
||||||
import { useGetFood, useToggleFavorite } from "./hooks/useFoodData";
|
import { useGetFood, useToggleFavorite } from "./hooks/useFoodData";
|
||||||
|
import FoodReviewsSection from "./components/FoodReviewsSection";
|
||||||
|
|
||||||
type Props = object;
|
type Props = object;
|
||||||
|
|
||||||
@@ -158,12 +159,13 @@ function FoodPage({}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="not-lg:max-w-lg not-lg:place-self-center not-lg:w-full not-lg:mb-10 lg:pt-6 not-lg:flex not-lg:flex-col lg:fixed lg:z-10 lg:top-20 xl:top-22 lg:bottom-24 lg:left-5 lg:right-4 xl:right-[285px] lg:grid lg:grid-cols-2 lg:min-h-0 lg:overflow-hidden lg:rounded-2xl">
|
<div className="not-lg:max-w-lg not-lg:place-self-center not-lg:w-full w-full flex flex-col gap-4 pt-6 lg:pt-8 pb-24 lg:pb-28">
|
||||||
<div
|
<div className="not-lg:flex not-lg:flex-col lg:grid lg:grid-cols-2 lg:min-h-[420px] lg:max-h-[480px] lg:rounded-2xl lg:overflow-hidden shrink-0">
|
||||||
className={glassSurface(
|
<div
|
||||||
"relative w-full lg:h-full min-h-0 rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1 lg:bg-transparent! lg:border-0! lg:shadow-none! lg:[backdrop-filter:none] not-lg:sticky not-lg:top-6 not-lg:z-0 not-lg:h-[min(50vh,400px)] not-lg:min-h-[280px] not-lg:rounded-b-none",
|
className={glassSurface(
|
||||||
)}
|
"relative w-full lg:h-full min-h-0 rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1 lg:bg-transparent! lg:border-0! lg:shadow-none! lg:[backdrop-filter:none] not-lg:sticky not-lg:top-6 not-lg:z-0 not-lg:h-[min(50vh,400px)] not-lg:min-h-[280px] not-lg:rounded-b-none",
|
||||||
>
|
)}
|
||||||
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
@@ -261,6 +263,11 @@ function FoodPage({}: Props) {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={glassSurfaceCard("w-full px-6 pt-6 pb-7.5 rounded-3xl bg-white/60! shrink-0")}>
|
||||||
|
<FoodReviewsSection foodId={foodId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { api } from "@/config/axios";
|
import { api } from "@/config/axios";
|
||||||
import { FoodResponse } from "../../types/Types";
|
import { FoodResponse } from "../../types/Types";
|
||||||
|
import { CreateReviewType, FoodReviewsResponse } from "../types/ReviewTypes";
|
||||||
|
|
||||||
export const getFood = async (id: string): Promise<FoodResponse> => {
|
export const getFood = async (id: string): Promise<FoodResponse> => {
|
||||||
const { data } = await api.get<FoodResponse>(`/public/foods/${id}`);
|
const { data } = await api.get<FoodResponse>(`/public/foods/${id}`);
|
||||||
@@ -10,3 +11,13 @@ export const toggleFavorite = async (foodId: string) => {
|
|||||||
const { data } = await api.post(`/public/foods/favorite/${foodId}`);
|
const { data } = await api.post(`/public/foods/favorite/${foodId}`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFoodReviews = async (foodId: string): Promise<FoodReviewsResponse> => {
|
||||||
|
const { data } = await api.get<FoodReviewsResponse>(`/public/reviews/${foodId}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createReview = async (params: CreateReviewType) => {
|
||||||
|
const { data } = await api.post("/public/reviews", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { BaseResponse } from "../../types/Types";
|
||||||
|
import { Review } from "../../about/types/Types";
|
||||||
|
|
||||||
|
export type CreateReviewType = {
|
||||||
|
foodId: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
positivePoints: string[];
|
||||||
|
negativePoints: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FoodReviewsResponse = BaseResponse<Review[]>;
|
||||||
@@ -12,7 +12,6 @@ import React, { useMemo, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useGetOrders } from './hooks/useHistoryData';
|
import { useGetOrders } from './hooks/useHistoryData';
|
||||||
import { HistoryOrderItem } from './types/Types';
|
import { HistoryOrderItem } from './types/Types';
|
||||||
import { OrderStatus } from './enum/Enum';
|
|
||||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
const fallbackImage = '/assets/images/food-preview.png';
|
const fallbackImage = '/assets/images/food-preview.png';
|
||||||
@@ -221,11 +220,6 @@ function OrdersIndex() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{order.status === OrderStatus.COMPLETED && (
|
|
||||||
<Link href={`rate/${order.id}?foodId=${item.food.id}`}>
|
|
||||||
<Button className='text-xs px-3 py-2 h-auto'>{t('Card.Rate')}</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import ToggleButton, { ToggleButtonClassNames } from '@/components/button/ToggleButton';
|
|
||||||
import TabContainer, { TabContainerClassNames, TabContainerRenderType } from '@/components/tab/TabContainer';
|
|
||||||
import { TabHeader } from '@/components/tab/TabHeader';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import RateSelectionBar from '@/components/utils/RateSelectionBar';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useCreateRate } from '../hooks/useRateData';
|
|
||||||
import { CreateRateType } from '../types/Types';
|
|
||||||
import { toast } from '@/components/Toast';
|
|
||||||
import { PositivePointEnum, NegativePointEnum } from '../enum/Enum';
|
|
||||||
import { extractErrorMessage } from '@/lib/func';
|
|
||||||
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
|
||||||
|
|
||||||
type Props = object
|
|
||||||
|
|
||||||
type Params = {
|
|
||||||
orderId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RatingOrderIndex({ }: Props) {
|
|
||||||
|
|
||||||
const { t } = useTranslation("rating");
|
|
||||||
const params: Params = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const { mutate: createRate, isPending } = useCreateRate();
|
|
||||||
|
|
||||||
const [rating, setRating] = useState<number>(5);
|
|
||||||
const [comment, setComment] = useState<string>('');
|
|
||||||
const [positivePoints, setPositivePoints] = useState<string[]>([]);
|
|
||||||
const [negativePoints, setNegativePoints] = useState<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,
|
|
||||||
];
|
|
||||||
|
|
||||||
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 badPoitns = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<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 goodPoitns = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRatingChange = (value: number[]) => {
|
|
||||||
setRating(value[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
router.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitForm = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (rating === 0) {
|
|
||||||
toast(t("Errors.RatingRequired") || "لطفا امتیاز خود را انتخاب کنید", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rateData: CreateRateType = {
|
|
||||||
foodId: urlParams.get('foodId') || '',
|
|
||||||
orderId: params.orderId,
|
|
||||||
rating,
|
|
||||||
comment,
|
|
||||||
positivePoints,
|
|
||||||
negativePoints,
|
|
||||||
};
|
|
||||||
|
|
||||||
createRate(rateData, {
|
|
||||||
onSuccess: () => {
|
|
||||||
toast(t("Success") || "نظر شما با موفقیت ثبت شد", "success");
|
|
||||||
router.back();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast(extractErrorMessage(error), "error");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className='flex flex-col h-full pt-6'>
|
|
||||||
<h1 className='font-medium'>{t("Heading")}</h1>
|
|
||||||
|
|
||||||
<form onSubmit={submitForm} className={glassSurfaceCard('flex flex-col h-min justify-between rounded-[30px] pt-10 px-6 pb-7.5 mt-4')}>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className='w-full'>
|
|
||||||
<RateSelectionBar
|
|
||||||
name='rating'
|
|
||||||
defaultValue={[rating]}
|
|
||||||
onValueChange={handleRatingChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-10'>
|
|
||||||
<TabContainer
|
|
||||||
defaultIndex={1}
|
|
||||||
changeType={TabContainerRenderType.VISIBILITY}
|
|
||||||
className={{
|
|
||||||
...TabContainerClassNames,
|
|
||||||
wrapper: 'p-2!',
|
|
||||||
scrollView: clsx(
|
|
||||||
'border-none! rounded-xl! gap-0! h-full bg-[#EAECF0]! dark:bg-neutral-900! 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',
|
|
||||||
titleActive: 'text-primary'
|
|
||||||
}}>
|
|
||||||
<TabHeader title={t("Tabs.Weeknesses.Title")} viewRenderer={badPoitns()}></TabHeader>
|
|
||||||
<TabHeader title={t("Tabs.Strengths.Title")} viewRenderer={goodPoitns()}></TabHeader>
|
|
||||||
</TabContainer>
|
|
||||||
<div className="w-full mt-6">
|
|
||||||
<label htmlFor='userOpinion' className='text-sm font-medium'>
|
|
||||||
{t("InputComment.Label")}
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<textarea
|
|
||||||
name='userOpinion'
|
|
||||||
id='userOpinion'
|
|
||||||
value={comment}
|
|
||||||
onChange={(e) => setComment(e.target.value)}
|
|
||||||
placeholder={t("InputComment.Placeholder")}
|
|
||||||
className='dark:border-neutral-600 dark:bg-neutral-800 mt-2 w-full h-21 px-4 py-2.5 text-xs text-foreground rounded-xl border border-[#D0D0D0] focus:outline-none focus:ring-2 focus:ring-accent'
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4 mt-6">
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
disabled={isPending}
|
|
||||||
className='mt-auto rounded-xl font-normal cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'
|
|
||||||
>
|
|
||||||
{isPending ? t("ButtonSubmitting") || "در حال ارسال..." : t("ButtonSubmit")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isPending}
|
|
||||||
className='
|
|
||||||
mt-auto rounded-xl bg-white dark:bg-container cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-700
|
|
||||||
border border-foreground dark:border-neutral-600 text-foreground font-normal disabled:opacity-50 disabled:cursor-not-allowed'
|
|
||||||
>
|
|
||||||
{t("ButtonCancel")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RatingOrderIndex
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import * as api from "../service/RateService";
|
|
||||||
|
|
||||||
export const useCreateRate = () => {
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: api.createRate,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { api } from "@/config/axios";
|
|
||||||
import { CreateRateType } from "../types/Types";
|
|
||||||
|
|
||||||
export const createRate = async (params: CreateRateType) => {
|
|
||||||
const { data } = await api.post("/public/reviews", params);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export type CreateRateType = {
|
|
||||||
foodId: string;
|
|
||||||
orderId: string;
|
|
||||||
rating: number;
|
|
||||||
comment: string;
|
|
||||||
positivePoints: string[];
|
|
||||||
negativePoints: string[];
|
|
||||||
};
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// export const API_BASE_URL = "https://dmenuplus-api-production.dev.danakcorp.com";
|
// export const API_BASE_URL = "https://dmenuplus-api-production.dev.danakcorp.com";
|
||||||
|
export const API_BASE_URL = "http://192.168.1.107:2000";
|
||||||
// export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
|
// export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
|
||||||
export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
|
|
||||||
process.env.NEXT_PUBLIC_API_URL ?? (process.env.NODE_ENV === "development" ? "http://192.168.99.131:2000" : "https://dmenu-api.danakcorp.com");
|
process.env.NEXT_PUBLIC_API_URL ?? (process.env.NODE_ENV === "development" ? "http://192.168.99.131:2000" : "https://dmenu-api.danakcorp.com");
|
||||||
export const TOKEN_NAME = "dmenu-t";
|
export const TOKEN_NAME = "dmenu-t";
|
||||||
export const REFRESH_TOKEN_NAME = "dmenu-rt";
|
export const REFRESH_TOKEN_NAME = "dmenu-rt";
|
||||||
|
|||||||
Reference in New Issue
Block a user