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 0000000..fdf4ff6 Binary files /dev/null and b/src/assets/defaultProfile.avif differ diff --git a/src/components/Products/ColorComponent.tsx b/src/components/Products/ColorComponent.tsx new file mode 100644 index 0000000..bd87afc --- /dev/null +++ b/src/components/Products/ColorComponent.tsx @@ -0,0 +1,10 @@ +interface Props { + name: string; +} +export const ColorComponent = ({ name }: Props) => { + 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' + /> +
+
+
+