Skip to content
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 changes: 1 addition & 1 deletion e-commerce-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export const App = () => {
getDetails(data.access_token).then((res) => {
if ('data' in res) {
dispatch(setAuth({ email: res.data.email }));
dispatch(setLogIn());
}
});
dispatch(setLogIn());
}
}, [isSuccess, data]);

Expand Down
84 changes: 84 additions & 0 deletions e-commerce-app/src/api/cartApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { ICartApiResponse } from '../types/slicesTypes/cart';
import { IUpdateCartApiObjectRequest } from '../types/slicesTypes/cart/updateCartApiTypes';
import { RootStateType } from '../store/store';

export const cartApi = createApi({
reducerPath: 'cartApi',
baseQuery: fetchBaseQuery({
baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}`,
}),
tagTypes: ['activeCart'],
endpoints: (build) => ({
getMyActiveCart: build.query<ICartApiResponse, string>({
query(token: string) {
return {
url: '/me/active-cart',
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
};
},
providesTags: ['activeCart'],
async onQueryStarted(token, { queryFulfilled, dispatch }) {
try {
queryFulfilled.catch(() => dispatch(cartApi.endpoints.createCart.initiate(token)));
} catch (e) {
dispatch(cartApi.endpoints.createCart.initiate(token));
}
},
}),
createCart: build.mutation<ICartApiResponse, string>({
query(token: string) {
return {
url: '/me/carts',
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: {
currency: 'EUR',
},
};
},
invalidatesTags: ['activeCart'],
}),
updateCart: build.mutation({
query(queryObj: IUpdateCartApiObjectRequest) {
return {
url: `/me/carts/${queryObj.cartId}`,
method: 'POST',
headers: {
Authorization: `Bearer ${queryObj.token}`,
'Content-Type': 'application/json',
},
body: queryObj.data,
};
},
invalidatesTags: ['activeCart'],
}),
deleteCart: build.mutation({
query(queryObj: { cartId: string; token: string }) {
return {
url: `/me/carts/${queryObj.cartId}`,
method: 'DELETE',
headers: {
Authorization: `Bearer ${queryObj.token}`,
},
};
},
}),
}),
});

export const { useLazyGetMyActiveCartQuery, useCreateCartMutation, useUpdateCartMutation } =
cartApi;

export const selectCart = (state: RootStateType) =>
cartApi.endpoints.getMyActiveCart.select(state.user.access_token as string)(state).data;
export const findProductInCart = (state: RootStateType, productId: string) =>
cartApi.endpoints.getMyActiveCart
.select(state.user.access_token as string)(state)
.data?.lineItems.find((item) => item.productId === productId);
12 changes: 5 additions & 7 deletions e-commerce-app/src/api/myCustomerApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import {
IAuthenticateMyCustomer,
IMyCustomerBaseResponse,
IMyCustomerLoginRequest,
ISignUpMyCustomer,
} from '../types/slicesTypes/myCustomerApiSliceTypes';
import {
Expand All @@ -16,16 +16,14 @@ export const myCustomerApi = createApi({
}),
tagTypes: ['myCustomerDetails'],
endpoints: (build) => ({
authenticateMyCustomer: build.mutation<IMyCustomerBaseResponse, IMyCustomerLoginRequest>({
query(customerData: IMyCustomerLoginRequest) {
authenticateMyCustomer: build.mutation<IMyCustomerBaseResponse, IAuthenticateMyCustomer>({
query(queryObj) {
return {
url: '/login',
method: 'POST',
body: JSON.stringify(customerData),
body: JSON.stringify(queryObj.customerData),
headers: {
Authorization: `Bearer ${btoa(
process.env.REACT_APP_CTP_CLIENT_ID + ':' + process.env.REACT_APP_CTP_CLIENT_SECRET,
)}`,
Authorization: `Bearer ${queryObj.token}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
};
Expand Down
7 changes: 3 additions & 4 deletions e-commerce-app/src/components/ProductCard/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { FC, useEffect, useState } from 'react';
import styles from './ProductCard.module.scss';
import { Box, Typography, Button, CardMedia, CardContent, CardActions, Card } from '@mui/material';
import { Box, Typography, CardMedia, CardContent, CardActions, Card } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { IProductApiResponse } from '../../types/slicesTypes/productsApiTypes';
import { getTaxes } from '../../store/slices/taxesSlice';
import { useAppSelector } from '../../store/hooks';
import { ITaxApiResponse } from '../../types/slicesTypes/taxApiTypes';
import CartAddLineItem from '../../requestsComponents/CartAddLineItem/CartAddLineItem';

interface ICardProps {
item: IProductApiResponse;
Expand Down Expand Up @@ -107,9 +108,7 @@ export const ProductCard: FC<ICardProps> = ({ item }) => {
)}
</CardContent>
<CardActions>
<Button color="success" variant="outlined">
Add to Cart
</Button>
<CartAddLineItem productId={item.id} props={{ color: 'success', variant: 'outlined' }} />
</CardActions>
</Card>
);
Expand Down
16 changes: 15 additions & 1 deletion e-commerce-app/src/pages/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { useLoginUserMutation } from '../../api/authApi';
import {
getAccessToken,
getLoggedIn,
isRememberedMe,
setAuth,
Expand All @@ -30,6 +31,8 @@ import { IResponseError } from '../../types/AuthTypes';
import { ILoginFormData } from '../../interfaces/ILoginFormData';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { useLocalToken } from '../../hooks/useLocalToken';
import { useAuthenticateMyCustomerMutation } from '../../api/myCustomerApi';
import { IAuthenticateMyCustomer } from '../../types/slicesTypes/myCustomerApiSliceTypes';

const defaultFormState: ILoginFormData = {
email: '',
Expand All @@ -48,6 +51,7 @@ export const LoginPage: FC = () => {
const navigate = useNavigate();
const from = '/';

const accessToken = useAppSelector(getAccessToken) as string;
const isLoggedIn = useAppSelector(getLoggedIn);
const isRememberedUser = useAppSelector(isRememberedMe);

Expand All @@ -68,6 +72,7 @@ export const LoginPage: FC = () => {
});

const [loginUser, { isSuccess, error: errorApi, isError, data }] = useLoginUserMutation();
const [authenticateUser] = useAuthenticateMyCustomerMutation();

const {
register,
Expand Down Expand Up @@ -155,7 +160,16 @@ export const LoginPage: FC = () => {
}),
);
try {
loginUser({ email: data.email, password: data.password });
const authObj: IAuthenticateMyCustomer = {
token: accessToken,
customerData: {
email: data.email,
password: data.password,
},
};
authenticateUser(authObj).then(() =>
loginUser({ email: data.email, password: data.password }),
);
} catch {
console.log('er');
}
Expand Down
2 changes: 2 additions & 0 deletions e-commerce-app/src/pages/LogoutPage/LogoutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { CircularProgress } from '@mui/material';
import { useLocalToken } from '../../hooks/useLocalToken';
import { useLogoutUserMutation } from '../../api/authApi';
import { resetCart } from '../../store/slices/cartSlice';

export const LogoutPage = (): JSX.Element => {
const navigate = useNavigate();
Expand All @@ -28,6 +29,7 @@ export const LogoutPage = (): JSX.Element => {
useEffect(() => {
dispatch(setLogOut());
delTokenFromStorage();
dispatch(resetCart());
}, [isSuccess, isError]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { FC, JSX } from 'react';
import { IUpdateCartApiObjectRequest } from '../../types/slicesTypes/cart/updateCartApiTypes';
import { useAppSelector } from '../../store/hooks';
import { getAccessToken } from '../../store/slices/userSlice';
import { findProductInCart, selectCart, useUpdateCartMutation } from '../../api/cartApi';
import {
IAddLineItemCart,
ICartApiResponse,
IRemoveLineItemCart,
} from '../../types/slicesTypes/cart';
import { ButtonProps } from '@mui/material';
import Button from '@mui/material/Button';
import CartQuery from '../CartQuery/CartQuery';
interface ICartAddLineItemProps {
children?: string | JSX.Element | JSX.Element[] | React.ReactNode;
productId: string;
variantId?: number;
props?: ButtonProps;
}
const CartAddLineItem: FC<ICartAddLineItemProps> = ({
children,
productId,
variantId,
props,
}): JSX.Element => {
const accessToken = useAppSelector(getAccessToken) as string;
const cartId = (useAppSelector(selectCart) as ICartApiResponse)?.id as string;
const cartVersion = (useAppSelector(selectCart) as ICartApiResponse)?.version as number;
const productInCart = useAppSelector((state) => findProductInCart(state, productId));

const [updateCart, { isLoading }] = useUpdateCartMutation();

if (!cartId || !cartVersion) {
return <CartQuery />;
}

const toggleLineItem = () => {
if (productInCart) {
const actionObject: IRemoveLineItemCart = {
action: 'removeLineItem',
lineItemId: productInCart.id,
quantity: productInCart.quantity,
};
const queryObj: IUpdateCartApiObjectRequest = {
cartId,
token: accessToken,
data: { version: cartVersion, actions: [actionObject] },
};
updateCart(queryObj).then((a) => console.log(a));
} else {
const actionObject: IAddLineItemCart = {
action: 'addLineItem',
productId,
quantity: 1,
};
if (variantId) {
actionObject.variantId = variantId;
}
const queryObj: IUpdateCartApiObjectRequest = {
cartId,
token: accessToken,
data: { version: cartVersion, actions: [actionObject] },
};
updateCart(queryObj).then((a) => console.log(a));
}
};

return (
<Button {...props} onClick={toggleLineItem} disabled={isLoading}>
{children}
{productInCart ? 'Remove from Cart' : 'Add to Cart'}
</Button>
);
};
export default CartAddLineItem;
36 changes: 36 additions & 0 deletions e-commerce-app/src/requestsComponents/CartQuery/CartQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FC, JSX, useEffect } from 'react';
import { selectCart, useLazyGetMyActiveCartQuery } from '../../api/cartApi';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { getAccessToken } from '../../store/slices/userSlice';
import { setCart } from '../../store/slices/cartSlice';
import LoadingProgress from '../../components/LoadingProgress/LoadingProgress';

interface ICartQueryProps {
children?: string | JSX.Element | JSX.Element[] | React.ReactNode;
}

const CartQuery: FC<ICartQueryProps> = ({ children }): JSX.Element => {
const [getMyActiveCart, { isLoading, isFetching, isSuccess, data }] =
useLazyGetMyActiveCartQuery();
const accessToken = useAppSelector(getAccessToken) as string;
const dispatch = useAppDispatch();
const cart = useAppSelector(selectCart);

useEffect(() => {
if (isSuccess && data) {
dispatch(setCart(data));
}
}, [isSuccess, data]);

useEffect(() => {
if (cart) return;
getMyActiveCart(accessToken);
}, [cart]);

if (isLoading || isFetching) {
return <LoadingProgress />;
}

return <>{children}</>;
};
export default CartQuery;
22 changes: 22 additions & 0 deletions e-commerce-app/src/store/slices/cartSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ICartApiResponse, ICartFromSlice } from '../../types/slicesTypes/cart';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

const initialState: ICartFromSlice = {
cart: null,
};

export const cartSlice = createSlice({
initialState,
name: 'cartSlice',
reducers: {
resetCart: (state) => {
state.cart = null;
},
setCart: (state, action: PayloadAction<ICartApiResponse>) => {
state.cart = action.payload;
},
},
});

export const CartReducer = cartSlice.reducer;
export const { resetCart, setCart } = cartSlice.actions;
5 changes: 5 additions & 0 deletions e-commerce-app/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { QueryParamsReducer } from './slices/queryParamsSlice';
import { taxApi } from '../api/taxApi';
import { TaxesReducer } from './slices/taxesSlice';
import { MyCustomerReducer } from './slices/myCustomerSlice';
import { cartApi } from '../api/cartApi';
import { CartReducer } from './slices/cartSlice';

export const store = configureStore({
reducer: {
Expand All @@ -20,12 +22,14 @@ export const store = configureStore({
[categoriesApi.reducerPath]: categoriesApi.reducer,
[taxApi.reducerPath]: taxApi.reducer,
[productProjectionApi.reducerPath]: productProjectionApi.reducer,
[cartApi.reducerPath]: cartApi.reducer,
user: UserReducer,
categories: CategoriesReducer,
products: ProductsReducer,
queryParams: QueryParamsReducer,
taxes: TaxesReducer,
myCustomer: MyCustomerReducer,
cart: CartReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({}).concat([
Expand All @@ -35,6 +39,7 @@ export const store = configureStore({
categoriesApi.middleware,
productProjectionApi.middleware,
taxApi.middleware,
cartApi.middleware,
]),
});

Expand Down
7 changes: 4 additions & 3 deletions e-commerce-app/src/types/customerCartTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface IAnonymousCartSignIn {
id: string;
typeId: 'cart' | string;
import { IBaseIdTypeResponse } from './slicesTypes/baseApiResponsesTypes';

export interface IAnonymousCartSignIn extends IBaseIdTypeResponse {
typeId: 'cart';
}
Loading