From 2995b11900fccc3f6fbccbf7c96d625edbd1b901 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 7 Sep 2025 09:17:58 +0330 Subject: [PATCH] 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 --- src/app/globals.css | 2 +- src/app/product/[id]/page.tsx | 81 ++------- src/app/product/components/ActionProduct.tsx | 90 +++++++++- .../product/components/BaseInformation.tsx | 60 ++++--- src/app/product/components/Description.tsx | 21 ++- src/app/product/components/ProductImage.tsx | 43 ++++- src/app/product/components/ProsCons.tsx | 39 +++-- src/app/product/components/Questions.tsx | 139 +++++++-------- src/app/product/components/Reviews.tsx | 110 +++++------- .../product/components/ShopInformation.tsx | 60 +++++-- src/app/product/components/SingleProduct.tsx | 89 ++++++++++ src/app/product/components/Specifications.tsx | 48 ++---- src/app/product/hooks/useProductData.ts | 14 ++ src/app/product/service/Service.ts | 9 + src/app/profile/page.tsx | 36 ++-- src/components/ProductCard.tsx | 6 +- src/types/product.types.ts | 163 ++++++++++++++++++ 17 files changed, 667 insertions(+), 343 deletions(-) create mode 100644 src/app/product/components/SingleProduct.tsx create mode 100644 src/app/product/hooks/useProductData.ts create mode 100644 src/app/product/service/Service.ts create mode 100644 src/types/product.types.ts diff --git a/src/app/globals.css b/src/app/globals.css index b3c72ba..b004c8c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -219,7 +219,7 @@ textarea.place-black::placeholder { .swiper-pagination { z-index: 1 !important; } -div { +* { direction: rtl; } diff --git a/src/app/product/[id]/page.tsx b/src/app/product/[id]/page.tsx index 09a0883..284b7dd 100644 --- a/src/app/product/[id]/page.tsx +++ b/src/app/product/[id]/page.tsx @@ -1,71 +1,28 @@ -import { NextPage } from 'next' import Layout from '@/hoc/Layout' -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 SingleProduct from '@/app/product/components/SingleProduct' +import { getProductQueryOptions } from '@/app/product/hooks/useProductData' +import { HydrationBoundary, QueryClient, dehydrate } from '@tanstack/react-query' +import { Suspense } from 'react' +import PageLoading from '@/components/PageLoading' -const SingleProduct: NextPage = () => { - return ( -
-
- - - - فروشگاه آناهیتا - - / - - بهداشت خانگی - - / - - مواد شوینده - - - - -
فروشنده شوید
-
- -
-
- -
-
-
- - -
-
- - - - - -
-
-
-
- ) +interface SingleProductPageProps { + params: { id: string } } -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 ( - + }> + + + + ) } \ No newline at end of file diff --git a/src/app/product/components/ActionProduct.tsx b/src/app/product/components/ActionProduct.tsx index b44010f..57760e4 100644 --- a/src/app/product/components/ActionProduct.tsx +++ b/src/app/product/components/ActionProduct.tsx @@ -1,15 +1,87 @@ +'use client' 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 = ({ 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 ( -
-
- - - - - +
+
+ + + + + + + + +
) diff --git a/src/app/product/components/BaseInformation.tsx b/src/app/product/components/BaseInformation.tsx index 69f86fa..3b72df1 100644 --- a/src/app/product/components/BaseInformation.tsx +++ b/src/app/product/components/BaseInformation.tsx @@ -1,20 +1,30 @@ +'use client' import { Star1 } from 'iconsax-react' import { FC } from 'react' +import { Product } from '@/types/product.types' + +interface BaseInformationProps { + product: Product +} + +const BaseInformation: FC = ({ product }) => { + const defaultVariant = product.default_variant + const brand = product.brand + const size = defaultVariant.size -const BaseInformation: FC = () => { return (

- گوشی موبایل سامسونگ مدل Galaxy S24 Ultra دو سیم کارت ظرفیت 1 ترابایت و رم 12 گیگابایت - ویتنام + {product.title_fa}

- Samsung Galaxy S24 Ultra Dual SIM 256GB And 12GB RAM Mobile Phone - Vietnam + {product.title_en}

- سامسونگ + {brand.title_fa}

@@ -28,13 +38,19 @@ const BaseInformation: FC = () => {
- رنگ : طلایی + اندازه : {size.value}
-
-
-
+ {product.variants.map((variant, index) => ( +
+ {index + 1} +
+ ))}
@@ -42,27 +58,15 @@ const BaseInformation: FC = () => {
-
-
-
-
فناوری صفحه‌ نمایش :
+ {product.specifications.slice(0, 3).map((spec, index) => ( +
+
+
+
{spec.title} :
+
+
{spec.values.join(', ')}
-
Dynamic LTPO AMOLED 2X
-
-
-
-
-
رزولوشن عکس :
-
-
200 مگاپیکسل
-
-
-
-
-
نسخه سیستم عامل :
-
-
Android 14
-
+ ))}
) diff --git a/src/app/product/components/Description.tsx b/src/app/product/components/Description.tsx index 160b115..e9a44b4 100644 --- a/src/app/product/components/Description.tsx +++ b/src/app/product/components/Description.tsx @@ -1,12 +1,17 @@ +'use client' import { FC } from 'react' +import { Product } from '@/types/product.types' -const DESCRIPTION: string[] = [ - 'همواره گوشی‌های هوشمند پرچمدار سامسونگ توانسته‌اند با بهره بردن از مشخصات فنی قدرتمند، توجه هر بیننده‌ای را به خود جلب کنند. سامسونگ Galaxy S24 Ultra نیز از این قاعده مستثنا نیست و با نمایشگری باکیفیت، پردازنده پرتوان و سیستم دوربین قدرتمند به میدان آمده است.', - 'صفحه‌نمایش 6.8 اینچی این گوشی با رزولوشن 1440×3120 پیکسل و نرخ نوسازی 120 هرتز، تجربه‌ای روان و چشم‌نواز ارائه می‌دهد. در بخش دوربین نیز سنسور اصلی 200 مگاپیکسلی در کنار سنسور 12 مگاپیکسلی فوق‌عریض و سنسور تله‌فوتو با بزرگنمایی اپتیکال 3 برابر، ثبت تصاویر با جزئیات و داینامیک رنج عالی را ممکن کرده است.', - 'ترکیب این سخت‌افزار قدرتمند با نرم‌افزار بهینه، باعث شده که Galaxy S24 Ultra انتخاب مناسبی برای بازی، عکاسی و استفاده روزمره باشد. اگر به دنبال گوشی‌ای با کارایی بالا و امکانات کامل هستید، این مدل می‌تواند گزینه‌ای ایده‌آل برای شما باشد.' -] +interface DescriptionProps { + product: Product +} + +const Description: FC = ({ product }) => { + // تقسیم توضیحات به پاراگراف‌ها بر اساس خطوط جدید + const descriptionParagraphs = product.description + .split('\n') + .filter(paragraph => paragraph.trim().length > 0) -const Description: FC = () => { return (
@@ -16,8 +21,8 @@ const Description: FC = () => {
- {DESCRIPTION.map((paragraph) => ( -

{paragraph}

+ {descriptionParagraphs.map((paragraph, index) => ( +

{paragraph}

))}
diff --git a/src/app/product/components/ProductImage.tsx b/src/app/product/components/ProductImage.tsx index d512ca1..3002e66 100644 --- a/src/app/product/components/ProductImage.tsx +++ b/src/app/product/components/ProductImage.tsx @@ -1,25 +1,50 @@ +'use client' import Image from 'next/image' -import { FC } from 'react' +import { FC, useState } from 'react' import ActionProduct from './ActionProduct' +import { Product } from '@/types/product.types' + +interface ProductImageProps { + product: Product +} + +const ProductImage: FC = ({ product }) => { + const [selectedImageIndex, setSelectedImageIndex] = useState(0) + + const allImages = [product.imagesUrl.cover, ...product.imagesUrl.list] + const selectedImage = allImages[selectedImageIndex] -const ProductImage: FC = () => { return (
- +
-
-
-
-
+ {allImages.map((image, index) => ( +
setSelectedImageIndex(index)} + > + {`${product.title_fa} +
+ ))}
) diff --git a/src/app/product/components/ProsCons.tsx b/src/app/product/components/ProsCons.tsx index 694d63e..4487c6b 100644 --- a/src/app/product/components/ProsCons.tsx +++ b/src/app/product/components/ProsCons.tsx @@ -1,20 +1,13 @@ +'use client' import { FC } from 'react' 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 = () => { +const ProsCons: FC = ({ product }) => { return (
@@ -30,9 +23,13 @@ const ProsCons: FC = () => {
- {PROS.map((v) => ( -
{v}
- ))} + {product.advantages.length > 0 ? ( + product.advantages.map((advantage, index) => ( +
{advantage}
+ )) + ) : ( +
نقطه مثبتی ثبت نشده است
+ )}
@@ -46,9 +43,13 @@ const ProsCons: FC = () => {
- {CONS.map((v) => ( -
{v}
- ))} + {product.disAdvantages.length > 0 ? ( + product.disAdvantages.map((disadvantage, index) => ( +
{disadvantage}
+ )) + ) : ( +
نقطه منفی ثبت نشده است
+ )}
diff --git a/src/app/product/components/Questions.tsx b/src/app/product/components/Questions.tsx index 4f9dc0f..1ccb8de 100644 --- a/src/app/product/components/Questions.tsx +++ b/src/app/product/components/Questions.tsx @@ -3,50 +3,28 @@ import { FC, Fragment, useMemo, useState } from 'react' import { Button } from '@/components/ui/button' import { ArrowDown2, MessageQuestion } from 'iconsax-react' +import { Product, Question } from '@/types/product.types' -type AnswerItem = { - id: number - text: string +interface QuestionsProps { + product: Product + questions?: Question[] } -type QuestionItem = { - 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 Questions: FC = ({ questions = [] }) => { const [sort, setSort] = useState<'new' | 'answers'>('new') - const [expandedId, setExpandedId] = useState(null) + const [expandedId, setExpandedId] = useState(null) const sorted = useMemo(() => { - if (sort === 'new') return [...QUESTIONS].sort((a, b) => b.id - a.id) - return [...QUESTIONS].sort((a, b) => b.answers.length - a.answers.length) - }, [sort]) + if (sort === 'new') { + return [...questions].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + } + 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 (
@@ -80,51 +58,60 @@ const Questions: FC = () => {
- {sorted.map((q) => ( - -
-
-
-
- + {sorted.length > 0 ? ( + sorted.map((question) => ( + +
+
+
+
+ +
+
+
+ {question.question}
-
- {q.text} + +
+ {question.user.name} • {formatDate(question.date)}
-
- {q.answers.length === 0 && ( -
ثبت پاسخ
- )} + {question.answers.length === 0 && ( +
ثبت پاسخ
+ )} - {q.answers.length > 0 && ( -
- {(expandedId === q.id ? q.answers : q.answers.slice(0, 1)).map((a) => ( -
-
پاسخ
-
- {a.text} + {question.answers.length > 0 && ( +
+ {(expandedId === question._id ? question.answers : question.answers.slice(0, 1)).map((answer) => ( +
+
پاسخ
+
+ {answer.answer} +
-
- ))} + ))} - {q.answers.length > 1 && ( - - )} -
- )} -
- {/* rows divided by parent divide-y */} - - ))} + {question.answers.length > 1 && ( + + )} +
+ )} +
+
+ )) + ) : ( +
+ هنوز سوالی ثبت نشده است +
+ )}
diff --git a/src/app/product/components/Reviews.tsx b/src/app/product/components/Reviews.tsx index d3c0da2..e2a52cb 100644 --- a/src/app/product/components/Reviews.tsx +++ b/src/app/product/components/Reviews.tsx @@ -3,65 +3,33 @@ import { FC, useMemo, useState } from 'react' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { Star1 } from 'iconsax-react' +import { Product, Review } from '@/types/product.types' -type ReviewItem = { - id: number - author: string - date: string - rating: number - text: string - isBuyer: boolean +interface ReviewsProps { + product: Product + reviews?: Review[] } -const REVIEWS: ReviewItem[] = [ - { - 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 Reviews: FC = ({ reviews = [] }) => { const [sort, setSort] = useState<'new' | 'helpful'>('new') const sortedReviews = useMemo(() => { 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) - }, [sort]) + return [...reviews].sort((a, b) => b.helpful - a.helpful) + }, [sort, reviews]) const average = useMemo(() => { - const sum = REVIEWS.reduce((acc, r) => acc + r.rating, 0) - return (sum / REVIEWS.length).toFixed(1) - }, []) + if (reviews.length === 0) return '0.0' + 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 (
@@ -97,26 +65,36 @@ const Reviews: FC = () => {
- {sortedReviews.map((r, idx) => ( -
-
-
{r.rating}
-
- {r.isBuyer && ( - خریدار - )} - - {r.date} - - {r.author} + {sortedReviews.length > 0 ? ( + sortedReviews.map((review, idx) => ( +
+
+
+ {review.rating.toFixed(1)} +
+
+ {review.isVerified && ( + خریدار + )} + + {formatDate(review.date)} + + {review.user.name} +
+ +
+ {review.comment} +
+ + {idx !== sortedReviews.length - 1 &&
}
- -
{r.text}
- - {idx !== sortedReviews.length - 1 &&
} + )) + ) : ( +
+ هنوز نظری ثبت نشده است
- ))} + )}
diff --git a/src/app/product/components/ShopInformation.tsx b/src/app/product/components/ShopInformation.tsx index bbe5585..67df80f 100644 --- a/src/app/product/components/ShopInformation.tsx +++ b/src/app/product/components/ShopInformation.tsx @@ -1,9 +1,29 @@ +'use client' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { ArchiveTick, BoxTick, I3DRotate } from 'iconsax-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 (
@@ -11,9 +31,17 @@ const ShopInformation = () => {
-
+
+ {shop.shopName} +
-
فروشگاه سندس
+
{shop.shopName}
50% رضایت کالا @@ -26,19 +54,19 @@ const ShopInformation = () => {
-
گارانتی اصالت و سلامت فیزیکی کالا
+
{warranty.name}
-
گارانتی اصالت و سلامت فیزیکی کالا
+
مدت گارانتی: {warranty.duration}
-
ارسال از سندس حداکثر پس از 3 روز کاری
+
ارسال از {shop.shopName} حداکثر پس از {postingTime} روز کاری
@@ -47,17 +75,21 @@ const ShopInformation = () => {
گارانتی اصالت و سلامت فیزیکی کالا
-
-
قیمت برای شما
-
70,400,000
-
+ {hasDiscount && ( +
+
قیمت برای شما
+
{formatPrice(price.retailPrice)}
+
+ )}
-
- 5% -
+ {hasDiscount && ( +
+ {discountPercent}% +
+ )}
- 70,400,000 تومان + {formatPrice(price.selling_price)} تومان
diff --git a/src/app/product/components/SingleProduct.tsx b/src/app/product/components/SingleProduct.tsx new file mode 100644 index 0000000..41b4053 --- /dev/null +++ b/src/app/product/components/SingleProduct.tsx @@ -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 + } + + if (error || !productData) { + notFound() + } + + const { product, categoryPath, reviews, questions } = productData.results + + return ( +
+
+ + + + فروشگاه آناهیتا + + {categoryPath.map((category, index) => ( +
+ / + + {index === categoryPath.length - 1 ? ( + {category.title_fa} + ) : ( + + {category.title_fa} + + )} + +
+ ))} +
+
+ +
فروشنده شوید
+
+ +
+
+
+ + +
+
+ + + + + +
+
+
+ +
+
+
+ ) +} + +export default SingleProduct diff --git a/src/app/product/components/Specifications.tsx b/src/app/product/components/Specifications.tsx index aac00fc..32d0d2d 100644 --- a/src/app/product/components/Specifications.tsx +++ b/src/app/product/components/Specifications.tsx @@ -1,29 +1,13 @@ +'use client' import { FC, Fragment } from 'react' import { Separator } from '@/components/ui/separator' +import { Product } from '@/types/product.types' -type SpecItem = { - label: string - value: string | string[] +interface SpecificationsProps { + product: Product } -const SPEC_ITEMS: SpecItem[] = [ - { label: 'ابعاد', value: 'میلی‌متر 163.3×79×8.6' }, - { label: 'وزن', value: '233 گرم' }, - { label: 'توضیحات سیم کارت', value: 'سایز نانو (12.3 × 8.8 میلی‌متر)' }, - { - label: 'ویژگی‌های خاص', - value: [ - 'مجهز به حس‌گر اثرانگشت', - 'مقاوم در برابر آب', - 'مناسب بازی', - 'مناسب عکاسی', - 'مناسب عکاسی سلفی', - ], - }, - { label: 'تعداد سیم کارت', value: 'دو عدد' }, -] - -const Specifications: FC = () => { +const Specifications: FC = ({ product }) => { return (
@@ -32,26 +16,22 @@ const Specifications: FC = () => {
- {SPEC_ITEMS.map((item, index) => ( - + {product.specifications.map((spec, index) => ( +
- {item.label} + {spec.title}
- {Array.isArray(item.value) ? ( -
- {item.value.map((v) => ( -
{v}
- ))} -
- ) : ( -
{item.value}
- )} +
+ {spec.values.map((value, valueIndex) => ( +
{value}
+ ))} +
- {index !== SPEC_ITEMS.length - 1 && } + {index !== product.specifications.length - 1 && }
))}
diff --git a/src/app/product/hooks/useProductData.ts b/src/app/product/hooks/useProductData.ts new file mode 100644 index 0000000..1993c75 --- /dev/null +++ b/src/app/product/hooks/useProductData.ts @@ -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), +}); diff --git a/src/app/product/service/Service.ts b/src/app/product/service/Service.ts new file mode 100644 index 0000000..3b1074e --- /dev/null +++ b/src/app/product/service/Service.ts @@ -0,0 +1,9 @@ +import axios from "@/config/axios"; +import { ProductDetailResponse } from "@/types/product.types"; + +export const getProduct = async ( + id: string +): Promise => { + const { data } = await axios.get(`/product/${id}`); + return data; +}; diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 810ab01..8992f7c 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -40,7 +40,6 @@ const ProfilePage = () => {
- {/* Left Content - Form */}
@@ -150,25 +149,32 @@ const ProfilePage = () => {
- {/* Quick Actions Cards */} -
-
-
- +
+
+
+
+ +
+
+ سفارشات +
-
- سفارشات +
+
+ +
+
+ علاقه مندی ها +
-
-
- -
-
- علاقه مندی ها -
+ +
+
+ +
diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx index c35dd65..5e923eb 100644 --- a/src/components/ProductCard.tsx +++ b/src/components/ProductCard.tsx @@ -1,3 +1,4 @@ +'use client' import { IProduct } from '@/types/landing.types' import { NumberFormat } from '@/config/func' import { Heart } from 'iconsax-react' @@ -5,6 +6,7 @@ import Image from 'next/image' import { FC, useState } from 'react' import { useAddWishlist, useRemoveWishlist } from '@/app/products/hooks/useProductsData' import { useSharedStore } from '@/share/store/sharedStore' +import Link from 'next/link' type Props = { item?: IProduct @@ -34,7 +36,7 @@ const ProductCard: FC = ({ item }) => { } if (item) return ( -
+
= ({ item }) => {
-
+ ) return (
diff --git a/src/types/product.types.ts b/src/types/product.types.ts new file mode 100644 index 0000000..f3e73de --- /dev/null +++ b/src/types/product.types.ts @@ -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[]; + }; +}