diff --git a/recent view.jpg b/recent view.jpg new file mode 100644 index 00000000..3f768b68 Binary files /dev/null and b/recent view.jpg differ diff --git a/src/api/productService.js b/src/api/productService.js index 65b0ccab..ee160c51 100644 --- a/src/api/productService.js +++ b/src/api/productService.js @@ -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, @@ -24,7 +25,7 @@ const transformProduct = (apiProduct) => { sizes: apiProduct.sizes || [], salePercentage: apiProduct.sale, longDescription: apiProduct.longDescription, - usageTips: apiProduct.usageTips + usageTips: apiProduct.usageTips, }; }; @@ -34,7 +35,7 @@ const transformApiResponse = (apiResponse) => { totalCount: apiResponse.total, currentPage: apiResponse.page, hasNextPage: apiResponse.page < apiResponse.pages, - totalPages: apiResponse.pages + totalPages: apiResponse.pages, }; }; @@ -42,7 +43,7 @@ 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; @@ -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), @@ -76,7 +79,7 @@ export const getProductById = async (id) => { } const response = await axiosInstance.get(`/products/${id}`); - + return { success: true, data: transformProduct(response.data), @@ -99,7 +102,7 @@ export const searchProducts = async (query, params = {}) => { ...params, }, }); - + return { success: true, data: response.data, @@ -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, @@ -130,4 +133,4 @@ export const getProductCategories = async () => { status: error.response?.status || 500, }; } -}; \ No newline at end of file +}; diff --git a/src/components/GarageSaleRecentlyViewed.jsx b/src/components/GarageSaleRecentlyViewed.jsx new file mode 100644 index 00000000..ced97e61 --- /dev/null +++ b/src/components/GarageSaleRecentlyViewed.jsx @@ -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) => ( + + + +); + +const ChevronRightIcon = (props) => ( + + + +); + +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 ( +
+ {/* Header section with title and navigation arrows */} +
+

+ RECENTLY + VIEWED +

+ + {/* Navigation Buttons - Only show if there are more than 5 items */} + {recentlyViewedItems.length > itemsPerPage && ( +
+ + +
+ )} +
+ + {/* Recently Viewed Products Slider */} +
+ {/* Left Arrow */} + {recentlyViewedItems.length > itemsPerPage && navigationState.canGoPrev && ( + + )} + + {/* Slider Container */} +
+
+ {recentlyViewedItems.map((product) => ( +
+ +
+ ))} +
+
+ + {/* Right Arrow */} + {recentlyViewedItems.length > itemsPerPage && navigationState.canGoNext && ( + + )} +
+ + {/* Pagination Dots */} + {recentlyViewedItems.length > itemsPerPage && ( +
+ {Array.from({ length: navigationState.totalPages }).map((_, index) => ( +
+ )} +
+ ); +} diff --git a/src/components/Header/MainHeader.jsx b/src/components/Header/MainHeader.jsx index 105f3a75..42b9abbd 100644 --- a/src/components/Header/MainHeader.jsx +++ b/src/components/Header/MainHeader.jsx @@ -50,9 +50,9 @@ export default function Header() { )} - + Garage Sale - + All Products @@ -119,9 +119,13 @@ export default function Header() { )} - + setMobileOpen(false)} + > Garage Sale - + { ({product.reviewCount || 0}) -
- {product.originalPrice && ( - {formatPrice(product.originalPrice)} - )} - {formatPrice(product.price)} +
+
+ {product.originalPrice && ( + {formatPrice(product.originalPrice)} + )} + {formatPrice(product.price)} +
@@ -242,33 +244,35 @@ const ProductCard = forwardRef(({ product }, ref) => { diff --git a/src/pages/GarageSale/GarageSale.jsx b/src/pages/GarageSale/GarageSale.jsx new file mode 100644 index 00000000..90f62645 --- /dev/null +++ b/src/pages/GarageSale/GarageSale.jsx @@ -0,0 +1,141 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchProducts } from '../../store/productSlice'; +import SEO from '../../components/SEO'; +import ProductGrid from '../../components/Products/ProductGrid'; +import ProductSkeleton from '../../components/Products/ProductSkeleton'; +import GarageSaleRecentlyViewed from '../../components/GarageSaleRecentlyViewed'; + +function GarageSale() { + const dispatch = useDispatch(); + const { + products: allProducts, + loading, + error, + } = useSelector((state) => state.products); + + // Filter products that are on sale (have sale > 0 or originalPrice > price) + const saleProducts = allProducts.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; + }); + + useEffect(() => { + dispatch(fetchProducts({ page: 1, limit: 1000 })); + }, [dispatch]); + + return ( + <> + + +
+ {/* Banner Section */} +
+
+
+

+ Garage Sale +

+

+ Limited time offers on premium sports nutrition supplements. + Don't miss out on these incredible deals! +

+
+ + {saleProducts.length} Products on Sale + +
+
+
+
+ + {/* Product Grid Section */} +
+
+
+ {error && ( +
+
+ + + +
+

+ Unable to Load Products +

+

{error}

+ +
+ )} + + {loading && } + + {!loading && !error && saleProducts.length > 0 && ( + <> + + + )} + + {!loading && !error && saleProducts.length === 0 && ( +
+
+ + + +
+

+ No Sale Products Available +

+

+ Check back later for amazing deals on our premium supplements. +

+
+ )} +
+
+
+ + {/* Recently Viewed Section (bottom of page) */} + {!loading && !error && } +
+ + ); +} + +export default GarageSale; diff --git a/src/routes/RouterConfig.jsx b/src/routes/RouterConfig.jsx index 12b5899a..36c0b5d8 100755 --- a/src/routes/RouterConfig.jsx +++ b/src/routes/RouterConfig.jsx @@ -17,6 +17,7 @@ const ShippingPolicy = lazy( ); const Products = lazy(() => import('../pages/Products/Products')); const ProductPage = lazy(() => import('../pages/Products/ProductPage')); +const GarageSale = lazy(() => import('../pages/GarageSale/GarageSale')); const TermsOfService = lazy( () => import('../pages/TermsOfService/TermsOfService') ); @@ -32,6 +33,7 @@ export const RouterConfig = () => } /> } /> } /> + } /> } /> } /> } />{' '}