Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[finishes #187816382] product details page #27

Merged
merged 1 commit into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 22 additions & 20 deletions src/components/Products/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -12,29 +11,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
Loading