diff --git a/src/components/WishlistCard.tsx b/src/components/WishlistCard.tsx
new file mode 100644
index 00000000..dbdf448d
--- /dev/null
+++ b/src/components/WishlistCard.tsx
@@ -0,0 +1,114 @@
+import { IoClose } from 'react-icons/io5';
+import { useAppDispatch, useAppSelector } from '@/app/hooks';
+import Button from './form/Button';
+import { Product } from '@/types/Product';
+import { removeFromWishlist } from '@/features/Products/ProductSlice';
+
+function WishlistCard({ product }: { product: Product }) {
+ const { token } = useAppSelector((state) => state.signIn);
+ const dispatch = useAppDispatch();
+ return (
+
+
dispatch(removeFromWishlist({ token, id: product.id }))}
+ />
+
+
+
+
+
+ {product.quantity > 0 ? 'In Stock' : 'Out of Stock'}
+
+
+ {product.name.slice(0, 20)}
+ {product.name.length > 20 ? '...' : ''}
+
+
+
+
{product.averageRating}
+ {Array.from({ length: Math.floor(product.averageRating) }).map(
+ (_, index) => {
+ return (
+
+ );
+ }
+ )}
+
+
+ {Array.from({ length: Math.floor(4 - product.averageRating) }).map(
+ (_, index) => {
+ return (
+
+ );
+ }
+ )}
+
+
+ ${500}
+ ${700}
+
+
+
+
+ );
+}
+
+export default WishlistCard;
diff --git a/src/components/home/ProductCard.tsx b/src/components/home/ProductCard.tsx
index a6841df7..d03cafca 100644
--- a/src/components/home/ProductCard.tsx
+++ b/src/components/home/ProductCard.tsx
@@ -1,3 +1,5 @@
+import { useAppDispatch, useAppSelector } from '@/app/hooks';
+import { addToWishlist } from '@/features/Products/ProductSlice';
import { Product } from '@/types/Product';
interface ProductCardProps {
@@ -5,6 +7,8 @@ interface ProductCardProps {
}
function ProductCard({ product }: ProductCardProps) {
+ const dispatch = useAppDispatch();
+ const { token } = useAppSelector((state) => state.signIn);
return (
{product.name.substring(0, 17)}
- {product.name.length > 18 && '...'}
+ {product.name.length > 17 && '...'}
{product.shortDesc.substring(0, 27)}
- {product.shortDesc.length > 28 && '...'}
+ {product.shortDesc.length > 27 && '...'}
diff --git a/src/features/Products/ProductSlice.ts b/src/features/Products/ProductSlice.ts
index 41302714..0424cff8 100644
--- a/src/features/Products/ProductSlice.ts
+++ b/src/features/Products/ProductSlice.ts
@@ -1,10 +1,10 @@
// In your counterSlice.ts or a similar file
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
-import axios from 'axios';
+import axios, { AxiosError } from 'axios';
import { Product } from '@/types/Product';
import User from '@/types/User';
import { RootState } from '../../app/store';
-import { showErrorToast } from '@/utils/ToastConfig';
+import { showErrorToast, showSuccessToast } from '@/utils/ToastConfig';
interface Payload {
message: string;
@@ -76,12 +76,95 @@ export const searchProducts = createAsyncThunk<
}
);
+export const fetchWishlistProducts = createAsyncThunk
(
+ 'products/fetchWishlistProducts',
+ async (token, thunkAPI) => {
+ try {
+ const response = await axios.get(
+ `${import.meta.env.VITE_BASE_URL}/buyer/getOneWishlist`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+ return response.data.data.product;
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ return thunkAPI.rejectWithValue(
+ (error.response.data as { message: string }).message
+ );
+ }
+ return thunkAPI.rejectWithValue('An error occured');
+ }
+ }
+);
+
+export const addToWishlist = createAsyncThunk<
+ Product[],
+ { id: number; token: string | null }
+>('products/addToWishlist', async ({ id, token }, thunkAPI) => {
+ try {
+ const response = await axios.post(
+ `${import.meta.env.VITE_BASE_URL}/buyer/addItemToWishlist`,
+ { productId: id },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ );
+
+ return response.data.data.product;
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ return thunkAPI.rejectWithValue(
+ (error.response.data as { message: string }).message
+ );
+ }
+ return thunkAPI.rejectWithValue('An error occured');
+ }
+});
+
+export const removeFromWishlist = createAsyncThunk<
+ Product[],
+ { id: number; token: string | null }
+>('products/removeFromWishlist', async ({ id, token }, thunkAPI) => {
+ try {
+ const response = await axios.delete(
+ `${import.meta.env.VITE_BASE_URL}/buyer/removeToWishlist`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ data: {
+ productId: id,
+ },
+ }
+ );
+
+ return response.data.data.product;
+ } catch (err) {
+ const error = err as AxiosError;
+ if (error.response) {
+ return thunkAPI.rejectWithValue(
+ (error.response.data as { message: string }).message
+ );
+ }
+ return thunkAPI.rejectWithValue('An error occured');
+ }
+});
+
interface ProductsState {
isLoading: boolean;
products: Product[];
allProducts: Product[];
recommendedProducts: Product[];
total: number;
+ wishlistProducts: Product[];
+ wishlistLoading: boolean;
}
const initialState: ProductsState = {
@@ -119,6 +202,8 @@ const initialState: ProductsState = {
],
allProducts: [],
recommendedProducts: [],
+ wishlistProducts: [],
+ wishlistLoading: false,
total: 0,
};
@@ -186,6 +271,41 @@ const productsSlice = createSlice({
})
.addCase(fetchRecommendedProducts.rejected, (_, action) => {
showErrorToast(action.payload as string);
+ })
+ .addCase(fetchWishlistProducts.pending, (state) => {
+ state.wishlistLoading = true;
+ })
+ .addCase(fetchWishlistProducts.fulfilled, (state, action) => {
+ state.wishlistLoading = false;
+ state.wishlistProducts = action.payload;
+ })
+ .addCase(fetchWishlistProducts.rejected, (state, action) => {
+ state.wishlistLoading = false;
+ showErrorToast(action.payload as string);
+ })
+ .addCase(addToWishlist.pending, (state) => {
+ state.wishlistLoading = true;
+ })
+ .addCase(addToWishlist.fulfilled, (state, action) => {
+ state.wishlistLoading = false;
+ showSuccessToast('Product succesfully added to wishlist');
+ state.wishlistProducts = action.payload;
+ })
+ .addCase(addToWishlist.rejected, (state, action) => {
+ state.wishlistLoading = false;
+ showErrorToast(action.payload as string);
+ })
+ .addCase(removeFromWishlist.pending, (state) => {
+ state.wishlistLoading = true;
+ })
+ .addCase(removeFromWishlist.fulfilled, (state, action) => {
+ state.wishlistLoading = false;
+ showSuccessToast('Product successfully removed from wishlist');
+ state.wishlistProducts = action.payload;
+ })
+ .addCase(removeFromWishlist.rejected, (state, action) => {
+ state.wishlistLoading = false;
+ showErrorToast(action.payload as string);
});
},
});
diff --git a/src/pages/Shop.tsx b/src/pages/Shop.tsx
index d6c2d530..38061bf3 100644
--- a/src/pages/Shop.tsx
+++ b/src/pages/Shop.tsx
@@ -566,7 +566,9 @@ function Shop() {
{recommendedProducts.length > 0 &&
Recommended?
}
-
+ {recommendedProducts.length > 0 && (
+
+ )}
diff --git a/src/pages/Wishlist.tsx b/src/pages/Wishlist.tsx
new file mode 100644
index 00000000..20dd204c
--- /dev/null
+++ b/src/pages/Wishlist.tsx
@@ -0,0 +1,35 @@
+import { useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '@/app/hooks';
+import WishlistCard from '@/components/WishlistCard';
+import { fetchWishlistProducts } from '@/features/Products/ProductSlice';
+
+function Wishlist() {
+ const dispatch = useAppDispatch();
+ const { wishlistProducts } = useAppSelector((state) => state.products);
+ const { token } = useAppSelector((state) => state.signIn);
+
+ useEffect(() => {
+ dispatch(fetchWishlistProducts(token));
+ }, [dispatch, token]);
+
+ return (
+
+
+ My Wishlist{' '}
+ ({wishlistProducts?.length} items)
+
+
+ {wishlistProducts.length === 0 && (
+
+ You currently have no products in your wishlist
+
+ )}
+ {wishlistProducts.map((product) => (
+
+ ))}
+
+
+ );
+}
+
+export default Wishlist;
diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx
index 013d18d0..fb04114b 100644
--- a/src/routes/AppRoutes.tsx
+++ b/src/routes/AppRoutes.tsx
@@ -11,6 +11,7 @@ import ResetPasswordForm from '@/components/password/PasswordResetForm';
import AdminRoutes from '@/pages/AdminRoutes';
import Admin from '@/pages/Admin';
import Shop from '@/pages/Shop';
+import Wishlist from '@/pages/Wishlist';
function AppRoutes() {
return (
@@ -18,6 +19,7 @@ function AppRoutes() {
}>
} />
} />
+
} />
} />
} />
diff --git a/tailwind.config.js b/tailwind.config.js
index 55a26228..c232a1c4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -40,6 +40,7 @@ export default {
dashbordblue: '#4079ED',
linkGrey: '#9095A1',
sliderBg: '#F0F9FF',
+ wishlistBg: '#F5F1F1',
},
fontFamily: {
Lexend: ['Lexend'],