Compare commits
3 Commits
29ad49185b
...
81fc074708
| Author | SHA1 | Date | |
|---|---|---|---|
| 81fc074708 | |||
| f2172e537d | |||
| f8bddfb428 |
@@ -0,0 +1,244 @@
|
||||
"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;
|
||||
@@ -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 { CreateReviewType } from "../types/ReviewTypes";
|
||||
|
||||
export const useGetFood = (id: string) => {
|
||||
return useQuery({
|
||||
@@ -13,3 +14,22 @@ export const useToggleFavorite = () => {
|
||||
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 { useGetAbout } from "../about/hooks/useAboutData";
|
||||
import { useGetFood, useToggleFavorite } from "./hooks/useFoodData";
|
||||
import FoodReviewsSection from "./components/FoodReviewsSection";
|
||||
|
||||
type Props = object;
|
||||
|
||||
@@ -158,12 +159,13 @@ function FoodPage({}: Props) {
|
||||
};
|
||||
|
||||
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={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",
|
||||
)}
|
||||
>
|
||||
<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 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">
|
||||
<div
|
||||
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 */}
|
||||
<img
|
||||
ref={imageRef}
|
||||
@@ -261,6 +263,11 @@ function FoodPage({}: Props) {
|
||||
</motion.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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { api } from "@/config/axios";
|
||||
import { FoodResponse } from "../../types/Types";
|
||||
import { CreateReviewType, FoodReviewsResponse } from "../types/ReviewTypes";
|
||||
|
||||
export const getFood = async (id: string): Promise<FoodResponse> => {
|
||||
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}`);
|
||||
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,6 +12,7 @@ import { useQueryState } from 'next-usequerystate';
|
||||
import Image from 'next/image';
|
||||
import React from 'react'
|
||||
import { useGetAbout, useGetReviews, useGetSchedules } from './hooks/useAboutData';
|
||||
import { isReviewApproved } from './types/Types';
|
||||
import AboutSkeleton from './components/AboutSkeleton';
|
||||
import { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from '@/lib/styles/glassSurface';
|
||||
|
||||
@@ -223,7 +224,7 @@ function AboutPage() {
|
||||
};
|
||||
|
||||
const secondTab = () => {
|
||||
const reviews = (reviewsData?.data || []).filter(review => review.isApproved);
|
||||
const reviews = (reviewsData?.data || []).filter(isReviewApproved);
|
||||
|
||||
const sortedReviews = [...reviews].sort((a, b) => {
|
||||
if (sorting === '0') {
|
||||
|
||||
@@ -55,65 +55,81 @@ export interface Restaurant {
|
||||
|
||||
export interface ReviewFood {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
restaurant: string;
|
||||
category: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
deletedAt?: string | null;
|
||||
restaurant?: string;
|
||||
category?: string;
|
||||
title: string;
|
||||
desc: string | null;
|
||||
content: string | null;
|
||||
price: number;
|
||||
points: number | null;
|
||||
order: number | null;
|
||||
prepareTime: number | null;
|
||||
sat: boolean;
|
||||
sun: boolean;
|
||||
mon: boolean;
|
||||
breakfast: boolean;
|
||||
noon: boolean;
|
||||
dinner: boolean;
|
||||
stock: number;
|
||||
stockDefault: number;
|
||||
isActive: boolean;
|
||||
images: string[];
|
||||
inPlaceServe: boolean;
|
||||
pickupServe: boolean;
|
||||
rate: number;
|
||||
discount: number;
|
||||
isSpecialOffer: boolean;
|
||||
desc?: string | null;
|
||||
content?: string[] | string | null;
|
||||
price?: number;
|
||||
points?: number | null;
|
||||
order?: number | null;
|
||||
prepareTime?: number | null;
|
||||
weekDays?: number[];
|
||||
mealTypes?: string[];
|
||||
sat?: boolean;
|
||||
sun?: boolean;
|
||||
mon?: boolean;
|
||||
breakfast?: boolean;
|
||||
noon?: boolean;
|
||||
dinner?: boolean;
|
||||
stock?: number;
|
||||
stockDefault?: number;
|
||||
isActive?: boolean;
|
||||
images?: string[];
|
||||
inPlaceServe?: boolean;
|
||||
pickupServe?: boolean;
|
||||
rate?: number;
|
||||
score?: number;
|
||||
discount?: number;
|
||||
isSpecialOffer?: boolean;
|
||||
}
|
||||
|
||||
export interface ReviewUser {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
deletedAt?: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthDate: string;
|
||||
marriageDate: string;
|
||||
referrer: string | null;
|
||||
isActive: boolean;
|
||||
gender: boolean;
|
||||
wallet: number;
|
||||
points: number;
|
||||
phone: string;
|
||||
birthDate?: string;
|
||||
marriageDate?: string;
|
||||
referrer?: string | null;
|
||||
isActive?: boolean;
|
||||
gender?: boolean;
|
||||
wallet?: number;
|
||||
points?: number;
|
||||
phone?: string;
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
export type ReviewStatus = "approved" | "pending" | "rejected";
|
||||
|
||||
export interface Review {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
order: string;
|
||||
isBuyer?: boolean;
|
||||
order?: string;
|
||||
food: ReviewFood;
|
||||
user: ReviewUser;
|
||||
comment: string;
|
||||
rating: number;
|
||||
positivePoints: string[];
|
||||
negativePoints: string[];
|
||||
isApproved: boolean;
|
||||
status?: ReviewStatus;
|
||||
/** @deprecated use status */
|
||||
isApproved?: boolean;
|
||||
}
|
||||
|
||||
export function isReviewApproved(review: Review): boolean {
|
||||
if (review.status) {
|
||||
return review.status === "approved";
|
||||
}
|
||||
return review.isApproved === true;
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
|
||||
@@ -12,7 +12,6 @@ import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetOrders } from './hooks/useHistoryData';
|
||||
import { HistoryOrderItem } from './types/Types';
|
||||
import { OrderStatus } from './enum/Enum';
|
||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||
|
||||
const fallbackImage = '/assets/images/food-preview.png';
|
||||
@@ -221,11 +220,6 @@ function OrdersIndex() {
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
// export const API_BASE_URL = "https://dmenuplus-api-production.dev.danakcorp.com";
|
||||
// export const API_BASE_URL = "https://dmenu-api.danakcorp.com";
|
||||
// export const API_BASE_URL = "http://192.168.1.107:2000";
|
||||
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");
|
||||
export const TOKEN_NAME = "dmenu-t";
|
||||
|
||||
Reference in New Issue
Block a user