feat: add product detail page with components and infinite scroll

- Add SingleProduct component for product detail page
- Implement product data hooks and services
- Add infinite scroll functionality for products list
- Update ProductCard component with new features
- Add product types and specifications
- Fix TypeScript errors in product page
- Update profile page layout
- Add global CSS improvements
This commit is contained in:
hamid zarghami
2025-09-07 09:17:58 +03:30
parent 5ec2b621bc
commit 2995b11900
17 changed files with 667 additions and 343 deletions
+1 -1
View File
@@ -219,7 +219,7 @@ textarea.place-black::placeholder {
.swiper-pagination { .swiper-pagination {
z-index: 1 !important; z-index: 1 !important;
} }
div { * {
direction: rtl; direction: rtl;
} }
+19 -62
View File
@@ -1,71 +1,28 @@
import { NextPage } from 'next'
import Layout from '@/hoc/Layout' import Layout from '@/hoc/Layout'
import { import SingleProduct from '@/app/product/components/SingleProduct'
Breadcrumb, import { getProductQueryOptions } from '@/app/product/hooks/useProductData'
BreadcrumbItem, import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query'
BreadcrumbLink, import { Suspense } from 'react'
BreadcrumbList, import PageLoading from '@/components/PageLoading'
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import ProductImage from '@/app/product/components/ProductImage'
import BaseInformation from '@/app/product/components/BaseInformation'
import ShopInformation from '@/app/product/components/ShopInformation'
import Specifications from '@/app/product/components/Specifications'
import ProsCons from '@/app/product/components/ProsCons'
import Description from '@/app/product/components/Description'
import Reviews from '@/app/product/components/Reviews'
import Questions from '@/app/product/components/Questions'
const SingleProduct: NextPage = () => { interface SingleProductPageProps {
return ( params: { id: string }
<div className='mt-14 px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20'>
<div className='flex flex-col sm:flex-row justify-between gap-4 sm:gap-0'>
<Breadcrumb aria-label="breadcrumb">
<BreadcrumbList className="text-sm text-muted-foreground">
<BreadcrumbItem>
<BreadcrumbLink href="/">فروشگاه آناهیتا</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="text-muted-foreground select-none">/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/products">بهداشت خانگی</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="text-muted-foreground select-none">/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbPage className="text-foreground">مواد شوینده</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className='text-primary text-xs sm:text-sm'>فروشنده شوید</div>
</div>
<div className='flex flex-col xl:flex-row gap-4 sm:gap-6 mt-8 sm:mt-12 md:mt-14'>
<div className='xl:w-[325px] xl:shrink-0'>
<ShopInformation />
</div>
<div className='flex-1 flex flex-col gap-6'>
<div className='flex flex-col lg:flex-row gap-4 sm:gap-6'>
<ProductImage />
<BaseInformation />
</div>
<div>
<Specifications />
<Description />
<ProsCons />
<Reviews />
<Questions />
</div>
</div>
</div>
</div>
)
} }
export default function SingleProductWithLayout() {
export default async function SingleProductPage({ params }: SingleProductPageProps) {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(getProductQueryOptions(params.id))
const dehydratedState = dehydrate(queryClient)
return ( return (
<Layout> <Layout>
<SingleProduct /> <Suspense fallback={<PageLoading />}>
<HydrationBoundary state={dehydratedState}>
<SingleProduct id={params.id} />
</HydrationBoundary>
</Suspense>
</Layout> </Layout>
) )
} }
+81 -9
View File
@@ -1,15 +1,87 @@
'use client'
import { Chart2, Heart, Hierarchy2, Notification, RowHorizontal } from 'iconsax-react' import { Chart2, Heart, Hierarchy2, Notification, RowHorizontal } from 'iconsax-react'
import { FC } from 'react' import { FC, useState } from 'react'
import { Product } from '@/types/product.types'
interface ActionProductProps {
product: Product
}
const ActionProduct: FC<ActionProductProps> = ({ product }) => {
const [isWishlist, setIsWishlist] = useState(false)
const [isCompare, setIsCompare] = useState(false)
const handleWishlist = () => {
setIsWishlist(!isWishlist)
// TODO: API call to add/remove from wishlist
}
const handleCompare = () => {
setIsCompare(!isCompare)
// TODO: API call to add/remove from compare
}
const handleShare = () => {
if (navigator.share) {
navigator.share({
title: product.title_fa,
text: product.description,
url: window.location.href,
})
} else {
navigator.clipboard.writeText(window.location.href)
// TODO: Show toast notification
}
}
const ActionProduct: FC = () => {
return ( return (
<div className='absolute top-6 right-6'> <div className='absolute top-6 right-4'>
<div className='flex flex-col gap-5'> <div className='flex flex-col gap-3'>
<Heart size={20} color='#333333' /> <button
<Hierarchy2 size={20} color='#333333' /> onClick={handleWishlist}
<Notification size={20} color='#333333' /> className='p-2 rounded-full bg-white/80 backdrop-blur-sm hover:bg-white transition-colors'
<Chart2 size={20} color='#333333' /> title={isWishlist ? 'حذف از علاقه‌مندی‌ها' : 'افزودن به علاقه‌مندی‌ها'}
<RowHorizontal size={20} color='#333333' variant='Bulk' /> >
<Heart
size={20}
color={isWishlist ? '#ff4757' : '#333333'}
variant={isWishlist ? 'Bold' : 'Outline'}
/>
</button>
<button
onClick={handleCompare}
className='p-2 rounded-full bg-white/80 backdrop-blur-sm hover:bg-white transition-colors'
title={isCompare ? 'حذف از مقایسه' : 'افزودن به مقایسه'}
>
<Hierarchy2
size={20}
color={isCompare ? '#2ed573' : '#333333'}
variant={isCompare ? 'Bold' : 'Outline'}
/>
</button>
<button
onClick={handleShare}
className='p-2 rounded-full bg-white/80 backdrop-blur-sm hover:bg-white transition-colors'
title='اشتراک‌گذاری'
>
<Notification size={20} color='#333333' />
</button>
<button
className='p-2 rounded-full bg-white/80 backdrop-blur-sm hover:bg-white transition-colors'
title='گزارش مشکل'
>
<Chart2 size={20} color='#333333' />
</button>
<button
className='p-2 rounded-full bg-white/80 backdrop-blur-sm hover:bg-white transition-colors'
title='گزینه‌های بیشتر'
>
<RowHorizontal size={20} color='#333333' variant='Bulk' />
</button>
</div> </div>
</div> </div>
) )
+32 -28
View File
@@ -1,20 +1,30 @@
'use client'
import { Star1 } from 'iconsax-react' import { Star1 } from 'iconsax-react'
import { FC } from 'react' import { FC } from 'react'
import { Product } from '@/types/product.types'
interface BaseInformationProps {
product: Product
}
const BaseInformation: FC<BaseInformationProps> = ({ product }) => {
const defaultVariant = product.default_variant
const brand = product.brand
const size = defaultVariant.size
const BaseInformation: FC = () => {
return ( return (
<div className='flex-1'> <div className='flex-1'>
<h1 className='text-base sm:text-lg text-[#191919] leading-relaxed'> <h1 className='text-base sm:text-lg text-[#191919] leading-relaxed'>
گوشی موبایل سامسونگ مدل Galaxy S24 Ultra دو سیم کارت ظرفیت 1 ترابایت و رم 12 گیگابایت - ویتنام {product.title_fa}
</h1> </h1>
<p className='mt-4 sm:mt-6 text-[#B2B2B2] text-xs sm:text-sm'> <p className='mt-4 sm:mt-6 text-[#B2B2B2] text-xs sm:text-sm'>
Samsung Galaxy S24 Ultra Dual SIM 256GB And 12GB RAM Mobile Phone - Vietnam {product.title_en}
</p> </p>
<div className='mt-4 flex flex-wrap gap-2 sm:gap-4 items-center'> <div className='mt-4 flex flex-wrap gap-2 sm:gap-4 items-center'>
<h3 className='text-primary text-sm sm:text-base'> <h3 className='text-primary text-sm sm:text-base'>
سامسونگ {brand.title_fa}
</h3> </h3>
<div className='size-1.5 rounded-full bg-[#E5E5E5]'></div> <div className='size-1.5 rounded-full bg-[#E5E5E5]'></div>
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
@@ -28,13 +38,19 @@ const BaseInformation: FC = () => {
</div> </div>
<div className='mt-4 sm:mt-6 text-[#333333] text-sm sm:text-base'> <div className='mt-4 sm:mt-6 text-[#333333] text-sm sm:text-base'>
رنگ : طلایی اندازه : {size.value}
</div> </div>
<div className='mt-2 flex gap-1.5'> <div className='mt-2 flex gap-1.5'>
<div className='size-6 sm:size-8 rounded-full bg-red-200'></div> {product.variants.map((variant, index) => (
<div className='size-6 sm:size-8 rounded-full bg-blue-200'></div> <div
<div className='size-6 sm:size-8 rounded-full bg-yellow-200'></div> key={variant._id}
className='size-6 sm:size-8 rounded-full bg-gray-200 flex items-center justify-center text-xs font-medium'
title={variant.size.value}
>
{index + 1}
</div>
))}
</div> </div>
<div className='mt-6 sm:mt-7 text-base sm:text-lg text-[#333333]'> <div className='mt-6 sm:mt-7 text-base sm:text-lg text-[#333333]'>
@@ -42,27 +58,15 @@ const BaseInformation: FC = () => {
</div> </div>
<div className='mt-6 sm:mt-10 flex flex-col gap-4 sm:gap-8'> <div className='mt-6 sm:mt-10 flex flex-col gap-4 sm:gap-8'>
<div className='flex flex-col sm:flex-row sm:gap-3 items-start sm:items-center text-xs sm:text-sm'> {product.specifications.slice(0, 3).map((spec, index) => (
<div className='flex items-center gap-2'> <div key={index} className='flex flex-col sm:flex-row sm:gap-3 items-start sm:items-center text-xs sm:text-sm'>
<div className='size-2 rounded-full bg-[#7F7F7F]'></div> <div className='flex items-center gap-2'>
<div className='text-[#7F7F7F]'>فناوری صفحه نمایش :</div> <div className='size-2 rounded-full bg-[#7F7F7F]'></div>
<div className='text-[#7F7F7F]'>{spec.title} :</div>
</div>
<div className='mt-1 sm:mt-0'>{spec.values.join(', ')}</div>
</div> </div>
<div className='mt-1 sm:mt-0'>Dynamic LTPO AMOLED 2X</div> ))}
</div>
<div className='flex flex-col sm:flex-row sm:gap-3 items-start sm:items-center text-xs sm:text-sm'>
<div className='flex items-center gap-2'>
<div className='size-2 rounded-full bg-[#7F7F7F]'></div>
<div className='text-[#7F7F7F]'>رزولوشن عکس :</div>
</div>
<div className='mt-1 sm:mt-0'>200 مگاپیکسل</div>
</div>
<div className='flex flex-col sm:flex-row sm:gap-3 items-start sm:items-center text-xs sm:text-sm'>
<div className='flex items-center gap-2'>
<div className='size-2 rounded-full bg-[#7F7F7F]'></div>
<div className='text-[#7F7F7F]'>نسخه سیستم عامل :</div>
</div>
<div className='mt-1 sm:mt-0'>Android 14</div>
</div>
</div> </div>
</div> </div>
) )
+13 -8
View File
@@ -1,12 +1,17 @@
'use client'
import { FC } from 'react' import { FC } from 'react'
import { Product } from '@/types/product.types'
const DESCRIPTION: string[] = [ interface DescriptionProps {
'همواره گوشی‌های هوشمند پرچمدار سامسونگ توانسته‌اند با بهره بردن از مشخصات فنی قدرتمند، توجه هر بیننده‌ای را به خود جلب کنند. سامسونگ Galaxy S24 Ultra نیز از این قاعده مستثنا نیست و با نمایشگری باکیفیت، پردازنده پرتوان و سیستم دوربین قدرتمند به میدان آمده است.', product: Product
'صفحه‌نمایش 6.8 اینچی این گوشی با رزولوشن 1440×3120 پیکسل و نرخ نوسازی 120 هرتز، تجربه‌ای روان و چشم‌نواز ارائه می‌دهد. در بخش دوربین نیز سنسور اصلی 200 مگاپیکسلی در کنار سنسور 12 مگاپیکسلی فوق‌عریض و سنسور تله‌فوتو با بزرگنمایی اپتیکال 3 برابر، ثبت تصاویر با جزئیات و داینامیک رنج عالی را ممکن کرده است.', }
'ترکیب این سخت‌افزار قدرتمند با نرم‌افزار بهینه، باعث شده که Galaxy S24 Ultra انتخاب مناسبی برای بازی، عکاسی و استفاده روزمره باشد. اگر به دنبال گوشی‌ای با کارایی بالا و امکانات کامل هستید، این مدل می‌تواند گزینه‌ای ایده‌آل برای شما باشد.'
] const Description: FC<DescriptionProps> = ({ product }) => {
// تقسیم توضیحات به پاراگراف‌ها بر اساس خطوط جدید
const descriptionParagraphs = product.description
.split('\n')
.filter(paragraph => paragraph.trim().length > 0)
const Description: FC = () => {
return ( return (
<div className="mt-12 sm:mt-16 w-full"> <div className="mt-12 sm:mt-16 w-full">
<div className="flex items-center justify-end gap-3 flex-row-reverse"> <div className="flex items-center justify-end gap-3 flex-row-reverse">
@@ -16,8 +21,8 @@ const Description: FC = () => {
<div className="mt-4 sm:mt-6 rounded-xl bg-white p-4 sm:p-6 max-h-[300px] sm:max-h-[420px] overflow-y-auto"> <div className="mt-4 sm:mt-6 rounded-xl bg-white p-4 sm:p-6 max-h-[300px] sm:max-h-[420px] overflow-y-auto">
<div className="text-xs sm:text-sm text-[#333333] leading-6 sm:leading-8 text-justify space-y-3 sm:space-y-4"> <div className="text-xs sm:text-sm text-[#333333] leading-6 sm:leading-8 text-justify space-y-3 sm:space-y-4">
{DESCRIPTION.map((paragraph) => ( {descriptionParagraphs.map((paragraph, index) => (
<p key={paragraph}>{paragraph}</p> <p key={index}>{paragraph}</p>
))} ))}
</div> </div>
</div> </div>
+34 -9
View File
@@ -1,25 +1,50 @@
'use client'
import Image from 'next/image' import Image from 'next/image'
import { FC } from 'react' import { FC, useState } from 'react'
import ActionProduct from './ActionProduct' import ActionProduct from './ActionProduct'
import { Product } from '@/types/product.types'
interface ProductImageProps {
product: Product
}
const ProductImage: FC<ProductImageProps> = ({ product }) => {
const [selectedImageIndex, setSelectedImageIndex] = useState(0)
const allImages = [product.imagesUrl.cover, ...product.imagesUrl.list]
const selectedImage = allImages[selectedImageIndex]
const ProductImage: FC = () => {
return ( return (
<div className='w-full max-w-[380px] mx-auto lg:mx-0'> <div className='w-full max-w-[380px] mx-auto lg:mx-0'>
<div className='w-full border border-border rounded-xl h-[300px] sm:h-[350px] md:h-[400px] flex justify-center items-center relative'> <div className='w-full border border-border rounded-xl h-[300px] sm:h-[350px] md:h-[400px] flex justify-center items-center relative'>
<Image <Image
src={'https://dkstatics-public.digikala.com/digikala-products/c0a7ce006d11c222bce16bd3cdd8d79aeb6689bc_1738134271.jpg?x-oss-process=image/resize,m_lfit,h_800,w_800/format,webp/quality,q_90'} src={selectedImage}
width={300} width={300}
height={300} height={300}
alt='' alt={product.title_fa}
className='max-w-[200px] max-h-[200px] sm:max-w-[240px] sm:max-h-[240px] object-contain' className='max-w-[200px] max-h-[200px] sm:max-w-[240px] sm:max-h-[240px] object-contain'
/> />
<ActionProduct /> <ActionProduct product={product} />
</div> </div>
<div className='flex gap-2 sm:gap-3 mt-4 justify-center lg:justify-start overflow-x-auto'> <div className='flex gap-2 sm:gap-3 mt-4 justify-center lg:justify-start overflow-x-auto'>
<div className='size-16 sm:size-20 border border-border rounded-xl shrink-0'></div> {allImages.map((image, index) => (
<div className='size-16 sm:size-20 border border-border rounded-xl shrink-0'></div> <div
<div className='size-16 sm:size-20 border border-border rounded-xl shrink-0'></div> key={index}
<div className='size-16 sm:size-20 border border-border rounded-xl shrink-0'></div> className={`size-16 flex justify-center items-center sm:size-20 border rounded-xl shrink-0 cursor-pointer transition-all ${selectedImageIndex === index
? 'border-primary ring-primary/20'
: 'border-border hover:border-primary/50'
}`}
onClick={() => setSelectedImageIndex(index)}
>
<Image
src={image}
width={50}
height={50}
alt={`${product.title_fa} - تصویر ${index + 1}`}
className='w-full h-full object-contain max-h-[50px] max-w-[50px] rounded-xl'
/>
</div>
))}
</div> </div>
</div> </div>
) )
+20 -19
View File
@@ -1,20 +1,13 @@
'use client'
import { FC } from 'react' import { FC } from 'react'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Product } from '@/types/product.types'
const PROS: string[] = [ interface ProsConsProps {
'مجهز به حس‌گر اثرانگشت', product: Product
'مقاوم در برابر آب', }
'مناسب بازی',
'مناسب عکاسی',
'مناسب عکاسی سلفی',
]
const CONS: string[] = [ const ProsCons: FC<ProsConsProps> = ({ product }) => {
'وزن نسبتاً بالا',
'عدم وجود شارژر در جعبه',
]
const ProsCons: FC = () => {
return ( return (
<div className="mt-12 sm:mt-16 w-full"> <div className="mt-12 sm:mt-16 w-full">
<div className="flex items-center justify-end gap-3 flex-row-reverse"> <div className="flex items-center justify-end gap-3 flex-row-reverse">
@@ -30,9 +23,13 @@ const ProsCons: FC = () => {
</div> </div>
<div className="col-span-12 md:col-span-9 mt-2 md:mt-0"> <div className="col-span-12 md:col-span-9 mt-2 md:mt-0">
<div className="flex flex-col gap-2 sm:gap-3 text-xs sm:text-sm text-[#333333] text-right"> <div className="flex flex-col gap-2 sm:gap-3 text-xs sm:text-sm text-[#333333] text-right">
{PROS.map((v) => ( {product.advantages.length > 0 ? (
<div key={v}>{v}</div> product.advantages.map((advantage, index) => (
))} <div key={index}>{advantage}</div>
))
) : (
<div className="text-[#999999]">نقطه مثبتی ثبت نشده است</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -46,9 +43,13 @@ const ProsCons: FC = () => {
</div> </div>
<div className="col-span-12 md:col-span-9 mt-2 md:mt-0"> <div className="col-span-12 md:col-span-9 mt-2 md:mt-0">
<div className="flex flex-col gap-2 sm:gap-3 text-xs sm:text-sm text-[#333333] text-right"> <div className="flex flex-col gap-2 sm:gap-3 text-xs sm:text-sm text-[#333333] text-right">
{CONS.map((v) => ( {product.disAdvantages.length > 0 ? (
<div key={v}>{v}</div> product.disAdvantages.map((disadvantage, index) => (
))} <div key={index}>{disadvantage}</div>
))
) : (
<div className="text-[#999999]">نقطه منفی ثبت نشده است</div>
)}
</div> </div>
</div> </div>
</div> </div>
+63 -76
View File
@@ -3,50 +3,28 @@
import { FC, Fragment, useMemo, useState } from 'react' import { FC, Fragment, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ArrowDown2, MessageQuestion } from 'iconsax-react' import { ArrowDown2, MessageQuestion } from 'iconsax-react'
import { Product, Question } from '@/types/product.types'
type AnswerItem = { interface QuestionsProps {
id: number product: Product
text: string questions?: Question[]
} }
type QuestionItem = { const Questions: FC<QuestionsProps> = ({ questions = [] }) => {
id: number
text: string
answers: AnswerItem[]
}
const QUESTIONS: QuestionItem[] = [
{
id: 1,
text: 'سلام ویتنام هست یا هند؟',
answers: [],
},
{
id: 2,
text:
'سلام من دوتا اپلیکیشن همراه بانک نصب نمیشه، دلیلش چیه؟ همچنین مشکل برای کسی پیش اومده؟',
answers: [
{ id: 1, text: 'مشکل از همراه بانک‌های هست که با نسخه جدید طراحی نشدن' },
{ id: 2, text: 'اپ رو آپدیت کنید یا کش رو پاک کنید، برای من حل شد' },
],
},
{
id: 3,
text: 'وزن چطاست؟ ویتنام که نیست؟',
answers: [
{ id: 1, text: 'زیر عکس که نوشتند ویتنام' },
],
},
]
const Questions: FC = () => {
const [sort, setSort] = useState<'new' | 'answers'>('new') const [sort, setSort] = useState<'new' | 'answers'>('new')
const [expandedId, setExpandedId] = useState<number | null>(null) const [expandedId, setExpandedId] = useState<string | null>(null)
const sorted = useMemo(() => { const sorted = useMemo(() => {
if (sort === 'new') return [...QUESTIONS].sort((a, b) => b.id - a.id) if (sort === 'new') {
return [...QUESTIONS].sort((a, b) => b.answers.length - a.answers.length) return [...questions].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
}, [sort]) }
return [...questions].sort((a, b) => b.answers.length - a.answers.length)
}, [sort, questions])
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('fa-IR')
}
return ( return (
<div className="mt-12 sm:mt-16 w-full"> <div className="mt-12 sm:mt-16 w-full">
@@ -80,51 +58,60 @@ const Questions: FC = () => {
</div> </div>
<div className="mt-4 sm:mt-6 rounded-xl bg-white divide-y divide-[#E6E6E6]"> <div className="mt-4 sm:mt-6 rounded-xl bg-white divide-y divide-[#E6E6E6]">
{sorted.map((q) => ( {sorted.length > 0 ? (
<Fragment key={q.id}> sorted.map((question) => (
<div className="px-4 sm:px-6 py-4 sm:py-5"> <Fragment key={question._id}>
<div className="flex items-start gap-2 sm:gap-3"> <div className="px-4 sm:px-6 py-4 sm:py-5">
<div className="shrink-0 mt-0.5"> <div className="flex items-start gap-2 sm:gap-3">
<div className="size-6 sm:size-8 rounded-full border border-[#DDE9FF] bg-[#F6FAFF] flex items-center justify-center"> <div className="shrink-0 mt-0.5">
<MessageQuestion size={14} color="#2F80ED" className="sm:w-[18px] sm:h-[18px]" /> <div className="size-6 sm:size-8 rounded-full border border-[#DDE9FF] bg-[#F6FAFF] flex items-center justify-center">
<MessageQuestion size={14} color="#2F80ED" className="sm:w-[18px] sm:h-[18px]" />
</div>
</div>
<div className="text-[#333333] text-xs sm:text-sm leading-6 sm:leading-7 text-right flex-1">
{question.question}
</div> </div>
</div> </div>
<div className="text-[#333333] text-xs sm:text-sm leading-6 sm:leading-7 text-right flex-1">
{q.text} <div className="mt-2 text-xs text-[#999999]">
{question.user.name} {formatDate(question.date)}
</div> </div>
</div>
{q.answers.length === 0 && ( {question.answers.length === 0 && (
<div className="mt-3 text-primary text-xs cursor-pointer w-fit">ثبت پاسخ</div> <div className="mt-3 text-primary text-xs cursor-pointer w-fit">ثبت پاسخ</div>
)} )}
{q.answers.length > 0 && ( {question.answers.length > 0 && (
<div className="mt-3 sm:mt-4 text-[#7F7F7F]"> <div className="mt-3 sm:mt-4 text-[#7F7F7F]">
{(expandedId === q.id ? q.answers : q.answers.slice(0, 1)).map((a) => ( {(expandedId === question._id ? question.answers : question.answers.slice(0, 1)).map((answer) => (
<div key={a.id} className='flex gap-2 items-start mt-2 first:mt-0'> <div key={answer._id} className='flex gap-2 items-start mt-2 first:mt-0'>
<div className="text-xs shrink-0">پاسخ</div> <div className="text-xs shrink-0">پاسخ</div>
<div className="text-xs leading-6 sm:leading-7 text-right text-[#333333]"> <div className="text-xs leading-6 sm:leading-7 text-right text-[#333333]">
{a.text} {answer.answer}
</div>
</div> </div>
</div> ))}
))}
{q.answers.length > 1 && ( {question.answers.length > 1 && (
<button <button
type="button" type="button"
onClick={() => setExpandedId(expandedId === q.id ? null : q.id)} onClick={() => setExpandedId(expandedId === question._id ? null : question._id)}
className="mt-3 flex items-center gap-1 text-xs text-primary" className="mt-3 flex items-center gap-1 text-xs text-primary"
> >
{expandedId === q.id ? 'بستن پاسخ‌ها' : 'مشاهده پاسخ های دیگر'} {expandedId === question._id ? 'بستن پاسخ‌ها' : 'مشاهده پاسخ های دیگر'}
<ArrowDown2 size={12} color="blue" className={expandedId === q.id ? 'rotate-180 transition' : 'transition sm:w-[14px] sm:h-[14px]'} /> <ArrowDown2 size={12} color="blue" className={expandedId === question._id ? 'rotate-180 transition' : 'transition sm:w-[14px] sm:h-[14px]'} />
</button> </button>
)} )}
</div> </div>
)} )}
</div> </div>
{/* rows divided by parent divide-y */} </Fragment>
</Fragment> ))
))} ) : (
<div className="px-4 sm:px-6 py-8 text-center text-[#999999] text-sm">
هنوز سوالی ثبت نشده است
</div>
)}
</div> </div>
</div> </div>
+44 -66
View File
@@ -3,65 +3,33 @@ import { FC, useMemo, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Star1 } from 'iconsax-react' import { Star1 } from 'iconsax-react'
import { Product, Review } from '@/types/product.types'
type ReviewItem = { interface ReviewsProps {
id: number product: Product
author: string reviews?: Review[]
date: string
rating: number
text: string
isBuyer: boolean
} }
const REVIEWS: ReviewItem[] = [ const Reviews: FC<ReviewsProps> = ({ reviews = [] }) => {
{
id: 1,
author: 'علی رضوانی',
date: '1401 آذر 18',
rating: 3.2,
text: 'قیمتش خوب بود',
isBuyer: true,
},
{
id: 2,
author: 'علی رضوانی',
date: '1401 آذر 18',
rating: 3.0,
text: 'گوشیشه همه چی تمام',
isBuyer: true,
},
{
id: 3,
author: 'علی رضوانی',
date: '1401 آذر 18',
rating: 1.5,
text: 'با کیفیت، خوش دست. بدون هیچ امتیاز قابل توجه نسبت به s23',
isBuyer: true,
},
{
id: 4,
author: 'علی رضوانی',
date: '1401 آذر 18',
rating: 4.0,
text: 'واقعاً گوشی فوق‌العاده‌ای هست هیچ نکته ی منفی ندارد',
isBuyer: true,
},
]
const Reviews: FC = () => {
const [sort, setSort] = useState<'new' | 'helpful'>('new') const [sort, setSort] = useState<'new' | 'helpful'>('new')
const sortedReviews = useMemo(() => { const sortedReviews = useMemo(() => {
if (sort === 'new') { if (sort === 'new') {
return [...REVIEWS].sort((a, b) => b.id - a.id) return [...reviews].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
} }
return [...REVIEWS].sort((a, b) => b.rating - a.rating) return [...reviews].sort((a, b) => b.helpful - a.helpful)
}, [sort]) }, [sort, reviews])
const average = useMemo(() => { const average = useMemo(() => {
const sum = REVIEWS.reduce((acc, r) => acc + r.rating, 0) if (reviews.length === 0) return '0.0'
return (sum / REVIEWS.length).toFixed(1) const sum = reviews.reduce((acc, r) => acc + r.rating, 0)
}, []) return (sum / reviews.length).toFixed(1)
}, [reviews])
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('fa-IR')
}
return ( return (
<div className="mt-12 sm:mt-16 w-full"> <div className="mt-12 sm:mt-16 w-full">
@@ -97,26 +65,36 @@ const Reviews: FC = () => {
</div> </div>
<div className="mt-4 sm:mt-6 rounded-xl bg-white"> <div className="mt-4 sm:mt-6 rounded-xl bg-white">
{sortedReviews.map((r, idx) => ( {sortedReviews.length > 0 ? (
<div key={r.id} className="px-4 sm:px-6 py-4 sm:py-5"> sortedReviews.map((review, idx) => (
<div className="flex flex-col sm:flex-row sm:items-center justify-start gap-2 sm:gap-3"> <div key={review._id} className="px-4 sm:px-6 py-4 sm:py-5">
<div className="px-2 py-0.5 rounded-full bg-[#02A55A] text-white text-xs w-fit">{r.rating}</div> <div className="flex flex-col sm:flex-row sm:items-center justify-start gap-2 sm:gap-3">
<div className="flex flex-wrap items-center gap-1 sm:gap-2 text-xs text-[#999999]"> <div className="px-2 py-0.5 rounded-full bg-[#02A55A] text-white text-xs w-fit">
{r.isBuyer && ( {review.rating.toFixed(1)}
<span className="px-2 py-0.5 rounded bg-[#F1F1F1] text-[#7F7F7F]">خریدار</span> </div>
)} <div className="flex flex-wrap items-center gap-1 sm:gap-2 text-xs text-[#999999]">
<span className="size-1.5 rounded-full bg-[#E5E5E5]"></span> {review.isVerified && (
<span>{r.date}</span> <span className="px-2 py-0.5 rounded bg-[#F1F1F1] text-[#7F7F7F]">خریدار</span>
<span className="size-1.5 rounded-full bg-[#E5E5E5]"></span> )}
<span>{r.author}</span> <span className="size-1.5 rounded-full bg-[#E5E5E5]"></span>
<span>{formatDate(review.date)}</span>
<span className="size-1.5 rounded-full bg-[#E5E5E5]"></span>
<span>{review.user.name}</span>
</div>
</div> </div>
<div className="mt-3 text-[#333333] text-xs sm:text-sm leading-6 sm:leading-7 text-right">
{review.comment}
</div>
{idx !== sortedReviews.length - 1 && <div className="mt-4 sm:mt-5"> <Separator /> </div>}
</div> </div>
))
<div className="mt-3 text-[#333333] text-xs sm:text-sm leading-6 sm:leading-7 text-right">{r.text}</div> ) : (
<div className="px-4 sm:px-6 py-8 text-center text-[#999999] text-sm">
{idx !== sortedReviews.length - 1 && <div className="mt-4 sm:mt-5"> <Separator /> </div>} هنوز نظری ثبت نشده است
</div> </div>
))} )}
</div> </div>
</div> </div>
+46 -14
View File
@@ -1,9 +1,29 @@
'use client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { ArchiveTick, BoxTick, I3DRotate } from 'iconsax-react' import { ArchiveTick, BoxTick, I3DRotate } from 'iconsax-react'
import React from 'react' import React from 'react'
import { Product } from '@/types/product.types'
import Image from 'next/image'
interface ShopInformationProps {
product: Product
}
const ShopInformation = ({ product }: ShopInformationProps) => {
const defaultVariant = product.default_variant
const shop = defaultVariant.shop
const price = defaultVariant.price
const warranty = defaultVariant.warranty
const postingTime = defaultVariant.postingTime
const formatPrice = (price: number) => {
return price.toLocaleString('fa-IR')
}
const discountPercent = price.discount_percent
const hasDiscount = discountPercent > 0
const ShopInformation = () => {
return ( return (
<div className='w-full xl:w-[325px] bg-[#FAFAFA] rounded-[10px] p-4 sm:p-6 lg:p-7 border border-border h-fit xl:sticky xl:top-[170px] xl:self-start'> <div className='w-full xl:w-[325px] bg-[#FAFAFA] rounded-[10px] p-4 sm:p-6 lg:p-7 border border-border h-fit xl:sticky xl:top-[170px] xl:self-start'>
<div className='text-lg text-[#191919]'> <div className='text-lg text-[#191919]'>
@@ -11,9 +31,17 @@ const ShopInformation = () => {
</div> </div>
<div className='mt-9 flex gap-3 border-b border-border pb-4'> <div className='mt-9 flex gap-3 border-b border-border pb-4'>
<div className='size-7 rounded-full bg-primary'></div> <div className='size-7 rounded-full bg-primary flex items-center justify-center overflow-hidden'>
<Image
src={shop.logo}
alt={shop.shopName}
width={28}
height={28}
className='w-full h-full object-cover'
/>
</div>
<div> <div>
<div className='text-[#333333] text-sm'>فروشگاه سندس</div> <div className='text-[#333333] text-sm'>{shop.shopName}</div>
<div className='mt-2 text-xs text-[#999999] font-light flex items-center gap-2'> <div className='mt-2 text-xs text-[#999999] font-light flex items-center gap-2'>
<div> <div>
<span className='text-[#019907]'>50%</span> رضایت کالا <span className='text-[#019907]'>50%</span> رضایت کالا
@@ -26,19 +54,19 @@ const ShopInformation = () => {
<div className='flex gap-4 text-sm font-light border-b border-border pb-4 mt-4'> <div className='flex gap-4 text-sm font-light border-b border-border pb-4 mt-4'>
<ArchiveTick size={20} color='#333333' /> <ArchiveTick size={20} color='#333333' />
<div>گارانتی اصالت و سلامت فیزیکی کالا</div> <div>{warranty.name}</div>
</div> </div>
<div className='mt-4'> <div className='mt-4'>
<div className='flex gap-4 text-sm font-light'> <div className='flex gap-4 text-sm font-light'>
<ArchiveTick size={20} color='#333333' /> <ArchiveTick size={20} color='#333333' />
<div>گارانتی اصالت و سلامت فیزیکی کالا</div> <div>مدت گارانتی: {warranty.duration}</div>
</div> </div>
</div> </div>
<div className='mt-6 border-b border-border pb-4'> <div className='mt-6 border-b border-border pb-4'>
<div className='flex gap-4 text-sm font-light'> <div className='flex gap-4 text-sm font-light'>
<BoxTick size={20} color='blue' /> <BoxTick size={20} color='blue' />
<div className='text-[#999999]'>ارسال از سندس حداکثر پس از 3 روز کاری</div> <div className='text-[#999999]'>ارسال از {shop.shopName} حداکثر پس از {postingTime} روز کاری</div>
</div> </div>
</div> </div>
@@ -47,17 +75,21 @@ const ShopInformation = () => {
<div className='text-primary'>گارانتی اصالت و سلامت فیزیکی کالا</div> <div className='text-primary'>گارانتی اصالت و سلامت فیزیکی کالا</div>
</div> </div>
<div className='mt-4 text-sm text-[#999999] font-light flex justify-between'> {hasDiscount && (
<div>قیمت برای شما</div> <div className='mt-4 text-sm text-[#999999] font-light flex justify-between'>
<div className='line-through text-base'>70,400,000</div> <div>قیمت برای شما</div>
</div> <div className='line-through text-base'>{formatPrice(price.retailPrice)}</div>
</div>
)}
<div className='mt-3 flex justify-between items-center'> <div className='mt-3 flex justify-between items-center'>
<div className='h-6 text-xs px-3 bg-primary rounded-full text-white w-fit flex items-center justify-center'> {hasDiscount && (
5% <div className='h-6 text-xs px-3 bg-primary rounded-full text-white w-fit flex items-center justify-center'>
</div> {discountPercent}%
</div>
)}
<div> <div>
<span className='text-lg'>70,400,000</span> <span className='text-xs'>تومان</span> <span className='text-lg'>{formatPrice(price.selling_price)}</span> <span className='text-xs'>تومان</span>
</div> </div>
</div> </div>
@@ -0,0 +1,89 @@
'use client'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import ProductImage from '@/app/product/components/ProductImage'
import BaseInformation from '@/app/product/components/BaseInformation'
import ShopInformation from '@/app/product/components/ShopInformation'
import Specifications from '@/app/product/components/Specifications'
import ProsCons from '@/app/product/components/ProsCons'
import Description from '@/app/product/components/Description'
import Reviews from '@/app/product/components/Reviews'
import Questions from '@/app/product/components/Questions'
import { useGetDetailProduct } from '@/app/product/hooks/useProductData'
import PageLoading from '@/components/PageLoading'
import { notFound } from 'next/navigation'
interface SingleProductProps {
id: string
}
const SingleProduct = ({ id }: SingleProductProps) => {
const { data: productData, isLoading, error } = useGetDetailProduct(id)
if (isLoading) {
return <PageLoading />
}
if (error || !productData) {
notFound()
}
const { product, categoryPath, reviews, questions } = productData.results
return (
<div className='mt-14 px-4 sm:px-6 md:px-8 lg:px-12 xl:px-20'>
<div className='flex flex-col sm:flex-row justify-between gap-4 sm:gap-0'>
<Breadcrumb aria-label="breadcrumb">
<BreadcrumbList className="text-sm text-muted-foreground">
<BreadcrumbItem>
<BreadcrumbLink href="/">فروشگاه آناهیتا</BreadcrumbLink>
</BreadcrumbItem>
{categoryPath.map((category, index) => (
<div key={category.id} className="flex items-center">
<BreadcrumbSeparator className="text-muted-foreground select-none">/</BreadcrumbSeparator>
<BreadcrumbItem>
{index === categoryPath.length - 1 ? (
<BreadcrumbPage className="text-foreground">{category.title_fa}</BreadcrumbPage>
) : (
<BreadcrumbLink href={`/products?category=${category.id}`}>
{category.title_fa}
</BreadcrumbLink>
)}
</BreadcrumbItem>
</div>
))}
</BreadcrumbList>
</Breadcrumb>
<div className='text-primary text-xs sm:text-sm'>فروشنده شوید</div>
</div>
<div className='flex flex-col xl:flex-row gap-4 sm:gap-6 mt-8 sm:mt-12 md:mt-14'>
<div className='flex-1 flex flex-col gap-6'>
<div className='flex flex-col lg:flex-row gap-4 sm:gap-6'>
<ProductImage product={product} />
<BaseInformation product={product} />
</div>
<div>
<Specifications product={product} />
<Description product={product} />
<ProsCons product={product} />
<Reviews product={product} reviews={reviews} />
<Questions product={product} questions={questions} />
</div>
</div>
<div className='xl:w-[325px] xl:shrink-0'>
<ShopInformation product={product} />
</div>
</div>
</div>
)
}
export default SingleProduct
+14 -34
View File
@@ -1,29 +1,13 @@
'use client'
import { FC, Fragment } from 'react' import { FC, Fragment } from 'react'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Product } from '@/types/product.types'
type SpecItem = { interface SpecificationsProps {
label: string product: Product
value: string | string[]
} }
const SPEC_ITEMS: SpecItem[] = [ const Specifications: FC<SpecificationsProps> = ({ product }) => {
{ label: 'ابعاد', value: 'میلی‌متر 163.3×79×8.6' },
{ label: 'وزن', value: '233 گرم' },
{ label: 'توضیحات سیم کارت', value: 'سایز نانو (12.3 × 8.8 میلی‌متر)' },
{
label: 'ویژگی‌های خاص',
value: [
'مجهز به حس‌گر اثرانگشت',
'مقاوم در برابر آب',
'مناسب بازی',
'مناسب عکاسی',
'مناسب عکاسی سلفی',
],
},
{ label: 'تعداد سیم کارت', value: 'دو عدد' },
]
const Specifications: FC = () => {
return ( return (
<div className="mt-12 sm:mt-16 w-full"> <div className="mt-12 sm:mt-16 w-full">
<div className="flex items-center justify-end gap-3 flex-row-reverse"> <div className="flex items-center justify-end gap-3 flex-row-reverse">
@@ -32,26 +16,22 @@ const Specifications: FC = () => {
</div> </div>
<div className="mt-4 sm:mt-6 rounded-xl bg-white"> <div className="mt-4 sm:mt-6 rounded-xl bg-white">
{SPEC_ITEMS.map((item, index) => ( {product.specifications.map((spec, index) => (
<Fragment key={item.label}> <Fragment key={spec.title}>
<div className="grid grid-cols-12 items-start px-4 sm:px-6 py-4 sm:py-5"> <div className="grid grid-cols-12 items-start px-4 sm:px-6 py-4 sm:py-5">
<div className="col-span-12 md:col-span-3 flex items-center justify-end gap-2 text-[#7F7F7F] flex-row-reverse"> <div className="col-span-12 md:col-span-3 flex items-center justify-end gap-2 text-[#7F7F7F] flex-row-reverse">
<span className="text-xs sm:text-sm">{item.label}</span> <span className="text-xs sm:text-sm">{spec.title}</span>
<span className="size-1.5 rounded-full bg-[#7F7F7F]"></span> <span className="size-1.5 rounded-full bg-[#7F7F7F]"></span>
</div> </div>
<div className="col-span-12 md:col-span-9 mt-2 md:mt-0"> <div className="col-span-12 md:col-span-9 mt-2 md:mt-0">
{Array.isArray(item.value) ? ( <div className="flex flex-col gap-2 sm:gap-3 text-xs sm:text-sm text-[#333333] text-right">
<div className="flex flex-col gap-2 sm:gap-3 text-xs sm:text-sm text-[#333333] text-right"> {spec.values.map((value, valueIndex) => (
{item.value.map((v) => ( <div key={valueIndex}>{value}</div>
<div key={v}>{v}</div> ))}
))} </div>
</div>
) : (
<div className="text-xs sm:text-sm text-[#333333] text-right">{item.value}</div>
)}
</div> </div>
</div> </div>
{index !== SPEC_ITEMS.length - 1 && <Separator />} {index !== product.specifications.length - 1 && <Separator />}
</Fragment> </Fragment>
))} ))}
</div> </div>
+14
View File
@@ -0,0 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
export const useGetDetailProduct = (id: string) => {
return useQuery({
queryKey: ["product", id],
queryFn: () => api.getProduct(id),
});
};
export const getProductQueryOptions = (id: string) => ({
queryKey: ["product", id],
queryFn: () => api.getProduct(id),
});
+9
View File
@@ -0,0 +1,9 @@
import axios from "@/config/axios";
import { ProductDetailResponse } from "@/types/product.types";
export const getProduct = async (
id: string
): Promise<ProductDetailResponse> => {
const { data } = await axios.get(`/product/${id}`);
return data;
};
+21 -15
View File
@@ -40,7 +40,6 @@ const ProfilePage = () => {
<Menu pageActive='profile' /> <Menu pageActive='profile' />
<div className='mt-4 sm:mt-6 md:mt-8 lg:mt-10'> <div className='mt-4 sm:mt-6 md:mt-8 lg:mt-10'>
<div className="flex flex-col lg:flex-row gap-4 sm:gap-6"> <div className="flex flex-col lg:flex-row gap-4 sm:gap-6">
{/* Left Content - Form */}
<div className="flex-1"> <div className="flex-1">
<div className="bg-[#FAFAFA] rounded-lg p-3 sm:p-4 md:p-6"> <div className="bg-[#FAFAFA] rounded-lg p-3 sm:p-4 md:p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-4 sm:mb-6 gap-3 sm:gap-0"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-4 sm:mb-6 gap-3 sm:gap-0">
@@ -150,25 +149,32 @@ const ProfilePage = () => {
</div> </div>
</div> </div>
{/* Quick Actions Cards */} <div className='flex-1 flex lg:flex-row flex-col gap-6'>
<div className='flex flex-row lg:flex-col gap-3 sm:gap-4 lg:gap-6 lg:min-w-[200px]'> <div className='flex flex-row lg:flex-col gap-3 sm:gap-4 lg:gap-6 flex-1'>
<div className='flex-1 lg:w-full bg-[#FAFAFA] rounded-[10px] flex flex-col gap-3 sm:gap-4 lg:gap-6 justify-center items-center h-[100px] sm:h-[120px] lg:h-[160px] xl:h-[200px]'> <div className='flex-1 lg:w-full bg-[#FAFAFA] rounded-[10px] flex flex-col gap-3 sm:gap-4 lg:gap-6 justify-center items-center h-[100px] sm:h-[120px] lg:h-[160px] xl:h-[200px]'>
<div className='size-10 sm:size-12 lg:size-16 xl:size-20 bg-[#F2F2F2] rounded-full flex justify-center items-center'> <div className='size-10 sm:size-12 lg:size-16 xl:size-20 bg-[#F2F2F2] rounded-full flex justify-center items-center'>
<ShoppingCart size={20} color='#000' className='sm:w-6 sm:h-6 lg:w-8 lg:h-8 xl:w-10 xl:h-10' /> <ShoppingCart size={20} color='#000' className='sm:w-6 sm:h-6 lg:w-8 lg:h-8 xl:w-10 xl:h-10' />
</div>
<div className='text-xs sm:text-sm lg:text-base text-center'>
سفارشات
</div>
</div> </div>
<div className='text-xs sm:text-sm lg:text-base text-center'> <div className='flex-1 lg:w-full bg-[#FAFAFA] rounded-[10px] flex flex-col gap-3 sm:gap-4 lg:gap-6 justify-center items-center h-[100px] sm:h-[120px] lg:h-[160px] xl:h-[200px]'>
سفارشات <div className='size-10 sm:size-12 lg:size-16 xl:size-20 bg-[#F2F2F2] rounded-full flex justify-center items-center'>
<Heart size={20} color='#000' className='sm:w-6 sm:h-6 lg:w-8 lg:h-8 xl:w-10 xl:h-10' />
</div>
<div className='text-xs sm:text-sm lg:text-base text-center'>
علاقه مندی ها
</div>
</div> </div>
</div> </div>
<div className='flex-1 lg:w-full bg-[#FAFAFA] rounded-[10px] flex flex-col gap-3 sm:gap-4 lg:gap-6 justify-center items-center h-[100px] sm:h-[120px] lg:h-[160px] xl:h-[200px]'>
<div className='size-10 sm:size-12 lg:size-16 xl:size-20 bg-[#F2F2F2] rounded-full flex justify-center items-center'> <div className='flex-1 bg-red-50 '>
<Heart size={20} color='#000' className='sm:w-6 sm:h-6 lg:w-8 lg:h-8 xl:w-10 xl:h-10' />
</div>
<div className='text-xs sm:text-sm lg:text-base text-center'>
علاقه مندی ها
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
+4 -2
View File
@@ -1,3 +1,4 @@
'use client'
import { IProduct } from '@/types/landing.types' import { IProduct } from '@/types/landing.types'
import { NumberFormat } from '@/config/func' import { NumberFormat } from '@/config/func'
import { Heart } from 'iconsax-react' import { Heart } from 'iconsax-react'
@@ -5,6 +6,7 @@ import Image from 'next/image'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useAddWishlist, useRemoveWishlist } from '@/app/products/hooks/useProductsData' import { useAddWishlist, useRemoveWishlist } from '@/app/products/hooks/useProductsData'
import { useSharedStore } from '@/share/store/sharedStore' import { useSharedStore } from '@/share/store/sharedStore'
import Link from 'next/link'
type Props = { type Props = {
item?: IProduct item?: IProduct
@@ -34,7 +36,7 @@ const ProductCard: FC<Props> = ({ item }) => {
} }
if (item) return ( if (item) return (
<div className='bg-white rounded-[20px] sm:rounded-[40px] p-4 sm:p-6 w-full max-w-[240px] sm:max-w-[280px] mx-auto shadow'> <Link href={`/product/${item._id}`} className='bg-white block rounded-[20px] sm:rounded-[40px] p-4 sm:p-6 w-full max-w-[240px] sm:max-w-[280px] mx-auto shadow'>
<div className='flex justify-center'> <div className='flex justify-center'>
<Image <Image
src={item.imagesUrl.cover} src={item.imagesUrl.cover}
@@ -89,7 +91,7 @@ const ProductCard: FC<Props> = ({ item }) => {
<div className='flex justify-end'> <div className='flex justify-end'>
<Heart onClick={handleWish} variant={isWished ? 'Bold' : 'Outline'} size={20} color='#000' /> <Heart onClick={handleWish} variant={isWished ? 'Bold' : 'Outline'} size={20} color='#000' />
</div> </div>
</div> </Link>
) )
return ( return (
<div className='bg-white rounded-[20px] sm:rounded-[40px] p-4 sm:p-6 w-full max-w-[240px] sm:max-w-[280px] mx-auto shadow'> <div className='bg-white rounded-[20px] sm:rounded-[40px] p-4 sm:p-6 w-full max-w-[240px] sm:max-w-[280px] mx-auto shadow'>
+163
View File
@@ -0,0 +1,163 @@
export interface ProductPrice {
order_limit: number;
retailPrice: number;
selling_price: number;
is_specialSale: boolean;
discount_percent: number;
specialSale_order_limit: number | null;
specialSale_quantity: number | null;
specialSale_endDate: string | null;
}
export interface ProductSize {
_id: number;
value: string;
}
export interface ProductWarranty {
_id: number;
duration: string;
logoUrl: string;
name: string;
}
export interface ProductShop {
_id: string;
shopName: string;
shopCode: number;
owner: string;
shopDescription: string;
telephoneNumber: string;
isChatActive: boolean;
shopPostalCode: string;
logo: string;
}
export interface ShipmentMethod {
_id: number;
name: string;
description: string;
deliveryTime: number;
deliveryType: string;
}
export interface ProductVariant {
_id: string;
market_status: string;
price: ProductPrice;
stock: number;
postingTime: number;
isFreeShip: boolean;
isWholeSale: boolean;
shop: ProductShop;
shipmentMethod: ShipmentMethod[];
warranty: ProductWarranty;
size: ProductSize;
}
export interface ProductBrand {
_id: string;
status: string;
title_en: string;
title_fa: string;
images: string[];
logoUrl: string;
}
export interface ProductCategory {
_id: string;
title_fa: string;
title_en: string;
icon: string;
imageUrl: string;
theme: string;
description: string;
leaf: boolean;
parent: string;
}
export interface ProductSpecification {
title: string;
values: string[];
}
export interface ProductImages {
cover: string;
list: string[];
}
export interface CategoryPathItem {
id: string;
title_fa: string;
title_en: string;
}
export interface Review {
_id: string;
user: {
name: string;
avatar?: string;
};
rating: number;
comment: string;
date: string;
isVerified: boolean;
helpful: number;
}
export interface Question {
_id: string;
user: {
name: string;
avatar?: string;
};
question: string;
answers: Answer[];
date: string;
helpful: number;
}
export interface Answer {
_id: string;
user: {
name: string;
avatar?: string;
};
answer: string;
date: string;
helpful: number;
}
export interface Product {
_id: number;
url: string;
title_fa: string;
title_en: string;
seoTitle: string | null;
seoDescription: string | null;
source: string;
description: string;
metaDescription: string;
tags: string[];
advantages: string[];
disAdvantages: string[];
imagesUrl: ProductImages;
isFake: string;
voice: string | null;
specifications: ProductSpecification[];
brand: ProductBrand;
category: ProductCategory;
default_variant: ProductVariant;
variants: ProductVariant[];
}
export interface ProductDetailResponse {
status: number;
success: boolean;
results: {
product: Product;
categoryPath: CategoryPathItem[];
reviews?: Review[];
questions?: Question[];
};
}