submit review
This commit is contained in:
@@ -6,9 +6,14 @@ import { TabHeader } from '@/components/tab/TabHeader';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import RateSelectionBar from '@/components/utils/RateSelectionBar';
|
import RateSelectionBar from '@/components/utils/RateSelectionBar';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import React from 'react'
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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
|
type Props = object
|
||||||
|
|
||||||
@@ -17,24 +22,61 @@ type Params = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RatingOrderIndex({ }: Props) {
|
function RatingOrderIndex({ }: Props) {
|
||||||
const params: Params = useParams();
|
|
||||||
console.log(params);
|
|
||||||
const { t } = useTranslation("rating");
|
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 = () => {
|
const badPoitns = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
||||||
{[...Array(6)].map((_, i) => (
|
{negativePointsArray.map((point) => (
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
className={{
|
className={{
|
||||||
...ToggleButtonClassNames,
|
...ToggleButtonClassNames,
|
||||||
wrapperActive: 'border-red-400',
|
wrapperActive: 'border-red-400',
|
||||||
contentActive: 'text-red-400'
|
contentActive: 'text-red-400'
|
||||||
}}
|
}}
|
||||||
key={i}
|
key={point}
|
||||||
name={`weekness${i}`}>
|
name={`weekness_${point}`}
|
||||||
{t(`Tabs.Weeknesses.Options.${i}`)}
|
// @ts-expect-error - Type mismatch between custom ToggleButton and React types
|
||||||
|
onToggle={handleToggle(point, false)}
|
||||||
|
>
|
||||||
|
{t(`Tabs.Weeknesses.Options.${point}`)}
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -46,9 +88,14 @@ function RatingOrderIndex({ }: Props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
<div className="grid grid-cols-2 gap-x-4.5 gap-y-4 mt-8">
|
||||||
{[...Array(6)].map((_, i) => (
|
{positivePointsArray.map((point) => (
|
||||||
<ToggleButton key={i} name={`strength${i}`}>
|
<ToggleButton
|
||||||
{t(`Tabs.Strengths.Options.${i}`)}
|
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>
|
</ToggleButton>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>) => {
|
const submitForm = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
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 (
|
return (
|
||||||
<section className='flex flex-col h-full pt-6'>
|
<section className='flex flex-col h-full pt-6'>
|
||||||
@@ -70,7 +147,11 @@ function RatingOrderIndex({ }: Props) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<RateSelectionBar name='rating' />
|
<RateSelectionBar
|
||||||
|
name='rating'
|
||||||
|
defaultValue={[rating]}
|
||||||
|
onValueChange={handleRatingChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-10'>
|
<div className='mt-10'>
|
||||||
@@ -81,33 +162,48 @@ function RatingOrderIndex({ }: Props) {
|
|||||||
...TabContainerClassNames,
|
...TabContainerClassNames,
|
||||||
wrapper: 'p-2!',
|
wrapper: 'p-2!',
|
||||||
scrollView: clsx(
|
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',
|
title: 'text-sm2! font-normal mt-1',
|
||||||
header: 'rounded-lg h-7! w-full',
|
header: 'rounded-lg h-7! w-full',
|
||||||
headerActive: 'bg-primary',
|
headerActive: 'bg-white',
|
||||||
titleActive: 'text-accent'
|
titleActive: 'text-black'
|
||||||
}}>
|
}}>
|
||||||
<TabHeader title={t("Tabs.Weeknesses.Title")} viewRenderer={badPoitns()}></TabHeader>
|
<TabHeader title={t("Tabs.Weeknesses.Title")} viewRenderer={badPoitns()}></TabHeader>
|
||||||
<TabHeader title={t("Tabs.Strengths.Title")} viewRenderer={goodPoitns()}></TabHeader>
|
<TabHeader title={t("Tabs.Strengths.Title")} viewRenderer={goodPoitns()}></TabHeader>
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
<div className="w-full mt-6">
|
<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 />
|
<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 />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4 mt-6">
|
<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
|
<Button
|
||||||
type='submit'
|
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='
|
className='
|
||||||
mt-auto rounded-xl bg-white dark:bg-container cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-700
|
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")}
|
{t("ButtonCancel")}
|
||||||
</Button>
|
</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
@@ -262,8 +262,10 @@ html[data-theme="light"] {
|
|||||||
/* #F4F5F9 */
|
/* #F4F5F9 */
|
||||||
--container: oklch(1 0 0);
|
--container: oklch(1 0 0);
|
||||||
/* #FFFFFF */
|
/* #FFFFFF */
|
||||||
--primary: oklch(0.14 0 0);
|
--primary: oklch(0.202 0 0);
|
||||||
/* #000000 */
|
/* #333333 - same as foreground for consistency */
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
/* #FFFFFF */
|
||||||
--border: oklch(0.885 0.007 248.3);
|
--border: oklch(0.885 0.007 248.3);
|
||||||
/* #E5E5E5 */
|
/* #E5E5E5 */
|
||||||
--invalid: oklch(0.515 0.118 27.5);
|
--invalid: oklch(0.515 0.118 27.5);
|
||||||
@@ -322,27 +324,29 @@ html[data-theme="dark"] {
|
|||||||
--sidebar-ring: oklch(0.556 0 0); */
|
--sidebar-ring: oklch(0.556 0 0); */
|
||||||
|
|
||||||
--background: oklch(23.26% 0.014 253.1);
|
--background: oklch(23.26% 0.014 253.1);
|
||||||
/* #F4F5F9 */
|
/* #2B2E3F */
|
||||||
--container: oklch(30.33% 0.016 252.42);
|
--container: oklch(30.33% 0.016 252.42);
|
||||||
/* #FFFFFF */
|
/* #3A3E52 */
|
||||||
--primary: oklch(14% 0.005 285.823);
|
--primary: oklch(97.807% 0.029 256.847);
|
||||||
/* #000000 */
|
/* same as foreground for consistency */
|
||||||
|
--primary-foreground: oklch(23.26% 0.014 253.1);
|
||||||
|
/* same as background */
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
/* #E5E5E5 */
|
/* #E5E5E5 with 10% opacity */
|
||||||
--invalid: oklch(0.515 0.118 27.5);
|
--invalid: oklch(0.515 0.118 27.5);
|
||||||
/* red (#FF0000 approx) */
|
/* red (#FF0000 approx) */
|
||||||
--valid: oklch(0.46 0.107 132.7);
|
--valid: oklch(0.46 0.107 132.7);
|
||||||
/* #439C46 */
|
/* #439C46 */
|
||||||
--foreground: oklch(97.807% 0.029 256.847);
|
--foreground: oklch(97.807% 0.029 256.847);
|
||||||
/* #333333 */
|
/* #F5F6FA */
|
||||||
--disabled: oklch(42.866% 0.00494 286.051);
|
--disabled: oklch(42.866% 0.00494 286.051);
|
||||||
/* #E7E7E7 */
|
/* #5A5D6F */
|
||||||
--disabled2: oklch(0.5 0 0);
|
--disabled2: oklch(0.5 0 0);
|
||||||
/* #7F7F7F */
|
/* #7F7F7F */
|
||||||
--disabled3: oklch(0.949 0.002 277.1);
|
--disabled3: oklch(0.949 0.002 277.1);
|
||||||
/* #F2F2F2 */
|
/* #F2F2F2 */
|
||||||
--disabled-text: oklch(75.73% 0.01398 251.72);
|
--disabled-text: oklch(75.73% 0.01398 251.72);
|
||||||
/* #8C90A3 */
|
/* #B8BBCC */
|
||||||
--menu-header: oklch(0.79 0.03 237.3);
|
--menu-header: oklch(0.79 0.03 237.3);
|
||||||
/* #C3C7DD */
|
/* #C3C7DD */
|
||||||
--icon-deactive: oklch(0.65 0.027 234.8);
|
--icon-deactive: oklch(0.65 0.027 234.8);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ type Props = {
|
|||||||
content?: Array<{ value: string, icon: React.ReactElement }>;
|
content?: Array<{ value: string, icon: React.ReactElement }>;
|
||||||
className?: string;
|
className?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
onValueChange?: (value: number[]) => void;
|
||||||
|
defaultValue?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultContent: Array<{ value: string, icon: React.ReactElement }> = [
|
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} /> },
|
{ 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 (
|
return (
|
||||||
<div className={`${className}`}>
|
<div className={`${className}`}>
|
||||||
<div className='relative w-full h-3 rounded-full'>
|
<div className='relative w-full h-3 rounded-full'>
|
||||||
@@ -26,7 +28,8 @@ function RateSelectionBar({ name, content = defaultContent, className = '' }: Pr
|
|||||||
min={1}
|
min={1}
|
||||||
max={content.length}
|
max={content.length}
|
||||||
name={name}
|
name={name}
|
||||||
defaultValue={[5]}
|
defaultValue={defaultValue}
|
||||||
|
onValueChange={onValueChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
|
|||||||
+35
-31
@@ -1,33 +1,37 @@
|
|||||||
{
|
{
|
||||||
"Heading": "امتیاز شما به رستوران",
|
"Heading": "امتیاز شما به رستوران",
|
||||||
"Tabs": {
|
"Tabs": {
|
||||||
"Strengths": {
|
"Strengths": {
|
||||||
"Title": "نقاط قوت",
|
"Title": "نقاط قوت",
|
||||||
"Options": [
|
"Options": {
|
||||||
"آماده سازی سریع",
|
"great_taste": "طعم عالی",
|
||||||
"بهداشت محیطی خوب",
|
"fast_delivery": "تحویل سریع",
|
||||||
"کیفیت خوب منو",
|
"good_quality": "کیفیت خوب",
|
||||||
"تنوع بالا منو",
|
"good_portion_size": "اندازه مناسب پرس",
|
||||||
"برخورد مناسب کارکنان",
|
"friendly_service": "خدمات دوستانه"
|
||||||
"قیمت مناسب"
|
}
|
||||||
]
|
|
||||||
},
|
|
||||||
"Weeknesses": {
|
|
||||||
"Title": "نقاط ضعف",
|
|
||||||
"Options": [
|
|
||||||
"آماده سازی زمانبر",
|
|
||||||
"بهداشت محیطی بد",
|
|
||||||
"کیفیت خوب پایین",
|
|
||||||
"تنوع کم منو",
|
|
||||||
"برخورد غیر حرفه ای کارکنان",
|
|
||||||
"قیمت نامناسب"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"InputComment": {
|
"Weeknesses": {
|
||||||
"Label": "نظر شما",
|
"Title": "نقاط ضعف",
|
||||||
"Placeholder": "نظر شما"
|
"Options": {
|
||||||
},
|
"small_portion": "پرس کوچک",
|
||||||
"ButtonSubmit": "ثبت بازخورد",
|
"slow_delivery": "تحویل کند",
|
||||||
"ButtonCancel": "بازگشت"
|
"poor_quality": "کیفیت پایین",
|
||||||
}
|
"overpriced": "قیمت بالا",
|
||||||
|
"unfriendly_service": "خدمات نامناسب"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"InputComment": {
|
||||||
|
"Label": "نظر شما",
|
||||||
|
"Placeholder": "نظر شما"
|
||||||
|
},
|
||||||
|
"ButtonSubmit": "ثبت بازخورد",
|
||||||
|
"ButtonCancel": "بازگشت",
|
||||||
|
"Errors": {
|
||||||
|
"RatingRequired": "لطفا امتیاز خود را انتخاب کنید",
|
||||||
|
"SubmitFailed": "خطا در ثبت نظر"
|
||||||
|
},
|
||||||
|
"Success": "نظر شما با موفقیت ثبت شد",
|
||||||
|
"ButtonSubmitting": "در حال ارسال..."
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user