diff --git a/e-commerce-app/package-lock.json b/e-commerce-app/package-lock.json index 403e379..375576e 100644 --- a/e-commerce-app/package-lock.json +++ b/e-commerce-app/package-lock.json @@ -15,12 +15,15 @@ "@mui/material": "^5.14.4", "@reduxjs/toolkit": "^1.9.5", "classnames": "^2.3.2", + "history": "^5.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", "react-hook-form": "^7.45.4", "react-redux": "^8.1.2", "react-router-dom": "^6.14.2", "react-scripts": "5.0.1", + "react-toastify": "^9.1.3", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, @@ -33,11 +36,13 @@ "@types/react": "^18.2.18", "@types/react-dom": "^18.2.7", "@types/react-router-dom": "^5.3.3", + "@types/redux-mock-store": "^1.0.3", "eslint": "^8.46.0", "eslint-config-prettier": "^8.9.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", "prettier": "^3.0.0", + "redux-mock-store": "^1.5.4", "sass": "^1.66.1" } }, @@ -4730,6 +4735,15 @@ "@types/react": "*" } }, + "node_modules/@types/redux-mock-store": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.3.tgz", + "integrity": "sha512-Wqe3tJa6x9MxMN4DJnMfZoBRBRak1XTPklqj4qkVm5VBpZnC8PSADf4kLuFQ9NAdHaowfWoEeUMz7NWc2GMtnA==", + "dev": true, + "dependencies": { + "redux": "^4.0.5" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -9515,6 +9529,14 @@ "he": "bin/he" } }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -12706,6 +12728,12 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -15291,6 +15319,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", + "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -15469,6 +15508,26 @@ } } }, + "node_modules/react-toastify": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", + "integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-toastify/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -15548,6 +15607,15 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/redux-mock-store": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz", + "integrity": "sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==", + "dev": true, + "dependencies": { + "lodash.isplainobject": "^4.0.6" + } + }, "node_modules/redux-thunk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", diff --git a/e-commerce-app/package.json b/e-commerce-app/package.json index 5c87270..f8f4c23 100644 --- a/e-commerce-app/package.json +++ b/e-commerce-app/package.json @@ -11,19 +11,22 @@ "@mui/material": "^5.14.4", "@reduxjs/toolkit": "^1.9.5", "classnames": "^2.3.2", + "history": "^5.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", "react-hook-form": "^7.45.4", "react-redux": "^8.1.2", "react-router-dom": "^6.14.2", "react-scripts": "5.0.1", + "react-toastify": "^9.1.3", "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test --coverage", + "test": "react-scripts test --coverage --watchAll", "eject": "react-scripts eject", "lint": "eslint src/**/*.{js,jsx,ts,tsx}", "lint:fix": "eslint --fix src/**/*.{js,jsx,ts,tsx}", @@ -37,6 +40,12 @@ "react-app/jest" ] }, + "jest": { + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/**/*.test.{js,jsx,ts,tsx}" + ] + }, "browserslist": { "production": [ ">0.2%", @@ -50,7 +59,6 @@ ] }, "devDependencies": { - "sass": "^1.66.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -59,10 +67,13 @@ "@types/react": "^18.2.18", "@types/react-dom": "^18.2.7", "@types/react-router-dom": "^5.3.3", + "@types/redux-mock-store": "^1.0.3", "eslint": "^8.46.0", "eslint-config-prettier": "^8.9.0", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", - "prettier": "^3.0.0" + "prettier": "^3.0.0", + "redux-mock-store": "^1.5.4", + "sass": "^1.66.1" } } diff --git a/e-commerce-app/src/App.tsx b/e-commerce-app/src/App.tsx index 39af6ea..737c978 100644 --- a/e-commerce-app/src/App.tsx +++ b/e-commerce-app/src/App.tsx @@ -10,7 +10,14 @@ import LoadingProgress from './components/LoadingProgress/LoadingProgress'; export const App = () => { const [getAnonymousToken] = useGetAnonymousTokenMutation(); - const { isTokenInStorage, getTokenFromStorage, delTokenFromStorage } = useLocalToken(); + const { + isTokenInStorage, + getTokenFromStorage, + delTokenFromStorage, + setTokenInSessionStorage, + getTokenFromSessionStorage, + isTokenInLocalStorage, + } = useLocalToken(); const [getAccessTokenApi, { data, isSuccess, isError, isLoading }] = useGetAccessTokenFromRefreshMutation(); const [getDetails] = useGetMyCustomerDetailsMutation(); @@ -23,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]); @@ -37,21 +44,34 @@ export const App = () => { }, [isError]); useEffect(() => { - if (accessToken) return; + if (accessToken) { + return; + } + + if (isTokenInLocalStorage()) { + const token = getTokenFromSessionStorage(); + if (token) { + getAccessTokenApi(token); + } + return; + } + if (isTokenInStorage()) { const token = getTokenFromStorage(); if (token) { getAccessTokenApi(token); } - } else { - getAnonymousToken().then((res) => { - if ('data' in res) { - dispatch( - setAuth({ access_token: res.data.access_token, refresh_token: res.data.refresh_token }), - ); - } - }); + return; } + + getAnonymousToken().then((res) => { + if ('data' in res) { + dispatch( + setAuth({ access_token: res.data.access_token, refresh_token: res.data.refresh_token }), + ); + setTokenInSessionStorage(res.data.refresh_token); + } + }); }, [accessToken]); if (isLoading) { diff --git a/e-commerce-app/src/api/cartApi.ts b/e-commerce-app/src/api/cartApi.ts new file mode 100644 index 0000000..02e7d82 --- /dev/null +++ b/e-commerce-app/src/api/cartApi.ts @@ -0,0 +1,92 @@ +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({ + 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({ + 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); + +export const getTotalQuantityLineItemsInCart = (state: RootStateType) => + cartApi.endpoints.getMyActiveCart.select(state.user.access_token as string)(state).data + ?.totalLineItemQuantity; + +export const getLineItemsInCart = (state: RootStateType) => + cartApi.endpoints.getMyActiveCart.select(state.user.access_token as string)(state).data + ?.lineItems; diff --git a/e-commerce-app/src/api/discountCodesApi.ts b/e-commerce-app/src/api/discountCodesApi.ts new file mode 100644 index 0000000..5339c91 --- /dev/null +++ b/e-commerce-app/src/api/discountCodesApi.ts @@ -0,0 +1,25 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; +import { fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'; +import { IGetDiscountCodesResponse } from '../types/slicesTypes/DiscountCodesTypes/DiscountCodesApiTypes'; + +export const discountCodesApi = createApi({ + reducerPath: 'discountCodesApi', + baseQuery: fetchBaseQuery({ + baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/discount-codes`, + }), + endpoints: (build) => ({ + getDiscountCodes: build.query({ + query(token) { + return { + url: '', + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }; + }, + }), + }), +}); + +export const { useGetDiscountCodesQuery, useLazyGetDiscountCodesQuery } = discountCodesApi; diff --git a/e-commerce-app/src/api/myCustomerApi.ts b/e-commerce-app/src/api/myCustomerApi.ts index c3fe44f..f896050 100644 --- a/e-commerce-app/src/api/myCustomerApi.ts +++ b/e-commerce-app/src/api/myCustomerApi.ts @@ -1,26 +1,29 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { + IAuthenticateMyCustomer, IMyCustomerBaseResponse, - IMyCustomerLoginRequest, ISignUpMyCustomer, } from '../types/slicesTypes/myCustomerApiSliceTypes'; +import { + IChangePasswordMyCustomerRequest, + IUpdateMyCustomerRequest, +} from '../types/updateMyCustomerTypes/updateMyCustomerTypes'; export const myCustomerApi = createApi({ reducerPath: 'myCustomerApi', baseQuery: fetchBaseQuery({ baseUrl: `${process.env.REACT_APP_CTP_API_URL}/${process.env.REACT_APP_CTP_PROJECT_KEY}/me`, }), + tagTypes: ['myCustomerDetails'], endpoints: (build) => ({ - authenticateMyCustomer: build.mutation({ - query(customerData: IMyCustomerLoginRequest) { + authenticateMyCustomer: build.mutation({ + 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', }, }; @@ -51,6 +54,50 @@ export const myCustomerApi = createApi({ }; }, }), + getMyCustomerDetailedInfo: build.query({ + query(token: string) { + return { + url: '/', + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }; + }, + providesTags: ['myCustomerDetails'], + }), + updateMyCustomer: build.mutation({ + query(queryObj) { + return { + url: '', + method: 'POST', + headers: { + Authorization: `Bearer ${queryObj.token}`, + 'Content-Type': 'application/json', + }, + body: queryObj.data, + }; + }, + invalidatesTags: ['myCustomerDetails'], + }), + changePasswordMyCustomer: build.mutation< + IMyCustomerBaseResponse, + IChangePasswordMyCustomerRequest + >({ + query(queryObj) { + return { + url: '/password', + method: 'POST', + headers: { + Authorization: `Bearer ${queryObj.token}`, + 'Content-Type': 'application/json', + }, + body: queryObj.data, + }; + }, + invalidatesTags: ['myCustomerDetails'], + }), }), }); @@ -58,4 +105,7 @@ export const { useAuthenticateMyCustomerMutation, useSignUpMyCustomerMutation, useGetMyCustomerDetailsMutation, + useUpdateMyCustomerMutation, + useGetMyCustomerDetailedInfoQuery, + useChangePasswordMyCustomerMutation, } = myCustomerApi; diff --git a/e-commerce-app/src/assets/AboutPageImg/Evguenia.jpeg b/e-commerce-app/src/assets/AboutPageImg/Evguenia.jpeg new file mode 100644 index 0000000..57926f3 Binary files /dev/null and b/e-commerce-app/src/assets/AboutPageImg/Evguenia.jpeg differ diff --git a/e-commerce-app/src/assets/AboutPageImg/Ilya.jpeg b/e-commerce-app/src/assets/AboutPageImg/Ilya.jpeg new file mode 100644 index 0000000..c24600b Binary files /dev/null and b/e-commerce-app/src/assets/AboutPageImg/Ilya.jpeg differ diff --git a/e-commerce-app/src/assets/AboutPageImg/Nargiza.jpg b/e-commerce-app/src/assets/AboutPageImg/Nargiza.jpg new file mode 100644 index 0000000..314dc29 Binary files /dev/null and b/e-commerce-app/src/assets/AboutPageImg/Nargiza.jpg differ diff --git a/e-commerce-app/src/assets/AboutPageImg/fon-trava.jpeg b/e-commerce-app/src/assets/AboutPageImg/fon-trava.jpeg new file mode 100644 index 0000000..214fefa Binary files /dev/null and b/e-commerce-app/src/assets/AboutPageImg/fon-trava.jpeg differ diff --git a/e-commerce-app/src/assets/HomePageImg/Banner.avif b/e-commerce-app/src/assets/HomePageImg/Banner.avif new file mode 100644 index 0000000..6ee2314 Binary files /dev/null and b/e-commerce-app/src/assets/HomePageImg/Banner.avif differ diff --git a/e-commerce-app/src/assets/HomePageImg/PlantsRoom.png b/e-commerce-app/src/assets/HomePageImg/PlantsRoom.png new file mode 100644 index 0000000..b37380e Binary files /dev/null and b/e-commerce-app/src/assets/HomePageImg/PlantsRoom.png differ diff --git a/e-commerce-app/src/assets/HomePageImg/promoIcon1.png b/e-commerce-app/src/assets/HomePageImg/promoIcon1.png new file mode 100644 index 0000000..5d9d54f Binary files /dev/null and b/e-commerce-app/src/assets/HomePageImg/promoIcon1.png differ diff --git a/e-commerce-app/src/assets/HomePageImg/promoIcon2.png b/e-commerce-app/src/assets/HomePageImg/promoIcon2.png new file mode 100644 index 0000000..126f344 Binary files /dev/null and b/e-commerce-app/src/assets/HomePageImg/promoIcon2.png differ diff --git a/e-commerce-app/src/assets/HomePageImg/promoIcon3.png b/e-commerce-app/src/assets/HomePageImg/promoIcon3.png new file mode 100644 index 0000000..d8b802f Binary files /dev/null and b/e-commerce-app/src/assets/HomePageImg/promoIcon3.png differ diff --git a/e-commerce-app/src/assets/images/BasketPageImg.png b/e-commerce-app/src/assets/images/BasketPageImg.png new file mode 100644 index 0000000..45f25dc Binary files /dev/null and b/e-commerce-app/src/assets/images/BasketPageImg.png differ diff --git a/e-commerce-app/src/assets/images/FooterImg.png b/e-commerce-app/src/assets/images/FooterImg.png new file mode 100644 index 0000000..620c041 Binary files /dev/null and b/e-commerce-app/src/assets/images/FooterImg.png differ diff --git a/e-commerce-app/src/assets/images/TestImg-1.png b/e-commerce-app/src/assets/images/TestImg-1.png deleted file mode 100644 index fad3a89..0000000 Binary files a/e-commerce-app/src/assets/images/TestImg-1.png and /dev/null differ diff --git a/e-commerce-app/src/assets/images/TestImg-2.jpeg b/e-commerce-app/src/assets/images/TestImg-2.jpeg deleted file mode 100644 index 3d802f8..0000000 Binary files a/e-commerce-app/src/assets/images/TestImg-2.jpeg and /dev/null differ diff --git a/e-commerce-app/src/assets/images/TestImg.jpeg b/e-commerce-app/src/assets/images/TestImg.jpeg deleted file mode 100644 index 15493c7..0000000 Binary files a/e-commerce-app/src/assets/images/TestImg.jpeg and /dev/null differ diff --git a/e-commerce-app/src/assets/logo/Logo.png b/e-commerce-app/src/assets/logo/Logo.png new file mode 100644 index 0000000..9e2b669 Binary files /dev/null and b/e-commerce-app/src/assets/logo/Logo.png differ diff --git a/e-commerce-app/src/assets/logo/RSlogo.png b/e-commerce-app/src/assets/logo/RSlogo.png new file mode 100644 index 0000000..2131840 Binary files /dev/null and b/e-commerce-app/src/assets/logo/RSlogo.png differ diff --git a/e-commerce-app/src/assets/logo/free-icon-tree-740936.png b/e-commerce-app/src/assets/logo/free-icon-tree-740936.png deleted file mode 100644 index aa191df..0000000 Binary files a/e-commerce-app/src/assets/logo/free-icon-tree-740936.png and /dev/null differ diff --git a/e-commerce-app/src/components/AddressList/AddressList.tsx b/e-commerce-app/src/components/AddressList/AddressList.tsx index c06a5c0..f7c97a5 100644 --- a/e-commerce-app/src/components/AddressList/AddressList.tsx +++ b/e-commerce-app/src/components/AddressList/AddressList.tsx @@ -2,7 +2,7 @@ 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 { AddressPanel } from '../AddressPanel/AddressPanel'; import { IMyCustomerApiAddressRequest } from '../../types/addressesTypes'; interface TodoListProps { @@ -15,7 +15,7 @@ interface TodoListProps { address: IMyCustomerApiAddressRequest | undefined; } -export const BoardList: FC = ({ +export const AddressList: FC = ({ todoList, editTodoId, onChangeAddr, diff --git a/e-commerce-app/src/components/Panel/Panel.tsx b/e-commerce-app/src/components/AddressPanel/AddressPanel.tsx similarity index 90% rename from e-commerce-app/src/components/Panel/Panel.tsx rename to e-commerce-app/src/components/AddressPanel/AddressPanel.tsx index 1bf0161..4112a81 100644 --- a/e-commerce-app/src/components/Panel/Panel.tsx +++ b/e-commerce-app/src/components/AddressPanel/AddressPanel.tsx @@ -90,17 +90,17 @@ export const AddressPanel: FC = (props) => { - } - label={ - - use asdefault address - - } - labelPlacement="end" - /> + } + label={ + + use as default address + + } + labelPlacement="end" + /> diff --git a/e-commerce-app/src/components/BasketDiscountForm/BasketDiscountForm.tsx b/e-commerce-app/src/components/BasketDiscountForm/BasketDiscountForm.tsx new file mode 100644 index 0000000..3049dcf --- /dev/null +++ b/e-commerce-app/src/components/BasketDiscountForm/BasketDiscountForm.tsx @@ -0,0 +1,65 @@ +import { JSX } from 'react'; +import { Box } from '@mui/system'; +import Grid from '@mui/material/Grid'; +import { Alert, Button, TextField } from '@mui/material'; +import { Controller, SubmitHandler, useForm } from 'react-hook-form'; +import { IAddDiscountCodeCart, ICartApiResponse } from '../../types/slicesTypes/cart'; +import { useAppSelector } from '../../store/hooks'; +import { getAccessToken } from '../../store/slices/userSlice'; +import { selectCart, useUpdateCartMutation } from '../../api/cartApi'; +import { IUpdateCartApiObjectRequest } from '../../types/slicesTypes/cart/updateCartApiTypes'; + +interface IDiscountForm { + discount: string; +} + +const BasketDiscountForm = (): JSX.Element => { + const { control, handleSubmit } = useForm(); + 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 [updateCart, { isLoading, isError, error }] = useUpdateCartMutation(); + const submitHandler: SubmitHandler = (data) => { + if (!data.discount) return; + const actionObject: IAddDiscountCodeCart = { + action: 'addDiscountCode', + code: data.discount.toUpperCase(), + }; + const queryObj: IUpdateCartApiObjectRequest = { + cartId, + token: accessToken, + data: { version: cartVersion, actions: [actionObject] }, + }; + updateCart(queryObj); + }; + return ( + + + + ( + + )} + name={'discount'} + control={control} + defaultValue={''} + /> + + + + + + {isError && error && This discount code is not available} + + ); +}; +export default BasketDiscountForm; diff --git a/e-commerce-app/src/components/BasketEmpty/BasketEmpty.tsx b/e-commerce-app/src/components/BasketEmpty/BasketEmpty.tsx new file mode 100644 index 0000000..9918038 --- /dev/null +++ b/e-commerce-app/src/components/BasketEmpty/BasketEmpty.tsx @@ -0,0 +1,33 @@ +import { JSX } from 'react'; +import { Box } from '@mui/system'; +import Typography from '@mui/material/Typography'; +import { Button } from '@mui/material'; +import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace'; +import EmptyBasket from '../../assets/images/BasketPageImg.png'; +import { useNavigate } from 'react-router-dom'; +import { lightGreen } from '@mui/material/colors'; + +const BasketEmpty = (): JSX.Element => { + const navigate = useNavigate(); + return ( + <> + + + Your cart is empty + + + + EmptyBasket + + ); +}; +export default BasketEmpty; diff --git a/e-commerce-app/src/components/BasketFull/BasketFull.tsx b/e-commerce-app/src/components/BasketFull/BasketFull.tsx new file mode 100644 index 0000000..32d6dd2 --- /dev/null +++ b/e-commerce-app/src/components/BasketFull/BasketFull.tsx @@ -0,0 +1,136 @@ +import { JSX } from 'react'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import { Box } from '@mui/system'; +import { Button } from '@mui/material'; +import KeyboardBackspaceIcon from '@mui/icons-material/KeyboardBackspace'; +import Paper from '@mui/material/Paper'; +import { useNavigate } from 'react-router-dom'; +import { lightGreen } from '@mui/material/colors'; +import { useAppSelector } from '../../store/hooks'; +import { getLineItemsInCart, selectCart } from '../../api/cartApi'; +import BasketLineItem from '../BasketLineItem/BasketLineItem'; +import { ICartApiResponse } from '../../types/slicesTypes/cart'; +import CartClear from '../../requestsComponents/CartClear/CartClear'; +import BasketDiscountForm from '../BasketDiscountForm/BasketDiscountForm'; + +const BasketFull = (): JSX.Element => { + const navigate = useNavigate(); + const cartLineItems = useAppSelector(getLineItemsInCart); + const cart = useAppSelector(selectCart) as ICartApiResponse; + + const totalCurrencyEUR = cart.totalPrice.currencyCode; + + const subTotalNumber = + cart.lineItems.reduce((accum, item) => { + return item.price.value.centAmount * item.quantity + accum; + }, 0) / + 10 ** cart.totalPrice.fractionDigits; + const subTotalPriceEUR = new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: totalCurrencyEUR, + }).format(subTotalNumber); + + const totalNumberEUR = cart.totalPrice.centAmount / 10 ** cart.totalPrice.fractionDigits; + const totalPriceEUR = new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: totalCurrencyEUR, + }).format(totalNumberEUR); + + return ( + + + + Product + + + Price + + + Quantity + + + Total + + + + + {cartLineItems && + cartLineItems.map((lineItem) => )} + + + + + + + + + + + + Subtotal + + + {subTotalPriceEUR} + + + + Taxes and shipping calculated at checkout + + + + + + + EST. TOTAL: + + + {totalPriceEUR} + + + + + + + + + + + + + + + + ); +}; +export default BasketFull; diff --git a/e-commerce-app/src/components/BasketLineItem/BasketLineItem.module.scss b/e-commerce-app/src/components/BasketLineItem/BasketLineItem.module.scss new file mode 100644 index 0000000..2e6d70c --- /dev/null +++ b/e-commerce-app/src/components/BasketLineItem/BasketLineItem.module.scss @@ -0,0 +1,9 @@ +.price { + &__sale { + text-decoration: line-through; + text-decoration-color: gray; + } + &__marked { + color: rgb(221, 65, 65); + } +} diff --git a/e-commerce-app/src/components/BasketLineItem/BasketLineItem.tsx b/e-commerce-app/src/components/BasketLineItem/BasketLineItem.tsx new file mode 100644 index 0000000..ab47501 --- /dev/null +++ b/e-commerce-app/src/components/BasketLineItem/BasketLineItem.tsx @@ -0,0 +1,152 @@ +import { FC } from 'react'; +import Grid from '@mui/material/Grid'; +import { Box } from '@mui/system'; +import { Divider, Stack } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import CartAddLineItem from '../../requestsComponents/CartAddLineItem/CartAddLineItem'; +import CartModifyQuantity from '../../requestsComponents/CartModifyQuantity/CartModifyQuantity'; +import { styled } from '@mui/material/styles'; +import { ICartLineItem } from '../../types/slicesTypes/cart'; +import { Link } from 'react-router-dom'; +import styles from './BasketLineItem.module.scss'; + +const Img = styled('img')({ + margin: 'auto', + display: 'block', + maxWidth: '100%', + maxHeight: '100%', +}); + +interface IBasketLineItemProps { + item: ICartLineItem; +} + +const BasketLineItem: FC = ({ item }) => { + const currencyEUR = item.price.value.currencyCode; + const numberEUR = item.price.value.centAmount / 10 ** item.price.value.fractionDigits; + const priceEUR = new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: currencyEUR, + }).format(numberEUR); + + let discountedNumberEUR = numberEUR; + if (item.price.discounted) { + discountedNumberEUR = + item.price.discounted.value.centAmount / 10 ** item.price.discounted.value.fractionDigits; + } + if (item.discountedPrice) { + discountedNumberEUR = + item.discountedPrice.value.centAmount / 10 ** item.discountedPrice.value.fractionDigits; + } + const discountedPriceEUR = new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: currencyEUR, + }).format(discountedNumberEUR); + + const totalCurrencyEUR = item.totalPrice.currencyCode; + + const subTotalNumberEUR = + (item.price.value.centAmount * item.quantity) / 10 ** item.price.value.fractionDigits; + const subTotalPriceEUR = new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: totalCurrencyEUR, + }).format(subTotalNumberEUR); + + const totalNumberEUR = item.totalPrice.centAmount / 10 ** item.totalPrice.fractionDigits; + const totalPriceEUR = new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: totalCurrencyEUR, + }).format(totalNumberEUR); + + return ( + <> + + + + complex + + + + + + + + + {item.name.en} + + + More Details... + + + + + + {discountedNumberEUR !== numberEUR ? ( + <> + + {discountedPriceEUR} + + + {priceEUR} + + + ) : ( + + {priceEUR} + + )} + + + + + + + + {subTotalNumberEUR !== totalNumberEUR ? ( + <> + + {totalPriceEUR} + + + {subTotalPriceEUR} + + + ) : ( + + {totalPriceEUR} + + )} + + + + + + + + + ); +}; +export default BasketLineItem; diff --git a/e-commerce-app/src/components/Footer/Footer.module.scss b/e-commerce-app/src/components/Footer/Footer.module.scss new file mode 100644 index 0000000..11d86e7 --- /dev/null +++ b/e-commerce-app/src/components/Footer/Footer.module.scss @@ -0,0 +1,48 @@ +@mixin animate($animation,$duration,$method) { + animation: $animation $duration $method; +} + +@mixin keyframes($name) { + @keyframes #{$name}{ + @content; + } +} + +.footer { + background-color: beige; + + &_container { + justify-content: end; + + &_title { + color: green; + } + + .span { + color: green; + font-weight: 500; + } + } + + &_img { + background-image: url(../../assets/images/FooterImg.png); + background-repeat: repeat-x; + background-size: contain; + background-position: center; + height: 150px; + margin-left: -20%; + overflow: hidden; + transition: all 2s; + @include keyframes(fade-in) { + 0% { + opacity: 0; + transform: translateX(30px); + } + 100% { + opacity: 1; + transform: translateX(0px); + } + } + @include animate(fade-in, 2s, ease-out); + } +} diff --git a/e-commerce-app/src/components/Footer/Footer.tsx b/e-commerce-app/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..1b57d64 --- /dev/null +++ b/e-commerce-app/src/components/Footer/Footer.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import styles from './Footer.module.scss'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import Link from '@mui/material/Link'; +import Grid from '@mui/material/Grid'; +import { Facebook, Instagram, Twitter } from '@mui/icons-material'; +import { Box } from '@mui/material'; + +export const Footer: React.FC = () => { + return ( + + + + + + About Us + + + We are RS-School students, dedicated to providing the best service to our customers. +
Hanna Dziahonskaya is a Mentor + ReactDreamTeam and RSdzen Project. +
+
+ + + Contact Us + + + 123 Example Street, EXAMPLE + + + Email: info@example.com + + + Phone: +1 234 567 8901 + + + + + Follow Us + + + + + + + + + + + + Rolling Scopes School 2023 + + +
+
+ + + +
+ ); +}; diff --git a/e-commerce-app/src/components/Header/Header.tsx b/e-commerce-app/src/components/Header/Header.tsx index 2fa27b0..b3cfd0c 100644 --- a/e-commerce-app/src/components/Header/Header.tsx +++ b/e-commerce-app/src/components/Header/Header.tsx @@ -6,13 +6,15 @@ import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import MenuIcon from '@mui/icons-material/Menu'; import Container from '@mui/material/Container'; -import logo from '../../assets/logo/free-icon-tree-740936.png'; +import logo from '../../assets/logo/Logo.png'; import Menu from '@mui/material/Menu'; import NavLinks from '../NavLinks/NavLinks'; import MenuLinks from '../MenuLinks/MenuLinks'; import { navigationRoutes } from '../../routes/navigation'; import UserMenu from '../UserMenu/UserMenu'; import { Link } from 'react-router-dom'; +import UserCart from '../UserCart/UserCart'; +import { Grid } from '@mui/material'; export const Header: React.FC = () => { const [anchorElNav, setAnchorElNav] = React.useState(null); @@ -26,7 +28,7 @@ export const Header: React.FC = () => { }; return ( - + @@ -107,7 +109,14 @@ export const Header: React.FC = () => {
- + + + + + + + + diff --git a/e-commerce-app/src/components/ProductCard/ProductCard.module.scss b/e-commerce-app/src/components/ProductCard/ProductCard.module.scss index 25a3d8e..1cb1bd4 100644 --- a/e-commerce-app/src/components/ProductCard/ProductCard.module.scss +++ b/e-commerce-app/src/components/ProductCard/ProductCard.module.scss @@ -15,6 +15,7 @@ } + &__text { height: 100%; display: flex; @@ -33,14 +34,12 @@ 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 { @@ -49,4 +48,4 @@ 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 index eb498c9..4737ef5 100644 --- a/e-commerce-app/src/components/ProductCard/ProductCard.tsx +++ b/e-commerce-app/src/components/ProductCard/ProductCard.tsx @@ -1,11 +1,9 @@ -import { FC, useEffect, useState } from 'react'; +import { FC } 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; @@ -13,23 +11,12 @@ interface ICardProps { 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 clickOnCardHandler = (e: React.MouseEvent) => { + if (!(e.target instanceof HTMLButtonElement)) { + navigate(`/products/${item.id}`); + } + }; const imgPath = item.masterData.current.masterVariant.images[0].url; const imgDescription = item.masterData.current.name.en; @@ -38,6 +25,12 @@ export const ProductCard: FC = ({ item }) => { const numberEUR = item.masterData.current.masterVariant.prices[0].value.centAmount / 10 ** item.masterData.current.masterVariant.prices[0].value.fractionDigits; + let discountEUR = numberEUR; + if (item.masterData.current.masterVariant.prices[0].discounted) { + discountEUR = + item.masterData.current.masterVariant.prices[0].discounted.value.centAmount / + 10 ** item.masterData.current.masterVariant.prices[0].discounted.value.fractionDigits; + } const priceEUR = new Intl.NumberFormat('en-IN', { style: 'currency', currency: currencyEUR, @@ -45,23 +38,10 @@ export const ProductCard: FC = ({ item }) => { 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); + }).format(discountEUR); return ( - + clickOnCardHandler(e)}> @@ -70,7 +50,7 @@ export const ProductCard: FC = ({ item }) => { {item.masterData.current.name.en} - {tax !== 0 ? ( + {discountEUR !== numberEUR ? ( <> {salePriceEUR} @@ -84,26 +64,9 @@ export const ProductCard: FC = ({ item }) => { {priceEUR} )} - - {tax !== 0 ? ( - <> - - {salePriceUSD} - - - {priceUSD} - - - ) : ( - - {priceUSD} - - )} - + ); diff --git a/e-commerce-app/src/components/ProductsFilterForm/ProductsFilterForm.tsx b/e-commerce-app/src/components/ProductsFilterForm/ProductsFilterForm.tsx index 5fbbb5b..fe542d8 100644 --- a/e-commerce-app/src/components/ProductsFilterForm/ProductsFilterForm.tsx +++ b/e-commerce-app/src/components/ProductsFilterForm/ProductsFilterForm.tsx @@ -1,4 +1,4 @@ -import React, { JSX, useState } from 'react'; +import React, { JSX, useId, useState } from 'react'; import { Box, Button, @@ -38,6 +38,9 @@ const ProductsFilterForm = (): JSX.Element => { const [sortRate, setSortRate] = useState(searchQuerySort); const [sortCategories, setSortCategories] = useState(searchQueryCategories); + const categoriesId = useId(); + const sortById = useId(); + const handleChange2 = (event: Event, newValue: number | number[], activeThumb: number) => { if (!Array.isArray(newValue)) { return; @@ -88,15 +91,15 @@ const ProductsFilterForm = (): JSX.Element => { return ( + + Catalog + - - Product Categories - - Categories + Categories - - Filter by price - - -

{priceSort[0]}

- -

{priceSort[1]}

-
-
-
- - Sort By + Sort By + + + Filter by price + + +

{priceSort[0]}

+ +

{priceSort[1]}

+
+
+
+ diff --git a/e-commerce-app/src/components/ProductsPagination/ProductsPagination.tsx b/e-commerce-app/src/components/ProductsPagination/ProductsPagination.tsx new file mode 100644 index 0000000..680fb6c --- /dev/null +++ b/e-commerce-app/src/components/ProductsPagination/ProductsPagination.tsx @@ -0,0 +1,47 @@ +import { Pagination } from '@mui/material'; +import { JSX, useEffect, useState } from 'react'; +import Box from '@mui/material/Box'; +import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { getProductsTotal } from '../../store/slices/productsSlice'; +import { getQueryLimit, getQueryOffset, setQueryOffset } from '../../store/slices/queryParamsSlice'; + +const ProductsPagination = (): JSX.Element => { + const totalProductsCount = useAppSelector(getProductsTotal); + const queryLimit = useAppSelector(getQueryLimit); + const queryOffset = useAppSelector(getQueryOffset); + const dispatch = useAppDispatch(); + + const [page, setPage] = useState(queryOffset / queryLimit + 1); + const [maxPages, setMaxPages] = useState(0); + + useEffect(() => { + const totalPagesCount = Math.ceil(totalProductsCount / queryLimit); + setMaxPages(totalPagesCount); + }, [totalProductsCount]); + + useEffect(() => { + const newOffset = (page - 1) * queryLimit; + if (newOffset < totalProductsCount) { + dispatch(setQueryOffset(newOffset)); + } + }, [page]); + const handleChange = (event: React.ChangeEvent, value: number) => { + setPage(value); + }; + + return ( + + + + ); +}; +export default ProductsPagination; diff --git a/e-commerce-app/src/components/PromoCard/PromoCard.module.scss b/e-commerce-app/src/components/PromoCard/PromoCard.module.scss new file mode 100644 index 0000000..246e640 --- /dev/null +++ b/e-commerce-app/src/components/PromoCard/PromoCard.module.scss @@ -0,0 +1,11 @@ +.card { + &__avatar { + width: 56px; + height: 56px; + margin-left: 42%; + } + + &__code { + border: 2px green dotted; + } +} diff --git a/e-commerce-app/src/components/PromoCard/PromoCard.tsx b/e-commerce-app/src/components/PromoCard/PromoCard.tsx new file mode 100644 index 0000000..e15e631 --- /dev/null +++ b/e-commerce-app/src/components/PromoCard/PromoCard.tsx @@ -0,0 +1,35 @@ +import React, { FC, JSX } from 'react'; +import CardContent from '@mui/material/CardContent'; +import Avatar from '@mui/material/Avatar'; +import styles from './PromoCard.module.scss'; +import Typography from '@mui/material/Typography'; +import Card from '@mui/material/Card'; + +interface IPromoCardProps { + avatarSrc: string; + code: string; + description: string; +} + +const PromoCard: FC = ({ avatarSrc, code, description }): JSX.Element => { + return ( + + + + + Coupon Code #1 + + + {code} + + + Copy the code above and paste at checkout + + + {description} + + + + ); +}; +export default PromoCard; diff --git a/e-commerce-app/src/components/PromoCodes/PromoCodes.module.scss b/e-commerce-app/src/components/PromoCodes/PromoCodes.module.scss new file mode 100644 index 0000000..f70f495 --- /dev/null +++ b/e-commerce-app/src/components/PromoCodes/PromoCodes.module.scss @@ -0,0 +1,12 @@ +.promo { + background-color: beige; + padding: 5%; + + &__title { + padding-bottom: 3%; + font-weight: 600; + text-align: center; + } + + +} diff --git a/e-commerce-app/src/components/PromoCodes/PromoCodes.tsx b/e-commerce-app/src/components/PromoCodes/PromoCodes.tsx new file mode 100644 index 0000000..543c21b --- /dev/null +++ b/e-commerce-app/src/components/PromoCodes/PromoCodes.tsx @@ -0,0 +1,63 @@ +import React, { JSX, useEffect, useState } from 'react'; +import styles from './PromoCodes.module.scss'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Grid'; +import Icon1 from '../../assets/HomePageImg/promoIcon1.png'; +import Icon2 from '../../assets/HomePageImg/promoIcon2.png'; +import Icon3 from '../../assets/HomePageImg/promoIcon3.png'; +import Container from '@mui/material/Container'; +import PromoCard from '../PromoCard/PromoCard'; +import { useLazyGetDiscountCodesQuery } from '../../api/discountCodesApi'; +import { useAppSelector } from '../../store/hooks'; +import { getAccessToken } from '../../store/slices/userSlice'; + +const initialData = { + code: 'XXX-XXX-XX', + description: 'Discount code will be here', +}; + +const icons = [Icon1, Icon2, Icon3]; + +const PromoCodes = (): JSX.Element => { + const accessToken = useAppSelector(getAccessToken) as string; + const [getDiscountCodes, { data, isSuccess }] = useLazyGetDiscountCodesQuery(); + + const [discounts, setDiscounts] = useState([initialData, initialData, initialData]); + + useEffect(() => { + if (accessToken) { + getDiscountCodes(accessToken); + } + }, [accessToken]); + + useEffect(() => { + if (isSuccess && data) { + const discountsArray = data.results.slice(0, 3).map((discount) => ({ + code: discount.code, + description: discount.description.en, + })); + console.log(discountsArray); + setDiscounts(discountsArray); + } + }, [isSuccess]); + + return ( + + + Active Promo Codes + + + {discounts.map((discount, idx) => ( + + + + ))} + + + ); +}; +export default PromoCodes; diff --git a/e-commerce-app/src/components/UserCart/UserCart.tsx b/e-commerce-app/src/components/UserCart/UserCart.tsx new file mode 100644 index 0000000..cab00ab --- /dev/null +++ b/e-commerce-app/src/components/UserCart/UserCart.tsx @@ -0,0 +1,23 @@ +import { JSX } from 'react'; +import { useAppSelector } from '../../store/hooks'; +import { getTotalQuantityLineItemsInCart } from '../../api/cartApi'; +import { Badge, IconButton } from '@mui/material'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import { useNavigate } from 'react-router-dom'; + +const UserCart = (): JSX.Element => { + const totalQuantity = useAppSelector(getTotalQuantityLineItemsInCart); + const navigate = useNavigate(); + const clickHandler = () => { + navigate('/basket'); + }; + + return ( + + + + + + ); +}; +export default UserCart; diff --git a/e-commerce-app/src/components/UserMenu/UserMenu.tsx b/e-commerce-app/src/components/UserMenu/UserMenu.tsx index 4ae74c2..bad2bf6 100644 --- a/e-commerce-app/src/components/UserMenu/UserMenu.tsx +++ b/e-commerce-app/src/components/UserMenu/UserMenu.tsx @@ -1,13 +1,15 @@ -import React, { JSX } from 'react'; +import React, { JSX, useId } from 'react'; import Box from '@mui/material/Box'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; import Avatar from '@mui/material/Avatar'; +import PersonIcon from '@mui/icons-material/Person'; import Menu from '@mui/material/Menu'; import { useAppSelector } from '../../store/hooks'; import { getLoggedIn } from '../../store/slices/userSlice'; import MenuLinks from '../MenuLinks/MenuLinks'; import { userLoginRoutes, userLogoutRoutes } from '../../routes/navigation'; +import { green } from '@mui/material/colors'; const UserMenu = (): JSX.Element => { const [anchorElUser, setAnchorElUser] = React.useState(null); @@ -18,18 +20,22 @@ const UserMenu = (): JSX.Element => { setAnchorElUser(null); }; + const menuId = useId(); + const isLoggedIn = useAppSelector(getLoggedIn); return ( - + + + { + const { + register, + handleSubmit, + formState: { errors }, + setError, + clearErrors, + reset, + control, + } = useForm({ + defaultValues: { + postalCode: '', + city: '', + country: 'DE', + street: '', + }, + mode: 'onChange', + }); + const accessToken = useAppSelector(getAccessToken) as string; + const myCustomerVersion = useAppSelector(getMyCustomerVersion); + const [updateMyCustomer] = useUpdateMyCustomerMutation(); + + const [street, setStreet] = useState(''); + const [city, setCity] = useState(''); + const [country, setCountry] = useState('DE'); + const [postalCode, setPostalCode] = useState(''); + + const { validateField } = useValidate(); + + const streetInputHandler = (e: ChangeEvent) => { + const val = e.target.value; + setStreet(val); + + const errString = validateField('validateStreet', val); + if (errString.length) { + setError('street', { + type: 'required', + message: errString, + }); + } else { + clearErrors('street'); + } + }; + + const cityInputHandler = (e: ChangeEvent) => { + const val = e.target.value; + setCity(val); + + const errString = validateStrictCity(val); + + if (errString.length) { + setError('city', { + type: 'required', + message: errString, + }); + } else { + clearErrors('city'); + } + }; + + const postalCodeInputHandler = (e: ChangeEvent) => { + const val = e.target.value; + setPostalCode(val); + + const errString = validatePostalCode( + val, + countries.find((c) => c.code === country)?.name || '', + ); + + if (errString.length) { + setError('postalCode', { + message: errString, + }); + } else { + clearErrors(['postalCode']); + } + }; + + const onResetForm = () => { + reset(); + setStreet(''); + setCity(''); + setPostalCode(''); + setCountry('DE'); + }; + + const submitForm: SubmitHandler = (data) => { + const addressObject: IUpdateMyCustomerActionAddAddress = { + action: 'addAddress', + address: { + streetName: data.street, + city: data.city, + country: data.country, + postalCode: data.postalCode, + }, + }; + const updateData: IUpdateMyCustomer = { + version: myCustomerVersion, + actions: [addressObject], + }; + updateMyCustomer({ data: updateData, token: accessToken }); + }; + + return ( + + + + + ( + streetInputHandler(e)} + value={street} + error={!!fieldState.error} + helperText={fieldState.error ? fieldState.error.message : null} + /> + )} + /> + + + ( + cityInputHandler(e)} + value={city} + error={!!fieldState.error} + helperText={fieldState.error ? fieldState.error.message : null} + /> + )} + /> + + + Country + + + + ( + postalCodeInputHandler(e)} + label="Postal Code" + autoComplete="off" + value={postalCode} + error={!!fieldState.error} + helperText={fieldState.error ? fieldState.error.message : null} + /> + )} + name={'postalCode'} + control={control} + /> + + + + + + + + + ); +}; +export default UserPersonalAddAddress; diff --git a/e-commerce-app/src/components/UserPersonalAddressTab/UserPersonalAddressTab.tsx b/e-commerce-app/src/components/UserPersonalAddressTab/UserPersonalAddressTab.tsx new file mode 100644 index 0000000..5f9251e --- /dev/null +++ b/e-commerce-app/src/components/UserPersonalAddressTab/UserPersonalAddressTab.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { IMyCustomerAddressResponse } from '../../types/addressesTypes'; +import UserPersonalContactRow from '../UserPersonalContactRow/UserPersonalContactRow'; +import Box from '@mui/material/Box'; + +interface IUserPersonalAddressTabProps { + addresses: IMyCustomerAddressResponse[]; + index: number; + value: number; +} + +const UserPersonalAddressTab: FC = ({ addresses, value, index }) => { + return ( + + ); +}; + +export default UserPersonalAddressTab; diff --git a/e-commerce-app/src/components/UserPersonalContactRow/UserPersonalContactRow.tsx b/e-commerce-app/src/components/UserPersonalContactRow/UserPersonalContactRow.tsx new file mode 100644 index 0000000..d34417c --- /dev/null +++ b/e-commerce-app/src/components/UserPersonalContactRow/UserPersonalContactRow.tsx @@ -0,0 +1,51 @@ +import { FC } from 'react'; +import { IMyCustomerAddressResponse } from '../../types/addressesTypes'; +import { Divider, Grid } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +interface IUserPersonalContactRowProps { + address: IMyCustomerAddressResponse; +} + +const UserPersonalContactRow: FC = ({ address }) => { + return ( + + + + Country: + + + {address.country} + + + + + City: + + + {address.city} + + + + + Street Name: + + + {address.streetName} + + + + + Postal Code: + + + {address.postalCode} + + + + + ); +}; + +export default UserPersonalContactRow; diff --git a/e-commerce-app/src/components/UserPersonalHeader/UserPersonalHeader.tsx b/e-commerce-app/src/components/UserPersonalHeader/UserPersonalHeader.tsx new file mode 100644 index 0000000..0d68cf3 --- /dev/null +++ b/e-commerce-app/src/components/UserPersonalHeader/UserPersonalHeader.tsx @@ -0,0 +1,25 @@ +import React, { FC, JSX } from 'react'; +import { Grid } from '@mui/material'; +import Typography from '@mui/material/Typography'; + +interface IUserPersonalHeaderProps { + title: string; + icon: React.ReactNode | string; +} + +const UserPersonalHeader: FC = ({ title, icon }): JSX.Element => { + return ( + + + {icon} + + + + {title} + + + + ); +}; + +export default UserPersonalHeader; diff --git a/e-commerce-app/src/components/UserPersonalRow/UserPersonalRow.tsx b/e-commerce-app/src/components/UserPersonalRow/UserPersonalRow.tsx new file mode 100644 index 0000000..be86a70 --- /dev/null +++ b/e-commerce-app/src/components/UserPersonalRow/UserPersonalRow.tsx @@ -0,0 +1,25 @@ +import React, { FC, JSX } from 'react'; +import { Grid } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +interface IUserPersonalRowProps { + title: string; + value: string; +} + +const UserPersonalRow: FC = ({ title, value }): JSX.Element => { + return ( + + + + {title} + + + {value} + + + + ); +}; +export default UserPersonalRow; diff --git a/e-commerce-app/src/components/UserRedirect/UserRedirect.tsx b/e-commerce-app/src/components/UserRedirect/UserRedirect.tsx index 2bce03e..f8bfe28 100644 --- a/e-commerce-app/src/components/UserRedirect/UserRedirect.tsx +++ b/e-commerce-app/src/components/UserRedirect/UserRedirect.tsx @@ -8,6 +8,6 @@ const UserRedirect = (): JSX.Element => { const myCustomerId = useAppSelector(getMyCustomerId); if (!myCustomerId) return ; - return ; + return ; }; export default UserRedirect; diff --git a/e-commerce-app/src/hooks/useLocalToken.ts b/e-commerce-app/src/hooks/useLocalToken.ts index 4ee040b..4284bc2 100644 --- a/e-commerce-app/src/hooks/useLocalToken.ts +++ b/e-commerce-app/src/hooks/useLocalToken.ts @@ -10,16 +10,32 @@ export const useLocalToken = () => { const delTokenFromStorage = (): void => { localStorage.removeItem(tokenName); + sessionStorage.removeItem(tokenName); }; const isTokenInStorage = (): boolean => { return !!localStorage.getItem(tokenName); }; + const setTokenInSessionStorage = (token: string): void => { + sessionStorage.setItem(tokenName, token); + }; + + const getTokenFromSessionStorage = (): string | null => { + return sessionStorage.getItem(tokenName); + }; + + const isTokenInLocalStorage = (): boolean => { + return !!sessionStorage.getItem(tokenName); + }; + return { setTokenInStorage, getTokenFromStorage, delTokenFromStorage, isTokenInStorage, + setTokenInSessionStorage, + getTokenFromSessionStorage, + isTokenInLocalStorage, }; }; diff --git a/e-commerce-app/src/pages/.gitkeep b/e-commerce-app/src/pages/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/e-commerce-app/src/pages/AbouPage/AboutPage.tsx b/e-commerce-app/src/pages/AbouPage/AboutPage.tsx deleted file mode 100644 index 9927b09..0000000 --- a/e-commerce-app/src/pages/AbouPage/AboutPage.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const AboutPage: React.FC = () => { - return
About Page
; -}; diff --git a/e-commerce-app/src/pages/AboutPage/AboutPage.tsx b/e-commerce-app/src/pages/AboutPage/AboutPage.tsx new file mode 100644 index 0000000..69cf730 --- /dev/null +++ b/e-commerce-app/src/pages/AboutPage/AboutPage.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import Grid from '@mui/material/Grid'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Avatar from '@mui/material/Avatar'; +import Link from '@mui/material/Link'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import Divider from '@mui/material/Divider'; +import Ilya from '../../assets/AboutPageImg/Ilya.jpeg'; +import Evguenia from '../../assets/AboutPageImg/Evguenia.jpeg'; +import Nargiza from '../../assets/AboutPageImg/Nargiza.jpg'; +import RSlogo from '../../assets/logo/RSlogo.png'; +import Backgroung from '../../assets/AboutPageImg/fon-trava.jpeg'; + +export const AboutPage: React.FC = () => { + return ( + + + + + About Us + + + We're on a mission to complete
eCommerce Application successfully. +
+ + + RSlogo + + + + The development of the project and its ecosystem is guided by a team of RS Scool + students + +
+
+ + + + + + + + + + + + + Ilya Shcherbakov + + + Front-end developer + + + + Python enjoyer, meme creator, great sense of humor. + + + + + + + + + + + + + + + + Evguenia Zelenko + + + Front-end developer + + + + Spotify lover, strong motivator, always positive. + + + + + + + + + + + + + + + + Nargiza Ruzieva + + + Front-end developer + + + + Mother-of-three, karaoke lover, a quiet person. + + + + + + +
+ ); +}; diff --git a/e-commerce-app/src/pages/BasketPage/BasketPage.tsx b/e-commerce-app/src/pages/BasketPage/BasketPage.tsx index d7d3561..1d4e8ac 100644 --- a/e-commerce-app/src/pages/BasketPage/BasketPage.tsx +++ b/e-commerce-app/src/pages/BasketPage/BasketPage.tsx @@ -1,5 +1,22 @@ -import React from 'react'; +import { FC } from 'react'; +import Container from '@mui/material/Container'; +import Typography from '@mui/material/Typography'; +import BasketEmpty from '../../components/BasketEmpty/BasketEmpty'; +import { useAppSelector } from '../../store/hooks'; +import { getTotalQuantityLineItemsInCart } from '../../api/cartApi'; +import BasketFull from '../../components/BasketFull/BasketFull'; +import CartQuery from '../../requestsComponents/CartQuery/CartQuery'; -export const BasketPage: React.FC = () => { - return
Basket Page
; +export const BasketPage: FC = () => { + const totalQuantity = useAppSelector(getTotalQuantityLineItemsInCart); + return ( + + + + Shopping cart + + {totalQuantity ? : } + + + ); }; diff --git a/e-commerce-app/src/pages/ErrorPage/ErrorPage.tsx b/e-commerce-app/src/pages/ErrorPage/ErrorPage.tsx index 1bbfcd6..6d1d5b1 100644 --- a/e-commerce-app/src/pages/ErrorPage/ErrorPage.tsx +++ b/e-commerce-app/src/pages/ErrorPage/ErrorPage.tsx @@ -16,16 +16,19 @@ export const ErrorPage: React.FC = () => { justifyContent: 'start', alignItems: 'center', flexDirection: 'column', - minHeight: '80vh', + minHeight: '90vh', marginBottom: '3%', }} + pt={5} > Sorry, the page you’re looking for doesn’t exist. home-icon ); diff --git a/e-commerce-app/src/pages/HomePage/HomePage.module.scss b/e-commerce-app/src/pages/HomePage/HomePage.module.scss new file mode 100644 index 0000000..6b38a86 --- /dev/null +++ b/e-commerce-app/src/pages/HomePage/HomePage.module.scss @@ -0,0 +1,48 @@ +.homePage { + display: block; + + .banner { + background-image: url(../../assets/HomePageImg/Banner.avif); + background-repeat: no-repeat; + background-size: cover; + background-position: center; + position: relative; + display: flex; + justify-content: center; + padding-top: 10%; + height: 400px; + color: green; + + p { + font-weight: 600; + text-align: center; + } + } + + .infoContainer { + padding: 7%; + display: flex; + justify-content: space-around; + align-items: center; + + .infoImage { + background-image: url(../../assets/HomePageImg/PlantsRoom.png); + background-repeat: no-repeat; + background-size: contain; + background-position: center; + height: 460px; + position: relative; + } + + .info_title { + font-family: Georgia, 'Times New Roman', Times, serif; + font-size: 30px; + font-weight: 600; + color: green; + } + + .info_text { + margin-top: 3%; + } + } +} diff --git a/e-commerce-app/src/pages/HomePage/HomePage.tsx b/e-commerce-app/src/pages/HomePage/HomePage.tsx index 69eea1e..cb0b6b7 100644 --- a/e-commerce-app/src/pages/HomePage/HomePage.tsx +++ b/e-commerce-app/src/pages/HomePage/HomePage.tsx @@ -1,11 +1,41 @@ import React from 'react'; import { useAppSelector } from '../../store/hooks'; +import Container from '@mui/material/Container'; +import styles from './HomePage.module.scss'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Grid'; +import PromoCodes from '../../components/PromoCodes/PromoCodes'; export const HomePage: React.FC = () => { const { email } = useAppSelector((state) => state.user); return ( -
- Home Page

Hello, {email ? email : 'Stranger. Can I call you a Friend?'}

-
+ + + + Hello, {email ? email : 'Stranger.'}
Can I call you a Friend? +
+
+ + + + + + We offer Plants Delivery + + Make celebrations particularly special by sending a friend or family member one of our + select plants with a personalized message! You can never go wrong with sending one of + our best sellers to show just how much you care. They can mark the occasion and serve + as a lasting reminder of the event. Certain plants may also have cultural or symbolic + significance, making them even more meaningful as gifts for specific celebrations. + Unlike cut flowers that wither within days, plants are long-lasting gifts that can + continue to grow and thrive for months or even years with proper care. Make sure to + point them in the direction of our catalog page that cover almost all of the plants we + have in stock. + + + + +
); }; diff --git a/e-commerce-app/src/pages/LoginPage/LoginPage.tsx b/e-commerce-app/src/pages/LoginPage/LoginPage.tsx index a58845b..9e1ecd0 100644 --- a/e-commerce-app/src/pages/LoginPage/LoginPage.tsx +++ b/e-commerce-app/src/pages/LoginPage/LoginPage.tsx @@ -12,13 +12,14 @@ import { } from '@mui/material'; import LoginImage from '../../assets/images/ImgLoginPage.png'; import { NavLink, useNavigate } from 'react-router-dom'; -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useId, useState } from 'react'; import { useValidate } from '../../hooks/useValidate'; import { SubmitHandler, useForm } from 'react-hook-form'; import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import { useLoginUserMutation } from '../../api/authApi'; import { + getAccessToken, getLoggedIn, isRememberedMe, setAuth, @@ -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: '', @@ -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); @@ -59,7 +63,7 @@ export const LoginPage: FC = () => { }, [isLoggedIn]); const { validateField } = useValidate(); - const { setTokenInStorage } = useLocalToken(); + const { setTokenInStorage, setTokenInSessionStorage } = useLocalToken(); const dispatch = useAppDispatch(); const [globalError, setGlobalError] = useState({ @@ -68,6 +72,7 @@ export const LoginPage: FC = () => { }); const [loginUser, { isSuccess, error: errorApi, isError, data }] = useLoginUserMutation(); + const [authenticateUser] = useAuthenticateMyCustomerMutation(); const { register, @@ -85,6 +90,7 @@ export const LoginPage: FC = () => { useEffect(() => { if (!isSuccess || !data) return; dispatch(setAuth({ access_token: data.access_token, refresh_token: data.refresh_token })); + setTokenInSessionStorage(data.refresh_token); if (isRememberedUser) { setTokenInStorage(data.refresh_token); } @@ -97,6 +103,8 @@ export const LoginPage: FC = () => { dispatch(setLogOut()); }, [isError]); + const loginFormId = useId(); + const submitHandler: SubmitHandler = (data) => { if (data.email) { const errStr = validateField('email', data.email); @@ -152,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'); } @@ -201,7 +218,7 @@ export const LoginPage: FC = () => { component="form" noValidate sx={{ mt: 5, ml: 8, mr: 8 }} - id="login-form" + id={loginFormId} onSubmit={handleSubmit(submitHandler)} > {globalError.status && ( @@ -212,7 +229,7 @@ export const LoginPage: FC = () => { { { const navigate = useNavigate(); @@ -28,6 +29,7 @@ export const LogoutPage = (): JSX.Element => { useEffect(() => { dispatch(setLogOut()); delTokenFromStorage(); + dispatch(resetCart()); }, [isSuccess, isError]); useEffect(() => { diff --git a/e-commerce-app/src/pages/ProductPage/ProductPage.tsx b/e-commerce-app/src/pages/ProductPage/ProductPage.tsx index ecaac96..29053e8 100644 --- a/e-commerce-app/src/pages/ProductPage/ProductPage.tsx +++ b/e-commerce-app/src/pages/ProductPage/ProductPage.tsx @@ -1,8 +1,6 @@ -import { FC, useEffect, useState } from 'react'; +import { FC, 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 { Box, Typography, Button, Grid, Stack } from '@mui/material'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'; import Modal from '@mui/material/Modal'; @@ -14,8 +12,8 @@ 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'; +import CartAddLineItem from '../../requestsComponents/CartAddLineItem/CartAddLineItem'; +import CartModifyQuantity from '../../requestsComponents/CartModifyQuantity/CartModifyQuantity'; const style = { bgcolor: 'background.paper', @@ -31,31 +29,14 @@ const styleArrows = { export const ProductPage: FC = () => { const { productId } = useParams(); const authToken = useAppSelector(getAccessToken); - const taxesArray = useAppSelector(getTaxes); - const { data, isSuccess, isLoading, isFetching } = useGetProductByIdQuery({ + const { data, 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); @@ -74,6 +55,12 @@ export const ProductPage: FC = () => { const priceNumber = data.masterData.current.masterVariant.prices[0].value.centAmount / 10 ** data.masterData.current.masterVariant.prices[0].value.fractionDigits; + let discountNumber = priceNumber; + if (data.masterData.current.masterVariant.prices[0].discounted) { + discountNumber = + data.masterData.current.masterVariant.prices[0].discounted.value.centAmount / + 10 ** data.masterData.current.masterVariant.prices[0].discounted.value.fractionDigits; + } const priceCommon = new Intl.NumberFormat('en-IN', { style: 'currency', currency: currencyCommon, @@ -82,7 +69,12 @@ export const ProductPage: FC = () => { const discountPrice = new Intl.NumberFormat('en-IN', { style: 'currency', currency: currencyCommon, - }).format(priceNumber - priceNumber * tax); + }).format(discountNumber); + + const priceDiff = new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: currencyCommon, + }).format(priceNumber - discountNumber); return ( @@ -162,56 +154,33 @@ export const ProductPage: FC = () => { {title} - {tax !== 0 ? ( + {discountPrice !== priceCommon ? ( <> Discount price:{discountPrice} - Full price:{priceCommon} + Full price: {priceCommon} - Tax: {`${tax * 100} %`} + Discount: {priceDiff} ) : ( - Price:{priceCommon} + Price: {priceCommon} )} {description} - - - - {count} - - - + - + diff --git a/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx b/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx index f36408e..c11fc05 100644 --- a/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx +++ b/e-commerce-app/src/pages/ProductsPage/ProductsPage.tsx @@ -10,6 +10,7 @@ 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'; +import ProductsPagination from '../../components/ProductsPagination/ProductsPagination'; export const ProductsPage: React.FC = () => { const dispatch = useDispatch(); @@ -62,6 +63,7 @@ export const ProductsPage: React.FC = () => { + ); diff --git a/e-commerce-app/src/pages/RegistrationPage/FormPage2.tsx b/e-commerce-app/src/pages/RegistrationPage/FormPage2.tsx index 58103ae..688ba25 100644 --- a/e-commerce-app/src/pages/RegistrationPage/FormPage2.tsx +++ b/e-commerce-app/src/pages/RegistrationPage/FormPage2.tsx @@ -2,8 +2,6 @@ import { TextField, Grid } from '@mui/material'; import { IFormPageProps } from '../../interfaces/IFormPageProps'; import { FC } from 'react'; -/* eslint-disable react/prop-types */ - export const FormPage2: FC = ({ register, errors, diff --git a/e-commerce-app/src/pages/RootPage/RootPage.tsx b/e-commerce-app/src/pages/RootPage/RootPage.tsx index 87b5343..6ae744a 100644 --- a/e-commerce-app/src/pages/RootPage/RootPage.tsx +++ b/e-commerce-app/src/pages/RootPage/RootPage.tsx @@ -1,15 +1,21 @@ import { JSX } from 'react'; import { Outlet } from 'react-router-dom'; import { Header } from '../../components/Header/Header'; +import { Footer } from '../../components/Footer/Footer'; +import Container from '@mui/material/Container'; +import { Box, Stack } from '@mui/material'; const RootPage = (): JSX.Element => { return ( - <> +
-
- -
- + + + + + +