diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..dd030e9
Binary files /dev/null and b/.DS_Store differ
diff --git a/e-commerce-app/src/App.css b/e-commerce-app/src/App.css
index 74b5e05..7cf4dce 100644
--- a/e-commerce-app/src/App.css
+++ b/e-commerce-app/src/App.css
@@ -1,5 +1,5 @@
-.App {
- text-align: center;
+.app {
+ min-height: max-content;
}
.App-logo {
diff --git a/e-commerce-app/src/App.tsx b/e-commerce-app/src/App.tsx
index 4000d1c..39af6ea 100644
--- a/e-commerce-app/src/App.tsx
+++ b/e-commerce-app/src/App.tsx
@@ -6,11 +6,13 @@ import { useLocalToken } from './hooks/useLocalToken';
import { useAppDispatch, useAppSelector } from './store/hooks';
import { getAccessToken, setAuth, setLogIn, setLogOut } from './store/slices/userSlice';
import { useGetMyCustomerDetailsMutation } from './api/myCustomerApi';
+import LoadingProgress from './components/LoadingProgress/LoadingProgress';
export const App = () => {
const [getAnonymousToken] = useGetAnonymousTokenMutation();
const { isTokenInStorage, getTokenFromStorage, delTokenFromStorage } = useLocalToken();
- const [getAccessTokenApi, { data, isSuccess, isError }] = useGetAccessTokenFromRefreshMutation();
+ const [getAccessTokenApi, { data, isSuccess, isError, isLoading }] =
+ useGetAccessTokenFromRefreshMutation();
const [getDetails] = useGetMyCustomerDetailsMutation();
const dispatch = useAppDispatch();
const accessToken = useAppSelector(getAccessToken);
@@ -52,6 +54,10 @@ export const App = () => {
}
}, [accessToken]);
+ if (isLoading) {
+ return ;
+ }
+
return (
<>
diff --git a/e-commerce-app/src/api/categoriesApi.ts b/e-commerce-app/src/api/categoriesApi.ts
new file mode 100644
index 0000000..daf435a
--- /dev/null
+++ b/e-commerce-app/src/api/categoriesApi.ts
@@ -0,0 +1,27 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { IGetAllCategoriesResponse } from '../types/slicesTypes/categoriesApiTypes';
+
+export const categoriesApi = createApi({
+ reducerPath: 'categoriesApi',
+ baseQuery: fetchBaseQuery({
+ baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/categories`,
+ }),
+ tagTypes: ['categories'],
+ endpoints: (build) => ({
+ getAllCategories: build.query({
+ query(token: string) {
+ return {
+ url: '',
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ };
+ },
+ providesTags: ['categories'],
+ }),
+ }),
+});
+
+export const { useGetAllCategoriesQuery } = categoriesApi;
diff --git a/e-commerce-app/src/api/productProjectionApi.ts b/e-commerce-app/src/api/productProjectionApi.ts
new file mode 100644
index 0000000..2abe0f4
--- /dev/null
+++ b/e-commerce-app/src/api/productProjectionApi.ts
@@ -0,0 +1,29 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { IGetAllProductsRequest } from '../types/slicesTypes/productsApiTypes';
+import { ISearchProductsResponse } from '../types/slicesTypes/productProjectionsApiTypes';
+
+export const productProjectionApi = createApi({
+ reducerPath: 'productProjectionApi',
+ baseQuery: fetchBaseQuery({
+ baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/product-projections`,
+ }),
+ endpoints: (build) => ({
+ searchProducts: build.mutation({
+ query(queryObject) {
+ let path = '';
+ if (queryObject.params && queryObject.params.resultPath) {
+ path = queryObject.params.resultPath;
+ }
+ return {
+ url: '/search' + path,
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${queryObject.token}`,
+ },
+ };
+ },
+ }),
+ }),
+});
+// ,'variants.price.centAmount:range+(1200+to+*)'].join('&filter=')
+export const { useSearchProductsMutation } = productProjectionApi;
diff --git a/e-commerce-app/src/api/productsApi.ts b/e-commerce-app/src/api/productsApi.ts
new file mode 100644
index 0000000..8ed314b
--- /dev/null
+++ b/e-commerce-app/src/api/productsApi.ts
@@ -0,0 +1,45 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import {
+ IGetAllProductsRequest,
+ IGetAllProductsResponse,
+ IGetProductByIdRequest,
+ IProductApiResponse,
+} from '../types/slicesTypes/productsApiTypes';
+
+export const productsApi = createApi({
+ reducerPath: 'productsApi',
+ baseQuery: fetchBaseQuery({
+ baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/products`,
+ }),
+ endpoints: (build) => ({
+ getAllProducts: build.mutation({
+ query(queryObject) {
+ return {
+ url: '',
+ method: 'GET',
+ params: {
+ ...queryObject.params,
+ },
+ headers: {
+ Authorization: `Bearer ${queryObject.token}`,
+ 'Content-Type': 'application/json',
+ },
+ };
+ },
+ }),
+ getProductById: build.query({
+ query(queryObject) {
+ return {
+ url: `/${queryObject.productId}`,
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${queryObject.token}`,
+ 'Content-Type': 'application/json',
+ },
+ };
+ },
+ }),
+ }),
+});
+
+export const { useGetAllProductsMutation, useGetProductByIdQuery } = productsApi;
diff --git a/e-commerce-app/src/api/taxApi.ts b/e-commerce-app/src/api/taxApi.ts
new file mode 100644
index 0000000..99e6735
--- /dev/null
+++ b/e-commerce-app/src/api/taxApi.ts
@@ -0,0 +1,24 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { IGetAllTaxesResponse } from '../types/slicesTypes/taxApiTypes';
+
+export const taxApi = createApi({
+ reducerPath: 'taxApi',
+ baseQuery: fetchBaseQuery({
+ baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/tax-categories`,
+ }),
+ endpoints: (build) => ({
+ getAllTaxes: build.query({
+ query(token: string) {
+ return {
+ url: '',
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ };
+ },
+ }),
+ }),
+});
+
+export const { useGetAllTaxesQuery } = taxApi;
diff --git a/e-commerce-app/src/assets/images/EmptyCatalogPage.svg b/e-commerce-app/src/assets/images/EmptyCatalogPage.svg
new file mode 100644
index 0000000..f3c5dbe
--- /dev/null
+++ b/e-commerce-app/src/assets/images/EmptyCatalogPage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/e-commerce-app/src/assets/images/ProductsPageImg.png b/e-commerce-app/src/assets/images/ProductsPageImg.png
new file mode 100644
index 0000000..353b37a
Binary files /dev/null and b/e-commerce-app/src/assets/images/ProductsPageImg.png differ
diff --git a/e-commerce-app/src/assets/images/TestImg-1.png b/e-commerce-app/src/assets/images/TestImg-1.png
new file mode 100644
index 0000000..fad3a89
Binary files /dev/null and b/e-commerce-app/src/assets/images/TestImg-1.png differ
diff --git a/e-commerce-app/src/assets/images/TestImg-2.jpeg b/e-commerce-app/src/assets/images/TestImg-2.jpeg
new file mode 100644
index 0000000..3d802f8
Binary files /dev/null and b/e-commerce-app/src/assets/images/TestImg-2.jpeg differ
diff --git a/e-commerce-app/src/assets/images/TestImg.jpeg b/e-commerce-app/src/assets/images/TestImg.jpeg
new file mode 100644
index 0000000..15493c7
Binary files /dev/null and b/e-commerce-app/src/assets/images/TestImg.jpeg differ
diff --git a/e-commerce-app/src/assets/images/UserPageImg.png b/e-commerce-app/src/assets/images/UserPageImg.png
new file mode 100644
index 0000000..3ccd1f5
Binary files /dev/null and b/e-commerce-app/src/assets/images/UserPageImg.png differ
diff --git a/e-commerce-app/src/components/AddressItem/AddressItem.tsx b/e-commerce-app/src/components/AddressItem/AddressItem.tsx
new file mode 100644
index 0000000..3ad171e
--- /dev/null
+++ b/e-commerce-app/src/components/AddressItem/AddressItem.tsx
@@ -0,0 +1,61 @@
+import { FC } from 'react';
+import { Typography, Box, Paper, IconButton } from '@mui/material';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import type { Board } from '../../pages/UserPage/UserAddresses';
+import { IMyCustomerApiAddressRequest } from '../../types/addressesTypes';
+
+interface addressItemProps {
+ todo: Board;
+ onDeleteAddr: (id: Board['id']) => void;
+ onCheckAddr: (id: Board['id']) => void;
+ onEdit: (id: Board['id']) => void;
+ address: IMyCustomerApiAddressRequest | undefined;
+}
+
+export const AddressItem: FC = ({
+ todo,
+ onDeleteAddr,
+ onCheckAddr,
+ onEdit,
+ address,
+}) => (
+
+
+ onCheckAddr(todo.id)}
+ sx={{ cursor: 'pointer', textDecorationLine: todo.checked ? 'line-through' : 'none' }}
+ variant="h6"
+ component="h6"
+ gutterBottom
+ >
+ {todo.name}
+
+
+
+ {address && (
+
+ {address.streetName}, {address.city}, {address.country}, {address.postalCode}
+
+ )}
+
+
+ onEdit(todo.id)} color="primary" aria-label="edit">
+
+
+ onDeleteAddr(todo.id)} color="error" aria-label="delete">
+
+
+
+
+);
diff --git a/e-commerce-app/src/components/AddressList/AddressList.tsx b/e-commerce-app/src/components/AddressList/AddressList.tsx
new file mode 100644
index 0000000..c06a5c0
--- /dev/null
+++ b/e-commerce-app/src/components/AddressList/AddressList.tsx
@@ -0,0 +1,43 @@
+import { FC } from 'react';
+import { Box } from '@mui/material';
+import { AddressItem } from '../AddressItem/AddressItem';
+import type { Board } from '../../pages/UserPage/UserAddresses';
+import { AddressPanel } from '../Panel/Panel';
+import { IMyCustomerApiAddressRequest } from '../../types/addressesTypes';
+
+interface TodoListProps {
+ editTodoId: Board['id'] | null;
+ todoList: Board[];
+ onDeleteAddr: (id: Board['id']) => void;
+ onCheckAddr: (id: Board['id']) => void;
+ onEdit: (id: Board['id']) => void;
+ onChangeAddr: ({ name, street, city, country, postcode }: Omit) => void;
+ address: IMyCustomerApiAddressRequest | undefined;
+}
+
+export const BoardList: FC = ({
+ todoList,
+ editTodoId,
+ onChangeAddr,
+ onDeleteAddr,
+ onCheckAddr,
+ onEdit,
+ address,
+}) => (
+
+ {todoList.map((todo) => {
+ if (todo.id === editTodoId)
+ return ;
+ return (
+
+ );
+ })}
+
+);
diff --git a/e-commerce-app/src/components/Header/Header.tsx b/e-commerce-app/src/components/Header/Header.tsx
index 91537e0..2fa27b0 100644
--- a/e-commerce-app/src/components/Header/Header.tsx
+++ b/e-commerce-app/src/components/Header/Header.tsx
@@ -83,11 +83,11 @@ export const Header: React.FC = () => {
-
+
{
+ return (
+
+
+
+ Loading...
+
+
+ );
+};
+export default LoadingProgress;
diff --git a/e-commerce-app/src/components/MenuLinks/MenuLinks.tsx b/e-commerce-app/src/components/MenuLinks/MenuLinks.tsx
index 46e9179..3514a99 100644
--- a/e-commerce-app/src/components/MenuLinks/MenuLinks.tsx
+++ b/e-commerce-app/src/components/MenuLinks/MenuLinks.tsx
@@ -16,7 +16,7 @@ const MenuLinks: FC = ({ navigation, handler }): JSX.Element =>
<>
{Object.entries(navigation).map(([title, path]) => (
diff --git a/e-commerce-app/src/components/Panel/Panel.tsx b/e-commerce-app/src/components/Panel/Panel.tsx
new file mode 100644
index 0000000..1bf0161
--- /dev/null
+++ b/e-commerce-app/src/components/Panel/Panel.tsx
@@ -0,0 +1,110 @@
+import { FC, useState } from 'react';
+import AddIcon from '@mui/icons-material/Add';
+import { TextField, Paper, Button, Box, Grid } from '@mui/material';
+import type { Board } from '../../pages/UserPage/UserAddresses';
+import Checkbox from '@mui/material/Checkbox';
+import FormControlLabel from '@mui/material/FormControlLabel';
+
+const DEFAULT_TODO = { name: '', street: '', city: '', country: '', postcode: '' };
+
+interface AddBoardPanelProps {
+ mode: 'add';
+ onAddAddress: ({ name, street, city, country, postcode }: Omit) => void;
+}
+
+interface EditBoardPanelProps {
+ mode: 'edit';
+ editTodo: Omit;
+ onChangeAddr: ({ name, street, city, country, postcode }: Omit) => void;
+}
+
+type PanelProps = AddBoardPanelProps | EditBoardPanelProps;
+
+export const AddressPanel: FC = (props) => {
+ const isEdit = props.mode === 'edit';
+ const [todo, setTodo] = useState(isEdit ? props.editTodo : DEFAULT_TODO);
+
+ const onClick = () => {
+ if (isEdit) {
+ return props.onChangeAddr(todo);
+ }
+ props.onAddAddress(todo);
+ setTodo(DEFAULT_TODO);
+ };
+
+ const onChange = (event: React.ChangeEvent) => {
+ const { value, name } = event.target;
+ setTodo({ ...todo, [name]: value });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ label={
+
+ use asdefault address
+
+ }
+ labelPlacement="end"
+ />
+ } variant="outlined" onClick={onClick}>
+ {isEdit ? 'EDIT' : 'ADD'}
+
+
+
+ );
+};
diff --git a/e-commerce-app/src/components/ProductCard/ProductCard.module.scss b/e-commerce-app/src/components/ProductCard/ProductCard.module.scss
new file mode 100644
index 0000000..25a3d8e
--- /dev/null
+++ b/e-commerce-app/src/components/ProductCard/ProductCard.module.scss
@@ -0,0 +1,52 @@
+.card {
+ display: flex;
+ width: 200px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem;
+ border: 2px green double;
+ transition: box-shadow .3s ease-in-out;
+ cursor: pointer;
+
+ &:hover {
+ box-shadow: 8px 10px 5px 2px #14763033;
+ transition: box-shadow .3s ease-in-out;
+ }
+
+
+ &__text {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ }
+
+ &__title {
+ width: 100%;
+ text-align: center;
+ color: cadetblue;
+ font-weight: 900;
+ }
+
+ &__price {
+ padding-top: 1rem;
+ width: 50%;
+ font-weight: 700;
+ text-align: center;
+
+ &__marked {
+ color: rgb(221, 65, 65);
+ padding-top: 1rem;
+ width: 50%;
+ font-weight: 700;
+ text-align: center;
+ }
+
+ &__sale {
+ text-decoration: line-through;
+ text-decoration-color: gray;
+ padding-top: 1rem;
+ }
+ }
+}
\ No newline at end of file
diff --git a/e-commerce-app/src/components/ProductCard/ProductCard.tsx b/e-commerce-app/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000..eb498c9
--- /dev/null
+++ b/e-commerce-app/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,110 @@
+import { FC, useEffect, useState } from 'react';
+import styles from './ProductCard.module.scss';
+import { Box, Typography, Button, 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';
+
+interface ICardProps {
+ item: IProductApiResponse;
+}
+
+export const ProductCard: FC = ({ item }) => {
+ const navigate = useNavigate();
+ const handlerNavigation = () => {
+ navigate(`/products/${item.id}`);
+ };
+
+ const taxesArray = useAppSelector(getTaxes);
+
+ const [tax, setTax] = useState(0);
+
+ useEffect(() => {
+ taxesArray
+ .filter((i) => i.id === item.taxCategory.id && i.key === 'sale')
+ .flatMap((elem) => elem.rates)
+ .filter((rate: ITaxApiResponse) => rate.country === 'DE')
+ .forEach((rate: ITaxApiResponse) => {
+ setTax(rate.amount);
+ });
+ }, [item]);
+
+ const imgPath = item.masterData.current.masterVariant.images[0].url;
+ const imgDescription = item.masterData.current.name.en;
+
+ const currencyEUR = item.masterData.current.masterVariant.prices[0].value.currencyCode;
+ const numberEUR =
+ item.masterData.current.masterVariant.prices[0].value.centAmount /
+ 10 ** item.masterData.current.masterVariant.prices[0].value.fractionDigits;
+ const priceEUR = new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: currencyEUR,
+ }).format(numberEUR);
+ const salePriceEUR = new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: currencyEUR,
+ }).format(numberEUR - numberEUR * tax);
+
+ const currencyUSD = item.masterData.current.masterVariant.prices[1].value.currencyCode;
+ const numberUSD =
+ item.masterData.current.masterVariant.prices[1].value.centAmount /
+ 10 ** item.masterData.current.masterVariant.prices[1].value.fractionDigits;
+ const priceUSD = new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: currencyUSD,
+ }).format(numberUSD);
+ const salePriceUSD = new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: currencyUSD,
+ }).format(numberUSD - numberUSD * tax);
+
+ return (
+
+
+
+
+
+
+ {item.masterData.current.name.en}
+
+
+ {tax !== 0 ? (
+ <>
+
+ {salePriceEUR}
+
+
+ {priceEUR}
+
+ >
+ ) : (
+
+ {priceEUR}
+
+ )}
+
+ {tax !== 0 ? (
+ <>
+
+ {salePriceUSD}
+
+
+ {priceUSD}
+
+ >
+ ) : (
+
+ {priceUSD}
+
+ )}
+
+
+
+ Read more
+
+
+
+ );
+};
diff --git a/e-commerce-app/src/components/ProductsFilterForm/ProductsFilterForm.tsx b/e-commerce-app/src/components/ProductsFilterForm/ProductsFilterForm.tsx
new file mode 100644
index 0000000..5fbbb5b
--- /dev/null
+++ b/e-commerce-app/src/components/ProductsFilterForm/ProductsFilterForm.tsx
@@ -0,0 +1,159 @@
+import React, { JSX, useState } from 'react';
+import {
+ Box,
+ Button,
+ FormControl,
+ InputLabel,
+ MenuItem,
+ Select,
+ Stack,
+ Typography,
+} from '@mui/material';
+import Slider from '@mui/material/Slider';
+import { useAppSelector } from '../../store/hooks';
+import { getAllCategories } from '../../store/slices/categoriesSlice';
+import { SubmitHandler, useForm } from 'react-hook-form';
+import { ISortByForm, SortFormType } from '../../types/searchProductsTypes/filterFormTypes';
+import { useDispatch } from 'react-redux';
+import {
+ getQueryCategories,
+ getQueryCentAmount,
+ getQuerySort,
+ setEmptySort,
+ setQueryCategories,
+ setQueryCentAmount,
+ setQuerySort,
+ setQueryText,
+} from '../../store/slices/queryParamsSlice';
+
+const minDistance = 10;
+const ProductsFilterForm = (): JSX.Element => {
+ const dispatch = useDispatch();
+ const searchQuerySort = useAppSelector(getQuerySort);
+ const searchQueryCategories = useAppSelector(getQueryCategories);
+ const categories = useAppSelector(getAllCategories);
+ const centAmount = useAppSelector(getQueryCentAmount);
+
+ const [priceSort, setPriceSort] = React.useState(centAmount);
+ const [sortRate, setSortRate] = useState(searchQuerySort);
+ const [sortCategories, setSortCategories] = useState(searchQueryCategories);
+
+ const handleChange2 = (event: Event, newValue: number | number[], activeThumb: number) => {
+ if (!Array.isArray(newValue)) {
+ return;
+ }
+
+ if (newValue[1] - newValue[0] < minDistance) {
+ if (activeThumb === 0) {
+ const clamped = Math.min(newValue[0], 100 - minDistance);
+ setPriceSort([clamped, clamped + minDistance]);
+ } else {
+ const clamped = Math.max(newValue[1], minDistance);
+ setPriceSort([clamped - minDistance, clamped]);
+ }
+ } else {
+ setPriceSort(newValue as number[]);
+ }
+ };
+
+ const { register, handleSubmit, reset } = useForm({
+ defaultValues: {
+ sort: sortRate,
+ },
+ });
+
+ const submitFormHandler: SubmitHandler = (data) => {
+ if (data.sort) {
+ dispatch(setQuerySort(data.sort));
+ }
+ if (!(priceSort[0] === 0 && priceSort[1] === 100)) {
+ dispatch(setQueryCentAmount(priceSort));
+ }
+ if (data.categories) {
+ dispatch(setQueryCategories(data.categories));
+ }
+ if (!data.sort && priceSort[0] === 0 && priceSort[1] === 100 && !data.categories) {
+ dispatch(setEmptySort());
+ }
+ };
+
+ const clearSortHandler = () => {
+ dispatch(setEmptySort());
+ dispatch(setQueryText(''));
+ reset();
+ setSortRate('');
+ setSortCategories('');
+ };
+
+ return (
+
+
+
+
+ Product Categories
+
+ Categories
+
+
+
+
+ Filter by price
+
+
+ {priceSort[0]}
+
+ {priceSort[1]}
+
+
+
+
+
+ Sort By
+
+
+
+ Sort
+
+
+ Clear Sort
+
+
+
+ );
+};
+export default ProductsFilterForm;
diff --git a/e-commerce-app/src/components/ProductsList/EmptyProducts.tsx b/e-commerce-app/src/components/ProductsList/EmptyProducts.tsx
new file mode 100644
index 0000000..f8b30ae
--- /dev/null
+++ b/e-commerce-app/src/components/ProductsList/EmptyProducts.tsx
@@ -0,0 +1,13 @@
+import { FC } from 'react';
+import styles from './ProductsList.module.scss';
+import { Box } from '@mui/material';
+import empty from '../../assets/images/EmptyCatalogPage.svg';
+
+export const EmptyProducts: FC = () => {
+ return (
+
+ This is an empty product list. There are no products available!
+
+
+ );
+};
diff --git a/e-commerce-app/src/components/ProductsList/ProductsList.module.scss b/e-commerce-app/src/components/ProductsList/ProductsList.module.scss
new file mode 100644
index 0000000..c6403f8
--- /dev/null
+++ b/e-commerce-app/src/components/ProductsList/ProductsList.module.scss
@@ -0,0 +1,18 @@
+.container {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 1.5rem;
+}
+
+.empty__products {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: gray;
+ font-size: 25px;
+}
+
+.img {
+ margin-top: 50px;
+}
\ No newline at end of file
diff --git a/e-commerce-app/src/components/ProductsList/ProductsList.tsx b/e-commerce-app/src/components/ProductsList/ProductsList.tsx
new file mode 100644
index 0000000..e737d07
--- /dev/null
+++ b/e-commerce-app/src/components/ProductsList/ProductsList.tsx
@@ -0,0 +1,21 @@
+import { Box } from '@mui/material';
+import { ProductCard } from '../ProductCard/ProductCard';
+import styles from './ProductsList.module.scss';
+import { useAppSelector } from '../../store/hooks';
+import { getProducts } from '../../store/slices/productsSlice';
+import { FC } from 'react';
+import { EmptyProducts } from './EmptyProducts';
+
+export const ProductsList: FC = () => {
+ const products = useAppSelector(getProducts);
+
+ return (
+
+ {products.length > 0 ? (
+ products.map((item) => )
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/e-commerce-app/src/components/UserRedirect/UserRedirect.tsx b/e-commerce-app/src/components/UserRedirect/UserRedirect.tsx
new file mode 100644
index 0000000..2bce03e
--- /dev/null
+++ b/e-commerce-app/src/components/UserRedirect/UserRedirect.tsx
@@ -0,0 +1,13 @@
+import { JSX } from 'react';
+import { useAppSelector } from '../../store/hooks';
+import { getMyCustomerId } from '../../store/slices/myCustomerSlice';
+import { Navigate } from 'react-router-dom';
+import LoadingProgress from '../LoadingProgress/LoadingProgress';
+
+const UserRedirect = (): JSX.Element => {
+ const myCustomerId = useAppSelector(getMyCustomerId);
+
+ if (!myCustomerId) return ;
+ return ;
+};
+export default UserRedirect;
diff --git a/e-commerce-app/src/hooks/useValidate.tsx b/e-commerce-app/src/hooks/useValidate.tsx
index ccb4044..0d46976 100644
--- a/e-commerce-app/src/hooks/useValidate.tsx
+++ b/e-commerce-app/src/hooks/useValidate.tsx
@@ -26,6 +26,7 @@ const defaultErrorsObj: IRegistrationFormData = {
postalCodeBilling: null,
streetAddressBilling: null,
streetAddressShipping: null,
+ currentPassword: null,
};
export const useValidate = () => {
diff --git a/e-commerce-app/src/index.tsx b/e-commerce-app/src/index.tsx
index 63e2335..9c2f94a 100644
--- a/e-commerce-app/src/index.tsx
+++ b/e-commerce-app/src/index.tsx
@@ -13,11 +13,9 @@ import { store } from './store/store';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
-
-
-
-
- ,
+
+
+ ,
);
// If you want to start measuring performance in your app, pass a function
diff --git a/e-commerce-app/src/interfaces/IRegistrationFormData.ts b/e-commerce-app/src/interfaces/IRegistrationFormData.ts
index 4b828e4..bd7b81b 100644
--- a/e-commerce-app/src/interfaces/IRegistrationFormData.ts
+++ b/e-commerce-app/src/interfaces/IRegistrationFormData.ts
@@ -13,4 +13,5 @@ export interface IRegistrationFormData {
cityBilling: string | null;
postalCodeShipping: string | null;
postalCodeBilling: string | null;
+ currentPassword: string | null;
}
diff --git a/e-commerce-app/src/interfaces/IUserProps.ts b/e-commerce-app/src/interfaces/IUserProps.ts
new file mode 100644
index 0000000..21d3c07
--- /dev/null
+++ b/e-commerce-app/src/interfaces/IUserProps.ts
@@ -0,0 +1,20 @@
+import { IRegistrationFormData } from './IRegistrationFormData';
+import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
+import { IValues } from './IValues';
+import { globalErrors } from '../types';
+import { IMyCustomerApiAddressRequest } from '../types/addressesTypes';
+
+export interface IUserProps {
+ register: UseFormRegister;
+ validationHandler: (
+ fieldName: keyof IRegistrationFormData,
+ value: string,
+ values?: IValues,
+ ) => void;
+ errors: globalErrors;
+ userData?: string[];
+ userAddresses?: (IMyCustomerApiAddressRequest[] | string[] | string | undefined)[];
+ setValue: UseFormSetValue;
+ password?: string;
+ getValues?: (fieldName: string) => string | undefined;
+}
diff --git a/e-commerce-app/src/pages/ProductPage/ProductPage.module.scss b/e-commerce-app/src/pages/ProductPage/ProductPage.module.scss
new file mode 100644
index 0000000..99478e9
--- /dev/null
+++ b/e-commerce-app/src/pages/ProductPage/ProductPage.module.scss
@@ -0,0 +1,89 @@
+.left {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ gap: 15px;
+
+ .images {
+ flex: 1;
+ min-width: max-content;
+
+ img {
+ width: 100%;
+ height: 100px;
+ margin-bottom: 15px;
+
+ }
+ }
+}
+
+.img {
+ &_small {
+ object-fit: contain;
+ cursor: pointer;
+ }
+
+ &_big {
+ width: 100%;
+ object-fit: contain;
+ max-height: 50vh;
+ cursor: pointer;
+ }
+}
+
+.price {
+ font-weight: 500;
+ color: green;
+
+ &_full {
+ text-decoration: line-through;
+ color: green;
+ text-decoration-color: gray;
+ }
+}
+
+.quantity {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+
+ button {
+ background-color: greenyellow;
+ cursor: pointer;
+ }
+}
+
+.btn {
+ button {
+ width: 250px;
+ padding: 10px;
+ display: flex;
+ justify-content: center;
+ font-weight: 700;
+ background-color: green;
+ color: white;
+ border: 1px solid green;
+ border-radius: 10px;
+ cursor: pointer;
+
+ &:hover {
+ color: green;
+ border: 1px solid green;
+ }
+ }
+}
+
+.modal {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ border: 2px solid #000;
+ max-height: 70vh;
+ max-width: 70vw;
+
+ &__img {
+ max-height: 60vh;
+ object-fit: contain;
+ }
+}
\ No newline at end of file
diff --git a/e-commerce-app/src/pages/ProductPage/ProductPage.tsx b/e-commerce-app/src/pages/ProductPage/ProductPage.tsx
index 7e71688..ecaac96 100644
--- a/e-commerce-app/src/pages/ProductPage/ProductPage.tsx
+++ b/e-commerce-app/src/pages/ProductPage/ProductPage.tsx
@@ -1,5 +1,227 @@
-import React from 'react';
+import { FC, useEffect, useState } from 'react';
+import styles from './ProductPage.module.scss';
+import { Box, Typography, ButtonGroup, Button, Grid, Stack } from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import RemoveIcon from '@mui/icons-material/Remove';
+import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
+import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart';
+import Modal from '@mui/material/Modal';
+import CloseIcon from '@mui/icons-material/Close';
+import IconButton from '@mui/material/IconButton';
+import { ArrowBackIos, ArrowForwardIos } from '@mui/icons-material';
+import { useGetProductByIdQuery } from '../../api/productsApi';
+import { useParams } from 'react-router-dom';
+import { useAppSelector } from '../../store/hooks';
+import { getAccessToken } from '../../store/slices/userSlice';
+import LoadingProgress from '../../components/LoadingProgress/LoadingProgress';
+import { getTaxes } from '../../store/slices/taxesSlice';
+import { ITaxApiResponse } from '../../types/slicesTypes/taxApiTypes';
-export const ProductPage: React.FC = () => {
- return Product Page
;
+const style = {
+ bgcolor: 'background.paper',
+ boxShadow: 24,
+ p: 4,
+};
+
+const styleArrows = {
+ display: 'flex',
+ justifyContent: 'space-around',
+};
+
+export const ProductPage: FC = () => {
+ const { productId } = useParams();
+ const authToken = useAppSelector(getAccessToken);
+ const taxesArray = useAppSelector(getTaxes);
+
+ const { data, isSuccess, isLoading, isFetching } = useGetProductByIdQuery({
+ productId: productId as string,
+ token: authToken as string,
+ });
+
+ const [tax, setTax] = useState(0);
+
+ useEffect(() => {
+ if (isSuccess) {
+ taxesArray
+ .filter((item) => item.id === data.taxCategory.id && item.key === 'sale')
+ .flatMap((elem) => elem.rates)
+ .filter((rate: ITaxApiResponse) => rate.country === 'DE')
+ .forEach((rate: ITaxApiResponse) => {
+ setTax(rate.amount);
+ });
+ }
+ }, [isSuccess]);
+
+ const [selectedImg, setSelectedImg] = useState(0);
+
+ const [count, setCount] = useState(1);
+
+ // Modal window
+
+ const [open, setOpen] = useState(false);
+ const handleOpen = () => setOpen(true);
+ const handleClose = () => setOpen(false);
+
+ if (isLoading || isFetching || !data) {
+ return ;
+ }
+
+ const images = data.masterData.current.masterVariant.images.map((img) => img.url);
+ const title = data.masterData.current.name.en;
+ const description = data.masterData.current.metaDescription.en;
+
+ const currencyCommon = data.masterData.current.masterVariant.prices[0].value.currencyCode;
+ const priceNumber =
+ data.masterData.current.masterVariant.prices[0].value.centAmount /
+ 10 ** data.masterData.current.masterVariant.prices[0].value.fractionDigits;
+ const priceCommon = new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: currencyCommon,
+ }).format(priceNumber);
+
+ const discountPrice = new Intl.NumberFormat('en-IN', {
+ style: 'currency',
+ currency: currencyCommon,
+ }).format(priceNumber - priceNumber * tax);
+
+ return (
+
+
+
+
+
+ {images.map((item, idx) => (
+
setSelectedImg(idx)}
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ selectedImg === 0
+ ? setSelectedImg(0)
+ : setSelectedImg((prevState) => prevState - 1)
+ }
+ disabled={selectedImg === 0}
+ >
+
+ Prev
+
+
+ selectedImg === images.length - 1
+ ? setSelectedImg((prevState) => prevState)
+ : setSelectedImg((prevState) => prevState + 1)
+ }
+ disabled={selectedImg === images.length - 1}
+ >
+ Next
+
+
+
+
+
+
+
+
+
+
+ {title}
+
+ {tax !== 0 ? (
+ <>
+
+ Discount price:{discountPrice}
+
+
+ Full price:{priceCommon}
+
+
+ Tax: {`${tax * 100} %`}
+
+ >
+ ) : (
+
+ Price:{priceCommon}
+
+ )}
+
+ {description}
+
+
+
+ {
+ setCount(Math.max(count - 1, 0));
+ }}
+ >
+
+
+
+ {count}
+
+ {
+ setCount(count + 1);
+ }}
+ >
+
+
+
+
+
+
+
+ ADD TO CART
+
+
+
+
+
+
+ ADD TO WISHLIST
+
+
+
+
+
+ );
};
diff --git a/e-commerce-app/src/pages/ProductsPage/ProductsPage.module.scss b/e-commerce-app/src/pages/ProductsPage/ProductsPage.module.scss
new file mode 100644
index 0000000..3510602
--- /dev/null
+++ b/e-commerce-app/src/pages/ProductsPage/ProductsPage.module.scss
@@ -0,0 +1,35 @@
+.top {
+ display: flex;
+ width: 100%;
+ min-height: min-content;
+ margin-top: 30px;
+
+ &__img {
+ height: fit-content;
+ }
+
+ &__form {
+ height: fit-content;
+ margin: auto;
+ background-color: beige;
+ padding: 2px 4px;
+ display: flex;
+ align-items: center;
+ max-width: 25rem;
+ }
+}
+
+.left {
+ padding-left: 1rem;
+ margin-left: 1rem;
+ color: green;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+}
+
+.right {
+ padding: 0.5rem;
+ margin: 0.5rem;
+}
+
diff --git a/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx b/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx
index 0fd36c3..f36408e 100644
--- a/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx
+++ b/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx
@@ -1,5 +1,68 @@
import React from 'react';
+import { Grid, Box, Divider, InputBase, IconButton, Paper } from '@mui/material';
+import ProductsPageImg from '../../assets/images/ProductsPageImg.png';
+import { ProductsList } from '../../components/ProductsList/ProductsList';
+import SearchIcon from '@mui/icons-material/Search';
+import styles from './ProductsPage.module.scss';
+import ProductsFilterForm from '../../components/ProductsFilterForm/ProductsFilterForm';
+import { useDispatch } from 'react-redux';
+import { SubmitHandler, useForm } from 'react-hook-form';
+import { ISearchProductForm } from '../../types/searchProductsTypes/searchFormTypes';
+import { getQueryText, setQueryText } from '../../store/slices/queryParamsSlice';
+import { useAppSelector } from '../../store/hooks';
export const ProductsPage: React.FC = () => {
- return Products Page
;
+ const dispatch = useDispatch();
+ const searchQueryText = useAppSelector(getQueryText);
+ const { register, handleSubmit } = useForm({
+ defaultValues: {
+ query: searchQueryText,
+ },
+ });
+ const submitHandler: SubmitHandler = (data) => {
+ dispatch(setQueryText(data.query || ''));
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
};
diff --git a/e-commerce-app/src/pages/UserPage/UserAddresses.tsx b/e-commerce-app/src/pages/UserPage/UserAddresses.tsx
new file mode 100644
index 0000000..f3abe0e
--- /dev/null
+++ b/e-commerce-app/src/pages/UserPage/UserAddresses.tsx
@@ -0,0 +1,164 @@
+import { FC, useState } from 'react';
+import { Box, Typography, Button } from '@mui/material';
+import { AddressPanel } from '../../components/Panel/Panel';
+import { BoardList } from '../../components/AddressList/AddressList';
+import { IUserProps } from '../../interfaces/IUserProps';
+import { IMyCustomerApiAddressRequest } from '../../types/addressesTypes';
+
+export type Board = {
+ id: number;
+ name: string;
+ street: string;
+ city: string;
+ country: string;
+ postcode: string;
+ checked: boolean;
+};
+
+const DEFAULT_TODO_LIST = [
+ {
+ id: 1,
+ name: 'Address 1',
+ street: 'street 1',
+ city: 'city 1',
+ country: 'country 1',
+ postcode: 'code 1',
+ checked: false,
+ },
+];
+
+export const UserAddresses: FC = ({
+ register,
+ validationHandler,
+ errors,
+ userAddresses,
+ setValue,
+}) => {
+ if (!userAddresses) {
+ return null;
+ }
+
+ let shippingAddress;
+ let billingAddress;
+
+ if (
+ userAddresses[0] !== undefined &&
+ userAddresses[1] !== undefined &&
+ Array.isArray(userAddresses[0]) &&
+ Array.isArray(userAddresses[1])
+ ) {
+ shippingAddress = (userAddresses[0] as IMyCustomerApiAddressRequest[]).find((item) => {
+ return (
+ typeof item === 'object' &&
+ item !== null &&
+ 'id' in item &&
+ item.id === (userAddresses[1] as string[])[0]
+ );
+ });
+
+ billingAddress = (userAddresses[0] as IMyCustomerApiAddressRequest[]).find((item) => {
+ return (
+ typeof item === 'object' &&
+ item !== null &&
+ 'id' in item &&
+ item.id === (userAddresses[2] as string[])[0]
+ );
+ });
+ }
+
+ const [editTodoId, setEditTodoId] = useState(null);
+ const [todoList, setTodoList] = useState(DEFAULT_TODO_LIST);
+
+ const onEdit = (id: Board['id']) => {
+ setEditTodoId(id);
+ };
+
+ const onDeleteAddr = (id: Board['id']) => {
+ setTodoList(todoList.filter((todo) => todo.id !== id));
+ };
+
+ const onAddAddress = ({
+ name,
+ street,
+ city,
+ country,
+ postcode,
+ }: Omit) => {
+ setTodoList([
+ ...todoList,
+ {
+ id: todoList[todoList.length - 1].id + 1,
+ name,
+ street,
+ city,
+ country,
+ postcode,
+ checked: false,
+ },
+ ]);
+ };
+
+ const onCheckAddr = (id: Board['id']) => {
+ setTodoList(
+ todoList.map((todo) => {
+ if (todo.id === id) {
+ return { ...todo, checked: !todo.checked };
+ }
+ return todo;
+ }),
+ );
+ };
+
+ const onChangeAddr = ({
+ name,
+ street,
+ city,
+ country,
+ postcode,
+ }: Omit) => {
+ setTodoList(
+ todoList.map((todo) => {
+ if (todo.id === editTodoId) {
+ return { ...todo, name, street, city, country, postcode };
+ }
+ return todo;
+ }),
+ );
+ setEditTodoId(null);
+ };
+
+ return (
+
+
+
+ Shipping adresses: {todoList.length}
+
+
+
+
+
+
+ Billing adresses: {todoList.length}
+
+
+
+
+
+ );
+};
diff --git a/e-commerce-app/src/pages/UserPage/UserData.tsx b/e-commerce-app/src/pages/UserPage/UserData.tsx
new file mode 100644
index 0000000..bc8949b
--- /dev/null
+++ b/e-commerce-app/src/pages/UserPage/UserData.tsx
@@ -0,0 +1,163 @@
+import TextField from '@mui/material/TextField';
+import Grid from '@mui/material/Grid';
+import { FC, useState, useEffect } from 'react';
+import Button from '@mui/material/Button';
+import Box from '@mui/material/Box';
+import EditIcon from '@mui/icons-material/Edit';
+import styles from './UserPage.module.scss';
+import { IUserProps } from '../../interfaces/IUserProps';
+
+export const UserData: FC = ({
+ register,
+ validationHandler,
+ errors,
+ userData,
+ setValue,
+}) => {
+ const [isEditableArray, setIsEditableArray] = useState([false, false, false, false]);
+
+ const [inputValues, setInputValues] = useState({
+ firstName: userData?.[0] || '',
+ lastName: userData?.[1] || '',
+ birthDate: userData?.[2] || '',
+ email: userData?.[3] || '',
+ });
+
+ const handleInputChange = (field: string, value: string) => {
+ setInputValues((prevInputValues) => ({
+ ...prevInputValues,
+ [field]: value,
+ }));
+ };
+
+ useEffect(() => {
+ setValue('firstName', inputValues.firstName);
+ setValue('lastName', inputValues.lastName);
+ setValue('birthDate', inputValues.birthDate);
+ setValue('email', inputValues.email);
+ }, [inputValues, setValue]);
+
+ const handleEditClick = (index: number) => {
+ const newIsEditableArray = [...isEditableArray];
+ newIsEditableArray[index] = !newIsEditableArray[index];
+ setIsEditableArray(newIsEditableArray);
+ };
+
+ return (
+
+
+ {
+ const newValue = e.target.value;
+ handleInputChange('firstName', newValue);
+ if (isEditableArray[0] && newValue !== inputValues.firstName) {
+ validationHandler('firstName', newValue);
+ }
+ },
+ })}
+ InputProps={{
+ className: !isEditableArray[0] ? styles.non__editable : '',
+ }}
+ disabled={!isEditableArray[0]}
+ />
+ handleEditClick(0)}>
+
+
+
+
+ {
+ const newValue = e.target.value;
+ handleInputChange('lastName', newValue);
+ if (isEditableArray[1] && newValue !== inputValues.lastName) {
+ validationHandler('lastName', newValue);
+ }
+ },
+ })}
+ InputProps={{
+ readOnly: !isEditableArray[1],
+ className: !isEditableArray[1] ? styles.non__editable : '',
+ }}
+ disabled={!isEditableArray[1]}
+ />
+ handleEditClick(1)}>
+
+
+
+
+ {
+ const newValue = e.target.value;
+ handleInputChange('birthDate', newValue);
+ if (isEditableArray[2] && newValue !== inputValues.birthDate) {
+ validationHandler('birthDate', newValue);
+ }
+ },
+ })}
+ InputProps={{
+ readOnly: !isEditableArray[2],
+ className: !isEditableArray[2] ? styles.non__editable : '',
+ }}
+ disabled={!isEditableArray[2]}
+ />
+ handleEditClick(2)}>
+
+
+
+
+ {
+ const newValue = e.target.value;
+ handleInputChange('email', newValue);
+ if (isEditableArray[3] && newValue !== inputValues.email) {
+ validationHandler('email', newValue);
+ }
+ },
+ })}
+ InputProps={{
+ readOnly: !isEditableArray[3],
+ className: !isEditableArray[3] ? styles.non__editable : '',
+ }}
+ disabled={!isEditableArray[3]}
+ />
+ handleEditClick(3)}>
+
+
+
+
+ );
+};
diff --git a/e-commerce-app/src/pages/UserPage/UserPage.module.scss b/e-commerce-app/src/pages/UserPage/UserPage.module.scss
new file mode 100644
index 0000000..24acbe3
--- /dev/null
+++ b/e-commerce-app/src/pages/UserPage/UserPage.module.scss
@@ -0,0 +1,10 @@
+.non__editable {
+
+ background-color: #f3eded;
+
+ &__icon {
+ stroke: gray;
+ fill: gray !important;
+ }
+
+}
\ No newline at end of file
diff --git a/e-commerce-app/src/pages/UserPage/UserPage.tsx b/e-commerce-app/src/pages/UserPage/UserPage.tsx
index 913ce7e..fada6f8 100644
--- a/e-commerce-app/src/pages/UserPage/UserPage.tsx
+++ b/e-commerce-app/src/pages/UserPage/UserPage.tsx
@@ -1,5 +1,287 @@
-import React from 'react';
+import React, { useState } from 'react';
+import CssBaseline from '@mui/material/CssBaseline';
+import Container from '@mui/material/Container';
+import Box from '@mui/material/Box';
+import Stepper from '@mui/material/Stepper';
+import Step from '@mui/material/Step';
+import StepLabel from '@mui/material/StepLabel';
+import Button from '@mui/material/Button';
+import Typography from '@mui/material/Typography';
+import { Grid } from '@mui/material';
+import Alert from '@mui/material/Alert';
+import { UserData } from './UserData';
+import { UserAddresses } from './UserAddresses';
+import { UserPassword } from './UserPassword';
+import { useValidate } from '../../hooks/useValidate';
+import { IRegistrationFormData } from '../../interfaces/IRegistrationFormData';
+import { useForm, SubmitHandler, FieldErrors } from 'react-hook-form';
+import { fieldNameType, globalErrors } from '../../types';
+import { IValues } from '../../interfaces/IValues';
+import { useAppSelector } from '../../store/hooks';
+import {
+ getMyCustomerFirstName,
+ getMyCustomerLastName,
+ getMyCustomerDateOfBirth,
+ getMyCustomerEmail,
+ getMyCustomerAddresses,
+ getMyCustomerShippingAddressIds,
+ getMyCustomerBillingAddressIds,
+ getMyCustomerDefaultShippingAddressId,
+ getMyCustomerDefaultBillingAddressId,
+ getMyCustomerPassword,
+} from '../../store/slices/myCustomerSlice';
+
+const steps = ['Personal information', 'Shipping/Billing address', 'Change password'];
export const UserPage: React.FC = () => {
- return User Page
;
+ const firstName = useAppSelector(getMyCustomerFirstName);
+ const lastName = useAppSelector(getMyCustomerLastName);
+ const birthDate = useAppSelector(getMyCustomerDateOfBirth);
+ const email = useAppSelector(getMyCustomerEmail);
+ const addresses = useAppSelector(getMyCustomerAddresses);
+ const shippingAddressId = useAppSelector(getMyCustomerShippingAddressIds);
+ const billingAddressId = useAppSelector(getMyCustomerBillingAddressIds);
+ const shippingDefaultAddressId = useAppSelector(getMyCustomerDefaultShippingAddressId);
+ const billingDefaultAddressId = useAppSelector(getMyCustomerDefaultBillingAddressId);
+ const password = useAppSelector(getMyCustomerPassword);
+
+ const [activeStep, setActiveStep] = useState(0);
+ const [skipped, setSkipped] = useState(new Set());
+ const { errors: validationErrors, validateField } = useValidate();
+
+ const {
+ register,
+ handleSubmit,
+ formState,
+ getValues,
+ setValue,
+ setError,
+ clearErrors,
+ reset: resetForm,
+ } = useForm();
+
+ const isStepSkipped = (step: number) => {
+ return skipped.has(step);
+ };
+
+ const handleNext = () => {
+ let newSkipped = skipped;
+ if (isStepSkipped(activeStep)) {
+ newSkipped = new Set(newSkipped.values());
+ newSkipped.delete(activeStep);
+ }
+
+ setActiveStep((prevActiveStep) => prevActiveStep + 1);
+ setSkipped(newSkipped);
+ };
+
+ const handleBack = () => {
+ setActiveStep((prevActiveStep) => prevActiveStep - 1);
+ };
+
+ const handleReset = () => {
+ setActiveStep(0);
+ };
+
+ const globalErrors = Object.keys(validationErrors).reduce>(
+ (acc, item) => {
+ if (validationErrors[item as keyof IRegistrationFormData]) {
+ acc[item as keyof IRegistrationFormData] = {
+ message: validationErrors[item as keyof IRegistrationFormData],
+ };
+ }
+
+ if (
+ formState.errors[item as keyof FieldErrors] &&
+ !validationErrors[item as keyof IRegistrationFormData]
+ ) {
+ acc[item as keyof IRegistrationFormData] = {
+ message:
+ formState.errors[item as keyof FieldErrors]?.message ?? null,
+ };
+ }
+
+ return acc;
+ },
+ {},
+ );
+
+ console.log('globalErrors', globalErrors);
+
+ const validationHandler = (fieldName: fieldNameType, value: string, values?: IValues): void => {
+ if (!value) {
+ const updatedErrors = {
+ ...validationErrors,
+ [fieldName]: '',
+ };
+
+ clearErrors(fieldName);
+ Object.assign(validationErrors, updatedErrors);
+ return;
+ }
+
+ const errString = validateField(fieldName, value, values);
+
+ if (errString.length) {
+ setError(fieldName, {
+ type: 'required',
+ message: errString,
+ });
+ } else {
+ clearErrors(fieldName);
+ }
+ };
+
+ const onSubmit: SubmitHandler = (data) => {
+ if (!Object.keys(globalErrors).length) {
+ console.log('data', data);
+ }
+ };
+
+ const buttonSubmitClick = () => {
+ console.log('button submit click');
+ };
+
+ const userData = [firstName, lastName, birthDate, email];
+
+ const userAddresses = [
+ addresses,
+ shippingAddressId,
+ billingAddressId,
+ shippingDefaultAddressId,
+ billingDefaultAddressId,
+ ];
+
+ return (
+
+
+
+
+
+ {steps.map((label, index) => {
+ const stepProps: { completed?: boolean } = {};
+
+ if (isStepSkipped(index)) {
+ stepProps.completed = false;
+ }
+ return (
+
+ {label}
+
+ );
+ })}
+
+ {activeStep === steps.length ? (
+
+
+ All pages completed - you're updated
+
+
+
+ Reset
+
+
+ ) : (
+
+
+ Profile Page {activeStep + 1}
+
+
+ {!!Object.keys(formState.errors).length && (
+
+
+ Please review and ensure all fields are correctly filled!
+
+
+ )}
+ {activeStep + 1 === 1 && (
+
+ )}
+ {activeStep + 1 === 2 && (
+
+ )}
+ {activeStep + 1 === 3 && (
+
+ )}
+
+
+ Back
+
+
+
+ {activeStep === steps.length - 1 ? 'Done' : 'Next'}
+
+
+
+
+ Update
+
+
+ Cancel
+
+
+
+ )}
+
+
+
+ );
};
diff --git a/e-commerce-app/src/pages/UserPage/UserPassword.tsx b/e-commerce-app/src/pages/UserPage/UserPassword.tsx
new file mode 100644
index 0000000..2c6cca6
--- /dev/null
+++ b/e-commerce-app/src/pages/UserPage/UserPassword.tsx
@@ -0,0 +1,74 @@
+import { FC } from 'react';
+import TextField from '@mui/material/TextField';
+import Grid from '@mui/material/Grid';
+import Box from '@mui/material/Box';
+import PageImg from '../../assets/images/UserPageImg.png';
+import { IUserProps } from '../../interfaces/IUserProps';
+
+export const UserPassword: FC = ({
+ register,
+ validationHandler,
+ errors,
+ setValue,
+ password,
+ getValues,
+}) => {
+ console.log('password', password);
+
+ const values = {
+ password: getValues?.('password') ?? '',
+ confirmPassword: getValues?.('confirmPassword') ?? '',
+ countryShipping: getValues?.('countryShipping') ?? '',
+ countryBilling: getValues?.('countryBilling') ?? '',
+ };
+
+ return (
+
+
+
+
+
+ {
+ return value === password || 'Current Password does not match';
+ },
+ })}
+ />
+
+ validationHandler('password', e.target.value, values),
+ })}
+ />
+
+
+ validationHandler('confirmPassword', e.target.value, values),
+ })}
+ />
+
+
+
+ );
+};
diff --git a/e-commerce-app/src/requestsComponents/CategoriesQuery/CategoriesQuery.tsx b/e-commerce-app/src/requestsComponents/CategoriesQuery/CategoriesQuery.tsx
new file mode 100644
index 0000000..19d6bff
--- /dev/null
+++ b/e-commerce-app/src/requestsComponents/CategoriesQuery/CategoriesQuery.tsx
@@ -0,0 +1,59 @@
+import React, { JSX, useEffect, useState } from 'react';
+import { useAppDispatch, useAppSelector } from '../../store/hooks';
+import { getAccessToken } from '../../store/slices/userSlice';
+import LoadingProgress from '../../components/LoadingProgress/LoadingProgress';
+import { Outlet } from 'react-router-dom';
+import { useGetAllCategoriesQuery } from '../../api/categoriesApi';
+import { setCategories, startLoadingCategories } from '../../store/slices/categoriesSlice';
+import { useGetAllTaxesQuery } from '../../api/taxApi';
+import { setTaxes, startLoadingTaxes } from '../../store/slices/taxesSlice';
+
+const CategoriesQuery = (): JSX.Element => {
+ const accessToken = useAppSelector(getAccessToken);
+ const dispatch = useAppDispatch();
+ const [waitGuard, setWaitGuard] = useState(false);
+
+ const {
+ isLoading: isLoadingTaxes,
+ isSuccess: isSuccessTaxes,
+ isFetching: isFetchingTaxes,
+ data: dataTaxes,
+ } = useGetAllTaxesQuery(accessToken as string);
+
+ const {
+ isLoading: isLoadingCategories,
+ isSuccess: isSuccessCategories,
+ isFetching: isFetchingCategories,
+ data: dataCategories,
+ } = useGetAllCategoriesQuery(accessToken as string);
+
+ useEffect(() => {
+ if (isFetchingCategories) {
+ dispatch(startLoadingCategories());
+ }
+ if (isFetchingTaxes) {
+ dispatch(startLoadingTaxes());
+ }
+ }, [isFetchingCategories, isFetchingTaxes, isLoadingCategories, isLoadingTaxes]);
+
+ useEffect(() => {
+ if (dataCategories && dataTaxes) {
+ setWaitGuard(false);
+ } else {
+ setWaitGuard(true);
+ }
+ if (dataCategories && 'results' in dataCategories) {
+ dispatch(setCategories(dataCategories));
+ }
+ if (dataTaxes && 'results' in dataTaxes) {
+ dispatch(setTaxes(dataTaxes));
+ }
+ }, [isSuccessCategories, dataCategories, isSuccessTaxes, dataTaxes]);
+
+ if (waitGuard) {
+ return ;
+ }
+
+ return ;
+};
+export default CategoriesQuery;
diff --git a/e-commerce-app/src/requestsComponents/ProductsQuery/ProductsQuery.tsx b/e-commerce-app/src/requestsComponents/ProductsQuery/ProductsQuery.tsx
new file mode 100644
index 0000000..35d0b4a
--- /dev/null
+++ b/e-commerce-app/src/requestsComponents/ProductsQuery/ProductsQuery.tsx
@@ -0,0 +1,165 @@
+import React, { JSX, useEffect, useState } from 'react';
+import { useGetAllProductsMutation } from '../../api/productsApi';
+import { useAppDispatch, useAppSelector } from '../../store/hooks';
+import { getAccessToken } from '../../store/slices/userSlice';
+import LoadingProgress from '../../components/LoadingProgress/LoadingProgress';
+import { ProductsPage } from '../../pages/ProductsPage/ProductsPage';
+import { setProducts, startLoadingProducts } from '../../store/slices/productsSlice';
+import {
+ getQueryCategories,
+ getQueryCentAmount,
+ getQuerySort,
+ getQueryText,
+} from '../../store/slices/queryParamsSlice';
+import { IBaseQueryParams } from '../../types/slicesTypes/baseApiRequestsTypes';
+import { useSearchProductsMutation } from '../../api/productProjectionApi';
+import { makeProductSliceObjectFromSearchApiRequest } from '../../utils/makeProductSliceObjectFromSearchApiRequest';
+import { makeGetQueryProductsString } from '../../utils/makeGetQueryProductsString';
+
+const ProductsQuery = (): JSX.Element => {
+ const dispatch = useAppDispatch();
+ const accessToken = useAppSelector(getAccessToken);
+ const searchQuerySort = useAppSelector(getQuerySort);
+ const searchQueryText = useAppSelector(getQueryText);
+ const searchQueryCentAmount = useAppSelector(getQueryCentAmount);
+ const searchQueryCategories = useAppSelector(getQueryCategories);
+
+ const [params, setParams] = useState({});
+
+ useEffect(() => {
+ if (searchQuerySort) {
+ setParams((prevState) => ({
+ ...prevState,
+ sort: searchQuerySort,
+ }));
+ } else {
+ setParams((prevState) => {
+ const newState = prevState;
+ delete newState.sort;
+ return newState;
+ });
+ }
+ }, [searchQuerySort]);
+
+ useEffect(() => {
+ if (searchQueryText) {
+ setParams((prevState) => ({
+ ...prevState,
+ ['text.en']: searchQueryText,
+ fuzzy: searchQueryText.length > 2,
+ fuzzyLevel: searchQueryText.length <= 2 ? 0 : searchQueryText.length > 4 ? 2 : 1,
+ }));
+ } else {
+ setParams((prevState) => {
+ const newObj = {
+ ...prevState,
+ };
+ delete newObj['text.en'];
+ delete newObj.fuzzyLevel;
+ delete newObj.fuzzy;
+ return newObj;
+ });
+ }
+ }, [searchQueryText]);
+
+ useEffect(() => {
+ const filterArr: string[] = [];
+
+ if (!(searchQueryCentAmount[0] === 0 && searchQueryCentAmount[1] === 100)) {
+ filterArr.push(
+ `variants.price.centAmount:range+(${searchQueryCentAmount[0] * 100}+to+${
+ searchQueryCentAmount[1] * 100
+ })`,
+ );
+ }
+ if (searchQueryCategories) {
+ filterArr.push(`categories.id:"${searchQueryCategories}"`);
+ }
+
+ if (filterArr.length === 0) {
+ setParams((prevState) => {
+ const newState = {
+ ...prevState,
+ };
+ delete newState.filter;
+ return newState;
+ });
+ }
+ setParams((prevState) => ({
+ ...prevState,
+ filter: filterArr,
+ }));
+ }, [searchQueryCentAmount, searchQueryCategories]);
+
+ const [
+ getAllProducts,
+ {
+ isLoading: isLoadingProducts,
+ isSuccess: isSuccessProducts,
+ isError: isErrorProducts,
+ data: dataProducts,
+ },
+ ] = useGetAllProductsMutation();
+ const [
+ searchProducts,
+ {
+ isLoading: isLoadingSearch,
+ isSuccess: isSuccessSearch,
+ isError: isErrorSearch,
+ data: dataSearch,
+ },
+ ] = useSearchProductsMutation();
+
+ useEffect(() => {
+ if (
+ (params['text.en']?.length && params['text.en']?.length > 0) ||
+ params.sort ||
+ (params.filter && params.filter.length > 0)
+ ) {
+ const resultPath = makeGetQueryProductsString(params);
+ searchProducts({
+ token: accessToken as string,
+ params: {
+ resultPath,
+ },
+ });
+ } else {
+ getAllProducts({
+ token: accessToken as string,
+ params,
+ });
+ }
+ }, [params]);
+
+ useEffect(() => {
+ if (isLoadingProducts || isLoadingSearch) {
+ dispatch(startLoadingProducts());
+ }
+ }, [isLoadingProducts, isLoadingSearch]);
+
+ useEffect(() => {
+ if (
+ (isSuccessSearch && params['text.en']) ||
+ searchQuerySort ||
+ (params.filter && params.filter.length > 0)
+ ) {
+ if (dataSearch && 'results' in dataSearch) {
+ const pushingObject = makeProductSliceObjectFromSearchApiRequest(dataSearch);
+ dispatch(setProducts(pushingObject));
+ }
+ return;
+ }
+ if (isSuccessProducts) {
+ if (dataProducts && 'results' in dataProducts) {
+ dispatch(setProducts(dataProducts));
+ }
+ }
+ }, [isSuccessProducts, dataProducts, isSuccessSearch, dataSearch]);
+
+ if (isLoadingProducts || isErrorProducts || isLoadingSearch || isErrorSearch) {
+ return ;
+ }
+
+ return ;
+};
+export default ProductsQuery;
diff --git a/e-commerce-app/src/requestsComponents/TokenGuard/TokenGuard.tsx b/e-commerce-app/src/requestsComponents/TokenGuard/TokenGuard.tsx
new file mode 100644
index 0000000..9c398fe
--- /dev/null
+++ b/e-commerce-app/src/requestsComponents/TokenGuard/TokenGuard.tsx
@@ -0,0 +1,16 @@
+import React, { JSX } from 'react';
+import { useAppSelector } from '../../store/hooks';
+import { getAccessToken } from '../../store/slices/userSlice';
+import LoadingProgress from '../../components/LoadingProgress/LoadingProgress';
+import { Outlet } from 'react-router-dom';
+
+const TokenGuard = (): JSX.Element => {
+ const accessToken = useAppSelector(getAccessToken);
+
+ if (!accessToken) {
+ return ;
+ }
+
+ return ;
+};
+export default TokenGuard;
diff --git a/e-commerce-app/src/requestsComponents/UserQuery/UserQuery.tsx b/e-commerce-app/src/requestsComponents/UserQuery/UserQuery.tsx
new file mode 100644
index 0000000..c0bf5f9
--- /dev/null
+++ b/e-commerce-app/src/requestsComponents/UserQuery/UserQuery.tsx
@@ -0,0 +1,34 @@
+import { JSX, useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../../store/hooks';
+import { getAccessToken } from '../../store/slices/userSlice';
+import { Outlet } from 'react-router-dom';
+import { useGetMyCustomerDetailsMutation } from '../../api/myCustomerApi';
+import LoadingProgress from '../../components/LoadingProgress/LoadingProgress';
+import { clearMyCustomerData, setMyCustomerData } from '../../store/slices/myCustomerSlice';
+
+const UserQuery = (): JSX.Element => {
+ const dispatch = useAppDispatch();
+ const accessToken = useAppSelector(getAccessToken);
+
+ const [getMyCustomerDetails, { data, isSuccess, isLoading }] = useGetMyCustomerDetailsMutation();
+
+ useEffect(() => {
+ dispatch(clearMyCustomerData());
+ if (accessToken) {
+ getMyCustomerDetails(accessToken);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!isSuccess) return;
+ if (data) {
+ dispatch(setMyCustomerData(data));
+ }
+ }, [isSuccess, data]);
+
+ if (isLoading) {
+ return ;
+ }
+ return ;
+};
+export default UserQuery;
diff --git a/e-commerce-app/src/routes/AppRoutes.tsx b/e-commerce-app/src/routes/AppRoutes.tsx
index aa532cd..5e3b7d6 100644
--- a/e-commerce-app/src/routes/AppRoutes.tsx
+++ b/e-commerce-app/src/routes/AppRoutes.tsx
@@ -12,9 +12,14 @@ import { LoginPage } from '../pages/LoginPage/LoginPage';
import { RegistrationPage } from '../pages/RegistrationPage/RegistrationPage';
import { UserPage } from '../pages/UserPage/UserPage';
import { ProductPage } from '../pages/ProductPage/ProductPage';
-import { ProductsPage } from '../pages/ProductsPage/ProductsPage';
import { ErrorPage } from '../pages/ErrorPage/ErrorPage';
import { LogoutPage } from '../pages/LogoutPage/LogoutPage';
+import { PrivateRoute } from './PrivateRoute';
+import ProductsQuery from '../requestsComponents/ProductsQuery/ProductsQuery';
+import CategoriesQuery from '../requestsComponents/CategoriesQuery/CategoriesQuery';
+import TokenGuard from '../requestsComponents/TokenGuard/TokenGuard';
+import UserQuery from '../requestsComponents/UserQuery/UserQuery';
+import UserRedirect from '../components/UserRedirect/UserRedirect';
const router = createHashRouter(
createRoutesFromElements(
@@ -22,13 +27,23 @@ const router = createHashRouter(
}>
} />
} />
- } />
} />
} />
- } />
- } />
- } />
- } />
+
+ }>
+ } />
+ } />
+ }>
+ }>
+ }/>
+ } />
+
+
+ }>
+ } />
+ } />
+
+
} />
>,
diff --git a/e-commerce-app/src/routes/PrivateRoute.tsx b/e-commerce-app/src/routes/PrivateRoute.tsx
new file mode 100644
index 0000000..cbadbe1
--- /dev/null
+++ b/e-commerce-app/src/routes/PrivateRoute.tsx
@@ -0,0 +1,15 @@
+import { FC } from 'react';
+import { getLoggedIn } from '../store/slices/userSlice';
+import { useAppSelector } from '../store/hooks';
+import { Outlet, Navigate } from 'react-router-dom';
+
+export const PrivateRoute: FC = () => {
+ const isLoggedIn = useAppSelector(getLoggedIn);
+ if (isLoggedIn) {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+export default PrivateRoute;
diff --git a/e-commerce-app/src/routes/navigation.ts b/e-commerce-app/src/routes/navigation.ts
index 0a6e5e1..cb6e003 100644
--- a/e-commerce-app/src/routes/navigation.ts
+++ b/e-commerce-app/src/routes/navigation.ts
@@ -5,18 +5,18 @@ export const userLoginRoutes = {
export const userLogoutRoutes = {
logout: '/logout',
+ user: '/user',
};
export const navigationRoutes = {
home: '/',
- ...userLoginRoutes,
- ...userLogoutRoutes,
+ products: '/products',
+ user: '/user',
};
export const unusedNavigation = {
about: '/about',
basket: '/basket',
- user: '/user',
product: '/product',
products: '/products',
error: '*',
diff --git a/e-commerce-app/src/store/slices/categoriesSlice.ts b/e-commerce-app/src/store/slices/categoriesSlice.ts
new file mode 100644
index 0000000..90cbf8b
--- /dev/null
+++ b/e-commerce-app/src/store/slices/categoriesSlice.ts
@@ -0,0 +1,50 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { RootStateType } from '../store';
+import { ICategoriesFromSlice } from '../../types/slicesTypes/categoriesSliceTypes';
+import { IGetAllCategoriesResponse } from '../../types/slicesTypes/categoriesApiTypes';
+
+const initialState: ICategoriesFromSlice = {
+ fetching: false,
+ categories: [],
+ total: 0,
+ limit: 0,
+ offset: 0,
+ count: 0,
+};
+
+export const categoriesSlice = createSlice({
+ initialState,
+ name: 'categoriesSlice',
+ reducers: {
+ startLoadingCategories: (state) => {
+ state.count = 0;
+ state.categories.length = 0;
+ state.fetching = true;
+ },
+ setLimit: (state, action: PayloadAction) => {
+ state.limit = action.payload;
+ },
+ setOffset: (state, action: PayloadAction) => {
+ state.offset = action.payload;
+ },
+ setCount: (state, action: PayloadAction) => {
+ state.count = action.payload;
+ },
+ setTotal: (state, action: PayloadAction) => {
+ state.total = action.payload;
+ },
+ setCategories: (state, action: PayloadAction) => {
+ state.total = action.payload.total;
+ state.count = action.payload.count;
+ state.offset = action.payload.offset;
+ state.limit = action.payload.limit;
+ state.categories = action.payload.results;
+ state.fetching = false;
+ },
+ },
+});
+
+export const CategoriesReducer = categoriesSlice.reducer;
+export const isFetchingCategories = (state: RootStateType) => state.categories.fetching;
+export const getAllCategories = (state: RootStateType) => state.categories.categories;
+export const { startLoadingCategories, setCategories } = categoriesSlice.actions;
diff --git a/e-commerce-app/src/store/slices/myCustomerSlice.ts b/e-commerce-app/src/store/slices/myCustomerSlice.ts
new file mode 100644
index 0000000..fe88b2e
--- /dev/null
+++ b/e-commerce-app/src/store/slices/myCustomerSlice.ts
@@ -0,0 +1,83 @@
+import { IMyCustomerBaseResponse } from '../../types/slicesTypes/myCustomerApiSliceTypes';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { RootStateType } from '../store';
+
+const initialState: IMyCustomerBaseResponse = {
+ addresses: [],
+ authenticationMode: 'Password',
+ billingAddressIds: [],
+ dateOfBirth: '',
+ email: '',
+ firstName: '',
+ id: '',
+ isEmailVerified: false,
+ lastName: '',
+ password: '',
+ shippingAddressIds: [],
+};
+
+const myCustomerSlice = createSlice({
+ initialState,
+ name: 'myCustomerSlice',
+ reducers: {
+ clearMyCustomerData: (state) => {
+ state.addresses.length = 0;
+ state.authenticationMode = 'Password';
+ state.billingAddressIds.length = 0;
+ state.dateOfBirth = '';
+ state.email = '';
+ state.firstName = '';
+ state.id = '';
+ state.isEmailVerified = false;
+ state.lastName = '';
+ state.password = '';
+ state.shippingAddressIds.length = 0;
+ if (state.defaultBillingAddressId) {
+ delete state.defaultBillingAddressId;
+ }
+ if (state.defaultShippingAddressId) {
+ delete state.defaultShippingAddressId;
+ }
+ },
+ setMyCustomerData: (state, action: PayloadAction) => {
+ state.addresses = action.payload.addresses;
+ state.authenticationMode = action.payload.authenticationMode;
+ state.billingAddressIds = action.payload.billingAddressIds;
+ state.dateOfBirth = action.payload.dateOfBirth;
+ state.email = action.payload.email;
+ state.firstName = action.payload.firstName;
+ state.id = action.payload.id;
+ state.isEmailVerified = action.payload.isEmailVerified;
+ state.lastName = action.payload.lastName;
+ state.password = action.payload.password;
+ state.shippingAddressIds = action.payload.shippingAddressIds;
+ if (action.payload.defaultBillingAddressId) {
+ state.defaultBillingAddressId = action.payload.defaultBillingAddressId;
+ }
+ if (action.payload.defaultShippingAddressId) {
+ state.defaultShippingAddressId = action.payload.defaultShippingAddressId;
+ }
+ },
+ },
+});
+
+export const getMyCustomerAddresses = (state: RootStateType) => state.myCustomer.addresses;
+export const getMyCustomerBillingAddressIds = (state: RootStateType) =>
+ state.myCustomer.billingAddressIds;
+export const getMyCustomerDateOfBirth = (state: RootStateType) => state.myCustomer.dateOfBirth;
+export const getMyCustomerEmail = (state: RootStateType) => state.myCustomer.email;
+export const getMyCustomerFirstName = (state: RootStateType) => state.myCustomer.firstName;
+export const getMyCustomerId = (state: RootStateType) => state.myCustomer.id;
+export const getMyCustomerIsEmailVerified = (state: RootStateType) =>
+ state.myCustomer.isEmailVerified;
+export const getMyCustomerLastName = (state: RootStateType) => state.myCustomer.lastName;
+export const getMyCustomerPassword = (state: RootStateType) => state.myCustomer.password;
+export const getMyCustomerShippingAddressIds = (state: RootStateType) =>
+ state.myCustomer.shippingAddressIds;
+export const getMyCustomerDefaultBillingAddressId = (state: RootStateType) =>
+ state.myCustomer.defaultBillingAddressId;
+export const getMyCustomerDefaultShippingAddressId = (state: RootStateType) =>
+ state.myCustomer.defaultShippingAddressId;
+
+export const MyCustomerReducer = myCustomerSlice.reducer;
+export const { clearMyCustomerData, setMyCustomerData } = myCustomerSlice.actions;
diff --git a/e-commerce-app/src/store/slices/productsSlice.ts b/e-commerce-app/src/store/slices/productsSlice.ts
new file mode 100644
index 0000000..e3cbaf2
--- /dev/null
+++ b/e-commerce-app/src/store/slices/productsSlice.ts
@@ -0,0 +1,44 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { IProductsFromSlice } from '../../types/slicesTypes/productsSliceTypes';
+import { IGetAllProductsResponse } from '../../types/slicesTypes/productsApiTypes';
+import { RootStateType } from '../store';
+
+const initialState: IProductsFromSlice = {
+ fetching: false,
+ products: [],
+ total: 0,
+ limit: 0,
+ offset: 0,
+ count: 0,
+};
+
+export const productsSlice = createSlice({
+ initialState,
+ name: 'productsSlice',
+ reducers: {
+ startLoadingProducts: (state) => {
+ state.fetching = true;
+ },
+ setProducts: (state, action: PayloadAction) => {
+ state.total = action.payload.total;
+ state.count = action.payload.count;
+ state.offset = action.payload.offset;
+ state.limit = action.payload.limit;
+ state.products = action.payload.results;
+ state.fetching = false;
+ },
+ resetProducts: (state) => {
+ state.fetching = false;
+ state.products = [];
+ state.total = 0;
+ state.limit = 0;
+ state.offset = 0;
+ state.count = 0;
+ },
+ },
+});
+
+export const getProducts = (state: RootStateType) => state.products.products;
+export const isFetchingProducts = (state: RootStateType) => state.products.fetching;
+export const ProductsReducer = productsSlice.reducer;
+export const { startLoadingProducts, setProducts, resetProducts } = productsSlice.actions;
diff --git a/e-commerce-app/src/store/slices/queryParamsSlice.ts b/e-commerce-app/src/store/slices/queryParamsSlice.ts
new file mode 100644
index 0000000..d0ed2eb
--- /dev/null
+++ b/e-commerce-app/src/store/slices/queryParamsSlice.ts
@@ -0,0 +1,56 @@
+import { IQueryParamsFromSlice } from '../../types/slicesTypes/queryParamsSliceTypes';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { RootStateType } from '../store';
+import { SortFormType } from '../../types/searchProductsTypes/filterFormTypes';
+
+const initialState: IQueryParamsFromSlice = {
+ limit: 500,
+ offset: 0,
+ sort: '',
+ text: '',
+ centAmount: [0, 100],
+ categories: '',
+};
+
+export const queryParamsSlice = createSlice({
+ initialState,
+ name: 'queryParamsSlice',
+ reducers: {
+ setQueryOffset: (state, action: PayloadAction) => {
+ state.offset = action.payload;
+ },
+ setQuerySort: (state, action: PayloadAction) => {
+ state.sort = action.payload;
+ },
+ setQueryText: (state, action: PayloadAction) => {
+ state.text = action.payload;
+ },
+ setEmptySort: (state) => {
+ state.sort = '';
+ state.categories = '';
+ state.centAmount = [0, 100];
+ },
+ setQueryCentAmount: (state, action: PayloadAction) => {
+ state.centAmount = [...action.payload];
+ },
+ setQueryCategories: (state, action: PayloadAction) => {
+ state.categories = action.payload;
+ },
+ },
+});
+
+export const getQueryOffset = (state: RootStateType) => state.queryParams.offset;
+export const getQueryLimit = (state: RootStateType) => state.queryParams.limit;
+export const getQuerySort = (state: RootStateType) => state.queryParams.sort;
+export const getQueryText = (state: RootStateType) => state.queryParams.text;
+export const getQueryCentAmount = (state: RootStateType) => state.queryParams.centAmount;
+export const getQueryCategories = (state: RootStateType) => state.queryParams.categories;
+export const QueryParamsReducer = queryParamsSlice.reducer;
+export const {
+ setQueryOffset,
+ setQuerySort,
+ setQueryText,
+ setEmptySort,
+ setQueryCentAmount,
+ setQueryCategories,
+} = queryParamsSlice.actions;
diff --git a/e-commerce-app/src/store/slices/taxesSlice.ts b/e-commerce-app/src/store/slices/taxesSlice.ts
new file mode 100644
index 0000000..3b17d8c
--- /dev/null
+++ b/e-commerce-app/src/store/slices/taxesSlice.ts
@@ -0,0 +1,35 @@
+import { ITaxFromSlice } from '../../types/slicesTypes/taxSliceTypes';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { IGetAllTaxesResponse } from '../../types/slicesTypes/taxApiTypes';
+import { RootStateType } from '../store';
+
+const initialState: ITaxFromSlice = {
+ fetching: false,
+ limit: 0,
+ count: 0,
+ offset: 0,
+ total: 0,
+ taxes: [],
+};
+
+export const taxesSlice = createSlice({
+ initialState,
+ name: 'taxesSlice',
+ reducers: {
+ startLoadingTaxes: (state) => {
+ state.fetching = true;
+ },
+ setTaxes: (state, action: PayloadAction) => {
+ state.total = action.payload.total;
+ state.count = action.payload.count;
+ state.offset = action.payload.offset;
+ state.limit = action.payload.limit;
+ state.taxes = action.payload.results;
+ state.fetching = false;
+ },
+ },
+});
+
+export const getTaxes = (state: RootStateType) => state.taxes.taxes;
+export const TaxesReducer = taxesSlice.reducer;
+export const { startLoadingTaxes, setTaxes } = taxesSlice.actions;
diff --git a/e-commerce-app/src/store/store.ts b/e-commerce-app/src/store/store.ts
index 1e0b2ba..11c3f9c 100644
--- a/e-commerce-app/src/store/store.ts
+++ b/e-commerce-app/src/store/store.ts
@@ -2,15 +2,40 @@ import { configureStore } from '@reduxjs/toolkit';
import { authApi } from '../api/authApi';
import { UserReducer } from './slices/userSlice';
import { myCustomerApi } from '../api/myCustomerApi';
+import { productsApi } from '../api/productsApi';
+import { categoriesApi } from '../api/categoriesApi';
+import { CategoriesReducer } from './slices/categoriesSlice';
+import { ProductsReducer } from './slices/productsSlice';
+import { productProjectionApi } from '../api/productProjectionApi';
+import { QueryParamsReducer } from './slices/queryParamsSlice';
+import { taxApi } from '../api/taxApi';
+import { TaxesReducer } from './slices/taxesSlice';
+import { MyCustomerReducer } from './slices/myCustomerSlice';
export const store = configureStore({
reducer: {
[authApi.reducerPath]: authApi.reducer,
[myCustomerApi.reducerPath]: myCustomerApi.reducer,
+ [productsApi.reducerPath]: productsApi.reducer,
+ [categoriesApi.reducerPath]: categoriesApi.reducer,
+ [taxApi.reducerPath]: taxApi.reducer,
+ [productProjectionApi.reducerPath]: productProjectionApi.reducer,
user: UserReducer,
+ categories: CategoriesReducer,
+ products: ProductsReducer,
+ queryParams: QueryParamsReducer,
+ taxes: TaxesReducer,
+ myCustomer: MyCustomerReducer,
},
middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({}).concat([authApi.middleware, myCustomerApi.middleware]),
+ getDefaultMiddleware({}).concat([
+ authApi.middleware,
+ myCustomerApi.middleware,
+ productsApi.middleware,
+ categoriesApi.middleware,
+ productProjectionApi.middleware,
+ taxApi.middleware,
+ ]),
});
export type RootStateType = ReturnType;
diff --git a/e-commerce-app/src/types/addressesTypes.ts b/e-commerce-app/src/types/addressesTypes.ts
index f1d9c70..436947d 100644
--- a/e-commerce-app/src/types/addressesTypes.ts
+++ b/e-commerce-app/src/types/addressesTypes.ts
@@ -8,3 +8,5 @@ export interface IMyCustomerApiAddressRequest {
export interface IMyCustomerAddressResponse extends IMyCustomerApiAddressRequest {
id: string;
}
+
+export type CountriesType = 'US' | 'DE' | 'CA' | 'PL' | string;
diff --git a/e-commerce-app/src/types/declaration.d.ts b/e-commerce-app/src/types/declaration.d.ts
new file mode 100644
index 0000000..d5cf927
--- /dev/null
+++ b/e-commerce-app/src/types/declaration.d.ts
@@ -0,0 +1 @@
+declare module '*.scss';
diff --git a/e-commerce-app/src/types/fieldNameType.ts b/e-commerce-app/src/types/fieldNameType.ts
index 32942e7..c9bc7a1 100644
--- a/e-commerce-app/src/types/fieldNameType.ts
+++ b/e-commerce-app/src/types/fieldNameType.ts
@@ -2,6 +2,7 @@ export type fieldNameType =
| 'email'
| 'password'
| 'confirmPassword'
+ | 'currentPassword'
| 'lastName'
| 'firstName'
| 'birthDate'
diff --git a/e-commerce-app/src/types/searchProductsTypes/filterFormTypes.ts b/e-commerce-app/src/types/searchProductsTypes/filterFormTypes.ts
new file mode 100644
index 0000000..932060f
--- /dev/null
+++ b/e-commerce-app/src/types/searchProductsTypes/filterFormTypes.ts
@@ -0,0 +1,10 @@
+export interface ISortByForm {
+ sort: SortFormType;
+ categories: string;
+}
+
+export type SortFormType = '' | SortFormPrice | SortFormName;
+
+export type SortFormPrice = 'price asc' | 'price desc';
+
+export type SortFormName = 'name.en asc' | 'name.en desc';
diff --git a/e-commerce-app/src/types/searchProductsTypes/searchFormTypes.ts b/e-commerce-app/src/types/searchProductsTypes/searchFormTypes.ts
new file mode 100644
index 0000000..0397778
--- /dev/null
+++ b/e-commerce-app/src/types/searchProductsTypes/searchFormTypes.ts
@@ -0,0 +1,3 @@
+export interface ISearchProductForm {
+ query: string;
+}
diff --git a/e-commerce-app/src/types/slicesTypes/baseApiRequestsTypes.ts b/e-commerce-app/src/types/slicesTypes/baseApiRequestsTypes.ts
new file mode 100644
index 0000000..f04e028
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/baseApiRequestsTypes.ts
@@ -0,0 +1,13 @@
+import { SortFormType } from '../searchProductsTypes/filterFormTypes';
+
+export interface IBaseQueryParams {
+ limit?: number;
+ offset?: number;
+ ['text.en']?: string;
+ ['name.en']?: string;
+ fuzzy?: boolean;
+ fuzzyLevel?: 0 | 1 | 2;
+ sort?: SortFormType | string;
+ resultPath?: string;
+ filter?: string[];
+}
diff --git a/e-commerce-app/src/types/slicesTypes/baseApiResponsesTypes.ts b/e-commerce-app/src/types/slicesTypes/baseApiResponsesTypes.ts
new file mode 100644
index 0000000..1d5f5ca
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/baseApiResponsesTypes.ts
@@ -0,0 +1,16 @@
+export interface IBaseGetAllQueryResponse {
+ limit: number;
+ offset: number;
+ count: number;
+ total: number;
+ results: Array;
+}
+
+export interface ICategoryTypeResponse {
+ id: string;
+ typeId: string;
+}
+
+export interface IMetaDescriptionProductResponse {
+ en: string;
+}
diff --git a/e-commerce-app/src/types/slicesTypes/categoriesApiTypes.ts b/e-commerce-app/src/types/slicesTypes/categoriesApiTypes.ts
new file mode 100644
index 0000000..cdffbef
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/categoriesApiTypes.ts
@@ -0,0 +1,21 @@
+import { IBaseGetAllQueryResponse, ICategoryTypeResponse } from './baseApiResponsesTypes';
+
+export interface ICategoryName {
+ en: string;
+}
+
+export interface ICategoryApiResponse {
+ ancestors: ICategoryTypeResponse[];
+ assets: [];
+ id: string;
+ key: string;
+ name: ICategoryName;
+ orderHint: string;
+ parent?: ICategoryTypeResponse;
+ slug: ICategoryName;
+ version: number;
+}
+
+export interface IGetAllCategoriesResponse extends IBaseGetAllQueryResponse {
+ results: ICategoryApiResponse[];
+}
diff --git a/e-commerce-app/src/types/slicesTypes/categoriesSliceTypes.ts b/e-commerce-app/src/types/slicesTypes/categoriesSliceTypes.ts
new file mode 100644
index 0000000..5e1dd3a
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/categoriesSliceTypes.ts
@@ -0,0 +1,10 @@
+import { ICategoryApiResponse } from './categoriesApiTypes';
+
+export interface ICategoriesFromSlice {
+ fetching: boolean;
+ categories: ICategoryApiResponse[];
+ total: number;
+ limit: number;
+ offset: number;
+ count: number;
+}
diff --git a/e-commerce-app/src/types/slicesTypes/myCustomerApiSliceTypes.ts b/e-commerce-app/src/types/slicesTypes/myCustomerApiSliceTypes.ts
index 2b6d5ea..f17b0be 100644
--- a/e-commerce-app/src/types/slicesTypes/myCustomerApiSliceTypes.ts
+++ b/e-commerce-app/src/types/slicesTypes/myCustomerApiSliceTypes.ts
@@ -8,19 +8,19 @@ export interface IMyCustomerLoginRequest {
}
export interface IMyCustomerBaseResponse {
- id: string;
+ addresses: IMyCustomerApiAddressRequest[];
+ authenticationMode: 'Password' | string;
+ billingAddressIds: string[];
+ dateOfBirth: string;
email: string;
firstName: string;
+ id: string;
+ isEmailVerified: boolean;
lastName: string;
- dateOfBirth: string;
password: string;
- addresses: IMyCustomerApiAddressRequest[];
- billingAddressIds: string[];
shippingAddressIds: string[];
defaultBillingAddressId?: string;
defaultShippingAddressId?: string;
- isEmailVerified: boolean;
- authenticationMode: 'Password' | string;
}
export interface IMyCustomerApiSignupRequest {
diff --git a/e-commerce-app/src/types/slicesTypes/productProjectionsApiTypes.ts b/e-commerce-app/src/types/slicesTypes/productProjectionsApiTypes.ts
new file mode 100644
index 0000000..577640b
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/productProjectionsApiTypes.ts
@@ -0,0 +1,13 @@
+import { IBaseGetAllQueryResponse, ICategoryTypeResponse } from './baseApiResponsesTypes';
+import { IProductApiDescriptionResponse } from './productsApiTypes';
+
+export interface ISearchApiResponse extends IProductApiDescriptionResponse {
+ id: string;
+ key: string;
+ productType: ICategoryTypeResponse;
+ taxCategory: ICategoryTypeResponse;
+}
+
+export interface ISearchProductsResponse extends IBaseGetAllQueryResponse {
+ results: ISearchApiResponse[];
+}
diff --git a/e-commerce-app/src/types/slicesTypes/productsApiTypes.ts b/e-commerce-app/src/types/slicesTypes/productsApiTypes.ts
new file mode 100644
index 0000000..6ebce6c
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/productsApiTypes.ts
@@ -0,0 +1,83 @@
+import {
+ IBaseGetAllQueryResponse,
+ ICategoryTypeResponse,
+ IMetaDescriptionProductResponse,
+} from './baseApiResponsesTypes';
+import { IBaseQueryParams } from './baseApiRequestsTypes';
+
+export interface IAttributeProductApiResponse {
+ name: string;
+ value: string;
+}
+
+export interface IImageProductApiResponse {
+ url: string;
+ dimensions: {
+ w: number;
+ h: number;
+ };
+}
+
+export interface IValuePriceProductApiResponse {
+ type: string;
+ currencyCode: string;
+ centAmount: number;
+ fractionDigits: number;
+ country: string;
+ channel: ICategoryTypeResponse;
+}
+
+export interface IPriceProductApiResponse {
+ id: string;
+ value: IValuePriceProductApiResponse;
+}
+
+export interface IMasterVariantProductApiResponse {
+ assets: [];
+ attributes: IAttributeProductApiResponse[];
+ id: number;
+ images: IImageProductApiResponse[];
+ key: string;
+ prices: IPriceProductApiResponse[];
+ sku: string;
+}
+
+export interface IProductApiDescriptionResponse {
+ categories: ICategoryTypeResponse[];
+ categoryOrderHints: object;
+ masterVariant: IMasterVariantProductApiResponse;
+ metaDescription: IMetaDescriptionProductResponse;
+ name: IMetaDescriptionProductResponse;
+ searchKeywords: object;
+ slug: IMetaDescriptionProductResponse;
+ variants: [];
+}
+
+export interface IMasterDataProductApiResponse {
+ current: IProductApiDescriptionResponse;
+ hasStagedChanges: boolean;
+ published: boolean;
+ staged: IProductApiDescriptionResponse;
+}
+
+export interface IProductApiResponse {
+ id: string;
+ key: string;
+ masterData: IMasterDataProductApiResponse;
+ productType: ICategoryTypeResponse;
+ taxCategory: ICategoryTypeResponse;
+}
+
+export interface IGetAllProductsResponse extends IBaseGetAllQueryResponse {
+ results: IProductApiResponse[];
+}
+
+export interface IGetAllProductsRequest {
+ token: string;
+ params?: IBaseQueryParams;
+}
+
+export interface IGetProductByIdRequest {
+ token: string;
+ productId: string;
+}
diff --git a/e-commerce-app/src/types/slicesTypes/productsSliceTypes.ts b/e-commerce-app/src/types/slicesTypes/productsSliceTypes.ts
new file mode 100644
index 0000000..3db7da7
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/productsSliceTypes.ts
@@ -0,0 +1,10 @@
+import { IProductApiResponse } from './productsApiTypes';
+
+export interface IProductsFromSlice {
+ fetching: boolean;
+ products: IProductApiResponse[];
+ total: number;
+ limit: number;
+ offset: number;
+ count: number;
+}
diff --git a/e-commerce-app/src/types/slicesTypes/queryParamsSliceTypes.ts b/e-commerce-app/src/types/slicesTypes/queryParamsSliceTypes.ts
new file mode 100644
index 0000000..0750362
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/queryParamsSliceTypes.ts
@@ -0,0 +1,10 @@
+import { SortFormType } from '../searchProductsTypes/filterFormTypes';
+
+export interface IQueryParamsFromSlice {
+ limit: number;
+ offset: number;
+ text: string;
+ sort: SortFormType;
+ centAmount: number[];
+ categories: string;
+}
diff --git a/e-commerce-app/src/types/slicesTypes/taxApiTypes.ts b/e-commerce-app/src/types/slicesTypes/taxApiTypes.ts
new file mode 100644
index 0000000..535cbe8
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/taxApiTypes.ts
@@ -0,0 +1,16 @@
+import { CountriesType } from '../addressesTypes';
+import { IBaseGetAllQueryResponse } from './baseApiResponsesTypes';
+
+export interface ITaxApiResponse {
+ amount: number;
+ country: CountriesType;
+ id: string;
+ includedInPrice: boolean;
+ name: string;
+ rates: [];
+ key: string;
+}
+
+export interface IGetAllTaxesResponse extends IBaseGetAllQueryResponse {
+ results: ITaxApiResponse[];
+}
diff --git a/e-commerce-app/src/types/slicesTypes/taxSliceTypes.ts b/e-commerce-app/src/types/slicesTypes/taxSliceTypes.ts
new file mode 100644
index 0000000..3505615
--- /dev/null
+++ b/e-commerce-app/src/types/slicesTypes/taxSliceTypes.ts
@@ -0,0 +1,10 @@
+import { ITaxApiResponse } from './taxApiTypes';
+
+export interface ITaxFromSlice {
+ fetching: boolean;
+ taxes: ITaxApiResponse[];
+ total: number;
+ limit: number;
+ offset: number;
+ count: number;
+}
diff --git a/e-commerce-app/src/utils/makeGetQueryProductsString.ts b/e-commerce-app/src/utils/makeGetQueryProductsString.ts
new file mode 100644
index 0000000..f327012
--- /dev/null
+++ b/e-commerce-app/src/utils/makeGetQueryProductsString.ts
@@ -0,0 +1,17 @@
+import { IBaseQueryParams } from '../types/slicesTypes/baseApiRequestsTypes';
+
+export const makeGetQueryProductsString = (params: IBaseQueryParams): string => {
+ const out = Object.entries(params)
+ .reduce((params, [k, v]) => {
+ if (!v || v.length === 0) return [...params];
+ if (Array.isArray(v)) {
+ const substr = v.join(`&${k}=`);
+ return [...params, `${k}=${substr}`];
+ }
+ return [...params, `${k}=${v}`];
+ }, [])
+ .join('&')
+ .replace(/\s/g, '+');
+
+ return `?${out}`;
+};
diff --git a/e-commerce-app/src/utils/makeProductSliceObjectFromSearchApiRequest.ts b/e-commerce-app/src/utils/makeProductSliceObjectFromSearchApiRequest.ts
new file mode 100644
index 0000000..b657458
--- /dev/null
+++ b/e-commerce-app/src/utils/makeProductSliceObjectFromSearchApiRequest.ts
@@ -0,0 +1,51 @@
+import {
+ ISearchApiResponse,
+ ISearchProductsResponse,
+} from '../types/slicesTypes/productProjectionsApiTypes';
+import {
+ IGetAllProductsResponse,
+ IMasterDataProductApiResponse,
+ IProductApiDescriptionResponse,
+ IProductApiResponse,
+} from '../types/slicesTypes/productsApiTypes';
+
+const makeFromSearchApiProductApiResponses = (el: ISearchApiResponse): IProductApiResponse => {
+ const current: IProductApiDescriptionResponse = {
+ categories: el.categories,
+ categoryOrderHints: el.categoryOrderHints,
+ masterVariant: el.masterVariant,
+ metaDescription: el.metaDescription,
+ name: el.name,
+ searchKeywords: el.searchKeywords,
+ slug: el.slug,
+ variants: el.variants,
+ };
+ const masterData: IMasterDataProductApiResponse = {
+ current: current,
+ hasStagedChanges: false,
+ published: true,
+ staged: current,
+ };
+ const newProduct: IProductApiResponse = {
+ id: el.id,
+ key: el.key,
+ masterData: masterData,
+ productType: el.productType,
+ taxCategory: el.taxCategory,
+ };
+ return newProduct;
+};
+
+export const makeProductSliceObjectFromSearchApiRequest = (
+ dataSearch: ISearchProductsResponse,
+): IGetAllProductsResponse => {
+ const productsArray = dataSearch.results.map((el) => makeFromSearchApiProductApiResponses(el));
+ const pushingObject: IGetAllProductsResponse = {
+ limit: dataSearch.limit,
+ offset: dataSearch.offset,
+ count: dataSearch.count,
+ total: dataSearch.total,
+ results: productsArray,
+ };
+ return pushingObject;
+};