diff --git a/src/api/collectionService.js b/src/api/collectionService.js
new file mode 100644
index 00000000..d59292dc
--- /dev/null
+++ b/src/api/collectionService.js
@@ -0,0 +1,11 @@
+import axiosInstance from './axiosInstance';
+
+/**
+ * Fetch collection by ID with its products
+ * @param {string} collectionId - The collection identifier (e.g., 'weight-management')
+ * @returns {Promise} Collection data with products array
+ */
+export const getCollectionById = async (collectionId) => {
+ const response = await axiosInstance.get(`/collections/${collectionId}`);
+ return response.data;
+};
diff --git a/src/api/index.js b/src/api/index.js
index cc7481ad..52e1e71e 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -18,5 +18,8 @@ export {
clearCart,
} from './cartService.js';
+export { getCollectionById } from './collectionService.js';
+
export * from './productService.js';
export * from './cartService.js';
+export * from './collectionService.js';
diff --git a/src/components/CollectionSection.jsx b/src/components/CollectionSection.jsx
index 0ea7aeea..20fc9858 100644
--- a/src/components/CollectionSection.jsx
+++ b/src/components/CollectionSection.jsx
@@ -6,7 +6,7 @@ import weightManagementImg from '/images/weight-management.png';
import healthWellnessImg from '/images/health-wellness.png';
import './collection.css';
-const collections = [
+export const collections = [
{ id: 'pre-workout', title: 'PRE-WORKOUT', image: preWorkoutImg },
{ id: 'build-muscle', title: 'BUILD MUSCLE', image: buildMuscleImg },
{
@@ -55,8 +55,8 @@ const CollectionSection = () => {
};
}, []);
- const handleCardClick = () => {
- navigate('/collections');
+ const handleCardClick = (collection) => {
+ navigate(`/collections/${collection.id}`);
};
return (
@@ -73,7 +73,7 @@ const CollectionSection = () => {
key={collection.id}
ref={(el) => (cardsRef.current[index] = el)}
className="collection-card"
- onClick={handleCardClick}
+ onClick={() => handleCardClick(collection)}
>
![]()
{
diff --git a/src/components/SupplementForGoalsSection.jsx b/src/components/SupplementForGoalsSection.jsx
index 316249be..ef3fadd8 100644
--- a/src/components/SupplementForGoalsSection.jsx
+++ b/src/components/SupplementForGoalsSection.jsx
@@ -9,21 +9,34 @@ import {
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
import './SupplementForGoalsSection.css';
-const goalCards = [
+export const goalCards = [
{
label: 'protein powder',
+ id: 'protein-powder',
image: proteinPowder,
href: '/collections/protein-powder',
},
- { label: 'pre-workout', image: preWorkout, href: '/collections/pre-workout' },
+ {
+ label: 'pre-workout',
+ id: 'pre-workout',
+ image: preWorkout,
+ href: '/collections/pre-workout',
+ },
{
label: 'intra-workout',
+ id: 'intra-workout',
image: intraWorkout,
href: '/collections/intra-workout',
},
- { label: 'amino acids', image: aminoAcids, href: '/collections/amino-acids' },
+ {
+ label: 'amino acids',
+ id: 'amino-acids',
+ image: aminoAcids,
+ href: '/collections/amino-acids',
+ },
{
label: 'weight management',
+ id: 'weight-management',
image: weightManagement,
href: '/collections/weight-management',
},
diff --git a/src/pages/Collections/CollectionsPage.jsx b/src/pages/Collections/CollectionsPage.jsx
index 5a94a808..73bd7324 100644
--- a/src/pages/Collections/CollectionsPage.jsx
+++ b/src/pages/Collections/CollectionsPage.jsx
@@ -1,3 +1,388 @@
-export default function CollectionsPage() {
- return <>>;
+import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
+import { useParams, Navigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { motion } from 'framer-motion';
+
+import {
+ fetchCollectionById,
+ clearCurrentCollection,
+} from '../../store/CollectionSlice';
+import ProductGrid from '../../components/Products/ProductGrid';
+import SortDropdown from '../../components/Products/SortDropdown';
+import ProductSkeleton from '../../components/Products/ProductSkeleton';
+import FilterPanel from '../../components/Products/FilterPanel/FilterPanel';
+import RecentlyViewed from '../../components/RecentlyViewed';
+import SEO from '../../components/SEO';
+import { collections } from '../../components/CollectionSection';
+
+export default function CollectionPage() {
+ const { name } = useParams();
+
+ const dispatch = useDispatch();
+ const {
+ currentCollection,
+ products: collectionProducts,
+ loading,
+ error,
+ pagination,
+ } = useSelector((state) => state.collections || {});
+
+ const observer = useRef();
+ const [displayedCount, setDisplayedCount] = useState(12);
+ const [isFilterOpen, setIsFilterOpen] = useState(false);
+ const [filters, setFilters] = useState({
+ priceRange: [0, 100],
+ categories: [],
+ goals: [],
+ garageSaleOnly: false,
+ });
+ const [sortBy, setSortBy] = useState('featured');
+ const [sortOrder, setSortOrder] = useState('desc');
+
+ // scroll to top on load
+ useEffect(() => {
+ const current = document.getElementById('collection-page');
+ if (current) {
+ const elementTop = current.getBoundingClientRect().top + window.scrollY;
+ window.scrollTo({
+ top: elementTop - 150,
+ behavior: 'auto',
+ });
+ }
+ }, []);
+
+ // Fetch collection data
+ useEffect(() => {
+ if (!name) return;
+ dispatch(fetchCollectionById(name));
+
+ return () => {
+ dispatch(clearCurrentCollection());
+ };
+ }, [dispatch, name]);
+
+ // Calculate max price from collection products
+ const maxProductPrice = useMemo(() => {
+ if (!collectionProducts || collectionProducts.length === 0) return 100;
+ const prices = collectionProducts
+ .map((p) => Number(p.price || 0))
+ .filter((p) => !isNaN(p) && p > 0);
+ return Math.ceil(Math.max(...prices)) || 100;
+ }, [collectionProducts]);
+
+ // Apply filters to collection products
+ const filteredProducts = useMemo(() => {
+ if (!collectionProducts || collectionProducts.length === 0) return [];
+ const [minPrice, maxPrice] = filters.priceRange || [0, maxProductPrice];
+
+ return collectionProducts.filter((p) => {
+ const price = Number(p.price || 0);
+ if (Number.isNaN(price)) return false;
+ if (price < minPrice || price > maxPrice) return false;
+
+ if (filters.garageSaleOnly) {
+ const saleVal = Number(p.sale || 0) || (p.onSale ? 1 : 0);
+ if (!(saleVal > 0)) return false;
+ }
+
+ if (filters.categories && filters.categories.length > 0) {
+ if (!filters.categories.includes(p.category)) return false;
+ }
+
+ if (filters.goals && filters.goals.length > 0) {
+ const productGoals = Array.isArray(p.goals) ? p.goals : [];
+ const hasGoal = filters.goals.some((g) => productGoals.includes(g));
+ if (!hasGoal) return false;
+ }
+
+ return true;
+ });
+ }, [collectionProducts, filters, maxProductPrice]);
+
+ // Apply sorting
+ const sortedProducts = useMemo(() => {
+ if (!filteredProducts || filteredProducts.length === 0) return [];
+ const sorted = [...filteredProducts];
+
+ if (sortBy === 'title') {
+ sorted.sort((a, b) => {
+ const titleA = (a.name || a.title || '').toLowerCase();
+ const titleB = (b.name || b.title || '').toLowerCase();
+ return sortOrder === 'asc'
+ ? titleA.localeCompare(titleB)
+ : titleB.localeCompare(titleA);
+ });
+ } else if (sortBy === 'price') {
+ sorted.sort((a, b) => {
+ const priceA = Number(a.price || 0);
+ const priceB = Number(b.price || 0);
+ return sortOrder === 'asc' ? priceA - priceB : priceB - priceA;
+ });
+ } else if (sortBy === 'rating') {
+ sorted.sort((a, b) => {
+ const ratingA = Number(a.rating || 0);
+ const ratingB = Number(b.rating || 0);
+ return sortOrder === 'asc' ? ratingA - ratingB : ratingB - ratingA;
+ });
+ }
+
+ return sorted;
+ }, [filteredProducts, sortBy, sortOrder]);
+
+ const displayedProducts = sortedProducts.slice(0, displayedCount);
+ const hasMoreProducts = displayedCount < sortedProducts.length;
+
+ // Infinite scroll observer
+ const lastProductElementRef = useCallback(
+ (node) => {
+ if (loading) return;
+ if (observer.current) observer.current.disconnect();
+ observer.current = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting && hasMoreProducts) {
+ setDisplayedCount((prevCount) =>
+ Math.min(prevCount + 8, sortedProducts.length)
+ );
+ }
+ });
+ if (node) observer.current.observe(node);
+ },
+ [loading, hasMoreProducts, sortedProducts.length]
+ );
+
+ // Format collection name for display
+ const collectionTitle =
+ currentCollection?.replace(/-/g, ' ') || name.replace(/-/g, ' ');
+ const collectionTitleCapitalized =
+ collectionTitle.charAt(0).toUpperCase() + collectionTitle.slice(1);
+
+ if (error) {
+ return (
+ <>
+
+
+
+
+
+ Unable to Load Collection
+
+
{error}
+
+
+
+ >
+ );
+ }
+
+ const imageUrl = collections.find((col) => col.id === name)?.image || null;
+
+ console.log(imageUrl);
+ return (
+ <>
+
+
+
+ {isFilterOpen && (
+ setIsFilterOpen(false)}
+ />
+ )}
+
+ {/* Collection Banner */}
+
+
+
+
+ {collectionTitleCapitalized}
+
+
+ Browse our {collectionTitle} collection and find premium
+ supplements that support your fitness goals.
+
+
+
+ {pagination?.total || sortedProducts.length} products available
+
+
+
+
+
+ {/* Toolbar */}
+
+
+
+
+
+ {
+ setSortBy(field || 'featured');
+ setSortOrder(order || 'desc');
+ setDisplayedCount(12);
+ }}
+ />
+
+
+
+
+ {/* Product Grid Section */}
+
+
+
+ {loading &&
}
+
+ {!loading && !error && displayedProducts.length > 0 && (
+ <>
+
+ {hasMoreProducts && (
+
+ )}
+ >
+ )}
+
+ {!loading && !error && displayedProducts.length === 0 && (
+
+
+
+ No Products Found
+
+
+ We couldn't find any products matching your filters in this
+ collection.
+
+
+
+ )}
+
+
+
+
+
setIsFilterOpen(false)}
+ products={collectionProducts || []}
+ filters={filters}
+ onChangeFilters={(next) => {
+ const pr = next.priceRange || [0, maxProductPrice];
+ setFilters({
+ priceRange: [pr[0] ?? 0, pr[1] ?? maxProductPrice],
+ categories: next.categories || [],
+ goals: next.goals || [],
+ garageSaleOnly: !!next.garageSaleOnly,
+ });
+ setDisplayedCount(12);
+ }}
+ />
+
+ {!loading && !error && }
+
+ >
+ );
}
diff --git a/src/routes/RouterConfig.jsx b/src/routes/RouterConfig.jsx
index f8e403a4..ec0e6ae1 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 TermsOfService = lazy(
() => import('../pages/TermsOfService/TermsOfService')
);
diff --git a/src/store/CollectionSlice.js b/src/store/CollectionSlice.js
new file mode 100644
index 00000000..6eb55743
--- /dev/null
+++ b/src/store/CollectionSlice.js
@@ -0,0 +1,69 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import { getCollectionById } from '../api/collectionService';
+
+// Async thunk to fetch a collection by ID/slug
+export const fetchCollectionById = createAsyncThunk(
+ 'collections/fetchById',
+ async (collectionId, { rejectWithValue }) => {
+ try {
+ const data = await getCollectionById(collectionId);
+ return data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || 'Failed to fetch collection'
+ );
+ }
+ }
+);
+
+const collectionSlice = createSlice({
+ name: 'collections',
+ initialState: {
+ currentCollection: null,
+ products: [],
+ loading: false,
+ error: null,
+ pagination: {
+ page: 1,
+ limit: 50,
+ total: 0,
+ },
+ },
+ reducers: {
+ clearCurrentCollection: (state) => {
+ state.currentCollection = null;
+ state.products = [];
+ state.error = null;
+ state.pagination = {
+ page: 1,
+ limit: 50,
+ total: 0,
+ };
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchCollectionById.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchCollectionById.fulfilled, (state, action) => {
+ state.loading = false;
+ state.currentCollection = action.payload.collections;
+ state.products = action.payload.products || [];
+ state.pagination = {
+ page: action.payload.page || 1,
+ limit: action.payload.limit || 50,
+ total: action.payload.total || 0,
+ };
+ })
+ .addCase(fetchCollectionById.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload || 'An error occurred';
+ });
+ },
+});
+
+export const { clearCurrentCollection } = collectionSlice.actions;
+
+export default collectionSlice.reducer;
diff --git a/src/store/index.js b/src/store/index.js
index 067b8c77..a68aebfd 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -2,12 +2,14 @@ import { configureStore } from '@reduxjs/toolkit';
import productSlice from './productSlice';
import cartSlice from './cartSlice';
import authSlice from './authSlice';
+import collectionSlice from './CollectionSlice';
const store = configureStore({
reducer: {
products: productSlice,
cart: cartSlice,
auth: authSlice,
+ collections: collectionSlice,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
@@ -17,4 +19,4 @@ const store = configureStore({
}),
});
-export default store;
\ No newline at end of file
+export default store;