diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 03ebaa99..d5151fee 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -68,7 +68,13 @@ function Navbar() { 5 - + navigate('/wishlist')} + /> {user ? (
diff --git a/src/components/WishlistCard.tsx b/src/components/WishlistCard.tsx new file mode 100644 index 00000000..769018e2 --- /dev/null +++ b/src/components/WishlistCard.tsx @@ -0,0 +1,114 @@ +import { useAppDispatch, useAppSelector } from '@/app/hooks'; +import Button from './form/Button'; +import { Product } from '@/types/Product'; +import { IoClose } from 'react-icons/io5'; +import { removeFromWishlist } from '@/features/Products/ProductSlice'; + +const WishlistCard = ({ product }: { product: Product }) => { + const { token } = useAppSelector((state) => state.signIn); + const dispatch = useAppDispatch(); + return ( +
+ dispatch(removeFromWishlist({ token, id: product.id }))} + /> +
+ wishlistImage +
+
+

+ {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..a0f53df7 100644 --- a/src/components/home/ProductCard.tsx +++ b/src/components/home/ProductCard.tsx @@ -1,10 +1,15 @@ +import { useAppDispatch, useAppSelector } from '@/app/hooks'; +import { addToWishlist } from '@/features/Products/ProductSlice'; import { Product } from '@/types/Product'; +import { useEffect } from 'react'; interface ProductCardProps { product: Product; } function ProductCard({ product }: ProductCardProps) { + const dispatch = useAppDispatch(); + const { token } = useAppSelector((state) => state.signIn); return (
18 && '...'} dispatch(addToWishlist({ token, id: product.id }))} xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-gray-600 cursor-pointer bg-gray-100 p-1" viewBox="0 0 24 24" diff --git a/src/features/Products/ProductSlice.ts b/src/features/Products/ProductSlice.ts index 41302714..87fb13ca 100644 --- a/src/features/Products/ProductSlice.ts +++ b/src/features/Products/ProductSlice.ts @@ -4,7 +4,7 @@ import axios 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,77 @@ 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) { + 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) { + 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) { + 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 +184,8 @@ const initialState: ProductsState = { ], allProducts: [], recommendedProducts: [], + wishlistProducts: [], + wishlistLoading: false, total: 0, }; @@ -186,6 +253,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/Wishlist.tsx b/src/pages/Wishlist.tsx new file mode 100644 index 00000000..9fe75e3b --- /dev/null +++ b/src/pages/Wishlist.tsx @@ -0,0 +1,35 @@ +import { useAppDispatch, useAppSelector } from '@/app/hooks'; +import WishlistCard from '@/components/WishlistCard'; +import { fetchWishlistProducts } from '@/features/Products/ProductSlice'; +import { useEffect } from 'react'; + +const Wishlist = () => { + const dispatch = useAppDispatch(); + const { wishlistProducts } = useAppSelector((state) => state.products); + const { token } = useAppSelector((state) => state.signIn); + + useEffect(() => { + dispatch(fetchWishlistProducts(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'],