add comments
This commit is contained in:
Generated
+54
@@ -23,6 +23,7 @@
|
||||
"iconsax-react": "^0.0.8",
|
||||
"lucide-react": "^0.539.0",
|
||||
"next": "15.4.5",
|
||||
"rc-rate": "^2.13.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
@@ -74,6 +75,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
||||
@@ -2882,6 +2892,12 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
@@ -5773,6 +5789,44 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rc-rate": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz",
|
||||
"integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
"classnames": "^2.2.5",
|
||||
"rc-util": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc-util": {
|
||||
"version": "5.44.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz",
|
||||
"integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"react-is": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.9.0",
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc-util/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"iconsax-react": "^0.0.8",
|
||||
"lucide-react": "^0.539.0",
|
||||
"next": "15.4.5",
|
||||
"rc-rate": "^2.13.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-hook-form": "^7.62.0",
|
||||
|
||||
@@ -244,3 +244,23 @@ textarea.place-black::placeholder {
|
||||
.filterBlack {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
|
||||
.modalGlass {
|
||||
background: rgba(0, 0, 0, 0.436);
|
||||
background-blend-mode: multiply;
|
||||
|
||||
backdrop-filter: blur(7px);
|
||||
}
|
||||
.modalGlass2 {
|
||||
background: rgba(255, 255, 255, 0.502);
|
||||
background-blend-mode: multiply;
|
||||
|
||||
/* backdrop-filter: blur(5px); */
|
||||
}
|
||||
|
||||
.modalGlass3 {
|
||||
background: rgba(62, 61, 61, 0.2);
|
||||
background-blend-mode: multiply;
|
||||
|
||||
backdrop-filter: blur(44px);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import DefaulModal from '@/components/DefaulModal'
|
||||
import Input from '@/components/Input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FC, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { AddCommentProps } from '../types/Types'
|
||||
import * as yup from 'yup'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { Add } from 'iconsax-react'
|
||||
import { PRIMARY_COLOR } from '@/config/const'
|
||||
import Rate from 'rc-rate'
|
||||
import 'rc-rate/assets/index.css'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useAddComment, useGetDetailProduct } from '../hooks/useProductData'
|
||||
import { toast } from '@/components/Toast'
|
||||
import { extractErrorMessage } from '@/helpers/errorUtils'
|
||||
|
||||
|
||||
type Props = {
|
||||
open: boolean,
|
||||
close: () => void,
|
||||
}
|
||||
|
||||
const AddComment: FC<Props> = (props) => {
|
||||
|
||||
|
||||
const { open, close } = props
|
||||
const { id } = useParams()
|
||||
const [advantages, setAdvantages] = useState<string[]>([])
|
||||
const [disadvantages, setDisadvantages] = useState<string[]>([])
|
||||
const [currentAdvantage, setCurrentAdvantage] = useState<string>('')
|
||||
const [currentDisadvantage, setCurrentDisadvantage] = useState<string>('')
|
||||
const [rating, setRating] = useState<number>(0)
|
||||
const { refetch } = useGetDetailProduct(id as string)
|
||||
const { mutate: addComment, isPending } = useAddComment()
|
||||
|
||||
const validationSchema = yup.object({
|
||||
title: yup.string().required('عنوان خود را وارد کنید'),
|
||||
advantage: yup.array().of(yup.string().required()).required('مزایای خود را وارد کنید'),
|
||||
disAdvantage: yup.array().of(yup.string().required()).required('معایب خود را وارد کنید'),
|
||||
content: yup.string().required('محتوای خود را وارد کنید'),
|
||||
rate: yup.number().required('رای خود را وارد کنید'),
|
||||
productId: yup.string().optional()
|
||||
})
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useForm({
|
||||
resolver: yupResolver(validationSchema),
|
||||
defaultValues: {
|
||||
title: '',
|
||||
advantage: [],
|
||||
disAdvantage: [],
|
||||
content: '',
|
||||
rate: 0,
|
||||
productId: id as string
|
||||
}
|
||||
})
|
||||
|
||||
const addAdvantage = () => {
|
||||
if (currentAdvantage.trim()) {
|
||||
const newAdvantages = [...advantages, currentAdvantage.trim()]
|
||||
setAdvantages(newAdvantages)
|
||||
setValue('advantage', newAdvantages)
|
||||
setCurrentAdvantage('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeAdvantage = (index: number) => {
|
||||
const newAdvantages = advantages.filter((_, i) => i !== index)
|
||||
setAdvantages(newAdvantages)
|
||||
setValue('advantage', newAdvantages)
|
||||
}
|
||||
|
||||
const addDisadvantage = () => {
|
||||
if (currentDisadvantage.trim()) {
|
||||
const newDisadvantages = [...disadvantages, currentDisadvantage.trim()]
|
||||
setDisadvantages(newDisadvantages)
|
||||
setValue('disAdvantage', newDisadvantages)
|
||||
setCurrentDisadvantage('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeDisadvantage = (index: number) => {
|
||||
const newDisadvantages = disadvantages.filter((_, i) => i !== index)
|
||||
setDisadvantages(newDisadvantages)
|
||||
setValue('disAdvantage', newDisadvantages)
|
||||
}
|
||||
|
||||
const handleRating = (rate: number) => {
|
||||
setRating(rate)
|
||||
setValue('rate', rate, { shouldValidate: true, shouldDirty: true })
|
||||
}
|
||||
|
||||
const onSubmit = (data: AddCommentProps) => {
|
||||
addComment(data, {
|
||||
onSuccess: (res) => {
|
||||
toast(res?.results?.message, 'success')
|
||||
refetch()
|
||||
close()
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast(extractErrorMessage(error), 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DefaulModal
|
||||
open={open}
|
||||
close={close}
|
||||
isHeader
|
||||
title_header='ثبت دیدگاه'
|
||||
>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* عنوان نظر */}
|
||||
<Input
|
||||
label="عنوان نظر"
|
||||
{...register('title')}
|
||||
error_text={errors.title?.message}
|
||||
placeholder="عنوان نظر خود را وارد کنید"
|
||||
/>
|
||||
|
||||
<div className='relative'>
|
||||
<Input
|
||||
value={currentAdvantage}
|
||||
onChange={(e) => setCurrentAdvantage(e.target.value)}
|
||||
placeholder="نکته مثبت خود را وارد کنید"
|
||||
className="flex-1"
|
||||
label="نکته مثبت"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addAdvantage()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addAdvantage}
|
||||
className="absolute top-[37px] left-3 rounded"
|
||||
>
|
||||
<Add size={22} color={PRIMARY_COLOR} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* لیست نکات مثبت اضافه شده */}
|
||||
{advantages.length > 0 && (
|
||||
<div className="space-y-2 flex gap-4 flex-wrap">
|
||||
{advantages.map((advantage, index) => (
|
||||
<div key={index} className="flex min-w-[45%] flex-1 items-center justify-between bg-gray-50 py-1 px-4 rounded-lg">
|
||||
<span className="text-sm text-gray-700">{advantage}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeAdvantage(index)}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className='flex-1 min-w-[45%] px-4'></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* نکات منفی */}
|
||||
<div className='relative'>
|
||||
<Input
|
||||
value={currentDisadvantage}
|
||||
onChange={(e) => setCurrentDisadvantage(e.target.value)}
|
||||
placeholder="نکته منفی خود را وارد کنید"
|
||||
className="flex-1"
|
||||
label="نکته منفی"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addDisadvantage()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addDisadvantage}
|
||||
className="absolute top-[37px] left-3 rounded"
|
||||
>
|
||||
<Add size={22} color={PRIMARY_COLOR} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* لیست نکات منفی اضافه شده */}
|
||||
{disadvantages.length > 0 && (
|
||||
<div className="space-y-2 flex gap-4 flex-wrap">
|
||||
{disadvantages.map((disadvantage, index) => (
|
||||
<div key={index} className="flex min-w-[45%] flex-1 items-center justify-between bg-gray-50 py-1 px-4 rounded-lg">
|
||||
<span className="text-sm text-gray-700">{disadvantage}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeDisadvantage(index)}
|
||||
className="text-red-500 hover:text-red-700 p-1"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className='flex-1 min-w-[45%] px-4'></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* امتیاز */}
|
||||
<div className="space-y-2 flex gap-1 items-center">
|
||||
<label className="text-sm text-black">امتیاز شما:</label>
|
||||
<div className='-mt-1.5'>
|
||||
<Rate
|
||||
value={rating}
|
||||
onChange={handleRating}
|
||||
count={5}
|
||||
allowHalf={false}
|
||||
style={{ fontSize: 20 }}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register('rate')}
|
||||
value={rating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* توضیحات */}
|
||||
<Textarea
|
||||
{...register('content')}
|
||||
placeholder="متن توضیحات خود را بنویسید"
|
||||
className="w-full h-24 p-3 text-sm rounded-xl border border-[#D0D0D0] resize-none focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
error_text={errors.content?.message}
|
||||
/>
|
||||
|
||||
{/* دکمه ثبت */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
isLoading={isPending}
|
||||
>
|
||||
ثبت دیدگاه
|
||||
</Button>
|
||||
</form>
|
||||
</DefaulModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddComment
|
||||
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Star1 } from 'iconsax-react'
|
||||
import { Product, Review } from '@/types/product.types'
|
||||
import AddComment from './AddComment'
|
||||
|
||||
interface ReviewsProps {
|
||||
product: Product
|
||||
@@ -12,7 +13,7 @@ interface ReviewsProps {
|
||||
|
||||
const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
|
||||
const [sort, setSort] = useState<'new' | 'helpful'>('new')
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const sortedReviews = useMemo(() => {
|
||||
if (sort === 'new') {
|
||||
return [...reviews].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
@@ -111,10 +112,12 @@ const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
|
||||
<div className="mt-3 sm:mt-4 text-xs text-[#7F7F7F] leading-5 sm:leading-6 text-justify">
|
||||
شما هم درباره این کالا نظر خود را ثبت کنید. نظر شما پس از بررسی و تایید نمایش داده میشود.
|
||||
</div>
|
||||
<Button className="w-full mt-4 sm:mt-5 text-xs sm:text-sm">ثبت دیدگاه</Button>
|
||||
<Button className="w-full mt-4 sm:mt-5 text-xs sm:text-sm" onClick={() => setOpen(true)}>ثبت دیدگاه</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddComment open={open} close={() => setOpen(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/Service";
|
||||
|
||||
export const useGetDetailProduct = (id: string) => {
|
||||
@@ -12,3 +12,9 @@ export const getProductQueryOptions = (id: string) => ({
|
||||
queryKey: ["product", id],
|
||||
queryFn: () => api.getProduct(id),
|
||||
});
|
||||
|
||||
export const useAddComment = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.addComment,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from "@/config/axios";
|
||||
import { ProductDetailResponse } from "@/types/product.types";
|
||||
import { AddCommentProps } from "../types/Types";
|
||||
|
||||
export const getProduct = async (
|
||||
id: string
|
||||
@@ -7,3 +8,11 @@ export const getProduct = async (
|
||||
const { data } = await axios.get(`/product/${id}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const addComment = async (params: AddCommentProps) => {
|
||||
const { data } = await axios.post(
|
||||
`/product/${params.productId}/comments`,
|
||||
params
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export type AddCommentProps = {
|
||||
title: string;
|
||||
advantage: string[];
|
||||
disAdvantage: string[];
|
||||
content: string;
|
||||
rate: number;
|
||||
productId?: string;
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FC, Fragment, ReactNode, useEffect } from 'react'
|
||||
import HeaderModal from './HeaderModal'
|
||||
|
||||
interface Props {
|
||||
open: boolean,
|
||||
close: () => void,
|
||||
children: ReactNode,
|
||||
isHeader?: boolean,
|
||||
title_header?: string,
|
||||
width?: number
|
||||
}
|
||||
|
||||
const DefaulModal: FC<Props> = (props: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = 'auto'
|
||||
}
|
||||
}, [props.open])
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
props.open && (
|
||||
<Fragment>
|
||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-y-auto fixed inset-0 z-[99999999999] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
||||
<div className='relative xl:h-full max-h-[80%] bottom-0 left-0 flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
||||
<div className='border-0 h-auto p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full bg-white max-h-[100%] outline-none focus:outline-none'>
|
||||
|
||||
|
||||
{
|
||||
props.isHeader && props.title_header &&
|
||||
<div onClick={props.close} className='pb-6 border-b border-white border-opacity-20'>
|
||||
<div className='h-[5px] w-[200px] mx-auto bg-[#D1D3D7] rounded-full mb-4 xl:hidden'></div>
|
||||
<HeaderModal close={props.close} label={props.title_header} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{props.children}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 modalGlass inset-0 z-[99999] '></div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaulModal
|
||||
@@ -0,0 +1,21 @@
|
||||
import { CloseCircle } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
close: () => void,
|
||||
}
|
||||
|
||||
const HeaderModal: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='flex justify-between items-center border-b border-border pb-4'>
|
||||
<div className='text-sm'>{props.label}</div>
|
||||
<div className='size-7 rounded-full flex justify-center items-center'>
|
||||
<CloseCircle size={20} color='black' onClick={props.close} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderModal
|
||||
@@ -1,17 +1,31 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import Error from "../Error"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
interface TextareaProps extends React.ComponentProps<"textarea"> {
|
||||
error_text?: string
|
||||
}
|
||||
|
||||
function Textarea({ className, error_text, ...props }: TextareaProps) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
error_text && "border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{
|
||||
error_text &&
|
||||
<Error
|
||||
errorText={error_text}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user