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 item 1 + ))} +
+
+

Cart Collection

+

My saved collection

+

For summer sales

+
+
+ +
+

Promo Code

+
+ setCoupon(e.target.value)} + value={coupon} + /> + +
+ +
+
+ Total + ${order.totalAmount} +
+
+ +
+
+ Shipping + $0 +
+
+ +
+
+ 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" }] }