From ac8d1bf355945377c515955198daa0d6fd4c8b35 Mon Sep 17 00:00:00 2001 From: Mag codes Date: Fri, 21 Jun 2024 10:21:37 +0200 Subject: [PATCH] [finishes #187816382] product details page --- package.json | 4 +- src/App.tsx | 6 + src/assets/defaultProfile.avif | Bin 0 -> 4712 bytes src/components/Products/ColorComponent.tsx | 10 + src/components/Products/ImageCard.tsx | 26 ++ src/components/Products/ImageToggle.tsx | 19 + src/components/Products/ProductCard.tsx | 42 +-- src/components/Products/ProductReviewCard.tsx | 39 +++ src/components/common/Button.tsx | 4 +- .../ProductDetail/ProductDetailSkeleton.tsx | 105 ++++++ src/hooks/useCheckToken.ts | 27 ++ src/pages/product/ProductDetail.tsx | 326 ++++++++++++++++++ src/redux/slices/userSlice.ts | 4 +- src/services/cartApi.ts | 17 + src/services/index.ts | 9 + src/services/productApi.ts | 19 +- src/services/userApi.ts | 3 - src/services/wishlistApi.ts | 22 ++ src/utils/index.ts | 13 + src/utils/schemas.ts | 71 ++++ src/utils/token.ts | 20 ++ tailwind.config.js | 3 +- 22 files changed, 759 insertions(+), 30 deletions(-) create mode 100644 src/assets/defaultProfile.avif create mode 100644 src/components/Products/ColorComponent.tsx create mode 100644 src/components/Products/ImageCard.tsx create mode 100644 src/components/Products/ImageToggle.tsx create mode 100644 src/components/Products/ProductReviewCard.tsx create mode 100644 src/containers/ProductDetail/ProductDetailSkeleton.tsx create mode 100644 src/hooks/useCheckToken.ts create mode 100644 src/pages/product/ProductDetail.tsx create mode 100644 src/services/cartApi.ts create mode 100644 src/services/wishlistApi.ts create mode 100644 src/utils/token.ts diff --git a/package.json b/package.json index 838e402..863c171 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", @@ -27,11 +27,13 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.51.5", "react-icons": "^5.2.1", + "react-medium-image-zoom": "^5.2.5", "react-redux": "^9.1.2", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", "sass": "^1.77.2", "tailwind-merge": "^2.3.0", + "tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar-hide": "^1.1.7", "zod": "^3.23.8" }, diff --git a/src/App.tsx b/src/App.tsx index 300c26e..19593e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,8 @@ import Settings from './pages/admin/Settings'; import CategoriesPage from './pages/CategoriesPage'; import ResetPassword from './pages/ResetPassword'; import NewPassword from './pages/NewPassword'; +import { ProductDetail } from './pages/product/ProductDetail'; + const App = () => { const { data, error, isLoading } = useGetProductsQuery(); const dispatch = useDispatch(); @@ -82,6 +84,10 @@ const App = () => { }, ], }, + { + path: 'products/:id', + element: , + }, ], }, { diff --git a/src/assets/defaultProfile.avif b/src/assets/defaultProfile.avif new file mode 100644 index 0000000000000000000000000000000000000000..fdf4ff6e3e37237a7a9579c2e47a807f35556a51 GIT binary patch literal 4712 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 { + return ( +
+ {name} +
+ ); +}; diff --git a/src/components/Products/ImageCard.tsx b/src/components/Products/ImageCard.tsx new file mode 100644 index 0000000..182b7f2 --- /dev/null +++ b/src/components/Products/ImageCard.tsx @@ -0,0 +1,26 @@ +import Zoom from 'react-medium-image-zoom'; +import 'react-medium-image-zoom/dist/styles.css'; + +interface Props { + styles: string; + image: string; + alt?: string; + handleClick?: (image: string) => void; + enableZoom?: boolean; + isSpotted?: boolean; +} + +export const ImageCard = ({ image, styles, alt, handleClick, enableZoom = true, isSpotted = false }: Props) => { + const imgClass = isSpotted ? `border border-greenColor rounded-lg p-1 ${styles}` : styles; + const imgElement = ( + handleClick && handleClick(image)} className={imgClass} src={image} alt={alt} /> + ); + + return enableZoom ? ( + + {imgElement} + + ) : ( + imgElement + ); +}; diff --git a/src/components/Products/ImageToggle.tsx b/src/components/Products/ImageToggle.tsx new file mode 100644 index 0000000..b5f7c63 --- /dev/null +++ b/src/components/Products/ImageToggle.tsx @@ -0,0 +1,19 @@ +import { IconType } from 'react-icons'; + +interface Props { + icon: IconType; + positionClass: string; + size?: string; + handleClick?: (e: any) => void; +} + +export const ImageToggle = ({ icon: Icon, positionClass, size, handleClick }: Props) => { + return ( +
+ +
+ ); +}; diff --git a/src/components/Products/ProductCard.tsx b/src/components/Products/ProductCard.tsx index 54628c5..de18618 100644 --- a/src/components/Products/ProductCard.tsx +++ b/src/components/Products/ProductCard.tsx @@ -2,7 +2,6 @@ import { FaHeart } from 'react-icons/fa6'; import { Product } from '../../types/Types'; import { TiShoppingCart } from 'react-icons/ti'; import StarRating from '../common/Ratings'; - interface ProductCardProps { product: Product; } @@ -12,29 +11,32 @@ const ProductCard: React.FC = ({ product }) => { const price = product?.sizes?.[0]?.price ?? ''; return ( -
-
- {product.name} +
{ + window.open(`/products/${product.id}`, '_blank'); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }} + className='product-card bg-whiteColor min-w-44 md:min-w-56 lg:min-w-56 + shadow-lg rounded-lg p-2 m-4 md:p-4 md:m-4 transition-transform hover:scale-105 cursor-pointer' + > +
+ {product.name}
-
-
-
-

{product.name}

-

${price}

+
+
+
+

{product.name}

+

${price}

-

{product.manufacturer}

+

{product.manufacturer}

-
-
-
- - +
+
+
+ +
-
+
diff --git a/src/components/Products/ProductReviewCard.tsx b/src/components/Products/ProductReviewCard.tsx new file mode 100644 index 0000000..b951609 --- /dev/null +++ b/src/components/Products/ProductReviewCard.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Review } from '../../utils/schemas'; +import { FaStar } from 'react-icons/fa'; +import defaultProfile from '../../assets/defaultProfile.avif'; + +interface Props { + review: Review; +} + +const ProductReviewCard: React.FC = ({ review }) => { + const dateReviewed = new Date(review.createdAt).toLocaleDateString(); + const reviewText = review.feedback; + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, index) => ( + + )); + }; + + return ( +
+
+ Buyer +
+
+
+

{review.user.firstName}

+
+ {renderStars(review.rating)} + {dateReviewed} +
+

{reviewText}

+
+
+
+ ); +}; + +export default ProductReviewCard; diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index 0bff237..472efaf 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -6,10 +6,12 @@ interface ButtonProps { type?: 'submit' | 'reset' | 'button'; className?: string; onClick?: () => void; + disabled?: boolean; } -const Button = ({ text, type, className, onClick }: ButtonProps) => { +const Button = ({ text, type, className, disabled, onClick }: ButtonProps) => { return (
+
+ {/* PRODUCT stars */} +
+ Review + + + +
+ {/* PRODUCT DESCRIPTION */} +
+

+ {isDescriptionExpanded + ? productData.data.description + : `${productData.data.description.substring(0, 100)}...`} +

+

+ {isDescriptionExpanded ? 'Show less' : 'Show more'} +

+
+
+
+
+
+
+ {/* PRODUCT REVIEW AND COMMENTS */} +
+ {productData.data.reviews.length > 0 && ( +
+
+

Product Reviews

+
+ {productData.data.reviews.map(review => ( + + ))} +
+
+
+ )} + + {/* RECOMMENDED PRODUCTS */} +
+

Similar Products

+
+ scrollRecommendedProducts('left')} + size='16px' + icon={IoIosArrowBack} + positionClass='absolute left-0 ml-2 z-50' + /> +
+ {similarProducts.map(product => ( + + ))} +
+ scrollRecommendedProducts('right')} + size='16px' + icon={IoIosArrowForward} + positionClass='absolute right-0 mr-2' + /> +
+
+
+