diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..ebf4d01 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +tailwind.config.js diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d51ddf7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,32 @@ +# Description + +Fixes # () + + + +## Motivation + + + +## Type of change + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has This Been Tested? + + + +- [ ] +- [ ] + +# Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have commented my code in hard-to-understand areas +- [ ] My changes generate no new warnings diff --git a/.gitignore b/.gitignore index c6bba59..c21b2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,28 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + + # Logs logs *.log @@ -128,3 +153,4 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* +yarn.lock diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..915c6b2 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn format +yarn test +yarn lint diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6fc2a81 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "singleQuote": true, + "printWidth": 110, + "tabWidth": 2, + "semi": true, + "arrowParens": "always", + "trailingComma": "none", + "bracketSpacing": true, + "singleAttributePerLine": true, + "plugins": [ + "prettier-plugin-tailwindcss" + ] +} diff --git a/README.md b/README.md index d416b84..7f30e53 100644 --- a/README.md +++ b/README.md @@ -1 +1,85 @@ -# eCommerce-Application \ No newline at end of file +# eCommerce Application ๐Ÿ›๏ธ๐ŸŒ + +Welcome to our eCommerce application! This platform replicates real-world shopping experiences in a digital environment ๐Ÿช. It's a comprehensive online shopping portal that provides an interactive and seamless experience to users. From product discovery to checkout, the application ensures a smooth journey for the user, enhancing their engagement and boosting their purchasing confidence ๐Ÿš€. + +Users can browse through a vast range of products ๐Ÿ“š๐Ÿ‘—๐Ÿ‘Ÿ, view detailed descriptions, add their favorite items to the basket ๐Ÿ›’, and proceed to checkout ๐Ÿ’ณ. It includes features such as user registration and login ๐Ÿ“๐Ÿ”, product search ๐Ÿ”, product categorization, and sorting to make the shopping experience more streamlined and convenient. + +An important aspect of our application is that it's responsive ๐Ÿ“ฒ, ensuring it looks great on various devices with a minimum resolution of 390px. This feature makes the shopping experience enjoyable, irrespective of the device users prefer. + +Key pages in the application include: + +- Login and Registration pages ๐Ÿ–ฅ๏ธ +- Main page ๐Ÿ  +- Catalog Product page ๐Ÿ“‹ +- Detailed Product page ๐Ÿ”Ž +- User Profile page ๐Ÿ‘ค +- Basket page ๐Ÿ›’ +- About Us page ๐Ÿ™‹โ€โ™‚๏ธ๐Ÿ™‹โ€โ™€๏ธ + +The application is powered by [CommerceTools](https://docs.commercetools.com/docs) ๐ŸŒ, a leading provider of commerce solutions for B2C and B2B enterprises. CommerceTools offers a cloud-native, microservices-based commerce platform that enables brands to create unique and engaging digital commerce experiences. + +The application developed by teams of three ๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป: ๐Ÿ„[glitch-surfer](https://github.com/glitch-surfer), ๐Ÿคนโ€โ™‚๏ธ[filmaxim](https://github.com/filmaxim) & ๐Ÿ™†โ€โ™‚๏ธ[pavelvl21](https://github.com/pavelvl21). + +## Technology Stack ๐Ÿ’ป๐Ÿ“š + +This app is SPA and develop with: +- React โš›๏ธ (It allows the application to have high performance and responsiveness, start with create-react-app and then customize some dependancies) +- TypeScript (It ensure type safety and improve the maintainability and scalability of the application ๐Ÿ“˜) +- Webpack (supporting both development and production build modes) +- ESlint (react+ts recommended rules to enforce consistent coding styles and identify potential issues in the codebase) +- Prettier (to automatically format code, ensuring a consistent and readable code style) +- Husky (to manage Git hooks, automating tasks such as code formatting and linting checks during the commit process) +- Jest (as the testing framework for the project, enabling comprehensive testing of code functionality) + +### Key Pages + +1. **Login Page ๐Ÿ–ฅ๏ธ๐Ÿ”** + The Login page is the user's entry point to the application. It have fields for entering the username and password. There validation checks in place to ensure that all fields are filled before allowing the user to submit the form. On successful login, the user directed to the main page of the application. + +2. **Registration Page ๐Ÿ–ฅ๏ธ๐Ÿ“** + The Registration page allow new users to sign up for the application. It have fields for entering user information including a username, email, and password. Once the user submits the form after entering all the necessary details, they will be registered and directed to the login page. + +3. **Main Page ๐Ÿ ** + The Main Page is the core of the application. This is where the users can browse the catalog of products. The page provide a user-friendly interface that allows users to easily navigate through different categories and products. Centralized Navigation is a key feature on this page, and include links to all functional pages of the application. Each link should redirect the user correctly to the corresponding page without any errors. The main page also displays active promo codes. + +4. **Catalog Product Page ๐Ÿ“‹** + The Catalog Product page display a list of products in a specific category. The products displayed as interactive cards with a picture and essential details about the product. Users can click on these cards to get more detailed information about the product. + +5. **Detailed Product Page ๐Ÿ”Ž** + The Detailed Product page is where the user can view detailed information about a specific product. It display an image of the product, a detailed description, the price, and the available options for the product. Users able to add the product to their basket from this page. + +6. **User Profile Page ๐Ÿ‘ค** + The User Profile page display the personal information of the user. This includes their name, date of birth, and a list of their addresses. This page also provide an interface for users to edit their personal details and addresses. + +7. **Basket Page ๐Ÿ›’** + The Basket page is where users can view the products they have added to their basket. They can increase or decrease the quantity of each item or remove items from the basket. The page also display the subtotal and total prices and provide an option for the users to proceed to the checkout page. + +8. **About Us Page ๐Ÿ™‹โ€โ™‚๏ธ๐Ÿ™‹โ€โ™€๏ธ** + The About Us page give a brief introduction to the development team. It highlight each member's contributions to the project and provide personal details such as names, roles, short bios, relevant photos, and GitHub profile links. + +# Locally setup instruction & available scripts + +- clone this repo + +- `yarn install` +If you don't have yarn, `npm i --global yarn` before + +- `yarn start` +Runs dev-server on http://localhost:3000/ + +- `yarn build` +Build production bundle + +## Other scripts + +- `yarn test` +Runs jest tests + +- `yarn lint` +Runs js, jsx, ts, tsx files linting + +- `yarn format` +Runs Prettier formatting across the codebase + +- `yarn eject` +Give access to dependancies config files diff --git a/package.json b/package.json new file mode 100644 index 0000000..f392dfe --- /dev/null +++ b/package.json @@ -0,0 +1,119 @@ +{ + "name": "ecommerce-application1", + "version": "0.1.0", + "private": true, + "dependencies": { + "@babel/runtime": "^7.22.11", + "@commercetools/api-request-builder": "^6.0.0", + "@commercetools/platform-sdk": "^5.0.0", + "@commercetools/sdk-client-v2": "^2.2.0", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@headlessui/react": "^1.7.16", + "@heroicons/react": "^2.0.18", + "@material-ui/core": "^4.12.4", + "@mui/icons-material": "^5.14.3", + "@mui/lab": "^5.0.0-alpha.143", + "@mui/material": "^5.14.7", + "@mui/styles": "^5.14.7", + "@reduxjs/toolkit": "^1.9.5", + "@testing-library/jest-dom": "^5.14.1", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^13.2.1", + "@types/jest": "^27.0.1", + "@types/lodash": "^4.14.197", + "@types/node": "^16.7.13", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/react-router-dom": "^5.3.3", + "@types/toastify-js": "^1.12.0", + "babel-loader": "^9.1.3", + "dayjs": "^1.11.9", + "formik": "^2.4.3", + "formik-material-ui": "^4.0.0-alpha.2", + "i18next": "^23.4.6", + "postcode-validator": "^3.8.14", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^13.2.1", + "react-redux": "^8.1.2", + "react-responsive-carousel": "^3.2.23", + "react-router-dom": "^6.14.2", + "react-scripts": "5.0.1", + "sass": "^1.64.2", + "toastify-js": "^1.12.0", + "yup": "^1.2.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --watchAll=false", + "eject": "react-scripts eject", + "lint": "eslint --ext .js,.jsx,.ts,.tsx src", + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", + "prepare": "husky install" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest", + "plugin:react/recommended", + "standard-with-typescript", + "plugin:jsx-a11y/recommended" + ], + "overrides": [], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": "./tsconfig.json" + }, + "plugins": [ + "react", + "jsx-a11y" + ], + "rules": { + "@typescript-eslint/semi": [ + "error", + "always" + ], + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/member-delimiter-style": "off", + "react/react-in-jsx-scope": "off", + "react/jsx-uses-react": "off", + "no-console": "warn" + } + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/plugin-transform-runtime": "^7.22.10", + "@tailwindcss/forms": "^0.5.4", + "@types/react-slick": "^0.23.10", + "@types/react-test-renderer": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^5.52.0", + "eslint": "^8.0.1", + "eslint-config-standard-with-typescript": "^37.0.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-react": "^7.33.1", + "husky": "^8.0.3", + "prettier": "^3.0.1", + "prettier-plugin-tailwindcss": "^0.4.1", + "react-test-renderer": "^18.2.0", + "tailwindcss": "^3.3.3", + "typescript": "^5.1.6" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..9e8eb29 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..62ae11b --- /dev/null +++ b/public/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + Mr. Robot + + + + +
+ + diff --git a/src/@types/i18next.d.ts b/src/@types/i18next.d.ts new file mode 100644 index 0000000..dd4fa00 --- /dev/null +++ b/src/@types/i18next.d.ts @@ -0,0 +1,8 @@ +import 'i18next'; +import type { resources } from './resources'; + +declare module 'i18next' { + interface CustomTypeOptions { + resources: typeof resources; + } +} diff --git a/src/@types/resources.ts b/src/@types/resources.ts new file mode 100644 index 0000000..91baf19 --- /dev/null +++ b/src/@types/resources.ts @@ -0,0 +1,5 @@ +import { en } from '../locales/en'; + +export const resources = { + en +}; diff --git a/src/Init.tsx b/src/Init.tsx new file mode 100644 index 0000000..6512e16 --- /dev/null +++ b/src/Init.tsx @@ -0,0 +1,13 @@ +import { Provider } from 'react-redux'; +import { App } from './components/App'; +import { store } from './slices'; + +export const Init = () => { + return ( + +
+ +
+
+ ); +}; diff --git a/src/__tests__/__snapshots__/registrationFormRender.test.tsx.snap b/src/__tests__/__snapshots__/registrationFormRender.test.tsx.snap new file mode 100644 index 0000000..a1566f0 --- /dev/null +++ b/src/__tests__/__snapshots__/registrationFormRender.test.tsx.snap @@ -0,0 +1,1450 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RegistrationForm renders correctly renders correctly 1`] = ` +
+ + + + + + + + + +
+ + +
+

+ Shipping address: +

+
+ +
+ + + + + + + + +
+ +
+ +

+ Have an account? + + + Log In + +

+
+`; diff --git a/src/__tests__/getAddressInitialValues.test.ts b/src/__tests__/getAddressInitialValues.test.ts new file mode 100644 index 0000000..7dfbac3 --- /dev/null +++ b/src/__tests__/getAddressInitialValues.test.ts @@ -0,0 +1,266 @@ +import type { Customer } from '@commercetools/platform-sdk'; +import { getAddressesInitialValues } from '../components/forms/customerProfile/util/getInitialValues'; + +describe('getAddressInitialValues', () => { + const fakeCustomerData: Customer = { + id: '', + version: 1, + createdAt: '', + lastModifiedAt: '', + email: '', + isEmailVerified: false, + authenticationMode: '', + addresses: [] + }; + + const fakeCustomerWithOnlyOneAddress: Customer = { + ...fakeCustomerData, + addresses: [ + { + city: 'fd', + country: 'AX', + id: 'FZxT-eI3', + postalCode: 'df', + streetName: 'sd' + } + ] + }; + + const fakeCustomerWithOneAddressFullData: Customer = { + ...fakeCustomerData, + addresses: [ + { + city: 'fd', + country: 'AX', + id: 'FZxT-eI3', + postalCode: 'df', + streetName: 'sd' + } + ], + defaultShippingAddressId: 'FZxT-eI3', + defaultBillingAddressId: 'FZxT-eI3', + shippingAddressIds: ['FZxT-eI3'], + billingAddressIds: ['FZxT-eI3'] + }; + + const fakeCustomerWithTwoAddresses: Customer = { + ...fakeCustomerData, + addresses: [ + { + city: 'fd', + country: 'AX', + id: 'FZxT-eI3', + postalCode: '12345', + streetName: 'sd' + }, + { + city: 'test', + country: 'AX', + id: 'TeST-eI3', + postalCode: 'df', + streetName: 'sdsfd' + } + ] + }; + + const fakeCustomerWithShippingAddressIds: Customer = { + ...fakeCustomerWithOnlyOneAddress, + shippingAddressIds: ['FZxT-eI3'] + }; + + const fakeCustomerWithBillingAddressIds: Customer = { + ...fakeCustomerWithTwoAddresses, + billingAddressIds: ['TeST-eI3'] + }; + + const fakeCustomerWithDefaultShippingAddress: Customer = { + ...fakeCustomerWithShippingAddressIds, + defaultShippingAddressId: 'FZxT-eI3' + }; + + const fakeCustomerWithDefaultBillingAddress: Customer = { + ...fakeCustomerWithBillingAddressIds, + defaultBillingAddressId: 'TeST-eI3' + }; + + const fakeCustomerwithFullData: Customer = { + ...fakeCustomerWithShippingAddressIds, + ...fakeCustomerWithBillingAddressIds, + ...fakeCustomerWithDefaultShippingAddress, + ...fakeCustomerWithDefaultBillingAddress + }; + + it('should return initial values for customer with no addresses', () => { + expect(getAddressesInitialValues(fakeCustomerData)).toEqual([]); + }); + + it('should return initial values for customer with only 1 address', () => { + expect(getAddressesInitialValues(fakeCustomerWithOnlyOneAddress)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sd', + shippingStateChecked: false, + billingStateChecked: false, + defaultShippingAddress: false, + defaultBillingAddress: false + } + ]); + }); + it('should return initial values for customer with 2 addresses', () => { + expect(getAddressesInitialValues(fakeCustomerWithTwoAddresses)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: '12345', + streetName: 'sd', + shippingStateChecked: false, + billingStateChecked: false, + defaultShippingAddress: false, + defaultBillingAddress: false + }, + { + id: 'TeST-eI3', + city: 'test', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sdsfd', + shippingStateChecked: false, + billingStateChecked: false, + defaultShippingAddress: false, + defaultBillingAddress: false + } + ]); + }); + it('should return initial values for customer with shippingAddressIds', () => { + expect(getAddressesInitialValues(fakeCustomerWithShippingAddressIds)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sd', + shippingStateChecked: true, + billingStateChecked: false, + defaultShippingAddress: false, + defaultBillingAddress: false + } + ]); + }); + + it('should return initial values for customer with billingAddressIds', () => { + expect(getAddressesInitialValues(fakeCustomerWithBillingAddressIds)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: '12345', + streetName: 'sd', + shippingStateChecked: false, + billingStateChecked: false, + defaultShippingAddress: false, + defaultBillingAddress: false + }, + { + id: 'TeST-eI3', + city: 'test', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sdsfd', + shippingStateChecked: false, + billingStateChecked: true, + defaultShippingAddress: false, + defaultBillingAddress: false + } + ]); + }); + + it('should return initial values for customer with defaultShippingAddressId', () => { + expect(getAddressesInitialValues(fakeCustomerWithDefaultShippingAddress)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sd', + shippingStateChecked: true, + billingStateChecked: false, + defaultShippingAddress: true, + defaultBillingAddress: false + } + ]); + }); + + it('should return initial values for customer with defaultBillingAddressId', () => { + expect(getAddressesInitialValues(fakeCustomerWithDefaultBillingAddress)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: '12345', + streetName: 'sd', + shippingStateChecked: false, + billingStateChecked: false, + defaultBillingAddress: false, + defaultShippingAddress: false + }, + { + id: 'TeST-eI3', + city: 'test', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sdsfd', + shippingStateChecked: false, + billingStateChecked: true, + defaultBillingAddress: true, + defaultShippingAddress: false + } + ]); + }); + + it('should return initial values for customer with full data & 2 address', () => { + expect(getAddressesInitialValues(fakeCustomerwithFullData)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: '12345', + streetName: 'sd', + shippingStateChecked: true, + billingStateChecked: false, + defaultShippingAddress: true, + defaultBillingAddress: false + }, + { + id: 'TeST-eI3', + city: 'test', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sdsfd', + shippingStateChecked: false, + billingStateChecked: true, + defaultBillingAddress: true, + defaultShippingAddress: false + } + ]); + }); + + it('should return initial values for customer with full data & 1 address', () => { + expect(getAddressesInitialValues(fakeCustomerWithOneAddressFullData)).toEqual([ + { + id: 'FZxT-eI3', + city: 'fd', + country: 'Aland Islands', + postalCode: 'df', + streetName: 'sd', + shippingStateChecked: true, + billingStateChecked: true, + defaultShippingAddress: true, + defaultBillingAddress: true + } + ]); + }); +}); diff --git a/src/__tests__/navBar.tsx b/src/__tests__/navBar.tsx new file mode 100644 index 0000000..10f91dd --- /dev/null +++ b/src/__tests__/navBar.tsx @@ -0,0 +1,58 @@ +import { screen, render } from '@testing-library/react'; +import { PrivateNavGroup } from '../components/header/NavBar/PrivateNavGroup'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +export const renderPrivateNavGroup = (isLogged: boolean) => + render( + + + {}} + isLogged={isLogged} + logout={() => {}} + /> + } + /> + + + ); + +describe('PrivateNavGroup', () => { + it('displays login & registration link while logout state', () => { + renderPrivateNavGroup(false); + + const LogInLink = screen.queryByTestId(/LoginOutlinedIcon/i); + expect(LogInLink).toBeInTheDocument(); + + const RegistrationLink = screen.queryByTestId(/HowToRegOutlinedIcon/i); + expect(RegistrationLink).toBeInTheDocument(); + }); + + it('not display logout link while logout state', () => { + renderPrivateNavGroup(false); + + const logout = screen.queryByTestId(/LogoutOutlinedIcon/i); + expect(logout).toBeFalsy(); + }); + + it('does not display login & registration link while logIn state', () => { + renderPrivateNavGroup(true); + + const LogInLink = screen.queryByTestId(/LoginOutlinedIcon/i); + expect(LogInLink).toBeFalsy(); + + const RegistrationLink = screen.queryByTestId(/HowToRegOutlinedIcon/i); + expect(RegistrationLink).toBeFalsy(); + }); + + it('display logout link while logIn state', () => { + renderPrivateNavGroup(true); + + const logout = screen.queryByTestId(/LogoutOutlinedIcon/i); + expect(logout).toBeInTheDocument(); + }); +}); diff --git a/src/__tests__/registrationFormRender.test.tsx b/src/__tests__/registrationFormRender.test.tsx new file mode 100644 index 0000000..5fbcf47 --- /dev/null +++ b/src/__tests__/registrationFormRender.test.tsx @@ -0,0 +1,45 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import renderer from 'react-test-renderer'; +import { initialValuesRegistration } from '../components/forms/inputs/inputsData'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { RegistrationForm } from '../components/forms/RegistrationForm/RegistrationForm'; +import { getValidationSchema } from '../components/forms/util/validationSchema'; + +export const TestRegistrationForm = () => { + return ( + + + + } + /> + + + ); +}; + +describe('RegistrationForm renders correctly', () => { + // TODO: fix snapshot + it.skip('renders correctly', () => { + const component = renderer.create(TestRegistrationForm()); + const tree = component.toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('expands when billing address checkbox is unchecked', () => { + render(TestRegistrationForm()); + + expect(screen.queryByText(/Billing address:/i)).toBeFalsy(); + + fireEvent.click(screen.getByLabelText(/same as billing address/i)); + + expect(screen.getByText(/Billing address:/i)).toBeTruthy(); + }); +}); diff --git a/src/__tests__/registrationFormSubmit.test.tsx b/src/__tests__/registrationFormSubmit.test.tsx new file mode 100644 index 0000000..f47362e --- /dev/null +++ b/src/__tests__/registrationFormSubmit.test.tsx @@ -0,0 +1,86 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { submitDataWithBilling } from '../components/forms/util/submitFakeData'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { RegistrationForm } from '../components/forms/RegistrationForm/RegistrationForm'; +import { getValidationSchema } from '../components/forms/util/validationSchema'; +import type { FakeOnSubmit, InitialValuesRegistration } from '../utils/types'; + +export const TestRegistrationForm = ( + fakeOnSubmit: FakeOnSubmit, + initialValues: InitialValuesRegistration +) => { + return ( + + + + } + /> + + + ); +}; + +describe('registrationForm submit correctly', () => { + it('submits with all correct fields', async () => { + const fakeOnSubmit = jest.fn(); + render(TestRegistrationForm(fakeOnSubmit, submitDataWithBilling)); + + await waitFor(() => { + // eslint-disable-next-line testing-library/no-wait-for-side-effects + fireEvent.click(screen.getByText(/Create Account/i)); + }); + + expect(fakeOnSubmit.mock.calls).toHaveLength(1); + expect(fakeOnSubmit.mock.calls[0][0]).not.toBe({ + ...submitDataWithBilling, + firstName: 'John', + lastName: 'Doe' + }); + expect(fakeOnSubmit.mock.calls[0][0]).toBe(submitDataWithBilling); + }); + + it('not submits with empty fields', async () => { + const fakeOnSubmit = jest.fn(); + render( + TestRegistrationForm(fakeOnSubmit, { + ...submitDataWithBilling, + firstName: '', + lastName: '' + }) + ); + + await waitFor(() => { + // eslint-disable-next-line testing-library/no-wait-for-side-effects + fireEvent.click(screen.getByText(/Create Account/i)); + }); + + expect(fakeOnSubmit.mock.calls).toHaveLength(0); + }); + + it('no default address fields if checkbox unchecked', async () => { + const fakeOnSubmit = jest.fn(); + render(TestRegistrationForm(fakeOnSubmit, submitDataWithBilling)); + // defaultShippingAddress; + await waitFor(() => { + // eslint-disable-next-line testing-library/no-wait-for-side-effects + fireEvent.click(screen.getByText(/as default/i)); + }); + + await waitFor(() => { + // eslint-disable-next-line testing-library/no-wait-for-side-effects + fireEvent.click(screen.getByText(/Create Account/i)); + }); + + expect(fakeOnSubmit.mock.calls[0][0]).toStrictEqual({ + ...submitDataWithBilling, + shippingStateChecked: false + }); + }); +}); diff --git a/src/__tests__/registrationSubmitAdapter.test.ts b/src/__tests__/registrationSubmitAdapter.test.ts new file mode 100644 index 0000000..b6e6f50 --- /dev/null +++ b/src/__tests__/registrationSubmitAdapter.test.ts @@ -0,0 +1,55 @@ +import { addressAdapter } from '../components/forms/util/addressDataAdapter'; +import type { HandleSubmitWithBoth } from '../utils/types'; +import { submitDataWithBilling } from '../components/forms/util/submitFakeData'; + +describe('correct adapt submit data into request data', () => { + const adaptedSubmitData = addressAdapter(submitDataWithBilling); + const submitDataWithoutBilling: HandleSubmitWithBoth = { + ...submitDataWithBilling, + billingStreetName: '' + }; + const adaptedSubmitDataWithoutBilling = addressAdapter(submitDataWithoutBilling); + const billingAddressIndex = adaptedSubmitData.billingAddresses[0]; + const shippingAddressIndex = adaptedSubmitData.shippingAddresses[0]; + const adaptedSubmitDatawithoutCheckedDefault = addressAdapter({ + ...submitDataWithBilling, + shippingStateChecked: false, + billingStateChecked: false + }); + + it('add shipping address into addresses array', () => { + expect(adaptedSubmitData.shippingAddresses).toHaveLength(1); + expect(shippingAddressIndex).toBe(0); + expect(adaptedSubmitData.addresses[shippingAddressIndex].city).toBe('shippingCity'); + }); + + it('ignore billing address, if empty billing fields', () => { + expect(adaptedSubmitDataWithoutBilling.addresses).toHaveLength(1); + expect(adaptedSubmitDataWithoutBilling.shippingAddresses).toHaveLength(1); + expect(adaptedSubmitDataWithoutBilling.addresses[0].city).toBe('shippingCity'); + expect(adaptedSubmitDataWithoutBilling.billingAddresses[0] === shippingAddressIndex).toBe(true); + }); + + it('add billing address into addresses array', () => { + expect(adaptedSubmitData.addresses).toHaveLength(2); + expect(adaptedSubmitData.billingAddresses).toHaveLength(1); + expect(billingAddressIndex).toBe(1); + expect(adaptedSubmitData.addresses[billingAddressIndex].city).toBe('billingCity'); + expect(adaptedSubmitData.addresses[billingAddressIndex].country).toBe('GR'); + }); + + it('add default billing & shipping address, if checked', () => { + expect(adaptedSubmitData.defaultBillingAddress).toBe(billingAddressIndex); + expect(adaptedSubmitData.defaultShippingAddress).toBe(shippingAddressIndex); + }); + + it('no default shipping/billing addresses, if not checked', () => { + expect(adaptedSubmitDatawithoutCheckedDefault.defaultBillingAddress).toBeUndefined(); + expect(adaptedSubmitDatawithoutCheckedDefault.defaultShippingAddress).toBeUndefined(); + }); + + it('return correct other fields', () => { + expect(adaptedSubmitData.dateOfBirth).toBe('2002-01-01'); + expect(adaptedSubmitData.firstName).toBe('firstName'); + }); +}); diff --git a/src/__tests__/validationSchema.test.ts b/src/__tests__/validationSchema.test.ts new file mode 100644 index 0000000..f770065 --- /dev/null +++ b/src/__tests__/validationSchema.test.ts @@ -0,0 +1,220 @@ +import { getValidationSchema } from '../components/forms/util/validationSchema'; +import { submitDataWithBilling, submitDataWithShipping } from '../components/forms/util/submitFakeData'; + +describe('registrationSubmitValidation', () => { + const schemaWithShipping = getValidationSchema(true); + const schemaWithBoth = getValidationSchema(false); + + it('correct yup schema', () => { + expect(schemaWithShipping.__isYupSchema__).toBe(true); + expect(schemaWithBoth.__isYupSchema__).toBe(true); + }); + + it('validate with shipping)', () => { + expect(schemaWithShipping.isValidSync(submitDataWithShipping)).toBe(true); + }); + + it('validate with both)', () => { + expect(schemaWithBoth.isValidSync(submitDataWithBilling)).toBe(true); + expect(schemaWithBoth.isValidSync(submitDataWithShipping)).toBe(false); + }); + + it('validate with shipping & billing', () => { + expect(schemaWithBoth.isValidSync(submitDataWithBilling)).toBe(true); + }); + + it('not validate incorrect firstName', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + firstName: '' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + firstName: '1' + }) + ).toBe(false); + }); + + it('not validate incorrect lastName', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + lastName: '' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + lastName: '1' + }) + ).toBe(false); + }); + + it('not validate incorrect password', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + password: '' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + password: 'fd1@fS' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + password: 'fd@fSsgsg' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + password: 'fd@fs12gsg' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + password: 'fdDfs12gsg' + }) + ).toBe(false); + }); + + it('not validate incorrect email', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + email: '' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + email: 'fd1dsfS' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + email: 'fd1@fS' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + email: 'fd1@fS.' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + email: 'fd1@fS.d' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + email: 'fd1@fS.dfdff' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + email: '@fS.dds' + }) + ).toBe(false); + }); + + it('not validate incorrect date', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + date: '' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + date: '2012-01-01' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithBilling, + date: '21-11-2000' + }) + ).toBe(false); + }); + + it('not validate incorrect country', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithShipping, + shippingCountry: '' + }) + ).toBe(false); + expect( + schemaWithBoth.isValidSync({ + ...submitDataWithBilling, + billingCountry: '' + }) + ).toBe(false); + }); + + it('not validate incorrect city', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithShipping, + shippingCity: '' + }) + ).toBe(false); + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithShipping, + shippingCity: '1' + }) + ).toBe(false); + expect( + schemaWithBoth.isValidSync({ + ...submitDataWithBilling, + billingCity: '' + }) + ).toBe(false); + }); + + it('not validate incorrect postalCode', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithShipping, + shippingPostalCode: '' + }) + ).toBe(false); + expect( + schemaWithBoth.isValidSync({ + ...submitDataWithBilling, + billingPostalCode: '' + }) + ).toBe(false); + }); + + it('not validate incorrect streetName', () => { + expect( + schemaWithShipping.isValidSync({ + ...submitDataWithShipping, + shippingStreetName: '' + }) + ).toBe(false); + expect( + schemaWithBoth.isValidSync({ + ...submitDataWithBilling, + billingStreetName: '' + }) + ).toBe(false); + }); +}); diff --git a/src/assets/404.webp b/src/assets/404.webp new file mode 100644 index 0000000..04dc299 Binary files /dev/null and b/src/assets/404.webp differ diff --git a/src/assets/burger-bender.webp b/src/assets/burger-bender.webp new file mode 100644 index 0000000..08e4c87 Binary files /dev/null and b/src/assets/burger-bender.webp differ diff --git a/src/assets/cart-bender.webp b/src/assets/cart-bender.webp new file mode 100644 index 0000000..1ef703c Binary files /dev/null and b/src/assets/cart-bender.webp differ diff --git a/src/assets/footer-bender.webp b/src/assets/footer-bender.webp new file mode 100644 index 0000000..46f5e8d Binary files /dev/null and b/src/assets/footer-bender.webp differ diff --git a/src/assets/index.d.ts b/src/assets/index.d.ts new file mode 100644 index 0000000..efa5e0d --- /dev/null +++ b/src/assets/index.d.ts @@ -0,0 +1,2 @@ +declare module '*.png'; +declare module '*.webp'; diff --git a/src/assets/logo.webp b/src/assets/logo.webp new file mode 100644 index 0000000..155d5d3 Binary files /dev/null and b/src/assets/logo.webp differ diff --git a/src/assets/main.webp b/src/assets/main.webp new file mode 100644 index 0000000..eb31f37 Binary files /dev/null and b/src/assets/main.webp differ diff --git a/src/assets/no-view.svg b/src/assets/no-view.svg new file mode 100644 index 0000000..6e78dce --- /dev/null +++ b/src/assets/no-view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/view.svg b/src/assets/view.svg new file mode 100644 index 0000000..f8e4e19 --- /dev/null +++ b/src/assets/view.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/AnonymOutlet.tsx b/src/components/AnonymOutlet.tsx new file mode 100644 index 0000000..14c8b04 --- /dev/null +++ b/src/components/AnonymOutlet.tsx @@ -0,0 +1,10 @@ +import { NavRoutes } from '../utils/routes'; +import { Navigate } from 'react-router-dom'; +import type { OutletProps, RootState } from '../utils/types'; +import { useSelector } from 'react-redux'; + +export const AnonymOutlet = ({ children }: OutletProps) => { + const isLogged = useSelector((state: { authData: RootState }) => state.authData.isLogged); + + return isLogged ? children : ; +}; diff --git a/src/components/App.tsx b/src/components/App.tsx new file mode 100644 index 0000000..919ce46 --- /dev/null +++ b/src/components/App.tsx @@ -0,0 +1,38 @@ +import { Routes, Route, BrowserRouter as Router } from 'react-router-dom'; +import { routesConfig } from '../utils/routesConfig'; +import { Header } from './header/header'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import { Footer } from './footer/Footer'; + +const theme = createTheme({ + palette: { + secondary: { + main: '#DB4444' + }, + primary: { + main: '#000000' + } + } +}); + +export const App = () => { + return ( + + +
+
+ + {routesConfig.map((route, index) => ( + + ))} + +
+