diff --git a/package.json b/package.json
index de7f8b7a..e993c1bd 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,8 @@
"axios": "^1.7.2",
"axios-mock-adapter": "^1.22.0",
"chart.js": "^4.4.3",
+ "chart.js": "^4.4.3",
+ "date-fns": "^3.6.0",
"cloudinary": "^2.2.0",
"cloudinary-core": "^2.13.1",
"date-fns": "^3.6.0",
diff --git a/public/icons/address.svg b/public/icons/address.svg
new file mode 100644
index 00000000..34e1c338
--- /dev/null
+++ b/public/icons/address.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/card.svg b/public/icons/card.svg
new file mode 100644
index 00000000..914aacc7
--- /dev/null
+++ b/public/icons/card.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/ccv.svg b/public/icons/ccv.svg
new file mode 100644
index 00000000..9a20e59a
--- /dev/null
+++ b/public/icons/ccv.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/date.svg b/public/icons/date.svg
new file mode 100644
index 00000000..5a1bee21
--- /dev/null
+++ b/public/icons/date.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/location.svg b/public/icons/location.svg
new file mode 100644
index 00000000..31b1bc22
--- /dev/null
+++ b/public/icons/location.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/user.svg b/public/icons/user.svg
new file mode 100644
index 00000000..36a314fb
--- /dev/null
+++ b/public/icons/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/momo.svg b/public/momo.svg
new file mode 100644
index 00000000..cc0b0152
--- /dev/null
+++ b/public/momo.svg
@@ -0,0 +1,125 @@
+
+
diff --git a/public/unionpay.svg b/public/unionpay.svg
new file mode 100644
index 00000000..0c1b1993
--- /dev/null
+++ b/public/unionpay.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/__test__/Checkout/cardInput.test.tsx b/src/__test__/Checkout/cardInput.test.tsx
new file mode 100644
index 00000000..06b58614
--- /dev/null
+++ b/src/__test__/Checkout/cardInput.test.tsx
@@ -0,0 +1,60 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+import CardInput from '@/components/Checkout/CardInput';
+
+describe('CardInput Component', () => {
+ it('renders CardInput component and types into card number input', () => {
+ const mockSaveCard = vi.fn();
+ render();
+ const cardNumberInput = screen.getByPlaceholderText('Card Number');
+ fireEvent.change(cardNumberInput, {
+ target: { value: '4111111111111111' },
+ });
+ expect(cardNumberInput).toHaveValue('4111111111111111');
+ });
+
+ it('detects Visa card type correctly', () => {
+ const mockSaveCard = vi.fn();
+ render();
+ const cardNumberInput = screen.getByPlaceholderText('Card Number');
+ fireEvent.change(cardNumberInput, {
+ target: { value: '4111111111111111' },
+ });
+ expect(screen.getByAltText('visa')).toBeInTheDocument();
+ });
+
+ test('detects Mastercard card type correctly', () => {
+ const mockSaveCard = vi.fn();
+ render();
+ const cardNumberInput = screen.getByPlaceholderText('Card Number');
+ fireEvent.change(cardNumberInput, {
+ target: { value: '5105105105105100' },
+ });
+ expect(screen.getByAltText('mastercard')).toBeInTheDocument();
+ });
+
+ test('shows error for invalid card type', () => {
+ const mockSaveCard = vi.fn();
+ render();
+ const cardNumberInput = screen.getByPlaceholderText('Card Number');
+ fireEvent.change(cardNumberInput, {
+ target: { value: '1234567890123456' },
+ });
+ expect(screen.getByText('Invalid Card')).toBeInTheDocument();
+ });
+ test('handles expiry date change correctly', () => {
+ const mockSaveCard = vi.fn();
+ render();
+ const expiryDateInput = screen.getByPlaceholderText('Expiry MM/YY');
+ fireEvent.change(expiryDateInput, { target: { value: '12/34' } });
+ expect(expiryDateInput).toHaveValue('12/34');
+ });
+
+ test('shows error for invalid expiry date', () => {
+ const mockSaveCard = vi.fn();
+ render();
+ const expiryDateInput = screen.getByPlaceholderText('Expiry MM/YY');
+ fireEvent.change(expiryDateInput, { target: { value: '19/11' } });
+ expect(screen.getByText('Invalid Expiry Date')).toBeInTheDocument();
+ });
+});
diff --git a/src/__test__/Checkout/checkout.test.tsx b/src/__test__/Checkout/checkout.test.tsx
new file mode 100644
index 00000000..26d52ab3
--- /dev/null
+++ b/src/__test__/Checkout/checkout.test.tsx
@@ -0,0 +1,143 @@
+import { configureStore } from '@reduxjs/toolkit';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import checkoutReducer, {
+ placeOrder,
+ getOrders,
+ makePayment,
+ updateDeliveryInfo,
+ updateCouponCode,
+} from '@/features/Checkout/checkoutSlice';
+import Order from '@/interfaces/order';
+
+// Create mock for axios
+const mock = new MockAdapter(axios);
+
+// Configure mock store with the checkout reducer
+const store = configureStore({
+ reducer: {
+ checkout: checkoutReducer,
+ },
+});
+
+describe('checkoutSlice', () => {
+ beforeEach(() => {
+ mock.reset();
+ });
+
+ it('should handle initial state', () => {
+ expect(store.getState().checkout).toEqual({
+ checkout: {
+ id: 31,
+ totalAmount: 160,
+ status: 'Pending',
+ couponCode: '',
+ deliveryInfo: {
+ address: '123 Main St',
+ city: 'Anytown',
+ zip: '12345',
+ },
+ country: 'US',
+ paymentInfo: null,
+ trackingNumber: 'Tr280585',
+ createdAt: '2024-07-22T01:48:05.301Z',
+ updatedAt: '2024-07-22T11:01:20.291Z',
+ paid: true,
+ orderDetails: [
+ {
+ id: 41,
+ quantity: 2,
+ price: 160,
+ },
+ ],
+ },
+ loading: false,
+ paying: false,
+ error: null,
+ });
+ });
+
+ it('placeOrder updates state on fulfilled', async () => {
+ const order = {
+ deliveryInfo: {
+ address: '123 Main St',
+ city: 'Anytown',
+ zip: '12345',
+ },
+ couponCode: 'string',
+ email: 'string',
+ firstName: 'string',
+ lastName: 'string',
+ };
+
+ mock.onPost('/checkout').reply(200, { order });
+
+ await store.dispatch(placeOrder(order));
+
+ expect(store.getState().checkout.checkout.deliveryInfo).toEqual(
+ expect.objectContaining(order.deliveryInfo)
+ );
+ expect(store.getState().checkout.checkout.status).toEqual('Pending');
+ });
+
+ it('getOrders updates state on fulfilled', async () => {
+ const orders: Order[] = [
+ {
+ country: 'US',
+ couponCode: '',
+ createdAt: '2024-07-22T01:48:05.301Z',
+ deliveryInfo: {
+ address: '123 Main St',
+ city: 'Anytown',
+ zip: '12345',
+ },
+ id: 31,
+ orderDetails: [
+ {
+ id: 41,
+ price: 160,
+ quantity: 2,
+ },
+ ],
+ paid: true,
+ paymentInfo: null,
+ status: 'Pending',
+ totalAmount: 160,
+ trackingNumber: 'Tr280585',
+ updatedAt: '2024-07-22T11:01:20.291Z',
+ },
+ ];
+
+ mock.onGet('/checkout/getall-order').reply(200, orders);
+
+ await store.dispatch(getOrders());
+
+ expect(store.getState().checkout.checkout).toEqual(
+ expect.objectContaining(orders[0])
+ );
+ });
+
+ it('makePayment updates state on fulfilled', async () => {
+ mock.onPost('/buyer/payment').reply(200, { success: true });
+
+ await store.dispatch(makePayment(31));
+
+ expect(store.getState().checkout.paying).toEqual(true);
+ });
+
+ it('updateDeliveryInfo updates delivery info', () => {
+ const deliveryInfo = { address: '456 Main St' };
+ store.dispatch(updateDeliveryInfo(deliveryInfo));
+
+ expect(store.getState().checkout.checkout.deliveryInfo).toEqual(
+ expect.objectContaining(deliveryInfo)
+ );
+ });
+
+ it('updateCouponCode updates coupon code', () => {
+ const couponCode = 'NEWYEAR';
+ store.dispatch(updateCouponCode(couponCode));
+
+ expect(store.getState().checkout.checkout.couponCode).toEqual(couponCode);
+ });
+});
diff --git a/src/__test__/Orders/Orders.test.tsx b/src/__test__/Orders/Orders.test.tsx
index 0ae08535..fe796cd5 100644
--- a/src/__test__/Orders/Orders.test.tsx
+++ b/src/__test__/Orders/Orders.test.tsx
@@ -13,8 +13,8 @@ const mockOrders: Order[] = [
updatedAt: '2023-07-17T00:00:00Z',
status: 'Pending',
totalAmount: 100,
- deliveryInfo:
- '{"address": "123 Main St", "city": "Anytown", "country": "USA"}',
+ country: 'USA',
+ deliveryInfo: { address: '123 Main St', city: 'Anytown', zip: '12345' },
paymentInfo: null,
createdAt: '',
paid: false,
@@ -26,8 +26,8 @@ const mockOrders: Order[] = [
updatedAt: '2023-07-16T00:00:00Z',
status: 'Completed',
totalAmount: 200,
- deliveryInfo:
- '{"address": "456 Elm St", "city": "Othertown", "country": "USA"}',
+ country: 'USA',
+ deliveryInfo: { address: '123 Main St', city: 'Anytown', zip: '12345' },
paymentInfo: null,
createdAt: '',
paid: false,
@@ -168,6 +168,6 @@ describe('Orders Component', () => {
);
const rows = screen.getAllByRole('row');
- expect(rows.length).toBe(11); //
+ expect(rows.length).toBe(6); //
});
});
diff --git a/src/app/store.ts b/src/app/store.ts
index b91ac69c..0f7f79c9 100644
--- a/src/app/store.ts
+++ b/src/app/store.ts
@@ -17,6 +17,7 @@ import {
import buyerSlice from '@/app/Dashboard/buyerSlice';
import orderSlice from './Dashboard/orderSlice';
import cartReducer from '@/features/Cart/cartSlice';
+import checkoutSlice from '@/features/Checkout/checkoutSlice';
import ordersSliceReducer from '@/features/Orders/ordersSlice';
import contactReducer from '@/features/contact/contactSlice';
@@ -41,6 +42,7 @@ export const store = configureStore({
cartItems: cartReducer,
allProducts: allProductSlice,
contact: contactReducer,
+ checkout: checkoutSlice,
},
});
diff --git a/src/components/Cart/Cart.tsx b/src/components/Cart/Cart.tsx
index c3074f3e..62f848d4 100644
--- a/src/components/Cart/Cart.tsx
+++ b/src/components/Cart/Cart.tsx
@@ -85,7 +85,7 @@ export default function Cart() {
Total:
${total}
-
+
diff --git a/src/components/Checkout/CardInput.tsx b/src/components/Checkout/CardInput.tsx
new file mode 100644
index 00000000..6f93f386
--- /dev/null
+++ b/src/components/Checkout/CardInput.tsx
@@ -0,0 +1,236 @@
+import { useState } from 'react';
+
+interface Errors {
+ expirydate: boolean;
+ cardType: boolean;
+}
+
+export interface Card {
+ cardNumber: string;
+ cardType: 'visa' | 'mastercard' | 'unionpay' | null;
+ cardHolder: string;
+ expiryDate: string;
+ cvv: string;
+}
+
+interface ComponentProps {
+ saveCard: (newCard: Card) => void;
+}
+
+export default function CardInput({ saveCard }: ComponentProps) {
+ const [card, setCard] = useState
({
+ cardNumber: '',
+ cardType: null,
+ cardHolder: '',
+ expiryDate: '',
+ cvv: '',
+ });
+ const [errors, setErrors] = useState({
+ expirydate: false,
+ cardType: false,
+ });
+ function detectCardType(details: string) {
+ const re = {
+ unionpay: /^(62|88)\d+$/,
+ visa: /^4[0-9]{12}(?:[0-9]{3})?$/,
+ mastercard: /^5[1-5][0-9]{14}$/,
+ };
+ const keys: ('unionpay' | 'visa' | 'mastercard')[] = [
+ 'unionpay',
+ 'visa',
+ 'mastercard',
+ ];
+ for (let i = 0; i < keys.length; i += 1) {
+ const key = keys[i];
+ if (re[key].test(details)) {
+ return key;
+ }
+ }
+ return null;
+ }
+ function handleCardChange(e: React.ChangeEvent) {
+ const cardType = detectCardType(e.target.value);
+ if (cardType === 'visa') {
+ setCard({ ...card, cardType: 'visa', cardNumber: e.target.value });
+ setErrors({ ...errors, cardType: false });
+ } else if (cardType === 'mastercard') {
+ setCard({ ...card, cardType: 'mastercard', cardNumber: e.target.value });
+ setErrors({ ...errors, cardType: false });
+ } else if (cardType === 'unionpay') {
+ setCard({ ...card, cardType: 'unionpay', cardNumber: e.target.value });
+ setErrors({ ...errors, cardType: false });
+ } else {
+ setCard({ ...card, cardType: null });
+ setErrors({ ...errors, cardType: true });
+ }
+ }
+
+ function handleNameChange(e: React.ChangeEvent) {
+ const cardHolder = e.target.value;
+ setCard({ ...card, cardHolder });
+ }
+
+ function handleExpiryChange(e: React.ChangeEvent) {
+ const today = new Date();
+ let expiryDate = e.target.value;
+ if (expiryDate.length === 2 && !expiryDate.includes('/')) {
+ expiryDate += '/';
+ }
+ if (expiryDate.length > 5 && expiryDate.includes('/')) {
+ expiryDate = expiryDate.slice(0, 5);
+ }
+ if (Number(expiryDate.slice(0, 2)) > 12) {
+ setErrors({ ...errors, expirydate: true });
+ } else if (Number(expiryDate.slice(3, 5)) > 99) {
+ setErrors({ ...errors, expirydate: true });
+ } else if (Number(expiryDate.slice(3, 5)) < today.getFullYear() % 100) {
+ setErrors({ ...errors, expirydate: true });
+ } else if (Number(expiryDate.slice(0, 2)) <= today.getMonth()) {
+ setErrors({ ...errors, expirydate: true });
+ } else setErrors({ ...errors, expirydate: false });
+ setCard({ ...card, expiryDate });
+ }
+ function handlesave() {
+ if (
+ !errors.cardType &&
+ !errors.expirydate &&
+ card.cardHolder &&
+ card.cardNumber &&
+ card.expiryDate &&
+ card.cardType &&
+ card.cvv
+ ) {
+ saveCard(card);
+ }
+ }
+ return (
+
+
+ {errors.cardType && (
+
Invalid Card
+ )}
+
+
+
+
+
+ {errors.expirydate && (
+
+ Invalid Expiry Date
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Checkout/Checkout.tsx b/src/components/Checkout/Checkout.tsx
new file mode 100644
index 00000000..a868a9aa
--- /dev/null
+++ b/src/components/Checkout/Checkout.tsx
@@ -0,0 +1,396 @@
+import { useState, useEffect } from 'react';
+import CardInput, { Card } from './CardInput';
+import { RootState } from '@/app/store';
+import { Checkout as CheckoutType } from '@/interfaces/checkout';
+import {
+ selectCheckout,
+ placeOrder,
+ makePayment,
+} from '@/features/Checkout/checkoutSlice';
+import { useAppDispatch, useAppSelector } from '@/app/hooks';
+import {
+ showSuccessToast,
+ showErrorToast,
+ showInfoToast,
+} from '@/utils/ToastConfig';
+
+function Direction({ rotate }: { rotate: number }) {
+ return (
+
+ );
+}
+
+function Checkout() {
+ const [chosen, setChosen] = useState('card');
+ const [cards, setCards] = useState([] as Card[]);
+ const [adding, setAdding] = useState(false);
+ const [address, setAddress] = useState('');
+ const [city, setCity] = useState('');
+ const [coupon, setCoupon] = useState('');
+
+ const dispatch = useAppDispatch();
+ const user = useAppSelector((state) => state.signIn.user);
+ function handleAdding() {
+ setAdding(!adding);
+ }
+ const checkoutState = useAppSelector((state: RootState) =>
+ selectCheckout(state)
+ );
+ const order = checkoutState.checkout;
+ const { loading, error, paying } = checkoutState;
+ function handleSave(newCard: Card) {
+ setCards((prev) => [...prev, newCard]);
+
+ const checkout: CheckoutType = {
+ deliveryInfo: {
+ address,
+ city,
+ zip: '12345',
+ },
+ couponCode: coupon,
+ email: user?.email || '',
+ firstName: user?.firstName || '',
+ lastName: user?.lastName || '',
+ };
+ dispatch(placeOrder(checkout));
+ }
+
+ function handlePayment() {
+ dispatch(makePayment(order.id));
+ }
+
+ useEffect(() => {
+ if (loading && paying) {
+ showInfoToast('Paying...');
+ } else if (!loading && paying && !error) {
+ showSuccessToast('Succesfully Paid');
+ } else if (paying && error) {
+ showErrorToast(error || 'failed');
+ }
+ }, [error, loading, paying]);
+
+ return (
+
+
+
Delivery Address
+
+
Payment Methods
+
+
+
+
+ {chosen === 'momo' && (
+
+
+
+ My Momo Number
+
+
+
+
+
+ )}
+
+
+
+
+
+ {chosen === 'card' && (
+
+
+
+ My Cards
+
+
+ {cards.map((card) => (
+
+
+
+ ))}
+
+ {adding && (
+
handleSave(newCard)} />
+ )}
+
+
+
+ )}
+
+
+
+
+
+ {order.orderDetails.map((item) => (
+
+ ))}
+
+
+
Cart Collection
+ My saved collection
+ For summer sales
+
+
+
+
+
Promo Code
+
+
setCoupon(e.target.value)}
+ value={coupon}
+ />
+
+
+
+
+
+ Total
+ ${order.totalAmount}
+
+
+
+
+
+
+
+ Total Cost
+ ${order.totalAmount}
+
+
+
+
+
+
+
+ );
+}
+
+export default Checkout;
diff --git a/src/components/Checkout/header.tsx b/src/components/Checkout/header.tsx
new file mode 100644
index 00000000..c6afcc3e
--- /dev/null
+++ b/src/components/Checkout/header.tsx
@@ -0,0 +1,60 @@
+import Item from '../home/headerItem';
+
+interface HeaderItem {
+ image: string;
+ title: string;
+ description: string;
+ key: number;
+}
+
+const headerItems: HeaderItem[] = [
+ {
+ image: '/icons/icon1.svg',
+ title: 'Free Shipping',
+ description: 'Free shipping on all orders',
+ key: 1,
+ },
+ {
+ image: '/icons/icon4.svg',
+ title: 'Online Support 24/7',
+ description: 'Support online 24 hours a day',
+ key: 2,
+ },
+ {
+ image: '/icons/icon3.svg',
+ title: 'Money Return',
+ description: 'Back guarantee under 7 days',
+ key: 3,
+ },
+ {
+ image: '/icons/icon2.svg',
+ title: 'Member Discount',
+ description: 'On every order over $20.00',
+ key: 4,
+ },
+];
+
+export default function Header() {
+ return (
+
+
+
Complete Your Order
+
+ You are just a few steps away to finsih
+
+ your order...
+
+
+ {headerItems.map((item) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/Orders/OrderDetailsModal.tsx b/src/components/Orders/OrderDetailsModal.tsx
index a98b69e2..133f7838 100644
--- a/src/components/Orders/OrderDetailsModal.tsx
+++ b/src/components/Orders/OrderDetailsModal.tsx
@@ -6,7 +6,7 @@ interface ModalProps {
}
function OrderDetailsModal({ close, order }: ModalProps) {
- const billigDetails = JSON.parse(order.deliveryInfo);
+ const billigDetails = order.deliveryInfo;
return (
diff --git a/src/components/Orders/Orders.tsx b/src/components/Orders/Orders.tsx
index 7d0f4341..55af4d39 100644
--- a/src/components/Orders/Orders.tsx
+++ b/src/components/Orders/Orders.tsx
@@ -23,7 +23,7 @@ export function Orders() {
);
const [currentPage, setCurrentPage] = useState(1);
- const [ordersPerPage] = useState(10);
+ const [ordersPerPage] = useState(5);
const [sortColumn, setSortColumn] = useState
(null);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
const [filterStatus, setFilterStatus] = useState('All');
@@ -75,7 +75,6 @@ export function Orders() {
const results = sortedOrders.filter(
(order) =>
order.id.toString().includes(query) ||
- order.deliveryInfo.toLowerCase().includes(query) ||
order.trackingNumber.toLowerCase().includes(query) ||
order.paymentInfo?.toLowerCase().includes(query) ||
order.updatedAt.toLowerCase().includes(query)
diff --git a/src/features/Checkout/checkoutSlice.tsx b/src/features/Checkout/checkoutSlice.tsx
new file mode 100644
index 00000000..0f37925f
--- /dev/null
+++ b/src/features/Checkout/checkoutSlice.tsx
@@ -0,0 +1,175 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import axios from 'axios';
+import { RootState } from '../../app/store';
+import { Checkout } from '@/interfaces/checkout';
+import Order from '@/interfaces/order';
+
+export interface CheckoutState {
+ checkout: Order;
+ loading: boolean;
+ error: string | null;
+ paying: boolean;
+}
+
+const initialOrder: Order = {
+ id: 31,
+ totalAmount: 160,
+ status: 'Pending',
+ couponCode: '',
+ deliveryInfo: {
+ address: '123 Main St',
+ city: 'Anytown',
+ zip: '12345',
+ },
+ country: 'US',
+ paymentInfo: null,
+ trackingNumber: 'Tr280585',
+ createdAt: '2024-07-22T01:48:05.301Z',
+ updatedAt: '2024-07-22T11:01:20.291Z',
+ paid: true,
+ orderDetails: [
+ {
+ id: 41,
+ quantity: 2,
+ price: 160,
+ },
+ ],
+};
+
+const initialState: CheckoutState = {
+ checkout: initialOrder,
+ loading: false,
+ paying: false,
+ error: null,
+};
+
+const baseUrl = import.meta.env.VITE_BASE_URL;
+
+export const placeOrder = createAsyncThunk(
+ 'order/create',
+ async (order: Checkout) => {
+ const tokenFromStorage = localStorage.getItem('token') || '';
+ const response = await axios.post(
+ `${baseUrl}/checkout`,
+ { ...order },
+ {
+ headers: {
+ Authorization: `Bearer ${tokenFromStorage}`,
+ },
+ }
+ );
+ return response.data.order;
+ }
+);
+
+export const getOrders = createAsyncThunk('order/get', async () => {
+ const tokenFromStorage = localStorage.getItem('token') || '';
+ const response = await axios.get(`${baseUrl}/checkout/getall-order`, {
+ headers: {
+ Authorization: `Bearer ${tokenFromStorage}`,
+ },
+ });
+ return {
+ ...response.data[0],
+ deliveryInfo: JSON.parse(response.data[0].deliveryInfo),
+ };
+});
+
+export const makePayment = createAsyncThunk(
+ 'order/pay',
+ async (orderId: number) => {
+ const tokenFromStorage = localStorage.getItem('token') || '';
+ const response = await axios.post(
+ `${baseUrl}/buyer/payment`,
+ {
+ token: 'tok_visa',
+ orderId,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${tokenFromStorage}`,
+ },
+ }
+ );
+ return response.data;
+ }
+);
+
+const checkoutSlice = createSlice({
+ name: 'checkout',
+ initialState,
+ reducers: {
+ updateDeliveryInfo: (
+ state,
+ action: PayloadAction>
+ ) => {
+ state.checkout.deliveryInfo = {
+ ...state.checkout.deliveryInfo,
+ ...action.payload,
+ };
+ },
+ updateCouponCode: (state, action: PayloadAction) => {
+ state.checkout.couponCode = action.payload;
+ },
+ updateEmail: (state, action: PayloadAction) => {
+ state.checkout.email = action.payload;
+ },
+ updateFirstName: (state, action: PayloadAction) => {
+ state.checkout.firstName = action.payload;
+ },
+ updateLastName: (state, action: PayloadAction) => {
+ state.checkout.lastName = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(placeOrder.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(placeOrder.fulfilled, (state, action: PayloadAction) => {
+ state.loading = false;
+ state.checkout = { ...state.checkout, ...action.payload };
+ })
+ .addCase(placeOrder.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.error.message || 'Failed to place order';
+ })
+ .addCase(getOrders.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(getOrders.fulfilled, (state, action: PayloadAction) => {
+ state.loading = false;
+ state.checkout = { ...state.checkout, ...action.payload[0] };
+ })
+ .addCase(getOrders.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.error.message || 'Failed to fetch orders';
+ })
+ .addCase(makePayment.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ state.paying = true;
+ })
+ .addCase(makePayment.fulfilled, (state) => {
+ state.loading = false;
+ })
+ .addCase(makePayment.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.error.message || 'Payment failed';
+ });
+ },
+});
+
+export const {
+ updateDeliveryInfo,
+ updateCouponCode,
+ updateEmail,
+ updateFirstName,
+ updateLastName,
+} = checkoutSlice.actions;
+
+export const selectCheckout = (state: RootState) => state.checkout;
+
+export default checkoutSlice.reducer;
diff --git a/src/features/Orders/ordersSlice.ts b/src/features/Orders/ordersSlice.ts
index 06161e70..6416c14a 100644
--- a/src/features/Orders/ordersSlice.ts
+++ b/src/features/Orders/ordersSlice.ts
@@ -23,15 +23,23 @@ interface Payload {
export const fetchOrders = createAsyncThunk('orders/fetchOrders', async () => {
const tokenFromStorage = localStorage.getItem('token') || '';
- const response = await axios.get(
- `${baseUrl}/checkout/getall-order`,
- {
- headers: {
- Authorization: `Bearer ${tokenFromStorage}`,
+ const response = await axios.get(`${baseUrl}/checkout/getall-order`, {
+ headers: {
+ Authorization: `Bearer ${tokenFromStorage}`,
+ },
+ });
+ const res = response.data.orders;
+ const orders = res.map((order: any) => {
+ return {
+ ...order,
+ deliveryInfo: {
+ address: JSON.parse(order.deliveryInfo).address,
+ city: JSON.parse(order.deliveryInfo).city,
+ zip: JSON.parse(order.deliveryInfo).zip,
},
- }
- );
- return response.data.orders;
+ };
+ });
+ return orders;
});
const ordersSlice = createSlice({
diff --git a/src/interfaces/checkout.ts b/src/interfaces/checkout.ts
new file mode 100644
index 00000000..f3e4690d
--- /dev/null
+++ b/src/interfaces/checkout.ts
@@ -0,0 +1,73 @@
+interface DeliveryInfo {
+ address: string;
+ city: string;
+ zip: string;
+}
+
+export interface Checkout {
+ deliveryInfo: DeliveryInfo;
+ couponCode: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+}
+
+export interface Order {
+ msg: string;
+ order: {
+ user: {
+ id: number;
+ firstName: string;
+ lastName: string;
+ email: string;
+ password: string;
+ googleId: string | null;
+ facebookId: string | null;
+ picture: string;
+ provider: string | null;
+ isVerified: boolean;
+ status: string;
+ twoFactorCode: string | null;
+ createdAt: string;
+ updatedAt: string;
+ };
+ totalAmount: number;
+ status: string;
+ deliveryInfo: {
+ address: string;
+ city: string;
+ zip: string;
+ };
+ country: string;
+ trackingNumber: string;
+ orderDetails: {
+ product: {
+ id: number;
+ name: string;
+ image: string;
+ gallery: string[];
+ shortDesc: string;
+ longDesc: string;
+ quantity: number;
+ regularPrice: number;
+ salesPrice: number;
+ tags: string[];
+ type: string;
+ isAvailable: boolean;
+ isFeatured: boolean;
+ averageRating: number;
+ createdAt: string;
+ updatedAt: string;
+ };
+ quantity: number;
+ price: number;
+ id: number;
+ }[];
+ paymentInfo: null;
+ paid: boolean;
+ id: number;
+ createdAt: string;
+ updatedAt: string;
+ };
+ trackingNumber: string;
+}
diff --git a/src/interfaces/order.ts b/src/interfaces/order.ts
index 61edd65f..24b7a09c 100644
--- a/src/interfaces/order.ts
+++ b/src/interfaces/order.ts
@@ -1,8 +1,19 @@
+interface DeliveryInfo {
+ address: string;
+ city: string;
+ zip: string;
+}
+
export interface Order {
id: number;
totalAmount: number;
+ country: string;
status: string;
- deliveryInfo: string;
+ couponCode?: string;
+ email?: string;
+ firstName?: string;
+ lastName?: string;
+ deliveryInfo: DeliveryInfo;
paymentInfo: string | null;
trackingNumber: string;
createdAt: string;
diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx
new file mode 100644
index 00000000..943612e5
--- /dev/null
+++ b/src/pages/Checkout.tsx
@@ -0,0 +1,11 @@
+import Checkout from '@/components/Checkout/Checkout';
+import Header from '@/components/Checkout/header';
+
+export default function CheckoutPage() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx
index c6c04c7e..75dccb42 100644
--- a/src/routes/AppRoutes.tsx
+++ b/src/routes/AppRoutes.tsx
@@ -15,12 +15,13 @@ import EditProductPage from '@/pages/EditPage';
import Shop from '@/pages/Shop';
import ContactPage from '@/pages/contact';
import Wishlist from '@/pages/Wishlist';
-import { Orders } from '@/components/Orders/Orders';
+import Orders from '@/pages/Orders';
import AddProducts from '@/components/dashBoard/addProducts';
import ProductDetails from '@/pages/ProductDetails';
import ProtectedRoute from '@/components/ProtectedRoute';
import Cart from '@/components/Cart/Cart';
import Seller from '@/pages/Seller';
+import CheckoutPage from '@/pages/Checkout';
function AppRoutes() {
return (
@@ -40,6 +41,7 @@ function AppRoutes() {
} />
} />
} />
+ } />
} />
} />
diff --git a/tsconfig.json b/tsconfig.json
index cce82ee2..d07df174 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -25,6 +25,6 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": [".eslintrc.cjs", "src", "dist/Cart", "dist/Checkout"],
+ "include": [".eslintrc.cjs", "src", "dist/Cart", "src/components/Checkout"],
"references": [{ "path": "./tsconfig.node.json" }]
}