Skip to content
Closed
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
Binary file added recent view.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 15 additions & 12 deletions src/api/productService.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import axiosInstance from './axiosInstance.js';

const transformProduct = (apiProduct) => {
const originalPrice = apiProduct.sale > 0
? Number((apiProduct.price / (1 - apiProduct.sale / 100)).toFixed(2))
: null;
const originalPrice =
apiProduct.sale > 0
? Number((apiProduct.price / (1 - apiProduct.sale / 100)).toFixed(2))
: null;

return {
id: apiProduct._id,
Expand All @@ -24,7 +25,7 @@ const transformProduct = (apiProduct) => {
sizes: apiProduct.sizes || [],
salePercentage: apiProduct.sale,
longDescription: apiProduct.longDescription,
usageTips: apiProduct.usageTips
usageTips: apiProduct.usageTips,
};
};

Expand All @@ -34,15 +35,15 @@ const transformApiResponse = (apiResponse) => {
totalCount: apiResponse.total,
currentPage: apiResponse.page,
hasNextPage: apiResponse.page < apiResponse.pages,
totalPages: apiResponse.pages
totalPages: apiResponse.pages,
};
};

export const getProducts = async (params = {}) => {
try {
// Map our filter names to API parameter names
const apiParams = {};

if (params.page) apiParams.page = params.page;
if (params.limit) apiParams.limit = params.limit;
if (params.category) apiParams.category = params.category;
Expand All @@ -53,8 +54,10 @@ export const getProducts = async (params = {}) => {
if (params.sortBy) apiParams.sortBy = params.sortBy;
if (params.sortOrder) apiParams.sortOrder = params.sortOrder;

const response = await axiosInstance.get('/products', { params: apiParams });

const response = await axiosInstance.get('/products', {
params: apiParams,
});

return {
success: true,
data: transformApiResponse(response.data),
Expand All @@ -76,7 +79,7 @@ export const getProductById = async (id) => {
}

const response = await axiosInstance.get(`/products/${id}`);

return {
success: true,
data: transformProduct(response.data),
Expand All @@ -99,7 +102,7 @@ export const searchProducts = async (query, params = {}) => {
...params,
},
});

return {
success: true,
data: response.data,
Expand All @@ -117,7 +120,7 @@ export const searchProducts = async (query, params = {}) => {
export const getProductCategories = async () => {
try {
const response = await axiosInstance.get('/products/categories');

return {
success: true,
data: response.data,
Expand All @@ -130,4 +133,4 @@ export const getProductCategories = async () => {
status: error.response?.status || 500,
};
}
};
};
163 changes: 163 additions & 0 deletions src/components/GarageSaleRecentlyViewed.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { useState, useMemo } from 'react';
import ProductCard from './Products/ProductCard';
import { getRecentlyViewed } from '../utils/recentlyViewed';

// SVG component for the navigation arrows
const ChevronLeftIcon = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
);

const ChevronRightIcon = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
);

export default function GarageSaleRecentlyViewed() {
// Filter recently viewed items to only show products that are on sale
const filterSaleProducts = (products) => {
return products.filter(product => {
// Check if product has a sale price (originalPrice exists and is greater than current price)
const hasSalePrice = product.originalPrice && product.originalPrice > product.price;
// Or check if there's a sale field/property
const hasSaleField = product.sale && product.sale > 0;
return hasSalePrice || hasSaleField;
});
};

// Load and filter recently viewed items using useState with function initializer
const [recentlyViewedItems] = useState(() => {
const allRecentlyViewed = getRecentlyViewed();
return filterSaleProducts(allRecentlyViewed);
});
const [currentIndex, setCurrentIndex] = useState(0);
const itemsPerPage = 5; // 5 items per view as per requirements

// Memoize navigation logic for better performance
const navigationState = useMemo(() => {
const canGoNext = currentIndex < recentlyViewedItems.length - itemsPerPage;
const canGoPrev = currentIndex > 0;
const visibleItems = recentlyViewedItems.slice(currentIndex, currentIndex + itemsPerPage);
const totalPages = Math.ceil(recentlyViewedItems.length / itemsPerPage);

return { canGoNext, canGoPrev, visibleItems, totalPages };
}, [currentIndex, recentlyViewedItems, itemsPerPage]);

const nextSlide = () => {
const maxStartIndex = Math.max(0, recentlyViewedItems.length - itemsPerPage);
const newIndex = Math.min(currentIndex + itemsPerPage, maxStartIndex);
setCurrentIndex(newIndex);
};

const prevSlide = () => {
const newIndex = Math.max(currentIndex - itemsPerPage, 0);
setCurrentIndex(newIndex);
};

if (!recentlyViewedItems || recentlyViewedItems.length === 0) {
return null;
}

return (
<section className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 pt-8 pb-12">
{/* Header section with title and navigation arrows */}
<div className="flex items-center justify-between mb-8">
<h2 className="text-5xl lg:text-heading-xxl font-montserrat text-black leading-none uppercase tracking-tight py-6">
<span className="font-bold">RECENTLY</span>
<span className="ml-5 text-[#f7faff] font-bold" style={{
WebkitTextStroke: '2px black',
color: 'transparent'
}}>VIEWED</span>
</h2>

{/* Navigation Buttons - Only show if there are more than 5 items */}
{recentlyViewedItems.length > itemsPerPage && (
<div className="flex items-center gap-4">
<button
onClick={prevSlide}
disabled={!navigationState.canGoPrev}
className="flex h-16 w-16 items-center justify-center rounded-full bg-white border-2 border-gray-300 text-gray-600 hover:bg-gray-50 hover:border-gray-400 disabled:cursor-not-allowed disabled:opacity-30 transition-all duration-200 shadow-md hover:shadow-lg cursor-pointer"
aria-label="Previous 5 items"
>
<ChevronLeftIcon className="h-6 w-6" />
</button>
<button
onClick={nextSlide}
disabled={!navigationState.canGoNext}
className="flex h-16 w-16 items-center justify-center rounded-full bg-white border-2 border-gray-300 text-gray-600 hover:bg-gray-50 hover:border-gray-400 disabled:cursor-not-allowed disabled:opacity-30 transition-all duration-200 shadow-md hover:shadow-lg cursor-pointer"
aria-label="Next 5 items"
>
<ChevronRightIcon className="h-6 w-6" />
</button>
</div>
)}
</div>

{/* Recently Viewed Products Slider */}
<div className="relative">
{/* Left Arrow */}
{recentlyViewedItems.length > itemsPerPage && navigationState.canGoPrev && (
<button
onClick={prevSlide}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white border-2 border-gray-300 text-gray-600 hover:bg-gray-50 hover:border-gray-400 transition-all duration-200 shadow-lg hover:shadow-xl cursor-pointer"
aria-label="Previous 5 items"
>
<ChevronLeftIcon className="h-5 w-5" />
</button>
)}

{/* Slider Container */}
<div className="overflow-hidden">
<div
className="flex gap-4 transition-transform duration-500 ease-in-out"
style={{
transform: `translateX(-${currentIndex * (100 / itemsPerPage)}%)`,
width: `${(recentlyViewedItems.length / itemsPerPage) * 100}%`
}}
>
{recentlyViewedItems.map((product) => (
<div
key={product.id}
className="flex-shrink-0 min-w-0"
style={{ width: `${100 / recentlyViewedItems.length}%` }}
>
<ProductCard product={product} />
</div>
))}
</div>
</div>

{/* Right Arrow */}
{recentlyViewedItems.length > itemsPerPage && navigationState.canGoNext && (
<button
onClick={nextSlide}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white border-2 border-gray-300 text-gray-600 hover:bg-gray-50 hover:border-gray-400 transition-all duration-200 shadow-lg hover:shadow-xl cursor-pointer"
aria-label="Next 5 items"
>
<ChevronRightIcon className="h-5 w-5" />
</button>
)}
</div>

{/* Pagination Dots */}
{recentlyViewedItems.length > itemsPerPage && (
<div className="flex justify-center mt-8 gap-2">
{Array.from({ length: navigationState.totalPages }).map((_, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index * itemsPerPage)}
className={`w-3 h-3 rounded-full transition-all duration-200 cursor-pointer ${
Math.floor(currentIndex / itemsPerPage) === index
? 'bg-gray-800 scale-110'
: 'bg-gray-300 hover:bg-gray-400'
}`}
aria-label={`Go to page ${index + 1}`}
/>
))}
</div>
)}
</section>
);
}
12 changes: 8 additions & 4 deletions src/components/Header/MainHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ export default function Header() {
)}
</div>

<a href="#" className="text-gray-700 hover:text-black">
<Link to="/garage-sale" className="text-gray-700 hover:text-black">
Garage Sale
</a>
</Link>
<Link to="/products" className="text-gray-700 hover:text-black">
All Products
</Link>
Expand Down Expand Up @@ -119,9 +119,13 @@ export default function Header() {
</a>
</div>
)}
<a href="#" className="text-gray-700 hover:text-black">
<Link
to="/garage-sale"
className="text-gray-700 hover:text-black"
onClick={() => setMobileOpen(false)}
>
Garage Sale
</a>
</Link>
<Link
to="/products"
className="text-gray-700 hover:text-black"
Expand Down
32 changes: 18 additions & 14 deletions src/components/Products/ProductCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,13 @@ const ProductCard = forwardRef(({ product }, ref) => {
</div>
<span className="text-xs text-gray-500">({product.reviewCount || 0})</span>
</div>
<div className="flex items-center gap-2">
{product.originalPrice && (
<span className="text-sm text-gray-500 line-through">{formatPrice(product.originalPrice)}</span>
)}
<span className="font-bold text-lg text-gray-800">{formatPrice(product.price)}</span>
<div className="flex items-center justify-between gap-2 mb-2">
<div className="flex flex-col">
{product.originalPrice && (
<span className="text-xs text-gray-500 line-through leading-none">{formatPrice(product.originalPrice)}</span>
)}
<span className="font-bold text-lg text-gray-800 leading-none">{formatPrice(product.price)}</span>
</div>
</div>
</div>

Expand Down Expand Up @@ -242,33 +244,35 @@ const ProductCard = forwardRef(({ product }, ref) => {
<button
onClick={(e) => handleActionClick(e, handleAddToCart)}
className={`
-ml-px flex-grow flex items-center justify-center gap-2
-ml-px flex-grow flex items-center justify-center gap-1
bg-[#023e8a] text-white font-medium
py-3 px-4 rounded-r-xl
py-3 px-2 sm:px-3 rounded-r-xl
hover:bg-[#1054ab] transition-colors duration-150 hover:shadow-lg cursor-pointer
focus:outline-none focus:z-10
min-w-0
`}
aria-live="polite"
>
{cartLoading ? (
<span className="ml-2 flex items-center gap-2 font-semibold">
<svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
<span className="flex items-center gap-1 font-semibold text-xs sm:text-sm">
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
<circle cx="12" cy="12" r="10" strokeWidth="3" stroke="currentColor" opacity="0.25" />
<path d="M22 12a10 10 0 00-10-10" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span>ADDING...</span>
<span className="hidden sm:inline">ADDING...</span>
<span className="sm:hidden">ADD...</span>
</span>
) : cartAdded ? (
<span className="ml-2 flex items-center gap-2 font-semibold">
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
<span className="flex items-center gap-1 font-semibold text-xs sm:text-sm">
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
<path strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span>ADDED</span>
</span>
) : (
<>
<CartIcon />
<span className="ml-2">ADD TO CART</span>
<CartIcon className="w-4 h-4 flex-shrink-0" />
<span className="text-xs sm:text-sm font-semibold whitespace-nowrap">ADD TO CART</span>
</>
)}
</button>
Expand Down
Loading