submit review

This commit is contained in:
hamid zarghami
2025-12-08 12:11:09 +03:30
parent 1522adf388
commit 6eb390e60c
8 changed files with 213 additions and 68 deletions
@@ -6,9 +6,14 @@ import { TabHeader } from '@/components/tab/TabHeader';
import { Button } from '@/components/ui/button';
import RateSelectionBar from '@/components/utils/RateSelectionBar';
import clsx from 'clsx';
import { useParams } from 'next/navigation';
import React from 'react'
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';
type Props = object
@@ -17,24 +22,61 @@ type Params = {
}
function RatingOrderIndex({ }: Props) {
const params: Params = useParams();
console.log(params);
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 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">
{[...Array(6)].map((_, i) => (
{negativePointsArray.map((point) => (
<ToggleButton
className={{
className={{
...ToggleButtonClassNames,
wrapperActive: 'border-red-400',
contentActive: 'text-red-400'
}}
key={i}
name={`weekness${i}`}>
{t(`Tabs.Weeknesses.Options.${i}`)}
key={point}
name={`weekness_${point}`}
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
onToggle={handleToggle(point, false)}
>
{t(`Tabs.Weeknesses.Options.${point}`)}
</ToggleButton>
))}
</div>
@@ -46,9 +88,14 @@ function RatingOrderIndex({ }: Props) {
return (
<>
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
{[...Array(6)].map((_, i) => (
<ToggleButton key={i} name={`strength${i}`}>
{t(`Tabs.Strengths.Options.${i}`)}
{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(`Tabs.Strengths.Options.${point}`)}
</ToggleButton>
))}
</div>
@@ -56,11 +103,41 @@ function RatingOrderIndex({ }: Props) {
)
}
const handleRatingChange = (value: number[]) => {
setRating(value[0]);
};
const handleCancel = () => {
router.back();
};
const submitForm = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log(formData.get('rating'))
}
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'>
@@ -70,7 +147,11 @@ function RatingOrderIndex({ }: Props) {
<div>
<div className='w-full'>
<RateSelectionBar name='rating' />
<RateSelectionBar
name='rating'
defaultValue={[rating]}
onValueChange={handleRatingChange}
/>
</div>
<div className='mt-10'>
@@ -81,33 +162,48 @@ function RatingOrderIndex({ }: Props) {
...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',
'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-primary',
titleActive: 'text-accent'
headerActive: 'bg-white',
titleActive: 'text-black'
}}>
<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'>{t("InputComment.Label")}</label>
<label htmlFor='userOpinion' className='text-sm font-medium'>
{t("InputComment.Label")}
</label>
<br />
<textarea name='userOpinion' id='userOpinion' placeholder={t("InputComment.Placeholder")} className='dark:border-neutral-600 mt-2 w-full h-21 px-4 py-2.5 text-xs text-[#888888] rounded-xl border border-[#D0D0D0]' />
<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' className='mt-auto rounded-xl font-normal cursor-pointer'>
{t("ButtonSubmit")}
</Button>
<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'
border border-foreground dark:border-neutral-600 text-foreground font-normal disabled:opacity-50 disabled:cursor-not-allowed'
>
{t("ButtonCancel")}
</Button>
@@ -0,0 +1,15 @@
export const enum PositivePointEnum {
GREAT_TASTE = "great_taste",
FAST_DELIVERY = "fast_delivery",
GOOD_QUALITY = "good_quality",
GOOD_PORTION_SIZE = "good_portion_size",
FRIENDLY_SERVICE = "friendly_service",
}
export const enum NegativePointEnum {
SMALL_PORTION = "small_portion",
SLOW_DELIVERY = "slow_delivery",
POOR_QUALITY = "poor_quality",
OVERPRICED = "overpriced",
UNFRIENDLY_SERVICE = "unfriendly_service",
}
@@ -0,0 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import * as api from "../service/RateService";
export const useCreateRate = () => {
return useMutation({
mutationFn: api.createRate,
});
};
@@ -0,0 +1,7 @@
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;
};
@@ -0,0 +1,8 @@
export type CreateRateType = {
foodId: string;
orderId: string;
rating: number;
comment: string;
positivePoints: string[];
negativePoints: string[];
};
+14 -10
View File
@@ -262,8 +262,10 @@ html[data-theme="light"] {
/* #F4F5F9 */
--container: oklch(1 0 0);
/* #FFFFFF */
--primary: oklch(0.14 0 0);
/* #000000 */
--primary: oklch(0.202 0 0);
/* #333333 - same as foreground for consistency */
--primary-foreground: oklch(1 0 0);
/* #FFFFFF */
--border: oklch(0.885 0.007 248.3);
/* #E5E5E5 */
--invalid: oklch(0.515 0.118 27.5);
@@ -322,27 +324,29 @@ html[data-theme="dark"] {
--sidebar-ring: oklch(0.556 0 0); */
--background: oklch(23.26% 0.014 253.1);
/* #F4F5F9 */
/* #2B2E3F */
--container: oklch(30.33% 0.016 252.42);
/* #FFFFFF */
--primary: oklch(14% 0.005 285.823);
/* #000000 */
/* #3A3E52 */
--primary: oklch(97.807% 0.029 256.847);
/* same as foreground for consistency */
--primary-foreground: oklch(23.26% 0.014 253.1);
/* same as background */
--border: oklch(1 0 0 / 10%);
/* #E5E5E5 */
/* #E5E5E5 with 10% opacity */
--invalid: oklch(0.515 0.118 27.5);
/* red (#FF0000 approx) */
--valid: oklch(0.46 0.107 132.7);
/* #439C46 */
--foreground: oklch(97.807% 0.029 256.847);
/* #333333 */
/* #F5F6FA */
--disabled: oklch(42.866% 0.00494 286.051);
/* #E7E7E7 */
/* #5A5D6F */
--disabled2: oklch(0.5 0 0);
/* #7F7F7F */
--disabled3: oklch(0.949 0.002 277.1);
/* #F2F2F2 */
--disabled-text: oklch(75.73% 0.01398 251.72);
/* #8C90A3 */
/* #B8BBCC */
--menu-header: oklch(0.79 0.03 237.3);
/* #C3C7DD */
--icon-deactive: oklch(0.65 0.027 234.8);
+5 -2
View File
@@ -8,6 +8,8 @@ type Props = {
content?: Array<{ value: string, icon: React.ReactElement }>;
className?: string;
name?: string;
onValueChange?: (value: number[]) => void;
defaultValue?: number[];
}
const defaultContent: Array<{ value: string, icon: React.ReactElement }> = [
@@ -18,7 +20,7 @@ const defaultContent: Array<{ value: string, icon: React.ReactElement }> = [
{ value: '1', icon: <Star className='fill-black dark:fill-foreground me-1 mb-1' size={24} /> },
]
function RateSelectionBar({ name, content = defaultContent, className = '' }: Props) {
function RateSelectionBar({ name, content = defaultContent, className = '', onValueChange, defaultValue = [5] }: Props) {
return (
<div className={`${className}`}>
<div className='relative w-full h-3 rounded-full'>
@@ -26,7 +28,8 @@ function RateSelectionBar({ name, content = defaultContent, className = '' }: Pr
min={1}
max={content.length}
name={name}
defaultValue={[5]}
defaultValue={defaultValue}
onValueChange={onValueChange}
/>
</div>
<div className="flex justify-between items-center mt-2">
+34 -30
View File
@@ -1,33 +1,37 @@
{
"Heading": "امتیاز شما به رستوران",
"Tabs": {
"Strengths": {
"Title": "نقاط قوت",
"Options": [
"آماده سازی سریع",
"بهداشت محیطی خوب",
"کیفیت خوب منو",
"تنوع بالا منو",
"برخورد مناسب کارکنان",
"قیمت مناسب"
]
},
"Weeknesses": {
"Title": "نقاط ضعف",
"Options": [
"آماده سازی زمانبر",
"بهداشت محیطی بد",
"کیفیت خوب پایین",
"تنوع کم منو",
"برخورد غیر حرفه ای کارکنان",
"قیمت نامناسب"
]
}
"Heading": "امتیاز شما به رستوران",
"Tabs": {
"Strengths": {
"Title": "نقاط قوت",
"Options": {
"great_taste": "طعم عالی",
"fast_delivery": "تحویل سریع",
"good_quality": "کیفیت خوب",
"good_portion_size": "اندازه مناسب پرس",
"friendly_service": "خدمات دوستانه"
}
},
"InputComment": {
"Label": ظر شما",
"Placeholder": "نظر شما"
},
"ButtonSubmit": "ثبت بازخورد",
"ButtonCancel": "بازگشت"
"Weeknesses": {
"Title": قاط ضعف",
"Options": {
"small_portion": "پرس کوچک",
"slow_delivery": "تحویل کند",
"poor_quality": "کیفیت پایین",
"overpriced": "قیمت بالا",
"unfriendly_service": "خدمات نامناسب"
}
}
},
"InputComment": {
"Label": "نظر شما",
"Placeholder": "نظر شما"
},
"ButtonSubmit": "ثبت بازخورد",
"ButtonCancel": "بازگشت",
"Errors": {
"RatingRequired": "لطفا امتیاز خود را انتخاب کنید",
"SubmitFailed": "خطا در ثبت نظر"
},
"Success": "نظر شما با موفقیت ثبت شد",
"ButtonSubmitting": "در حال ارسال..."
}