Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: User is now able to perform payments #127

Merged
merged 3 commits into from
Jul 26, 2024
Merged
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
2,990 changes: 2,572 additions & 418 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"pre-commit": "npm run lint:fix && npm run format"
},
"dependencies": {
"@emotion/styled": "^11.13.0",
"@mui/material": "^5.16.4",
"@react-jvectormap/core": "^1.0.4",
"@react-jvectormap/world": "^1.1.2",
"@reduxjs/toolkit": "^2.2.6",
Expand Down
13 changes: 10 additions & 3 deletions src/__test__/Cart/Cart.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ describe('cartSlice', () => {
});

it('should fetch cart items successfully', async () => {
const mockCartItems = [{ id: 1, name: 'Product 1', quantity: 2 }];
const mockCartItems = [
{ id: 1, name: 'Product 1', quantity: 2, image: 'product_image.png' },
];
httpMock
.onGet(`${process.env.VITE_BASE_URL}/cart`)
.reply(200, { cartItems: mockCartItems });
Expand Down Expand Up @@ -91,13 +93,18 @@ describe('Cart component', () => {
it('renders cart item', async () => {
render(
<Provider store={store}>
<CartItem id={1} price={100} name="Test Product" quantity={3} />
<CartItem
id={1}
price={100}
name="Test Product"
image="product_IMAGE.png"
quantity={3}
/>
</Provider>
);

expect(screen.getByText('$300')).toBeInTheDocument();
expect(screen.getByText('Test Product')).toBeInTheDocument();
expect(screen.getByText('Size')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
});
});
13 changes: 13 additions & 0 deletions src/assets/icons/add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/discount.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions src/assets/icons/sports.svg

This file was deleted.

3 changes: 3 additions & 0 deletions src/components/Cart/Cart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function Cart() {
const products: Product[] = useAppSelector((state: RootState) =>
selectProducts(state)
);

const cartItems = useAppSelector((state: RootState) =>
selectCartItems(state)
);
Expand Down Expand Up @@ -64,6 +65,7 @@ export default function Cart() {
<CartItem
id={item.id}
quantity={item.quantity}
image={item.product.image}
price={item.product.salesPrice}
name={item.product.name}
key={item.id}
Expand All @@ -75,6 +77,7 @@ export default function Cart() {
.map((item) => (
<CartItem
id={item.id}
image={item.product.image}
quantity={item.quantity}
price={item.product.salesPrice}
name={item.product.name}
Expand Down
58 changes: 4 additions & 54 deletions src/components/Cart/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import {
updateCartItemQuantity,
removeCartItem,
Expand All @@ -10,11 +9,11 @@ interface CartProps {
price: number;
name: string;
quantity: number;
image: string;
}

function CartItem({ id, price, name, quantity }: CartProps) {
function CartItem({ id, price, name, quantity, image }: CartProps) {
const dispatch = useAppDispatch();
const [size, setSize] = useState<'M' | 'S' | 'L'>('M');

const handleQuantityChange = (amount: number) => {
if (quantity + amount < 1) {
Expand All @@ -26,66 +25,17 @@ function CartItem({ id, price, name, quantity }: CartProps) {
}
};

const handleSize = (newSize: 'M' | 'S' | 'L') => {
setSize(newSize);
};

return (
<div className="flex items-center border-b border-gray-400 pt-8 pb-4 h-max max-w-screen-md">
<img
src="/product.svg"
src={image}
alt="Product"
className="w-48 h-32 object-cover rounded-md"
/>
<div className="flex flex-col flex-grow w-full justify-between px-8">
<span className="font-bold text-2xl">{name}</span>
<div className="flex justify-between">
<div className="flex items-center gap-4 py-2 flex-col">
<span className="text-gray-600 text-center w-full text-sm">
Color
</span>
<div className="flex gap-2">
<button
type="button"
className="w-6 h-6 rounded-full border-2 border-primary bg-white"
>
{' '}
</button>
<button
type="button"
className="w-6 h-6 rounded-full border-2 border-primary bg-gray-200"
>
{' '}
</button>
</div>
</div>
<div className="flex items-center gap-4 py-2 flex-col">
<span className="text-gray-600 text-center text-sm">Size</span>
<div className="flex">
<button
onClick={() => handleSize('L')}
type="button"
className={`px-3 text-sm py-1 border border-primary ${size === 'L' ? 'text-white bg-primary' : 'text-gray-600'}`}
>
L
</button>
<button
onClick={() => handleSize('M')}
type="button"
className={`px-3 text-sm py-1 border border-primary ${size === 'M' ? 'text-white bg-primary' : 'text-gray-600'}`}
>
M
</button>
<button
onClick={() => handleSize('S')}
type="button"
className={`px-3 text-sm py-1 border border-primary ${size === 'S' ? 'text-white bg-primary' : 'text-gray-600'}`}
>
S
</button>
</div>
</div>
<div className="flex items-center gap-4 py-2 flex-col text-gary-600 text-sm">
<div className="flex items-start gap-4 py-2 flex-col text-gary-600 text-sm">
<span className="text-gray-600 text-center">Quantity</span>
<div className="flex items-center space-x-2">
<button
Expand Down
46 changes: 31 additions & 15 deletions src/components/Checkout/Checkout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import BeatLoader from 'react-spinners/BeatLoader';
import CardInput, { Card } from './CardInput';
import { RootState } from '@/app/store';
import { fetchCartItems, selectCartItems } from '@/features/Cart/cartSlice';
import { Checkout as CheckoutType } from '@/interfaces/checkout';

import {
selectCheckout,
placeOrder,
makePayment,
updateStatus,
resetState,
makeMomoPayment,
} from '@/features/Checkout/checkoutSlice';
import { useAppDispatch, useAppSelector } from '@/app/hooks';
import {
Expand Down Expand Up @@ -43,6 +46,7 @@ function Checkout() {
const [address, setAddress] = useState('');
const [city, setCity] = useState('');
const [coupon, setCoupon] = useState('');
const [momoNumber, setMomoNumber] = useState('');

const dispatch = useAppDispatch();
const navigate = useNavigate();
Expand All @@ -61,6 +65,7 @@ function Checkout() {
);
const order = checkoutState.checkout;
const { loading, error, paying } = checkoutState;

function handleSave(newCard: Card) {
setCards((prev) => [...prev, newCard]);
}
Expand Down Expand Up @@ -94,10 +99,19 @@ function Checkout() {
firstName: user?.firstName || '',
lastName: user?.lastName || '',
};
dispatch(placeOrder(checkout)).then((res) =>
dispatch(makePayment(res.payload.id))
);
} else dispatch(makePayment(order.id));
dispatch(placeOrder(checkout)).then((res) => {
const orderId = res.payload.id;
if (chosen === 'momo') {
dispatch(makeMomoPayment({ momoNumber, orderId }));
} else if (chosen === 'card') {
dispatch(makePayment(orderId));
}
});
} else if (chosen === 'momo') {
dispatch(makeMomoPayment({ momoNumber, orderId: order.id }));
} else if (chosen === 'card') {
dispatch(makePayment(order.id));
}
}

useEffect(() => {
Expand All @@ -110,7 +124,7 @@ function Checkout() {
dispatch(resetState());
navigate('/');
} else if (paying && error) {
showErrorToast(error || 'failed');
showErrorToast('failed');
}
}, [error, loading, paying, navigate, dispatch]);

Expand Down Expand Up @@ -173,24 +187,21 @@ function Checkout() {
{chosen === 'momo' && (
<div className="py-6 px-4 border border-gray-300 rounded-md mb-6">
<div className="mb">
<h3 className="w-full text-xl text-gray-500 ounded-md text-left">
<h3 className="w-full text-xl text-gray-500 rounded-md text-left">
My Momo Number
</h3>

<div className="rounded-md py-4 flex flex-col gap-6">
<div className="rounded-md p-4 border border-gray-300">
<label
htmlFor="card"
htmlFor="momoNumber"
className="flex items-center w-full bg-gray-100 rounded"
>
<img
src="momo.svg"
alt="Mastercard"
className="w-12 mr-4"
/>
<img src="momo.svg" alt="Momo" className="w-12 mr-4" />
<input
className="text-gray-500 h-full w-full outline-none bg-gray-100"
placeholder="078* *** *34"
value={momoNumber}
onChange={(e) => setMomoNumber(e.target.value)}
/>
</label>
</div>
Expand Down Expand Up @@ -415,11 +426,16 @@ function Checkout() {
</div>

<button
className="w-full bg-primary text-white py-4 text-2xl font-medium rounded-md"
className={`w-full bg-primary text-white py-4 text-2xl font-medium rounded-md ${chosen ? '' : 'opacity-50 cursor-not-allowed'} ${paying ? 'opacity-75 cursor-not-allowed' : ''}`}
type="button"
onClick={handlePayment}
disabled={!chosen || paying}
>
Pay Here
{paying ? (
<BeatLoader data-testid="Loading" color="#ffffff" size={8} />
) : (
'Pay Here'
)}
</button>
</div>
</div>
Expand Down
33 changes: 21 additions & 12 deletions src/features/Cart/cartSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import Cart from '@/interfaces/cart';
import { RootState } from '../../app/store';
import { showErrorToast } from '@/utils/ToastConfig';

interface CartState {
cartItems: Cart[];
Expand Down Expand Up @@ -82,18 +83,26 @@ export const removeCartItem = createAsyncThunk(

export const addCartItem = createAsyncThunk(
'cart/addCartItem',
async ({ productId, quantity }: { productId: number; quantity: number }) => {
const tokenFromStorage = localStorage.getItem('token') || '';
const response = await axios.post<AddPayload>(
`${baseUrl}/cart`,
{ productId, quantity },
{
headers: {
Authorization: `Bearer ${tokenFromStorage}`,
},
}
);
return response.data.cartItem;
async (
{ productId, quantity }: { productId: number; quantity: number },
{ rejectWithValue }
) => {
try {
const tokenFromStorage = localStorage.getItem('token') || '';
const response = await axios.post<AddPayload>(
`${baseUrl}/cart`,
{ productId, quantity },
{
headers: {
Authorization: `Bearer ${tokenFromStorage}`,
},
}
);
return response.data.cartItem;
} catch (error) {
showErrorToast('Failed to add cart item');
return rejectWithValue((error as any).response.data);
}
}
);

Expand Down
Loading
Loading