Skip to content

Commit

Permalink
[finishes #187816382] product details page
Browse files Browse the repository at this point in the history
  • Loading branch information
Mag codes committed Jul 12, 2024
1 parent c39186d commit 26b218d
Show file tree
Hide file tree
Showing 22 changed files with 760 additions and 29 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand Down
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -82,6 +84,10 @@ const App = () => {
},
],
},
{
path: 'products/:id',
element: <ProductDetail />,
},
],
},
{
Expand Down
Binary file added src/assets/defaultProfile.avif
Binary file not shown.
10 changes: 10 additions & 0 deletions src/components/Products/ColorComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
interface Props {
name: string;
}
export const ColorComponent = ({ name }: Props) => {
return (
<div className='border hover:border-greenColor hover:border active:border-greenColor rounded-full py-1 px-4 hover:cursor-pointer'>
{name}
</div>
);
};
26 changes: 26 additions & 0 deletions src/components/Products/ImageCard.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<img onClick={() => handleClick && handleClick(image)} className={imgClass} src={image} alt={alt} />
);

return enableZoom ? (
<Zoom wrapElement='span' a11yNameButtonZoom='Tap to zoom'>
{imgElement}
</Zoom>
) : (
imgElement
);
};
19 changes: 19 additions & 0 deletions src/components/Products/ImageToggle.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
onClick={handleClick}
className={`max-w-fit absolute p-2 shadow-sm rounded-full bg-whiteColor hover:cursor-pointer border ${positionClass}`}
>
<Icon size={size} />
</div>
);
};
42 changes: 23 additions & 19 deletions src/components/Products/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FaHeart } from 'react-icons/fa6';
import { Product } from '../../types/Types';
import { TiShoppingCart } from 'react-icons/ti';
import StarRating from '../common/Ratings';
import { useNavigate } from 'react-router-dom';

Check failure on line 5 in src/components/Products/ProductCard.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

'useNavigate' is declared but its value is never read.

interface ProductCardProps {
product: Product;
Expand All @@ -12,29 +13,32 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
const price = product?.sizes?.[0]?.price ?? '';

return (
<div className="product-card bg-whiteColor shadow-lg rounded-lg p-2 m-4 md:p-4 md:m-4 transition-transform hover:scale-105 cursor-pointer">
<div className="product-image flex justify-center">
<img
src={imageUrl}
alt={product.name}
className="w-32 h-32 rounded-sm object-cover md:h-48 md:w-48"
/>
<div
onClick={() => {
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'
>
<div className='product-image flex justify-center'>
<img src={imageUrl} alt={product.name} className='w-32 h-32 rounded-sm object-cover md:h-48 md:w-48' />
</div>
<div className="product-name-cart-button pt-2">
<div className="product-manufacturer">
<div className="name-price flex justify-between">
<h3 className="text-md font-bold">{product.name}</h3>
<p className="text-md font-bold">${price}</p>
<div className='product-name-cart-button pt-2'>
<div className='product-manufacturer'>
<div className='name-price flex justify-between'>
<h3 className='text-md font-bold'>{product.name}</h3>
<p className='text-md font-bold'>${price}</p>
</div>
<p className="text-sm p-2 text-[#949191]">{product.manufacturer}</p>
<p className='text-sm p-2 text-[#949191]'>{product.manufacturer}</p>
</div>
<div className="cart-button mt-4">
<div className="cart-wish-icons-ratings flex justify-between">
<div className="cart-wish-icons text-2xl flex gap-2">
<TiShoppingCart className='cursor-pointer'/>
<FaHeart className='cursor-pointer'/>
<div className='cart-button mt-4'>
<div className='cart-wish-icons-ratings flex justify-between'>
<div className='cart-wish-icons text-2xl flex gap-2'>
<TiShoppingCart className='cursor-pointer' />
<FaHeart className='cursor-pointer' />
</div>
<div className="ratings flex ">
<div className='ratings flex '>
<StarRating reviews={product.reviews} />
</div>
</div>
Expand Down
39 changes: 39 additions & 0 deletions src/components/Products/ProductReviewCard.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ review }) => {
const dateReviewed = new Date(review.createdAt).toLocaleDateString();
const reviewText = review.feedback;

const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, index) => (
<FaStar key={index} size='15px' color={index < rating ? '#ffc107' : '#e4e5e9'} />
));
};

return (
<div className='flex border-b border-grayColor py-2 snap-center'>
<div className='flex p-2 w-1/5 min-h-14 justify-center'>
<img className='rounded-full h-14 w-14' src={review.user.photoUrl || defaultProfile} alt='Buyer' />
</div>
<div className='w-4/5 p-2'>
<div className='space-y-1'>
<p className='font-medium'>{review.user.firstName}</p>
<div className='flex gap-4 items-center'>
<span className='flex'>{renderStars(review.rating)}</span>
<span className='text-xs text-gray-500'>{dateReviewed}</span>
</div>
<p className='text-sm'>{reviewText}</p>
</div>
</div>
</div>
);
};

export default ProductReviewCard;
4 changes: 3 additions & 1 deletion src/components/common/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<button
disabled={disabled}
type={type}
className={cn(
'p-2 rounded-lg bg-greenColor hover:bg-darkGreen transition-all text-whiteColor font-bold',
Expand Down
105 changes: 105 additions & 0 deletions src/containers/ProductDetail/ProductDetailSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io';
import Navbar from '../../components/navbar/Navbar';
import Footer from '../../components/footer/Footer';
import { ImageToggle } from '../../components/Products/ImageToggle';
import { ImageCard } from '../../components/Products/ImageCard';

const ProductDetailSkeleton = () => {
return (
<div className='w-full space-y-2'>
<Navbar />
<div className='w-full lg:flex lg:px-12'>
<div className='lg:flex md:flex relative lg:flex-auto'>
<div className='hidden lg:max-h-[500px] gap-2 md:flex md:flex-col md:mx-auto md:max-h-[400px] lg:flex lg:flex-col overflow-hidden'>
{[...Array(4)].map((_, index) => (
<div key={index} className='w-full h-20 mx-auto mt-2 bg-slate-700 rounded-md animate-pulse' />
))}
</div>
<div className='relative w-full overflow-hidden md:w-4/5 lg:h-[500px] lg:w-4/5 mx-auto'>
<div className='md:min-h-full max-h-80 lg:min-h-full w-full bg-slate-700 rounded-md animate-pulse' />
<ImageCard image='' styles='animate-pulse bg-slate-700 w-full h-full rounded-md' />
<ImageToggle size='20px' icon={IoIosArrowForward} positionClass='right-4 lg:right-10 top-1/2' />
<ImageToggle size='20px' icon={IoIosArrowBack} positionClass='left-4 lg:left-10 top-1/2' />
</div>
</div>
<div className='lg:w-2/5 px-3 space-y-3 mt-3'>
<div className='lg:flex lg:flex-col md:flex md:gap-8'>
<div className='lg:w-full md:w-1/2'>
<div className='w-full gap-3 flex mb-3 justify-between text-xl font-medium'>
<div className='w-3/5 flex-1 bg-slate-700 h-6 rounded-md animate-pulse'></div>
<div className='bg-slate-700 h-6 w-20 rounded-md animate-pulse'></div>
</div>
<div>
<p className='font-medium'>Color</p>
<div className='flex gap-2 w-full p-2 flex-wrap'>
{[...Array(3)].map((_, index) => (
<div key={index} className='bg-slate-700 w-8 h-8 rounded-full animate-pulse'></div>
))}
</div>
</div>
<div className='flex'>
<div className='w-full flex flex-col gap-2'>
<label htmlFor='size' className='leading-none font-medium'>
Size
</label>
<div className='flex w-full gap-2'>
<div className='border-greenColor border-2 lg:py-2 text-sm px-2 bg-slate-700 w-1/2 h-10 rounded-md animate-pulse'></div>
<div className='flex border-2 items-center py-1 px-6 gap-4 w-1/2 bg-slate-700 h-10 rounded-md animate-pulse'></div>
</div>
</div>
</div>
<div className='w-full py-2 my-4 bg-slate-700 h-10 rounded-md animate-pulse'></div>
</div>
<div className='lg:w-full md:w-1/2'>
<div className='border-y border-grayColor gap-3 flex flex-row items-center py-4'>
<span>Review</span>
<span className='flex gap-1'>
{[...Array(5)].map((_, index) => (
<div key={index} className='bg-slate-700 w-5 h-5 rounded-full animate-pulse'></div>
))}
</span>
<span>(8)</span>
</div>
<div className='border-b text-sm border-grayColor py-2'>
<div className='bg-slate-700 h-6 w-full rounded-md animate-pulse mb-2'></div>
<div className='bg-slate-700 h-6 w-3/4 rounded-md animate-pulse'></div>
<div className='text-greenColor mt-4 font-medium text-right hover:cursor-pointer'>Show more</div>
</div>
</div>
</div>
</div>
</div>
<div className='lg:px-12 lg:flex'>
<div className='lg:w-1/2 lg:border border-grayColor lg:rounded-sm lg:p-2'>
<div>
<h1 className='font-bold text-2xl border-b border-grayColor py-4'>Product Reviews</h1>
{[...Array(2)].map((_, index) => (
<div key={index} className='flex gap-4 py-4 border-b border-grayColor'>
<div className='w-12 h-12 rounded-full bg-slate-700 animate-pulse'></div>
<div className='flex-1'>
<div className='bg-slate-700 h-4 w-1/2 rounded-md animate-pulse mb-2'></div>
<div className='bg-slate-700 h-4 w-3/4 rounded-md animate-pulse'></div>
</div>
</div>
))}
</div>
</div>
<div className='lg:w-1/2 lg:border border-grayColor lg:rounded-sm lg:p-2'>
<h1 className='font-bold text-2xl border-b border-grayColor py-4'>Recommended Products</h1>
{[...Array(3)].map((_, index) => (
<div key={index} className='flex gap-4 py-4 border-b border-grayColor'>
<div className='w-20 h-20 bg-slate-700 rounded-md animate-pulse'></div>
<div className='flex-1'>
<div className='bg-slate-700 h-4 w-3/4 rounded-md animate-pulse mb-2'></div>
<div className='bg-slate-700 h-4 w-1/2 rounded-md animate-pulse'></div>
</div>
</div>
))}
</div>
</div>
<Footer />
</div>
);
};

export default ProductDetailSkeleton;
27 changes: 27 additions & 0 deletions src/hooks/useCheckToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';
import isTokenExpired from '../utils/token';

const useCheckToken = () => {
const [isExpired, setIsExpired] = useState(false);

useEffect(() => {
const checkToken = async () => {
const token = localStorage.getItem('token');
if (token) {
const expired = await isTokenExpired(token);
setIsExpired(expired);

if (expired) {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
}
};

checkToken();
}, []);

return isExpired;
};

export default useCheckToken;
Loading

0 comments on commit 26b218d

Please sign in to comment.