report product
This commit is contained in:
Generated
+61
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
@@ -1123,6 +1124,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-collapsible": {
|
"node_modules/@radix-ui/react-collapsible": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
|||||||
@@ -43,16 +43,14 @@ const BlogDetail: NextPage = () => {
|
|||||||
const handleNativeShare = async () => {
|
const handleNativeShare = async () => {
|
||||||
const blog = data?.results?.blog
|
const blog = data?.results?.blog
|
||||||
if (blog && currentUrl && typeof navigator !== 'undefined' && 'share' in navigator) {
|
if (blog && currentUrl && typeof navigator !== 'undefined' && 'share' in navigator) {
|
||||||
try {
|
|
||||||
const shareData = {
|
const shareData = {
|
||||||
title: blog.title_fa,
|
title: blog.title_fa,
|
||||||
text: `مشاهده مقاله: ${blog.title_fa}`,
|
text: `مشاهده مقاله: ${blog.title_fa}`,
|
||||||
url: currentUrl,
|
url: currentUrl,
|
||||||
}
|
|
||||||
await (navigator as { share: (data: typeof shareData) => Promise<void> }).share(shareData)
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error sharing:', error)
|
|
||||||
}
|
}
|
||||||
|
await (navigator as { share: (data: typeof shareData) => Promise<void> }).share(shareData)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +95,7 @@ const BlogDetail: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Calendar size={16} color="#9CA3AF" />
|
<Calendar size={16} color="#9CA3AF" />
|
||||||
<span>{new Date(blog.createdAt).toLocaleDateString('fa-IR')}</span>
|
<span>{blog.createdAt}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Eye size={16} color="#9CA3AF" />
|
<Eye size={16} color="#9CA3AF" />
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { InfoCircle } from 'iconsax-react'
|
||||||
|
import { useState, type FC } from 'react'
|
||||||
|
import { useGetProductQuestion, useSaveReport } from '../hooks/useProductData'
|
||||||
|
import { ProductQuestion } from '../types/Types'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
productId: number
|
||||||
|
productName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalReport: FC<Props> = (props) => {
|
||||||
|
const { productId, productName } = props
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<string[]>([])
|
||||||
|
const [description, setDescription] = useState<string>('')
|
||||||
|
|
||||||
|
const { data: questionsData, isLoading } = useGetProductQuestion(productId.toString())
|
||||||
|
const saveReportMutation = useSaveReport()
|
||||||
|
|
||||||
|
const reportOptions = questionsData?.results?.questions || []
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (selectedOptions.length === 0) return
|
||||||
|
|
||||||
|
await saveReportMutation.mutateAsync({
|
||||||
|
productId: productId.toString(),
|
||||||
|
answers: selectedOptions.map(id => ({ questionId: parseInt(id) })),
|
||||||
|
description: description.trim() || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsOpen(false)
|
||||||
|
setSelectedOptions([])
|
||||||
|
setDescription('')
|
||||||
|
toast('گزارش شما با موفقیت ارسال شد', 'success')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div onClick={() => setIsOpen(true)} className='mt-6 flex gap-2 cursor-pointer'>
|
||||||
|
<InfoCircle size={18} color='#878787' />
|
||||||
|
<span className='text-sm text-gray-500 font-light'>گزارش نادرستی کالا</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DefaulModal
|
||||||
|
open={isOpen}
|
||||||
|
close={() => setIsOpen(false)}
|
||||||
|
title_header='گزارش نادرستی مشخصات'
|
||||||
|
isHeader
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* Product Name */}
|
||||||
|
{productName && (
|
||||||
|
<div className='mb-6'>
|
||||||
|
<p className='text-sm text-gray-600'>{productName}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Report Options */}
|
||||||
|
<div className='space-y-4 mb-6'>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className='flex justify-center py-4'>
|
||||||
|
<div className='animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500'></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
reportOptions.map((option: ProductQuestion) => (
|
||||||
|
<div key={option._id} className='flex items-center gap-3'>
|
||||||
|
<Checkbox
|
||||||
|
id={`option-${option._id}`}
|
||||||
|
checked={selectedOptions.includes(option._id.toString())}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedOptions(prev => [...prev, option._id.toString()])
|
||||||
|
} else {
|
||||||
|
setSelectedOptions(prev => prev.filter(id => id !== option._id.toString()))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`option-${option._id}`}
|
||||||
|
className='text-sm text-gray-700 cursor-pointer'
|
||||||
|
>
|
||||||
|
{option.title}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className='mb-6'>
|
||||||
|
<h3 className='text-sm font-medium text-gray-700 mb-2'>توضیحات</h3>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder='متن توضیحات خود را بنویسید'
|
||||||
|
className='w-full h-24 text-sm p-3 border border-gray-300 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={selectedOptions.length === 0 || saveReportMutation.isPending}
|
||||||
|
isLoading={saveReportMutation.isPending}
|
||||||
|
className='w-full'
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
ارسال
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalReport
|
||||||
@@ -3,6 +3,7 @@ import Image from 'next/image'
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import ActionProduct from './ActionProduct'
|
import ActionProduct from './ActionProduct'
|
||||||
import { Product } from '@/types/product.types'
|
import { Product } from '@/types/product.types'
|
||||||
|
import ModalReport from './ModalReport'
|
||||||
|
|
||||||
interface ProductImageProps {
|
interface ProductImageProps {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -46,6 +47,7 @@ const ProductImage: FC<ProductImageProps> = ({ product }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<ModalReport productId={product._id} productName={product.title_fa} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,3 +42,17 @@ export const useGetPriceHistory = (productId: string) => {
|
|||||||
enabled: !!productId,
|
enabled: !!productId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetProductQuestion = (productId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["product-question", productId],
|
||||||
|
queryFn: () => api.getProductQuestion(productId),
|
||||||
|
enabled: !!productId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSaveReport = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: api.saveReport,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { ProductDetailResponse } from "@/types/product.types";
|
|||||||
import {
|
import {
|
||||||
AddCommentProps,
|
AddCommentProps,
|
||||||
AddQuestionProps,
|
AddQuestionProps,
|
||||||
|
SubmitReportProps,
|
||||||
PriceHistoryResponse,
|
PriceHistoryResponse,
|
||||||
|
ProductQuestionsResponse,
|
||||||
|
SaveReportTypes,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const getProduct = async (
|
export const getProduct = async (
|
||||||
@@ -45,3 +48,26 @@ export const getPriceHistory = async (
|
|||||||
const { data } = await axios.get(`/product/${productId}/price-history`);
|
const { data } = await axios.get(`/product/${productId}/price-history`);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProductQuestion = async (
|
||||||
|
productId: string
|
||||||
|
): Promise<ProductQuestionsResponse> => {
|
||||||
|
const { data } = await axios.get(`/product/${productId}/report/questions`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const submitReport = async (params: SubmitReportProps) => {
|
||||||
|
const { data } = await axios.post(`/product/${params.productId}/report`, {
|
||||||
|
questionIds: params.questionIds,
|
||||||
|
description: params.description,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveReport = async (params: SaveReportTypes) => {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
`/product/${params.productId}/report/save`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export type AddQuestionProps = {
|
|||||||
productId?: string;
|
productId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SubmitReportProps = {
|
||||||
|
productId: string;
|
||||||
|
questionIds: string[];
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProductStoreType = {
|
export type ProductStoreType = {
|
||||||
variantId: string;
|
variantId: string;
|
||||||
setVariantId: (value: string) => void;
|
setVariantId: (value: string) => void;
|
||||||
@@ -40,3 +46,28 @@ export type PriceHistoryResponse = {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
results: PriceHistoryResults;
|
results: PriceHistoryResults;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProductQuestion = {
|
||||||
|
_id: number;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
placeholder: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductQuestionsResults = {
|
||||||
|
questions: ProductQuestion[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductQuestionsResponse = {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: ProductQuestionsResults;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SaveReportTypes = {
|
||||||
|
answers: { questionId: number }[];
|
||||||
|
description: string;
|
||||||
|
productId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="flex items-center justify-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
Reference in New Issue
Block a user