├── cypress.json ├── .dockerignore ├── src ├── react-app-env.d.ts ├── tests │ └── setupTests.ts ├── constants │ ├── api-url.ts │ ├── navigation.ts │ ├── validation.ts │ ├── placeholder.ts │ ├── types-reducers.ts │ └── local-storage.ts ├── assets │ ├── check.png │ └── shopping-cart.png ├── services │ ├── history.ts │ └── api.js ├── setupTests.ts ├── styles │ ├── styles.ts │ ├── themes │ │ └── default.js │ └── global.ts ├── helpers │ ├── formatCurrency.ts │ ├── local-storage.ts │ ├── validations.ts │ ├── set-items-quantity.ts │ └── masks.ts ├── components │ ├── Loading │ │ ├── index.tsx │ │ └── styles.ts │ ├── EmptyMessage │ │ ├── index.tsx │ │ ├── EmptyMessage.spec.tsx │ │ └── styles.ts │ ├── Button │ │ ├── Button.spec.tsx │ │ ├── index.tsx │ │ └── styles.ts │ ├── Tooltip │ │ ├── index.tsx │ │ └── styles.ts │ ├── PaymentInfo │ │ ├── index.tsx │ │ └── styles.ts │ ├── SumInfo │ │ ├── styles.ts │ │ └── index.tsx │ ├── Toast │ │ ├── styles.ts │ │ └── index.tsx │ ├── Input │ │ ├── styles.ts │ │ └── index.tsx │ └── ItemsList │ │ ├── styles.ts │ │ └── index.tsx ├── index.tsx ├── pages │ ├── Cart │ │ ├── styles.ts │ │ ├── index.tsx │ │ └── components │ │ │ └── Header │ │ │ ├── styles.ts │ │ │ └── index.tsx │ ├── CartList │ │ ├── styles.ts │ │ └── index.tsx │ ├── Home │ │ ├── components │ │ │ └── Header │ │ │ │ ├── index.tsx │ │ │ │ └── styles.ts │ │ ├── styles.ts │ │ └── index.tsx │ ├── CartConfirmation │ │ ├── styles.ts │ │ └── index.tsx │ └── CartPayment │ │ ├── styles.ts │ │ └── index.tsx ├── interfaces │ ├── ToastInterface.ts │ └── Cart.ts ├── providers │ └── AppProvider.tsx ├── mappers │ └── cart-mapper.ts ├── App.tsx ├── hooks │ ├── useToast.tsx │ └── useCart.tsx └── mocks │ └── cart-mock.json ├── commitlint.config.js ├── public ├── robots.txt ├── favicon.ico ├── manifest.json └── index.html ├── readme └── Home.gif ├── .husky └── commit-msg ├── .env ├── .prettierrc ├── babel.config.js ├── jest.config.js ├── .gitignore ├── .prettierignore ├── .editorconfig ├── Dockerfile ├── .github └── workflows │ └── ci.yml ├── docker-compose.yml ├── tsconfig.json ├── .eslintrc.json ├── 404.html ├── package.json ├── README.md └── .eslintcache /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/tests/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /readme/Home.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxwebster/computer-shop-react/HEAD/readme/Home.gif -------------------------------------------------------------------------------- /src/constants/api-url.ts: -------------------------------------------------------------------------------- 1 | export const API_URL_CART = '2756bcf4-0cb3-4009-aeea-0cac0ed0fdfe'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxwebster/computer-shop-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_ENVIRONMENT = "development" 2 | NODE_ENV = "development" 3 | ENVIRONMENT = "development" 4 | -------------------------------------------------------------------------------- /src/assets/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxwebster/computer-shop-react/HEAD/src/assets/check.png -------------------------------------------------------------------------------- /src/assets/shopping-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxwebster/computer-shop-react/HEAD/src/assets/shopping-cart.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 100, 5 | "endOfLine": "lf" 6 | } 7 | -------------------------------------------------------------------------------- /src/services/history.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | const history = createBrowserHistory(); 4 | 5 | export default history; 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | module.exports = { 3 | presets: ['babel-preset-env', 'babel-preset-react', 'babel-preset-typescript'] 4 | }; 5 | -------------------------------------------------------------------------------- /src/constants/navigation.ts: -------------------------------------------------------------------------------- 1 | export const NAV_TITLE_CART = 'sacola'; 2 | export const NAV_TITLE_PAYMENT = 'pagamento'; 3 | export const NAV_TITLE_CONFIRMATION = 'confirmação'; 4 | -------------------------------------------------------------------------------- /src/constants/validation.ts: -------------------------------------------------------------------------------- 1 | export const INPUT_ERROR = 'isErrored'; 2 | export const INPUT_FILLED = 'isFilled'; 3 | export const INPUT_FOCUSED = 'isFocused'; 4 | export const ICON_ERROR_COLOR = '#F30'; 5 | -------------------------------------------------------------------------------- /src/constants/placeholder.ts: -------------------------------------------------------------------------------- 1 | export const CARD_NUMBER_PLACEHOLDER = '_ _ _ _._ _ _ _._ _ _ _._ _ _ _'; 2 | export const TITULAR_NAME_PLACEHOLDER = 'Como no cartão'; 3 | export const DATE_PLACEHOLDER = '_ _/_ _ _ _'; 4 | export const CVV_PLACEHOLDER = '_ _ _'; 5 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/styles/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const AppContainer = styled.main` 4 | width: 100%; 5 | height: 100vh; 6 | margin: 0 auto; 7 | 8 | @media (min-width: 40rem) { 9 | width: 80%; 10 | margin: 0 auto; 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/helpers/formatCurrency.ts: -------------------------------------------------------------------------------- 1 | const formatCurrency = (decimal: any) => { 2 | const formatter = new Intl.NumberFormat('pt-BR', { 3 | style: 'currency', 4 | currency: 'BRL' 5 | }); 6 | 7 | decimal = formatter.format(decimal); 8 | return decimal; 9 | }; 10 | 11 | export default formatCurrency; 12 | -------------------------------------------------------------------------------- /src/constants/types-reducers.ts: -------------------------------------------------------------------------------- 1 | export const TYPE_CART_REQUEST = '@cart/CART_REQUEST'; 2 | export const TYPE_CART_SUCCESS = '@cart/CART_SUCCESS'; 3 | export const TYPE_CART_FAILURE = '@cart/CART_FAILURE'; 4 | export const TYPE_CART_LOADING = '@cart/CART_LOADING'; 5 | 6 | export const TYPE_CART_CREDIT_CARD_INFO = '@cart/CREDIT_CARD_INFO'; 7 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from './styles'; 2 | 3 | export default function Loading() { 4 | return ( 5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/constants/local-storage.ts: -------------------------------------------------------------------------------- 1 | export const BELEZA_NA_WEB_ALL_ITEMS = '@BelezaNaWeb: allItems'; 2 | export const BELEZA_NA_WEB_CART = '@BelezaNaWeb: cart'; 3 | export const BELEZA_NA_WEB_CART_ITEMS = '@BelezaNaWeb: cartItems'; 4 | export const BELEZA_NA_WEB_CREDIT_CARD = '@BelezaNaWeb: creditCard'; 5 | export const BELEZA_NA_WEB_SUM_INFO = '@BelezaNaWeb: sumInfo'; 6 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | 8 | render( 9 | 10 | 11 | , 12 | rootElement 13 | ); 14 | -------------------------------------------------------------------------------- /src/components/EmptyMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { Container } from './styles'; 5 | 6 | export default function EmptyMessage() { 7 | return ( 8 | 9 | Não há itens no carrinho 10 |
11 | Ver Produtos 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/Cart/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | gap: 2rem; 8 | `; 9 | 10 | export const MainContent = styled.div` 11 | width: 100%; 12 | height: calc(100vh - 37.5px); 13 | background-color: ${(props) => props.theme.background}; 14 | padding: 1rem; 15 | `; 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | module.exports = { 3 | testPathIgnorePatterns: ['/node_modules/'], 4 | setupFilesAfterEnv: ['/src/tests/setupTests.ts'], 5 | testEnvironment: 'jsdom', 6 | transform: { 7 | '^.+\\.(js|jsx|mjs|cjs|ts|tsx)$': '/node_modules/babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^.+\\.(css|sass|scss)$': 'identity-obj-proxy' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/pages/Cart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | import Header from './components/Header'; 5 | 6 | import { Container, MainContent } from './styles'; 7 | 8 | export default function Cart() { 9 | return ( 10 | 11 |
12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/helpers/local-storage.ts: -------------------------------------------------------------------------------- 1 | export const getFromLocalStorage = (title: string) => { 2 | const storage = localStorage.getItem(title); 3 | if (storage) return JSON.parse(storage); 4 | return null; 5 | }; 6 | 7 | export const setToLocalStorage = (title: string, value: any) => { 8 | localStorage.setItem(title, JSON.stringify(value)); 9 | }; 10 | 11 | export const cleanLocalStorage = () => { 12 | localStorage.clear(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/Button/Button.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import Button from '.'; 4 | 5 | describe('Button Component', () => { 6 | it('should renders correctly', () => { 7 | render(, { wrapper: BrowserRouter }); 8 | 9 | expect(screen.getByText('Olá')).toBeInTheDocument(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/EmptyMessage/EmptyMessage.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import EmptyMessage from '.'; 4 | 5 | describe('EmptyMessage Component', () => { 6 | it('should renders correctly', () => { 7 | render(, { wrapper: BrowserRouter }); 8 | 9 | expect(screen.getByTestId('empty-message')).toBeInTheDocument(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/EmptyMessage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | gap: 2rem; 8 | 9 | text-transform: uppercase; 10 | color: ${(props) => props.theme.title}; 11 | font-size: 1.4rem; 12 | font-weight: bold; 13 | 14 | a { 15 | text-decoration: underline; 16 | color: ${(props) => props.theme.primary}; 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /src/helpers/validations.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from 'yup'; 2 | 3 | interface Errors { 4 | [key: string]: string; 5 | } 6 | 7 | export default function getValidationError(err: ValidationError): Errors { 8 | const validationErrors: Errors = {}; 9 | 10 | err.inner.forEach((error) => { 11 | if (typeof error.path !== 'undefined') { 12 | validationErrors[error.path] = error.message; 13 | } 14 | }); 15 | return validationErrors; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/themes/default.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | primary: '#F93943', 3 | primaryDark: '#bb1119', 4 | white: '#FCFDFF', 5 | black: '#232129', 6 | grey: '#F0F0F7', 7 | 8 | text: '#212122', 9 | title: '#999', 10 | disabled: '#CCC', 11 | placeholder: '#E0E7EE', 12 | 13 | focus: '#A43287', 14 | error: '#F30', 15 | success: '#04d361', 16 | 17 | shadow: 'rgba(0,0,29,0.22)', 18 | background: '#EEE', 19 | border: '#E7E7E7' 20 | }; 21 | 22 | export default theme; 23 | -------------------------------------------------------------------------------- /src/interfaces/ToastInterface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | export interface ToastMessageState { 3 | id: string; 4 | type?: 'success' | 'error' | 'info'; 5 | title: string; 6 | description?: string; 7 | } 8 | 9 | export interface ToastMessage { 10 | type?: 'success' | 'error' | 'info'; 11 | title: string; 12 | description?: string; 13 | } 14 | 15 | export interface ToastContextData { 16 | addToast(message: ToastMessage): void; 17 | removeToast(id: string): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/require-default-props */ 2 | import React from 'react'; 3 | 4 | import { Container } from './styles'; 5 | 6 | interface TooltipProps { 7 | title: string; 8 | className?: string; 9 | children: React.ReactNode; 10 | } 11 | 12 | function Tooltip({ title, className, children }: TooltipProps) { 13 | return ( 14 | 15 | {children} 16 | {title} 17 | 18 | ); 19 | } 20 | 21 | export default Tooltip; 22 | -------------------------------------------------------------------------------- /src/providers/AppProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ThemeProvider } from 'styled-components'; 4 | 5 | import theme from '../styles/themes/default'; 6 | 7 | import { CartProvider } from '../hooks/useCart'; 8 | import { ToastProvider } from '../hooks/useToast'; 9 | 10 | const AppProvider = ({ children }: any) => ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | 18 | export default AppProvider; 19 | -------------------------------------------------------------------------------- /src/interfaces/Cart.ts: -------------------------------------------------------------------------------- 1 | export interface Cart { 2 | id: string; 3 | items: CartItem[]; 4 | subTotal: number; 5 | shippingTotal: number; 6 | discount: number; 7 | total: number; 8 | } 9 | 10 | export interface CartItem { 11 | quantity: number; 12 | product: { 13 | sku: string; 14 | name: string; 15 | imageObjects: { 16 | small: string; 17 | medium: string; 18 | }[]; 19 | priceSpecification: { 20 | sku: string; 21 | price: number; 22 | discount: number; 23 | }; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /src/helpers/set-items-quantity.ts: -------------------------------------------------------------------------------- 1 | interface CartItemsQuantity { 2 | [key: string]: number; 3 | } 4 | 5 | export default function setCartItemsQuantity(cartItems: any, productSku: any) { 6 | const itemsQuantity = cartItems?.reduce((itemsQuantity: any, item: any) => { 7 | const itemsQuantityObj = { ...itemsQuantity }; 8 | itemsQuantityObj[item.product.sku] = item.quantity; 9 | 10 | return itemsQuantityObj; 11 | }, {} as CartItemsQuantity); 12 | 13 | const quantity = itemsQuantity ? itemsQuantity[productSku] : 0; 14 | 15 | return quantity; 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | # 4 space indentation 23 | [*.html] 24 | indent_style = space 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Imagem de Origem 2 | FROM node:14-alpine 3 | 4 | # Instala pacotes no container alpine 5 | RUN apk update && apk add --no-cache curl vim wget bash 6 | 7 | # Diretório de trabalho(é onde a aplicação ficará dentro do container). 8 | WORKDIR /app 9 | 10 | # Adicionando `/app/node_modules/.bin` para o $PATH 11 | ENV PATH /app/node_modules/.bin:$PATH 12 | 13 | # Instalando dependências da aplicação e armazenando em cache. 14 | COPY package.json /app/package.json 15 | RUN yarn --silent 16 | 17 | # expondo porta que vai rodar 18 | EXPOSE 3000 19 | 20 | # Inicializa a aplicação 21 | CMD ["yarn", "start"] 22 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'https://run.mocky.io/v3/' 5 | }); 6 | 7 | api.registerInterceptWithStore = (store) => { 8 | api.interceptors.response.use( 9 | (response) => { 10 | const { data } = response; 11 | if (data && !data.success && (data.httpStatusCode === 403 || data.httpStatusCode === 401)) 12 | alert('SignOut'); 13 | return response; 14 | }, 15 | (err) => { 16 | if (err.response.status === 403 || err.response.status === 401) alert('SignOut'); 17 | return err; 18 | } 19 | ); 20 | }; 21 | 22 | export default api; 23 | -------------------------------------------------------------------------------- /src/components/PaymentInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container } from './styles'; 4 | 5 | interface PaymentInfoProps { 6 | creditCardInfo: any; 7 | } 8 | 9 | export default function PaymentInfo({ creditCardInfo }: PaymentInfoProps) { 10 | return ( 11 | 12 |

Pagamento

13 | 14 | {creditCardInfo && ( 15 |
16 |
  • {creditCardInfo.number.replace(/(?!(?:\D*\d){14}$|(?:\D*\d){1,3}$)\d/gm, '#')}
  • 17 |
  • {creditCardInfo.name}
  • 18 |
  • {creditCardInfo.expiry}
  • 19 |
    20 | )} 21 |
    22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build-test-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Set-up Node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: '14.x' 17 | - run: yarn 18 | - run: yarn build 19 | - name: Deploy 20 | uses: crazy-max/ghaction-github-pages@v1 21 | with: 22 | target_branch: gh-pages 23 | build_dir: build 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | web: 4 | container_name: computershopreact 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - '.:/web' 10 | - '/web/node_modules' 11 | ports: 12 | - '3000:3000' 13 | environment: 14 | - NODE_ENV=development 15 | 16 | # https://www.cypress.io/blog/2019/05/02/run-cypress-with-a-single-docker-command/ 17 | cypress: 18 | image: "cypress/included:3.2.0" 19 | depends_on: 20 | - web 21 | environment: 22 | - CYPRESS_baseUrl=http://web:3000 23 | working_dir: /e2e 24 | volumes: 25 | - ./:/e2e 26 | 27 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes, ReactNode } from 'react'; 2 | import { Container } from './styles'; 3 | 4 | interface ButtonProps extends ButtonHTMLAttributes { 5 | children: ReactNode; 6 | width?: string; 7 | isProgressive: boolean; 8 | } 9 | 10 | export default function Button({ children, width = '100%', isProgressive, ...props }: ButtonProps) { 11 | return ( 12 | 18 | {!isProgressive ? children : 'Finalizando...'} 19 |
    20 |
    21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/SumInfo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | height: 15rem; 5 | display: flex; 6 | flex-direction: column; 7 | gap: 1rem; 8 | padding: 1.5rem; 9 | 10 | border: 1px solid #ccc; 11 | border-radius: 0.3rem; 12 | 13 | @media (min-width: 48rem) { 14 | grid-area: sumInfo; 15 | margin-top: 3rem; 16 | } 17 | 18 | li { 19 | font-size: 1.4rem; 20 | line-height: 1.7rem; 21 | color: ${(props) => props.theme.text}; 22 | text-transform: uppercase; 23 | 24 | display: flex; 25 | justify-content: space-between; 26 | 27 | &.discount { 28 | color: ${(props) => props.theme.primary}; 29 | } 30 | 31 | &.total { 32 | font-weight: bold; 33 | } 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /src/pages/Cart/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.header` 4 | width: 100%; 5 | background-color: ${(props) => props.theme.white}; 6 | box-shadow: 0.1rem 0.1rem 0.5rem 0 ${(props) => props.theme.shadow}; 7 | 8 | nav { 9 | display: flex; 10 | align-items: center; 11 | justify-content: space-around; 12 | padding: 1.2rem; 13 | 14 | button { 15 | font-size: 1.3rem; 16 | font-weight: bold; 17 | line-height: 1.6rem; 18 | text-transform: uppercase; 19 | color: ${(props) => props.theme.disabled}; 20 | background-color: ${(props) => props.theme.white}; 21 | } 22 | 23 | button.active { 24 | color: ${(props) => props.theme.primary}; 25 | } 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /src/pages/CartList/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | width: 100%; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | `; 9 | 10 | export const Content = styled.section` 11 | width: 100%; 12 | display: flex; 13 | flex-direction: column; 14 | gap: 2rem; 15 | 16 | @media (min-width: 48rem) { 17 | display: grid; 18 | grid-template-columns: 70% 1fr; 19 | grid-template-rows: 17rem 1fr; 20 | gap: 2rem; 21 | 22 | grid-template-areas: 23 | 'list sumInfo' 24 | 'list button'; 25 | } 26 | 27 | h2 { 28 | text-transform: uppercase; 29 | color: ${(props) => props.theme.title}; 30 | font-size: 1.4rem; 31 | font-weight: bold; 32 | margin: 1rem 0 0.5rem; 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": ["./node_modules/@types", "./src/types"], 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": [ 21 | "src/**/*.ts" // *** The files TypeScript should type check *** 22 | , "src/assets/SvgLoading.js", "src/index.tsx", "src/App.tsx" ], 23 | "exclude": ["node_modules", "dist", "src/server/**/*"], // *** The files to not type check *** 24 | "types": ["node"] 25 | } 26 | -------------------------------------------------------------------------------- /src/mappers/cart-mapper.ts: -------------------------------------------------------------------------------- 1 | import { Cart, CartItem } from '../interfaces/Cart'; 2 | 3 | export default function cartMapper(cart: Cart) { 4 | const itemsWrapper: CartItem[] = cart.items.map((item: CartItem) => { 5 | return { 6 | quantity: item.quantity, 7 | product: { 8 | sku: item.product.sku, 9 | name: item.product.name, 10 | imageObjects: [ 11 | { 12 | small: item.product.imageObjects[0].small, 13 | medium: item.product.imageObjects[0].medium 14 | } 15 | ], 16 | priceSpecification: { 17 | sku: item.product.priceSpecification.sku, 18 | price: item.product.priceSpecification.price, 19 | discount: item.product.priceSpecification.discount 20 | } 21 | } 22 | }; 23 | }); 24 | 25 | const dataWrapper = { 26 | id: cart.id, 27 | shippingTotal: cart.shippingTotal, 28 | items: itemsWrapper 29 | }; 30 | 31 | return dataWrapper; 32 | } 33 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "https://res.cloudinary.com/beleza-na-web/image/upload/f_png,w_57,h_57,fl_progressive,q_auto:eco/v1/blz/assets-store/0.0.46/images/store/1/icon.svg", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "https://res.cloudinary.com/beleza-na-web/image/upload/f_png,w_192,h_192,fl_progressive,q_auto:eco/v1/blz/assets-store/0.0.46/images/store/1/icon.svg", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "https://res.cloudinary.com/beleza-na-web/image/upload/f_png,w_512,h_512,fl_progressive,q_auto:eco/v1/blz/assets-store/0.0.46/images/store/1/icon.svg", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Tooltip/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | position: relative; 5 | z-index: 9999; 6 | 7 | span { 8 | background: white; 9 | border-radius: 4px; 10 | 11 | bottom: calc(100% + 12px); 12 | color: #312e38; 13 | 14 | font-size: 1rem; 15 | letter-spacing: 0.5px; 16 | font-weight: 500; 17 | 18 | left: 50%; 19 | opacity: 0; 20 | padding: 8px; 21 | 22 | position: absolute; 23 | transform: translateX(-50%); 24 | transition: all 0.2s; 25 | visibility: hidden; 26 | min-width: 5rem; 27 | 28 | white-space: nowrap; 29 | 30 | &::before { 31 | content: ''; 32 | border-color: white transparent; 33 | border-style: solid; 34 | border-width: 6px 6px 0 6px; 35 | left: 50%; 36 | position: absolute; 37 | top: 100%; 38 | transform: translateX(-50%); 39 | } 40 | } 41 | 42 | &:hover span { 43 | opacity: 1; 44 | visibility: visible; 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/pages/CartList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router'; 3 | 4 | import Button from '../../components/Button'; 5 | import SumInfo from '../../components/SumInfo'; 6 | import ItemsList from '../../components/ItemsList'; 7 | import EmptyMessage from '../../components/EmptyMessage'; 8 | 9 | import { Container, Content } from './styles'; 10 | import { useCart } from '../../hooks/useCart'; 11 | 12 | export default function CartList() { 13 | const { cartItems } = useCart(); 14 | 15 | const navigate = useNavigate(); 16 | 17 | return ( 18 | 19 | {cartItems?.length > 0 ? ( 20 | 21 | 22 | 23 | 29 | 30 | ) : ( 31 | 32 | )} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/SumInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useCart } from '../../hooks/useCart'; 4 | import { Container } from './styles'; 5 | import EmptyMessage from '../../components/EmptyMessage'; 6 | import formatCurrency from '../../helpers/formatCurrency'; 7 | 8 | export default function SumInfo() { 9 | const { cartItems, sumInfo } = useCart(); 10 | 11 | return ( 12 | 13 | {cartItems?.length > 0 ? ( 14 | <> 15 |
  • 16 | Produtos {formatCurrency(sumInfo.itemsSubTotal)} 17 |
  • 18 |
  • 19 | Frete {formatCurrency(sumInfo.shippingTotal)} 20 |
  • 21 |
  • 22 | Desconto -{formatCurrency(sumInfo.itemsDiscount)} 23 |
  • 24 |
  • 25 | Total {formatCurrency(sumInfo.itemsTotal)} 26 |
  • 27 | 28 | ) : ( 29 | 30 | )} 31 |
    32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Loading/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | width: 100%; 5 | height: 50rem; 6 | 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | .lds-ring { 12 | display: inline-block; 13 | position: relative; 14 | width: 80px; 15 | height: 80px; 16 | } 17 | .lds-ring div { 18 | box-sizing: border-box; 19 | display: block; 20 | position: absolute; 21 | width: 64px; 22 | height: 64px; 23 | margin: 8px; 24 | border: 8px solid ${(props) => props.theme.primary}; 25 | border-radius: 50%; 26 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 27 | border-color: ${(props) => props.theme.primary} transparent transparent transparent; 28 | } 29 | .lds-ring div:nth-child(1) { 30 | animation-delay: -0.45s; 31 | } 32 | .lds-ring div:nth-child(2) { 33 | animation-delay: -0.3s; 34 | } 35 | .lds-ring div:nth-child(3) { 36 | animation-delay: -0.15s; 37 | } 38 | @keyframes lds-ring { 39 | 0% { 40 | transform: rotate(0deg); 41 | } 42 | 100% { 43 | transform: rotate(360deg); 44 | } 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /src/pages/Home/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { MdShoppingBasket } from 'react-icons/md'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | import { Header, CartInfo } from './styles'; 6 | import Logo from '../../../../assets/shopping-cart.png'; 7 | 8 | import { useCart } from '../../../../hooks/useCart'; 9 | 10 | export default function Home() { 11 | const { cartItems } = useCart(); 12 | 13 | const [cartQtd, setCartQtd] = useState(0); 14 | 15 | useEffect(() => { 16 | let qtd = 0; 17 | cartItems.forEach((items) => (qtd += items.quantity)); 18 | setCartQtd(qtd); 19 | }, [cartItems]); 20 | 21 | return ( 22 |
    23 | 24 |

    25 |
    26 | 27 |
    28 |
    29 | /Computer 30 | Shop/ 31 |
    32 |

    33 | 34 | 35 | 0 ? '/cart' : '/'}> 36 |

    Meu carrinho

    37 |
    38 | {cartQtd} 39 | 40 |
    41 |
    42 |
    43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRoutes } from 'react-router-dom'; 3 | 4 | import Home from './pages/Home'; 5 | import Cart from './pages/Cart'; 6 | 7 | import CartList from './pages/CartList'; 8 | import CartPayment from './pages/CartPayment'; 9 | import CartConfirmation from './pages/CartConfirmation'; 10 | 11 | import AppProvider from './providers/AppProvider'; 12 | 13 | import GlobalStyle from './styles/global'; 14 | import { AppContainer } from './styles/styles'; 15 | 16 | function App() { 17 | console.info(`==> 🌎 Você está no modo ${process.env.NODE_ENV}`); 18 | console.info(`==> 🌎 Você está no ambiente ${process.env.REACT_APP_ENVIRONMENT}`); 19 | 20 | const mainRoutes = { 21 | path: '/', 22 | element: 23 | }; 24 | 25 | const cartRoutes = { 26 | path: 'cart/*', 27 | element: , 28 | children: [ 29 | { path: '*', element: }, 30 | { path: 'payment', element: }, 31 | { path: 'confirmation', element: } 32 | ] 33 | }; 34 | 35 | const routing = useRoutes([mainRoutes, cartRoutes]); 36 | 37 | return ( 38 | 39 | 40 | {routing} 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /src/helpers/masks.ts: -------------------------------------------------------------------------------- 1 | export function creditCardMask(e: React.FormEvent) { 2 | e.currentTarget.maxLength = 19; 3 | let value = e.currentTarget.value; 4 | 5 | value = value.replace(/\D/g, ''); 6 | value = value.replace(/(\d{4})/g, '$1 '); 7 | value = value.replace(/\.$/, ''); 8 | value = value.substring(0, 19); 9 | e.currentTarget.value = value; 10 | 11 | return e; 12 | } 13 | 14 | export function titularNameMask(e: React.FormEvent) { 15 | e.currentTarget.maxLength = 20; 16 | let value = e.currentTarget.value; 17 | 18 | value = value.replace(/[0-9!@#¨$%^&*)(+=._-]+/g, ''); 19 | e.currentTarget.value = value; 20 | 21 | return e; 22 | } 23 | 24 | export function dateMask(e: React.FormEvent) { 25 | e.currentTarget.maxLength = 7; 26 | let value = e.currentTarget.value; 27 | 28 | value = value.replace(/\D/g, ''); 29 | value = value.replace(/(\d{2})(\d)/, '$1/$2'); 30 | value = value.replace(/(\d{2})(\d)/, '$1'); 31 | e.currentTarget.value = value; 32 | 33 | return e; 34 | } 35 | 36 | export function cvvMask(e: React.FormEvent) { 37 | e.currentTarget.maxLength = 3; 38 | let value = e.currentTarget.value; 39 | 40 | value = value.replace(/\D/g, ''); 41 | value = value.replace(/(\d{3})(\d)/, '$1'); 42 | e.currentTarget.value = value; 43 | 44 | return e; 45 | } 46 | -------------------------------------------------------------------------------- /src/hooks/useToast.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useCallback, useState, useContext } from 'react'; 2 | import { v4 } from 'uuid'; 3 | import Toast from '../components/Toast'; 4 | 5 | import { ToastMessageState, ToastMessage, ToastContextData } from '../interfaces/ToastInterface'; 6 | 7 | const ToastContext = createContext({} as ToastContextData); 8 | 9 | function ToastProvider({ children }: any) { 10 | const [messages, setMessages] = useState([]); 11 | 12 | const addToast = ({ type, title, description }: ToastMessage) => { 13 | const toast = { 14 | id: v4(), 15 | type, 16 | title, 17 | description 18 | }; 19 | 20 | setMessages((state) => [...state, toast]); 21 | }; 22 | 23 | const removeToast = useCallback((id: string) => { 24 | setMessages((state) => state.filter((message) => message.id !== id)); 25 | }, []); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | function useToast(): ToastContextData { 37 | const context = useContext(ToastContext); 38 | 39 | if (!context) { 40 | throw new Error('useToast must be used within a ToastPRovider'); 41 | } 42 | 43 | return context; 44 | } 45 | 46 | export { ToastProvider, useToast }; 47 | -------------------------------------------------------------------------------- /src/pages/Home/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | width: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | gap: 2rem; 8 | `; 9 | 10 | export const ProductList = styled.ul` 11 | display: flex; 12 | flex-direction: column; 13 | gap: 2rem; 14 | margin: 2rem; 15 | 16 | @media (min-width: 40rem) { 17 | display: grid; 18 | grid-template-columns: repeat(2, 1fr); 19 | grid-gap: 2rem; 20 | list-style: none; 21 | } 22 | 23 | @media (min-width: 66rem) { 24 | display: grid; 25 | grid-template-columns: repeat(3, 1fr); 26 | grid-gap: 2rem; 27 | list-style: none; 28 | margin: 0; 29 | } 30 | 31 | li { 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: space-between; 35 | align-items: center; 36 | gap: 2rem; 37 | 38 | padding: 2rem; 39 | 40 | background: #fff; 41 | border-radius: 0.4rem; 42 | box-shadow: 0.1rem 0.1rem 0.5rem 0 ${(props) => props.theme.shadow}; 43 | 44 | img { 45 | align-self: center; 46 | max-width: 25rem; 47 | } 48 | 49 | > p { 50 | font-size: 1.4rem; 51 | color: #333; 52 | margin-top: 5px; 53 | } 54 | 55 | > span { 56 | font-size: 2rem; 57 | font-weight: bold; 58 | margin: 1rem 0 1rem; 59 | } 60 | } 61 | `; 62 | 63 | export const StockCounter = styled.div` 64 | font-size: 1.2rem; 65 | `; 66 | -------------------------------------------------------------------------------- /src/components/Button/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ButtonDefault } from '../../styles/global'; 3 | 4 | export const Container = styled(ButtonDefault)` 5 | &.progress-btn { 6 | position: relative; 7 | display: inline-block; 8 | transition: all 0.4s ease; 9 | } 10 | 11 | &.progress-btn:not(.active) { 12 | cursor: pointer; 13 | } 14 | 15 | &.progress-btn span { 16 | position: absolute; 17 | left: 0; 18 | top: 0; 19 | right: 0; 20 | bottom: 0; 21 | line-height: 50px; 22 | text-align: center; 23 | z-index: 10; 24 | opacity: 1; 25 | } 26 | 27 | &.progress-btn .progress { 28 | width: 0%; 29 | z-index: 5; 30 | background: #d32f2f; 31 | opacity: 0; 32 | transition: all 0.3s ease; 33 | } 34 | 35 | &.progress-btn.active .progress { 36 | opacity: 1; 37 | animation: progress-anim 5s ease 0s; 38 | } 39 | 40 | &.progress-btn[data-progress-style='fill-back'] .progress { 41 | position: absolute; 42 | left: 0; 43 | top: 0; 44 | right: 0; 45 | bottom: 0; 46 | } 47 | 48 | @keyframes progress-anim { 49 | 0% { 50 | width: 0%; 51 | } 52 | 5% { 53 | width: 0%; 54 | } 55 | 10% { 56 | width: 15%; 57 | } 58 | 30% { 59 | width: 40%; 60 | } 61 | 50% { 62 | width: 55%; 63 | } 64 | 80% { 65 | width: 100%; 66 | } 67 | 100% { 68 | width: 100%; 69 | } 70 | } 71 | `; 72 | -------------------------------------------------------------------------------- /src/components/PaymentInfo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | 7 | h2 { 8 | text-transform: uppercase; 9 | color: ${(props) => props.theme.title}; 10 | font-size: 1.4rem; 11 | font-weight: bold; 12 | 13 | margin: 1rem 0 0.5rem; 14 | } 15 | 16 | @media (min-width: 48rem) { 17 | grid-area: method; 18 | } 19 | 20 | div { 21 | background-color: white; 22 | box-shadow: 0.1rem 0.1rem 0.5rem 0 ${(props) => props.theme.shadow}; 23 | padding: 0.9rem; 24 | 25 | li { 26 | font-size: 1.4rem; 27 | line-height: 1.7rem; 28 | text-transform: uppercase; 29 | } 30 | } 31 | `; 32 | 33 | export const CheckConfirm = styled.div` 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | flex-direction: column; 38 | 39 | @media (min-width: 48rem) { 40 | flex-direction: row; 41 | gap: 2rem; 42 | margin-top: 2rem; 43 | } 44 | 45 | .check { 46 | width: 4rem; 47 | height: 4rem; 48 | 49 | border-radius: 50%; 50 | border: 1px solid ${(props) => props.theme.primary}; 51 | 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | } 56 | 57 | span { 58 | font-size: 1.4rem; 59 | line-height: 1.7rem; 60 | font-weight: bold; 61 | color: ${(props) => props.theme.primary}; 62 | text-transform: uppercase; 63 | margin-top: 1rem; 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /src/components/Toast/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css, keyframes } from 'styled-components'; 2 | import { animated } from 'react-spring'; 3 | 4 | interface ContainerProps { 5 | type?: 'success' | 'error' | 'info'; 6 | hasDescription?: number; 7 | } 8 | 9 | const toastTypeVariations = { 10 | info: css` 11 | background: #ebf8ff; 12 | color: #3172b7; 13 | `, 14 | success: css` 15 | background: #e6fffa; 16 | color: #2e656a; 17 | `, 18 | error: css` 19 | background: #fddede; 20 | color: #c53030; 21 | ` 22 | }; 23 | 24 | export const ToastContent = styled(animated.div)` 25 | width: 36rem; 26 | position: absolute; 27 | left: calc(50% - 16rem); 28 | 29 | padding: 1.6rem; 30 | display: flex; 31 | 32 | z-index: 99999; 33 | border-radius: 10px; 34 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2); 35 | 36 | & + div { 37 | margin-top: 8px; 38 | } 39 | 40 | ${(props) => toastTypeVariations[props.type || 'info']} 41 | 42 | > svg { 43 | margin: 4px 12px 0 0; 44 | } 45 | 46 | strong { 47 | font-size: 1.6rem; 48 | } 49 | 50 | div { 51 | flex: 1; 52 | 53 | p { 54 | margin-top: 4px; 55 | font-size: 1.6rem; 56 | opacity: 0.8; 57 | line-height: 20px; 58 | } 59 | } 60 | 61 | button { 62 | position: absolute; 63 | right: 16px; 64 | top: 19px; 65 | opacity: 0.6; 66 | border: 0; 67 | background: transparent; 68 | color: inherit; 69 | } 70 | 71 | ${(props) => 72 | !props.hasDescription && 73 | css` 74 | align-items: center; 75 | 76 | svg { 77 | margin-top: 0; 78 | } 79 | `} 80 | `; 81 | -------------------------------------------------------------------------------- /src/components/Toast/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { FiAlertCircle, FiCheckCircle, FiInfo, FiXCircle } from 'react-icons/fi'; 3 | import { useLocation } from 'react-router'; 4 | import { useTransition, animated, easings } from 'react-spring'; 5 | import { useToast } from '../../hooks/useToast'; 6 | import { ToastMessageState } from '../../interfaces/ToastInterface'; 7 | 8 | import { ToastContent } from './styles'; 9 | 10 | const icons = { 11 | info: , 12 | error: , 13 | success: 14 | }; 15 | 16 | interface ToastContainerProps { 17 | messages: ToastMessageState[]; 18 | } 19 | 20 | export default function Toast({ messages }: ToastContainerProps) { 21 | const { removeToast } = useToast(); 22 | 23 | useEffect(() => { 24 | messages.forEach((message: any) => { 25 | const messageId = message.id; 26 | setTimeout(() => removeToast(messageId), 2500); 27 | }); 28 | }, [removeToast, messages]); 29 | 30 | const messagesWithtransitions = useTransition(messages, { 31 | from: { top: '-20%', opacity: 0 }, 32 | enter: { top: '1rem', opacity: 1 }, 33 | leave: { top: '-20%', opacity: 0 }, 34 | config: { duration: 200, easing: easings.easeInOutQuart } 35 | }); 36 | 37 | return messagesWithtransitions((styles, message) => ( 38 | 39 | {icons.error} 40 |
    41 | {message.title} 42 | {message.description &&

    {message.description}

    } 43 |
    44 | 45 | 48 |
    49 | )); 50 | } 51 | -------------------------------------------------------------------------------- /src/pages/Home/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export const Header = styled.header` 5 | width: 100%; 6 | background-color: ${(props) => props.theme.primary}; 7 | box-shadow: 0.1rem 0.1rem 0.5rem 0 ${(props) => props.theme.shadow}; 8 | 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | 13 | margin-bottom: 5rem; 14 | padding: 1rem 2rem; 15 | border-radius: 0.3rem; 16 | 17 | a { 18 | color: ${(props) => props.theme.white}; 19 | transition: opacity 0.2s; 20 | 21 | h1 { 22 | font-size: 1.8rem; 23 | font-weight: bold; 24 | 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | gap: 1rem; 29 | 30 | img { 31 | color: white; 32 | width: 3rem; 33 | } 34 | 35 | i { 36 | color: ${(props) => props.theme.primaryDark}; 37 | } 38 | } 39 | } 40 | `; 41 | 42 | export const CartInfo = styled(Link)` 43 | display: flex; 44 | align-items: center; 45 | text-decoration: none; 46 | 47 | p { 48 | font-size: 1.2rem; 49 | margin-right: 1rem; 50 | } 51 | 52 | div { 53 | text-align: right; 54 | margin-right: 10px; 55 | position: relative; 56 | 57 | svg { 58 | color: black; 59 | } 60 | 61 | span { 62 | width: 2rem; 63 | height: 2rem; 64 | 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | 69 | border-radius: 50%; 70 | background-color: red; 71 | position: absolute; 72 | right: -1rem; 73 | 74 | font-size: 12px; 75 | color: ${(props) => props.theme.white}; 76 | } 77 | } 78 | `; 79 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | 7 | "parser": "@typescript-eslint/parser", 8 | 9 | "parserOptions": { 10 | "ecmaFeatures": { "jsx": true }, 11 | "ecmaVersion": 12 12 | }, 13 | 14 | "plugins": ["react", "import", "@typescript-eslint"], 15 | 16 | "extends": [ 17 | "eslint:recommended", 18 | "plugin:react/recommended", 19 | "plugin:import/recommended", 20 | "plugin:import/typescript", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:prettier/recommended" 23 | ], 24 | 25 | "settings": { 26 | "import/parsers": { 27 | "@typescript-eslint/parser": [".ts", ".tsx"] 28 | }, 29 | "import/resolver": { 30 | "typescript": { "alwaysTryTypes": true }, 31 | "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] }, 32 | "react": { "extensions": [".ts", ".tsx", ".js", ".jsx"] } 33 | }, 34 | "react": { 35 | "version": "detect" 36 | } 37 | }, 38 | 39 | "rules": { 40 | "prettier/prettier": "off", 41 | 42 | "no-unused-vars": "off", 43 | "no-empty-function": "warn", 44 | "no-case-declarations": "off", 45 | "no-console": "off", 46 | 47 | "@typescript-eslint/no-unused-vars": ["off"], 48 | "@typescript-eslint/no-empty-function": ["warn"], 49 | "@typescript-eslint/no-explicit-any": ["off"], 50 | "@typescript-eslint/explicit-module-boundary-types": ["off"], 51 | "@typescript-eslint/no-non-null-assertion": ["off"], 52 | 53 | "import/no-unresolved": "error", 54 | "import/namespace": "off", 55 | "import/no-named-as-default-member": "off", 56 | 57 | "react/prop-types": "off", 58 | "react/react-in-jsx-scope": "off", 59 | "react/jsx-one-expression-per-line": "off" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/pages/CartConfirmation/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | display: flex; 5 | flex-direction: column; 6 | gap: 2rem; 7 | 8 | h2 { 9 | text-transform: uppercase; 10 | color: ${(props) => props.theme.title}; 11 | font-size: 1.4rem; 12 | font-weight: bold; 13 | 14 | margin: 1rem 0 0.5rem; 15 | } 16 | `; 17 | 18 | export const Content = styled.div` 19 | display: flex; 20 | flex-direction: column; 21 | gap: 2rem; 22 | 23 | @media (min-width: 48rem) { 24 | display: grid; 25 | grid-template-columns: 70% 1fr; 26 | grid-template-rows: 7rem 1fr; 27 | gap: 2rem; 28 | 29 | grid-template-areas: 30 | 'list method' 31 | 'list sumInfo'; 32 | } 33 | `; 34 | 35 | export const PaymentMethod = styled.div` 36 | display: flex; 37 | flex-direction: column; 38 | 39 | @media (min-width: 48rem) { 40 | grid-area: method; 41 | } 42 | 43 | div { 44 | background-color: white; 45 | padding: 0.9rem; 46 | 47 | li { 48 | font-size: 1.4rem; 49 | line-height: 1.7rem; 50 | text-transform: uppercase; 51 | } 52 | } 53 | `; 54 | 55 | export const CheckConfirm = styled.div` 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | flex-direction: column; 60 | 61 | @media (min-width: 48rem) { 62 | flex-direction: row; 63 | gap: 2rem; 64 | margin-top: 2rem; 65 | } 66 | 67 | .check { 68 | width: 4rem; 69 | height: 4rem; 70 | 71 | border-radius: 50%; 72 | border: 1px solid ${(props) => props.theme.primary}; 73 | 74 | display: flex; 75 | align-items: center; 76 | justify-content: center; 77 | } 78 | 79 | span { 80 | font-size: 1.4rem; 81 | line-height: 1.7rem; 82 | font-weight: bold; 83 | color: ${(props) => props.theme.primary}; 84 | text-transform: uppercase; 85 | margin-top: 1rem; 86 | } 87 | `; 88 | -------------------------------------------------------------------------------- /src/mocks/cart-mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5b15c171e4b0023bb624f616", 3 | "items": [ 4 | { 5 | "quantity": 1, 6 | "product": { 7 | "sku": "24413", 8 | "name": "Acer SB220Q bi 21.5 inches Full HD (1920 x 1080) IPS Ultra-Thin", 9 | "imageObjects": [ 10 | { 11 | "small": "https://fakestoreapi.com/img/81QpkIctqPL._AC_SX679_.jpg", 12 | "medium": "https://fakestoreapi.com/img/81QpkIctqPL._AC_SX679_.jpg" 13 | } 14 | ], 15 | "priceSpecification": { 16 | "sku": "38273", 17 | "price": 958.99, 18 | "discount": 0 19 | } 20 | } 21 | }, 22 | { 23 | "quantity": 1, 24 | "product": { 25 | "sku": "24414", 26 | "name": "Samsung 49-Inch CHG90 144Hz Curved Gaming Monitor (LC49HG90DMNXZA) – Super Ultrawide Screen QLED ", 27 | "imageObjects": [ 28 | { 29 | "featured": true, 30 | "small": "https://fakestoreapi.com/img/81Zt42ioCgL._AC_SX679_.jpg", 31 | "medium": "https://fakestoreapi.com/img/81Zt42ioCgL._AC_SX679_.jpg" 32 | } 33 | ], 34 | "priceSpecification": { 35 | "sku": "3019", 36 | "price": 3259.99, 37 | "discount": 0 38 | } 39 | } 40 | }, 41 | { 42 | "quantity": 1, 43 | "product": { 44 | "sku": "24416", 45 | "name": "SanDisk SSD PLUS 1TB Internal SSD - SATA III 6 Gb/s", 46 | "imageObjects": [ 47 | { 48 | "featured": true, 49 | "small": "https://fakestoreapi.com/img/61U7T1koQqL._AC_SX679_.jpg", 50 | "medium": "https://fakestoreapi.com/img/61U7T1koQqL._AC_SX679_.jpg" 51 | } 52 | ], 53 | "priceSpecification": { 54 | "sku": "3019", 55 | "price": 624.90, 56 | "discount": 0 57 | } 58 | } 59 | } 60 | ], 61 | "subTotal": 624.8, 62 | "shippingTotal": 5.3, 63 | "discount": 30, 64 | "total": 618.9 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Input/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import Tooltip from '../Tooltip'; 4 | import { INPUT_ERROR, INPUT_FOCUSED } from '../../constants/validation'; 5 | 6 | interface ContainerProps { 7 | inputHeight?: string; 8 | validationType: string; 9 | } 10 | 11 | const returnColorPerType = (props: any) => { 12 | const { validationType } = props; 13 | 14 | switch (validationType) { 15 | case INPUT_ERROR: 16 | return props.theme.error; 17 | case INPUT_FOCUSED: 18 | return props.theme.focus; 19 | default: 20 | return props.theme.grey; 21 | } 22 | }; 23 | 24 | export const Container = styled.div` 25 | height: 4.5rem; 26 | position: relative; 27 | 28 | display: flex; 29 | align-items: center; 30 | 31 | border: 1px solid; 32 | border-color: ${(props) => returnColorPerType(props)}; 33 | 34 | background: ${(props) => props.theme.white}; 35 | border-radius: 0.3rem; 36 | padding: 1rem 0.5rem 1rem 2rem; 37 | width: 100%; 38 | 39 | box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.2); 40 | 41 | & + div { 42 | margin-top: 0.5rem; 43 | color: ${(props) => props.theme.error}; 44 | } 45 | 46 | svg { 47 | margin-right: 1rem; 48 | } 49 | 50 | input { 51 | width: 100%; 52 | background: transparent; 53 | border: 0; 54 | outline: none; 55 | font-size: 1.8rem; 56 | color: ${(props) => props.theme.text}; 57 | letter-spacing: 1.2px; 58 | 59 | &::placeholder { 60 | color: ${(props) => props.theme.placeholder}; 61 | font-size: 1.6rem; 62 | letter-spacing: 1.2px; 63 | } 64 | } 65 | `; 66 | 67 | export const Error = styled(Tooltip)` 68 | display: flex; 69 | align-items: center; 70 | justify-content: center; 71 | 72 | position: absolute; 73 | top: 1.5rem; 74 | right: 1rem; 75 | 76 | svg { 77 | width: 1.4rem; 78 | height: 1.4rem; 79 | margin: 0; 80 | } 81 | 82 | span { 83 | background: #c53030; 84 | color: #fff; 85 | text-align: center; 86 | 87 | &::before { 88 | border-color: #c53030 transparent; 89 | } 90 | } 91 | `; 92 | -------------------------------------------------------------------------------- /src/pages/CartConfirmation/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router'; 3 | 4 | import SumInfo from '../../components/SumInfo'; 5 | import ItemsList from '../../components/ItemsList'; 6 | import PaymentMethod from '../../components/PaymentInfo'; 7 | import EmptyMessage from '../../components/EmptyMessage'; 8 | 9 | import { Container, CheckConfirm, Content } from './styles'; 10 | 11 | import Check from '../../assets/check.png'; 12 | import { useCart } from '../../hooks/useCart'; 13 | import { cleanLocalStorage, getFromLocalStorage } from '../../helpers/local-storage'; 14 | import { BELEZA_NA_WEB_CREDIT_CARD } from '../../constants/local-storage'; 15 | import { useToast } from '../../hooks/useToast'; 16 | 17 | export default function CartConfirmation() { 18 | const { cartItems, creditCardInfo, setIsPurchaseConfirm } = useCart(); 19 | const navigate = useNavigate(); 20 | const { addToast } = useToast(); 21 | 22 | useEffect(() => { 23 | const creditCardFromStorage = getFromLocalStorage(BELEZA_NA_WEB_CREDIT_CARD); 24 | 25 | if (creditCardFromStorage) { 26 | setIsPurchaseConfirm(true); 27 | 28 | addToast({ 29 | type: 'success', 30 | title: 'Sucesso!', 31 | description: 'Aguarde para ser redirecionado a loja' 32 | }); 33 | 34 | setTimeout(() => { 35 | cleanLocalStorage(); 36 | navigate('/', { replace: true }); 37 | }, 4000); 38 | } else { 39 | navigate('/', { replace: true }); 40 | } 41 | }, []); 42 | 43 | return ( 44 | 45 | {cartItems?.length > 0 ? ( 46 | <> 47 | 48 |
    49 | check 50 |
    51 | Compra efetuada com Sucesso 52 |
    53 | 54 | 55 | {creditCardInfo?.number && } 56 | 57 | 58 | 59 | 60 | ) : ( 61 | 62 | )} 63 |
    64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/pages/CartPayment/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.section` 4 | display: flex; 5 | flex-direction: column; 6 | gap: 2rem; 7 | 8 | h2 { 9 | text-transform: uppercase; 10 | color: ${(props) => props.theme.title}; 11 | font-size: 1.4rem; 12 | font-weight: bold; 13 | margin: 1rem 0 0.5rem; 14 | } 15 | 16 | form { 17 | display: flex; 18 | flex-direction: column; 19 | 20 | fieldset { 21 | display: flex; 22 | flex-direction: column; 23 | 24 | label { 25 | font-size: 1.2rem; 26 | line-height: 1.4rem; 27 | font-weight: bold; 28 | color: #ccc; 29 | margin-bottom: 5px; 30 | } 31 | } 32 | } 33 | `; 34 | 35 | export const Content = styled.div` 36 | display: flex; 37 | flex-direction: column; 38 | gap: 2rem; 39 | 40 | @media (min-width: 48rem) { 41 | display: grid; 42 | grid-template-columns: 70% 1fr; 43 | grid-template-rows: 17rem 1fr; 44 | gap: 2rem; 45 | 46 | grid-template-areas: 47 | 'form sumInfo' 48 | 'form button'; 49 | } 50 | `; 51 | 52 | export const FormContent = styled.div` 53 | display: flex; 54 | flex-direction: column; 55 | align-items: center; 56 | justify-content: center; 57 | 58 | gap: 2rem; 59 | padding: 1.2rem; 60 | background-color: ${(props) => props.theme.white}; 61 | box-shadow: 0.1rem 0.1rem 0.5rem 0 ${(props) => props.theme.shadow}; 62 | 63 | @media (min-width: 65rem) { 64 | grid-area: form; 65 | flex-direction: row; 66 | } 67 | 68 | a { 69 | display: flex; 70 | gap: 1rem; 71 | padding: 1rem 1.2rem; 72 | 73 | font-size: 1.3rem; 74 | line-height: 1.6rem; 75 | 76 | border: 1px solid ${(props) => props.theme.border}; 77 | border-radius: 0.3rem; 78 | 79 | img { 80 | width: 6.5rem; 81 | height: 6.5rem; 82 | } 83 | } 84 | `; 85 | 86 | export const FormGroup = styled.div` 87 | display: flex; 88 | gap: 2rem; 89 | `; 90 | 91 | export const InputsContent = styled.div` 92 | display: flex; 93 | flex-direction: column; 94 | gap: 2rem; 95 | width: 100%; 96 | 97 | @media (min-width: 65rem) { 98 | width: 50%; 99 | } 100 | `; 101 | export const CartContent = styled.div` 102 | display: flex; 103 | `; 104 | -------------------------------------------------------------------------------- /src/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState, useCallback, InputHTMLAttributes } from 'react'; 2 | import { useField } from '@unform/core'; 3 | import { FiAlertCircle } from 'react-icons/fi'; 4 | import { Container, Error } from './styles'; 5 | import { IconBaseProps } from 'react-icons/lib'; 6 | 7 | import { INPUT_ERROR, INPUT_FOCUSED, ICON_ERROR_COLOR } from '../../constants/validation'; 8 | import { creditCardMask, titularNameMask, dateMask, cvvMask } from '../../helpers/masks'; 9 | 10 | interface InputProps extends InputHTMLAttributes { 11 | name: string; 12 | icon?: React.ComponentType; 13 | defaultValue?: string; 14 | inputHeight?: string; 15 | radius?: string; 16 | } 17 | 18 | export default function Input({ 19 | name, 20 | icon: Icon, 21 | defaultValue, 22 | inputHeight, 23 | radius, 24 | ...rest 25 | }: InputProps) { 26 | const inputRef = useRef(null); 27 | 28 | const [validationType, setValidationType] = useState(''); 29 | const { fieldName, registerField, error } = useField(name); 30 | 31 | useEffect(() => { 32 | registerField({ 33 | name: fieldName, 34 | ref: inputRef, 35 | getValue: (ref) => ref.current.value, 36 | setValue: (ref, value) => (ref.current.value = value), 37 | clearValue: (ref) => (ref.current.value = '') 38 | }); 39 | }, [fieldName, registerField]); 40 | 41 | useEffect(() => { 42 | if (error) setValidationType(INPUT_ERROR); 43 | }, [error]); 44 | 45 | const handleInputFocus = useCallback(() => { 46 | setValidationType(INPUT_FOCUSED); 47 | }, []); 48 | 49 | const handleInputBlur = useCallback(() => { 50 | setValidationType(''); 51 | }, []); 52 | 53 | return ( 54 | <> 55 | 56 | {Icon && } 57 | 66 | 67 | {error && validationType === INPUT_ERROR && ( 68 | 69 | 70 | 71 | )} 72 | 73 | 74 | {error && validationType === INPUT_ERROR &&
    {error}
    } 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ItemsList/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | `; 7 | 8 | export const ProductListContent = styled.div` 9 | width: 100%; 10 | display: flex; 11 | flex-direction: column; 12 | gap: 2rem; 13 | padding: 1.2rem; 14 | background-color: ${(props) => props.theme.white}; 15 | box-shadow: 0.1rem 0.1rem 0.5rem 0 ${(props) => props.theme.shadow}; 16 | 17 | @media (min-width: 48rem) { 18 | grid-area: list; 19 | } 20 | 21 | li { 22 | display: flex; 23 | gap: 1rem; 24 | padding: 1rem 1.2rem; 25 | gap: 2rem; 26 | 27 | font-size: 1.3rem; 28 | line-height: 1.6rem; 29 | 30 | border: 1px solid ${(props) => props.theme.border}; 31 | border-radius: 0.3rem; 32 | 33 | img { 34 | width: 6.5rem; 35 | height: 6.5rem; 36 | } 37 | } 38 | `; 39 | 40 | export const ItemTitle = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | gap: 1.5rem; 44 | width: 100%; 45 | 46 | span { 47 | align-self: flex-end; 48 | font-size: 1.4rem; 49 | font-weight: bold; 50 | line-height: 1.7rem; 51 | } 52 | `; 53 | 54 | export const CartSum = styled.div` 55 | display: flex; 56 | flex-direction: column; 57 | gap: 2rem; 58 | padding: 1.2rem; 59 | 60 | border: 1px solid #ccc; 61 | border-radius: 0.3rem; 62 | 63 | li { 64 | font-size: 1.4rem; 65 | line-height: 1.7rem; 66 | color: ${(props) => props.theme.text}; 67 | text-transform: uppercase; 68 | } 69 | `; 70 | 71 | export const UpdateItemControl = styled.div` 72 | display: flex; 73 | align-items: center; 74 | justify-content: center; 75 | border-left: 1px solid ${(props) => props.theme.border}; 76 | padding-left: 1rem; 77 | 78 | button { 79 | background: none; 80 | border: 0; 81 | padding: 6px; 82 | 83 | svg { 84 | color: ${(props) => props.theme.primaryDark}; 85 | transition: color 0.2s; 86 | } 87 | 88 | &:hover { 89 | svg { 90 | color: ${(props) => props.theme.primaryDark}; 91 | } 92 | } 93 | 94 | &:disabled { 95 | svg { 96 | color: ${(props) => props.theme.disabled}; 97 | cursor: not-allowed; 98 | } 99 | } 100 | } 101 | `; 102 | 103 | export const DeleteItemControl = styled(UpdateItemControl)``; 104 | -------------------------------------------------------------------------------- /src/pages/Cart/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { useNavigate } from 'react-router'; 4 | 5 | import { Container } from './styles'; 6 | import { useCart } from '../../../../hooks/useCart'; 7 | import { 8 | NAV_TITLE_CART, 9 | NAV_TITLE_PAYMENT, 10 | NAV_TITLE_CONFIRMATION 11 | } from '../../../../constants/navigation'; 12 | 13 | export default function Header() { 14 | const navItems = [ 15 | { 16 | title: 'Ver Produtos', 17 | url: '/' 18 | }, 19 | { 20 | title: NAV_TITLE_CART, 21 | url: '/cart' 22 | }, 23 | { 24 | title: NAV_TITLE_PAYMENT, 25 | url: '/cart/payment' 26 | }, 27 | { 28 | title: NAV_TITLE_CONFIRMATION, 29 | url: '/cart/confirmation' 30 | } 31 | ]; 32 | 33 | const [optionSelected, setOptionSelected] = useState(); 34 | const { cartItems, creditCardInfo, isPurchaseConfirm } = useCart(); 35 | 36 | const location = useLocation(); 37 | const navigate = useNavigate(); 38 | 39 | useEffect(() => { 40 | switch (location.pathname) { 41 | case '/cart': 42 | setOptionSelected(NAV_TITLE_CART); 43 | break; 44 | case '/cart/payment': 45 | setOptionSelected(NAV_TITLE_PAYMENT); 46 | break; 47 | case '/cart/confirmation': 48 | setOptionSelected(NAV_TITLE_CONFIRMATION); 49 | break; 50 | default: 51 | setOptionSelected(NAV_TITLE_CART); 52 | break; 53 | } 54 | }, [location]); 55 | 56 | function handleOptionSelected(item: any) { 57 | const { title, url } = item; 58 | 59 | if (title === NAV_TITLE_PAYMENT && cartItems?.length === 0) return; 60 | if (title === NAV_TITLE_CONFIRMATION) { 61 | if (isPurchaseConfirm) return; 62 | 63 | const { number, cvc, name, expiry } = creditCardInfo; 64 | if (!number || !cvc || !name || !expiry) return; 65 | } 66 | 67 | navigate(url, { replace: true }); 68 | setOptionSelected(item); 69 | } 70 | 71 | return ( 72 | 73 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import styled, { createGlobalStyle } from 'styled-components'; 2 | 3 | export default createGlobalStyle` 4 | // reset 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | button { 12 | cursor: pointer; 13 | } 14 | 15 | h1, 16 | input, 17 | button, 18 | fieldset { 19 | border: none; 20 | } 21 | 22 | li { 23 | list-style: none; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5 { 31 | font-weight: 600; 32 | font-family: "Montserrat", sans-serif; 33 | } 34 | 35 | a { 36 | color: inherit; 37 | text-decoration: none; 38 | } 39 | 40 | 41 | // base styles 42 | body { 43 | font-family: sans-serif; 44 | font-family: 'Roboto', sans-serif; 45 | font-weight: 200; 46 | -webkit-font-smoothing: antialiased; 47 | -moz-osx-font-smoothing: grayscale; 48 | background-color: #EEE; 49 | height: 100vh; 50 | } 51 | 52 | 53 | html { 54 | font-size: 62.5%; 55 | } 56 | 57 | @media (max-width: 1080px) { 58 | html { 59 | font-size: 58.59375%; 60 | } 61 | } 62 | 63 | @media (max-width: 720px) { 64 | html { 65 | font-size: 54.6875%; 66 | } 67 | } 68 | `; 69 | 70 | interface ButtonProps { 71 | width?: string; 72 | type?: string; 73 | } 74 | 75 | export const ButtonDefault = styled.button` 76 | width: ${(props) => props.width}; 77 | height: 5rem; 78 | 79 | display: flex; 80 | justify-content: center; 81 | align-items: center; 82 | 83 | background-color: ${(props) => props.theme.primary}; 84 | border-color: ${(props) => props.theme.primary}; 85 | 86 | padding: 1.8rem 0.94rem; 87 | border-radius: 0.3rem; 88 | 89 | font-size: 1.4rem; 90 | font-weight: bold; 91 | line-height: 2.4rem; 92 | letter-spacing: 0.05rem; 93 | text-transform: uppercase; 94 | 95 | color: ${(props) => props.theme.white}; 96 | box-shadow: 0 0.3rem 0 0 ${(props) => props.theme.primaryDark}; 97 | cursor: pointer; 98 | 99 | & > span { 100 | display: flex; 101 | justify-content: center; 102 | align-items: center; 103 | 104 | gap: 2rem; 105 | 106 | .icon { 107 | display: flex; 108 | align-items: center; 109 | padding: 1rem 2rem; 110 | border-right: 1px solid rgba(0, 0, 0, 0.1); 111 | gap: 1rem; 112 | 113 | @media (min-width: 36rem) { 114 | margin-left: -2rem; 115 | } 116 | } 117 | } 118 | 119 | :hover { 120 | background-color: ${(props) => props.theme.primaryDark}; 121 | } 122 | 123 | @media (min-width: 48rem) { 124 | grid-area: button; 125 | } 126 | `; 127 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { MdShoppingBasket } from 'react-icons/md'; 3 | 4 | import { ProductList, StockCounter } from './styles'; 5 | import api from '../../services/api'; 6 | 7 | import { useCart } from '../../hooks/useCart'; 8 | import { Cart, CartItem } from '../../interfaces/Cart'; 9 | import Header from './components/Header'; 10 | import Loading from '../../components/Loading'; 11 | import Button from '../../components/Button'; 12 | import formatCurrency from '../../helpers/formatCurrency'; 13 | import cartMapper from '../../mappers/cart-mapper'; 14 | import { setToLocalStorage } from '../../helpers/local-storage'; 15 | import { BELEZA_NA_WEB_ALL_ITEMS } from '../../constants/local-storage'; 16 | import { API_URL_CART } from '../../constants/api-url'; 17 | 18 | interface CartItemsQuantity { 19 | [key: string]: number; 20 | } 21 | 22 | export default function Home() { 23 | const { cartItems, stockquantity, setSumInfo, sumInfo, addProduct, isPurchaseConfirm } = 24 | useCart(); 25 | const [allProducts, setAllProducts] = useState([] as CartItem[]); 26 | 27 | useEffect(() => { 28 | async function loadProducts() { 29 | const response = await api.get(API_URL_CART); 30 | 31 | const cartWrapper = cartMapper(response.data); 32 | const { items, shippingTotal } = cartWrapper; 33 | 34 | setAllProducts(items); 35 | setSumInfo({ ...sumInfo, shippingTotal }); 36 | setToLocalStorage(BELEZA_NA_WEB_ALL_ITEMS, items); 37 | } 38 | 39 | loadProducts(); 40 | }, []); 41 | 42 | useEffect(() => { 43 | if (isPurchaseConfirm) { 44 | window.location.reload(); 45 | } 46 | }, [isPurchaseConfirm]); 47 | 48 | const cartItemsQuantity = cartItems.reduce((itemsQuantity, item) => { 49 | const itemsQuantityObj = { ...itemsQuantity }; 50 | itemsQuantityObj[item.product.sku] = item.quantity; 51 | return itemsQuantityObj; 52 | }, {} as CartItemsQuantity); 53 | 54 | return ( 55 | <> 56 |
    57 | 58 | {allProducts.map((item: CartItem) => ( 59 |
  • 60 | {item.product.name} 61 |

    {item.product.name}

    62 | {formatCurrency(item.product.priceSpecification.price)} 63 | 64 | 75 | 76 | 77 | Restam {stockquantity - (cartItemsQuantity[item.product.sku] || 0)} no estoque 78 | 79 |
  • 80 | ))} 81 |
    82 | 83 | {allProducts?.length === 0 && } 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beleza-na-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "export ENVIRONMENT=${ENVIRONMENT:=development} NODE_ENV=development && react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test --verbose", 9 | "test:coverage": "react-scripts test --coverage", 10 | "test:all": "react-scripts test --watchAll", 11 | "eject": "react-scripts eject", 12 | "predeploy": "yarn build", 13 | "deploy": "gh-pages -d build" 14 | }, 15 | "homepage": "https://dxwebster.github.io/computer-shop-react", 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | }, 28 | "dependencies": { 29 | "@babel/plugin-syntax-flow": "^7.14.5", 30 | "@babel/plugin-transform-react-jsx": "^7.14.9", 31 | "@svgr/cli": "^6.2.1", 32 | "@testing-library/jest-dom": "^5.16.2", 33 | "@testing-library/react": "^12.1.3", 34 | "@testing-library/user-event": "^13.5.0", 35 | "@unform/core": "^2.1.6", 36 | "@unform/web": "^2.1.6", 37 | "autoprefixer": "^10.0.2", 38 | "axios": "^0.21.1", 39 | "card-validator": "^8.1.1", 40 | "dotenv": "^16.0.0", 41 | "gh-pages": "^3.2.3", 42 | "history": "^5.0.0", 43 | "identity-obj-proxy": "^3.0.0", 44 | "polished": "^4.0.5", 45 | "react": "^17.0.2", 46 | "react-credit-cards": "^0.8.3", 47 | "react-dom": "^17.0.1", 48 | "react-icons": "^4.3.1", 49 | "react-router-dom": "^6.2.1", 50 | "react-scripts": "^5.0.0", 51 | "react-spring": "^9.4.3", 52 | "source-map-resolve": "^0.6.0", 53 | "source-map-url": "^0.4.1", 54 | "styled-components": "^5.2.1", 55 | "typescript": "^4.3.4", 56 | "uuid": "^8.3.2", 57 | "yup": "^0.32.11" 58 | }, 59 | "devDependencies": { 60 | "@commitlint/cli": "^16.2.1", 61 | "@commitlint/config-conventional": "^16.2.1", 62 | "@testing-library/dom": "^8.11.3", 63 | "@types/jest": "^27.4.0", 64 | "@types/js-cookie": "^3.0.1", 65 | "@types/node": "^17.0.1", 66 | "@types/react": "^17.0.37", 67 | "@types/react-credit-cards": "^0.8.1", 68 | "@types/react-dom": "^17.0.10", 69 | "@types/react-redux": "^7.1.16", 70 | "@types/react-router-dom": "^5.1.7", 71 | "@types/styled-components": "^5.1.21", 72 | "@types/uuid": "^8.3.4", 73 | "@types/yup": "^0.29.13", 74 | "@typescript-eslint/eslint-plugin": "^5.11.0", 75 | "@typescript-eslint/parser": "^5.11.0", 76 | "axios-mock-adapter": "^1.20.0", 77 | "babel-jest": "^27.5.1", 78 | "eslint": "^7.32.0", 79 | "eslint-config-next": "12.0.7", 80 | "eslint-config-prettier": "^8.3.0", 81 | "eslint-import-resolver-typescript": "^2.5.0", 82 | "eslint-import-resolver-webpack": "^0.13.2", 83 | "eslint-plugin-import": "^2.25.4", 84 | "eslint-plugin-jsx-a11y": "^6.5.1", 85 | "eslint-plugin-prettier": "^4.0.0", 86 | "eslint-plugin-react": "^7.28.0", 87 | "eslint-plugin-react-hooks": "^4", 88 | "husky": "^7.0.4", 89 | "jest": "^27.5.1", 90 | "jest-dom": "testing-library/jest-dom", 91 | "prettier": "^2.5.1" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 17 | 18 | 27 | 28 | 29 | 57 | 58 | 59 | Components Shop 60 | 61 | 62 | 63 |
    64 | 74 | 75 | -------------------------------------------------------------------------------- /src/components/ItemsList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CartItem } from '../../interfaces/Cart'; 3 | import formatCurrency from '../../helpers/formatCurrency'; 4 | 5 | import { 6 | Container, 7 | ItemTitle, 8 | ProductListContent, 9 | UpdateItemControl, 10 | DeleteItemControl 11 | } from './styles'; 12 | import { MdAddCircleOutline, MdDelete, MdRemoveCircleOutline } from 'react-icons/md'; 13 | import { useCart } from '../../hooks/useCart'; 14 | import EmptyMessage from '../../components/EmptyMessage'; 15 | import Loading from '../Loading'; 16 | 17 | interface ItemsListProps { 18 | showControlers: boolean; 19 | showPrice?: boolean; 20 | } 21 | 22 | export default function ItemsList({ showControlers, showPrice = true }: ItemsListProps) { 23 | const { removeProduct, updateItemQuantity, cartItems } = useCart(); 24 | 25 | function handleProductIncrement(item: CartItem) { 26 | updateItemQuantity({ 27 | productSku: item.product.sku, 28 | quantity: item.quantity + 1 29 | }); 30 | } 31 | 32 | function handleProductDecrement(item: CartItem) { 33 | updateItemQuantity({ 34 | productSku: item.product.sku, 35 | quantity: item.quantity - 1 36 | }); 37 | } 38 | 39 | function handleRemoveProduct(productSku: string) { 40 | removeProduct(productSku); 41 | } 42 | 43 | return ( 44 | 45 | {cartItems?.length > 0 ? ( 46 | <> 47 |

    Produtos

    48 | 49 | {cartItems?.map((item: CartItem) => ( 50 |
  • 51 |
    52 | {!item.product.imageObjects[0].small ? ( 53 | 54 | ) : ( 55 | 56 | )} 57 |
    58 | 59 | 60 | {item.product.name} 61 | {showPrice && ( 62 | 63 | {formatCurrency(item.product.priceSpecification.price * item.quantity)} 64 | 65 | )} 66 | 67 | 68 | {showControlers && ( 69 | <> 70 | 71 | 79 | {item.quantity} 80 | 87 | 88 | 89 | 90 | 97 | 98 | 99 | )} 100 |
  • 101 | ))} 102 |
    103 | 104 | ) : ( 105 | 106 | )} 107 |
    108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

    Computer Shop

    2 | 3 |

    Aplicação de carrinho de e-commerce com consumo de API e steps de pagamento e sucesso 4 | 5 |

    6 | 7 | 💻 **Acesse a aplicação [aqui](https://dxwebster.github.io/computer-shop-react/)** 8 | 9 |

    10 | 11 |

    12 | 13 |

    14 |
    15 | 16 |
    17 | 18 |

    19 | 20 | 21 | ## 🛠 Tecnologias utilizadas 22 | 23 | - react: `Biblioteca Javascript baseado em componentes` 24 | 25 | - typescript: `Linguagem de programação com tipagem estática` 26 | 27 | - javascript: `Linguagem de programação com tipagem dinâmica` 28 | 29 | - axios: `Cliente HTTP para fazer requisições à API` 30 | 31 | - styled-components: `Estilização dos componentes com CSS-in-JS` 32 | 33 | - react-router-dom: `Roteamento do sistema` 34 | 35 | - unform: `Criação de formulários para React e React Native` 36 | 37 | - yup: `Construtor de esquemas de validações de formulários` 38 | 39 | - polished: `Biblioteca de Estilização js no css` 40 | 41 | - react-spring: `Biblioteca de animação em js` 42 | 43 | ## 🛠 Ferramentas de desenvolvimento 44 | 45 | - eslint: `Ferramenta de análise de erros em códigos` 46 | 47 | - prettier: `Ferramenta de formatação de códigos` 48 | 49 | - commitlint: `Ferramenta para verificar padrão de commits` 50 | 51 | - husky: `Ferramenta para melhorar commits` 52 | 53 | 54 | ## ✨ Features implementadas 55 | 56 | - Layout Responsivo 57 | - Desenvolvimento Mobile First 58 | - CI com Github Actions 59 | - Persistência de dados em LocalStorage e contexto 60 | - Toasts de success, error, warning 61 | ### Página de Produtos 62 | - Listagem de produtos por consumo de API 63 | - Loading de carregamento 64 | - Botão de adicionar item no carrinho 65 | - Contador de quantidade de produtos no carrinho 66 | - Header com navegação para "Meu carrinho" e contador de itens 67 | - Toast de success por adição de item no carrinho 68 | - Toast de erro de adição de item fora de estoque no carrinho 69 | ### Carrinho 70 | - 3 steps de carrinho: lista de itens > pagamento > compra confirmada 71 | - Header com navegação dos steps do carrinho 72 | - Bloqueio de step de pagamento se não houver items no carrinho 73 | - Bloqueio de step confirmação se não houver dados de pagamento 74 | - Redirecionamento caso tentar acessar pela url, sem dados requeridos para o step 75 | - Toast de erro de adição de item fora de estoque no carrinho 76 | ### Carrinho > Lista 77 | - Listagem de itens com controles para incrementar ou decrementar quantidade 78 | - Botão para remover item do carrinho 79 | - Visualização de subtotal, frete, desconto e total 80 | - Atualização automática de valores ao modificar algum dos items 81 | - Mensagem de carrinho vazio caso não tenha items com navegação para "ver produtos" 82 | - Botão seguir para pagamento 83 | ### Carrinho > Pagamento 84 | - Formulário para inclusão de dados de cartão de crédito 85 | - Máscara para validar inserção correta de dados em cada campo 86 | - Ícone de erro com tooltip com mensagem personalizada de validação de cada campo 87 | - Validação de campos não preenchidos ao clicar no botão Finalizar Pagamento 88 | - Representação visual do cartão de crédito com os dados inseridos ([dados para teste](https://docs.moip.com.br/docs/cartoes-de-credito-para-teste)) 89 | - Botão de Finalizar Pagamento com estilização de progresso 90 | ### Carrinho > Confirmação 91 | - Mensagem de Compra efetuada com sucesso 92 | - Listagem de Produtos 93 | - Visualização de dados de cartão de crédito mascarados 94 | ## 📥 Execute esse projeto no seu computador 95 | 96 | - Clonar Repositório: `git clone https://github.com/dxwebster/computer-shop-react` 97 | - Instalar dependências: `yarn` 98 | - Criar aquivo .env com as seguintes variáveis: 99 | ``` 100 | REACT_APP_ENVIRONMENT = "development", 101 | NODE_ENV = "development", 102 | ENVIRONMENT = "development" 103 | ``` 104 | - Rodar Aplicação: `yarn start` 105 | 106 | ## 📕 Licença 107 | 108 | Todos os arquivos incluídos aqui, incluindo este _Readme_, estão sob Licença MIT.
    109 | Criado com ❤ por [Adriana Lima](https://github.com/dxwebster) 110 | -------------------------------------------------------------------------------- /src/pages/CartPayment/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback, useState, useEffect } from 'react'; 2 | import * as Yup from 'yup'; 3 | import Cards, { Focused } from 'react-credit-cards'; 4 | import 'react-credit-cards/es/styles-compiled.css'; 5 | import { Form } from '@unform/web'; 6 | import { FormHandles } from '@unform/core'; 7 | import { useNavigate } from 'react-router'; 8 | import valid from 'card-validator'; 9 | 10 | import getValidationError from '../../helpers/validations'; 11 | import { setToLocalStorage } from '../../helpers/local-storage'; 12 | import { BELEZA_NA_WEB_CREDIT_CARD } from '../../constants/local-storage'; 13 | import { creditCardMask, titularNameMask, dateMask, cvvMask } from '../../helpers/masks'; 14 | 15 | import { 16 | CARD_NUMBER_PLACEHOLDER, 17 | CVV_PLACEHOLDER, 18 | DATE_PLACEHOLDER, 19 | TITULAR_NAME_PLACEHOLDER 20 | } from '../../constants/placeholder'; 21 | 22 | import Button from '../../components/Button'; 23 | import Input from '../../components/Input'; 24 | import SumInfo from '../../components/SumInfo'; 25 | import { useCart } from '../../hooks/useCart'; 26 | 27 | import { Container, FormContent, FormGroup, Content, InputsContent, CartContent } from './styles'; 28 | 29 | export default function CartPayment() { 30 | const { creditCardInfo, setCreditCardInfo, cartItems } = useCart(); 31 | const [isProgressive, setIsProgressive] = useState(false); 32 | 33 | const navigate = useNavigate(); 34 | const formRef = useRef(null); 35 | 36 | useEffect(() => { 37 | if (cartItems?.length === 0) navigate('/', { replace: true }); 38 | }, []); 39 | 40 | const validForm = async () => { 41 | try { 42 | const data = formRef?.current?.getData(); 43 | 44 | formRef.current?.setErrors({}); 45 | 46 | const schema = Yup.object().shape({ 47 | number: Yup.string() 48 | .min(17, 'Número do cartão deve ter pelo menos 14 dígitos') 49 | .test( 50 | 'test-number', 51 | 'Insira um número de cartão de crédito válido', 52 | (value) => valid.number(value).isValid 53 | ) 54 | .required('Digite o número do cartão'), 55 | name: Yup.string() 56 | .required('Digite o nome do titular') 57 | .min(3, 'Nome deve ter pelo menos 3 letras'), 58 | expiry: Yup.string() 59 | .required('Digite a validade do cartão') 60 | .min(5, 'Data deve ter pelo menos 4 dígitos'), 61 | cvc: Yup.string() 62 | .required('Digite o código do cartão') 63 | .min(3, 'CVC deve ter pelo menos 3 dígitos') 64 | }); 65 | 66 | await schema.validate(data, { abortEarly: false }); 67 | 68 | return true; 69 | } catch (err) { 70 | if (err instanceof Yup.ValidationError) { 71 | const errors = getValidationError(err); 72 | formRef.current?.setErrors(errors); 73 | return; 74 | } 75 | 76 | return false; 77 | } 78 | }; 79 | 80 | const handleChange = useCallback( 81 | (e: React.FormEvent) => { 82 | switch (e.currentTarget.id) { 83 | case 'number': 84 | creditCardMask(e); 85 | break; 86 | case 'name': 87 | titularNameMask(e); 88 | break; 89 | case 'expiry': 90 | dateMask(e); 91 | break; 92 | case 'cvc': 93 | cvvMask(e); 94 | break; 95 | default: 96 | } 97 | 98 | setCreditCardInfo({ 99 | ...creditCardInfo, 100 | [e.currentTarget.id]: e.currentTarget.value 101 | }); 102 | }, 103 | [creditCardInfo] 104 | ); 105 | 106 | const handleFocus = useCallback( 107 | (value) => { 108 | const f: Focused = value; 109 | 110 | setCreditCardInfo({ 111 | ...creditCardInfo, 112 | focused: f 113 | }); 114 | }, 115 | [creditCardInfo] 116 | ); 117 | 118 | const handleSubmit = useCallback(async (data: any) => { 119 | const formIsValid = await validForm(); 120 | 121 | if (formIsValid) { 122 | setCreditCardInfo(data); 123 | 124 | const maskData = { 125 | ...data, 126 | cvc: '###' 127 | }; 128 | 129 | setToLocalStorage(BELEZA_NA_WEB_CREDIT_CARD, maskData); 130 | 131 | setIsProgressive(true); 132 | setTimeout(() => navigate('/cart/confirmation', { replace: true }), 5000); 133 | } 134 | }, []); 135 | 136 | return ( 137 | 138 |
    139 | 140 |
    141 |

    CARTÃO DE CRÉDITO

    142 | 143 | 144 |
    145 | 146 | handleFocus(e.target.name)} 154 | radius="all" 155 | /> 156 |
    157 | 158 |
    159 | 160 | handleFocus(e.target.name)} 168 | radius="all" 169 | /> 170 |
    171 | 172 | 173 |
    174 | 175 | handleFocus(e.target.name)} 183 | radius="all" 184 | /> 185 |
    186 | 187 |
    188 | 189 | handleFocus(e.target.name)} 197 | radius="all" 198 | /> 199 |
    200 |
    201 |
    202 | 203 | 204 | 211 | 212 |
    213 |
    214 | 215 | 218 |
    219 |
    220 |
    221 | ); 222 | } 223 | -------------------------------------------------------------------------------- /src/hooks/useCart.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | ReactNode, 4 | useContext, 5 | useState, 6 | useRef, 7 | useEffect, 8 | SetStateAction, 9 | Dispatch 10 | } from 'react'; 11 | import api from '../services/api'; 12 | import { Cart, CartItem } from '../interfaces/Cart'; 13 | import { 14 | BELEZA_NA_WEB_ALL_ITEMS, 15 | BELEZA_NA_WEB_CART_ITEMS, 16 | BELEZA_NA_WEB_CREDIT_CARD, 17 | BELEZA_NA_WEB_SUM_INFO 18 | } from '../constants/local-storage'; 19 | 20 | import { API_URL_CART } from '../constants/api-url'; 21 | import { 22 | cleanLocalStorage, 23 | getFromLocalStorage, 24 | setToLocalStorage 25 | } from '../helpers/local-storage'; 26 | import { Focused } from 'react-credit-cards'; 27 | import { useToast } from './useToast'; 28 | 29 | interface CartProviderProps { 30 | children: ReactNode; 31 | } 32 | interface UpdateItemQuantity { 33 | productSku: string; 34 | quantity: number; 35 | } 36 | interface CreditCardInfo { 37 | number: string; 38 | name: string; 39 | expiry: string; 40 | cvc: string; 41 | focused: Focused; 42 | } 43 | interface SumInfo { 44 | itemsSubTotal: number; 45 | itemsDiscount: number; 46 | itemsTotal: number; 47 | shippingTotal: number; 48 | } 49 | interface CartContextData { 50 | sumInfo: SumInfo; 51 | setSumInfo: Dispatch>; 52 | creditCardInfo: CreditCardInfo; 53 | setCreditCardInfo: Dispatch>; 54 | cartItems: CartItem[]; 55 | addProduct: (productSku: string) => Promise; 56 | removeProduct: (productSku: string) => void; 57 | updateItemQuantity: ({ productSku, quantity }: UpdateItemQuantity) => void; 58 | stockquantity: number; 59 | isPurchaseConfirm: boolean; 60 | setIsPurchaseConfirm: Dispatch>; 61 | } 62 | 63 | const CartContext = createContext({} as CartContextData); 64 | 65 | export function CartProvider({ children }: CartProviderProps): JSX.Element { 66 | const cartItemsFromLocalStorage = getFromLocalStorage(BELEZA_NA_WEB_CART_ITEMS); 67 | const sumInfoFromLocalStorage = getFromLocalStorage(BELEZA_NA_WEB_SUM_INFO); 68 | const creditCardFromStorage = getFromLocalStorage(BELEZA_NA_WEB_CREDIT_CARD); 69 | 70 | const [cartItems, setCartItems] = useState(cartItemsFromLocalStorage || []); 71 | const [sumInfo, setSumInfo] = useState(sumInfoFromLocalStorage || {}); 72 | const [creditCardInfo, setCreditCardInfo] = useState(creditCardFromStorage || {}); 73 | const [isPurchaseConfirm, setIsPurchaseConfirm] = useState(false); 74 | 75 | const prevCartRef = useRef(); 76 | const cartPreviousValue = prevCartRef.current ?? cartItems; 77 | const { addToast } = useToast(); 78 | const stockquantity = 4; 79 | 80 | useEffect(() => { 81 | prevCartRef.current = cartItems; 82 | }); 83 | 84 | useEffect(() => { 85 | if (cartPreviousValue !== cartItems) { 86 | setToLocalStorage(BELEZA_NA_WEB_CART_ITEMS, cartItems); 87 | } 88 | }, [cartPreviousValue, cartItems]); 89 | 90 | const addProduct = async (productSku: string) => { 91 | try { 92 | const updatedCartItems = [...cartItems]; 93 | 94 | const itemAlreadyInCart = updatedCartItems.find((item) => item.product.sku === productSku); 95 | const quantitySum = itemAlreadyInCart ? itemAlreadyInCart.quantity : 0; 96 | const currentItemQuantity = quantitySum + 1; 97 | 98 | // se for chegar no limite do estoque 99 | if (currentItemQuantity > stockquantity) { 100 | addToast({ 101 | type: 'error', 102 | title: 'Erro', 103 | description: 'Quantidade solicitada fora de estoque' 104 | }); 105 | 106 | return; 107 | } 108 | 109 | // soma mais 1 no item que já está no carrinho 110 | if (itemAlreadyInCart) { 111 | itemAlreadyInCart.quantity = quantitySum + 1; 112 | } 113 | 114 | // armazena novo produto no carrinho 115 | else { 116 | const response = await api.get(API_URL_CART); 117 | const newItem = response.data.items.find( 118 | (item: CartItem) => item.product.sku === productSku 119 | ); 120 | if (newItem) updatedCartItems.push(newItem); 121 | 122 | addToast({ 123 | type: 'success', 124 | title: 'Sucesso!', 125 | description: `"${newItem?.product?.name}" foi adicionado ao carrinho` 126 | }); 127 | } 128 | 129 | setCartItems(updatedCartItems); 130 | setSumInfoItems(updatedCartItems); 131 | } catch { 132 | addToast({ 133 | type: 'error', 134 | title: 'Erro', 135 | description: 'Erro ao tentar incluir item no carrinho' 136 | }); 137 | } 138 | }; 139 | 140 | const updateItemQuantity = ({ productSku, quantity }: UpdateItemQuantity) => { 141 | try { 142 | if (quantity <= 0) return; 143 | 144 | if (quantity > stockquantity) { 145 | addToast({ 146 | type: 'error', 147 | title: 'Erro', 148 | description: 'Quantidade solicitada fora de estoque' 149 | }); 150 | return; 151 | } 152 | 153 | const updatedCartItems = [...cartItems]; 154 | const itemAlreadyInCart = updatedCartItems.find((item) => item.product.sku === productSku); 155 | 156 | if (itemAlreadyInCart) { 157 | itemAlreadyInCart.quantity = quantity; 158 | 159 | setCartItems(updatedCartItems); 160 | setSumInfoItems(updatedCartItems); 161 | } 162 | // 163 | else throw Error(); 164 | } catch { 165 | addToast({ 166 | type: 'error', 167 | title: 'Erro', 168 | description: 'Erro ao tentar alterar quantidade' 169 | }); 170 | } 171 | }; 172 | 173 | const removeProduct = (productSku: string) => { 174 | try { 175 | const updatedCartItems = [...cartItems]; 176 | const productIndex = updatedCartItems.findIndex((item) => item.product.sku === productSku); 177 | 178 | if (productIndex >= 0) { 179 | updatedCartItems.splice(productIndex, 1); 180 | setCartItems(updatedCartItems); 181 | setSumInfoItems(updatedCartItems); 182 | 183 | if (updatedCartItems?.length === 0) cleanLocalStorage(); 184 | } 185 | // 186 | else throw Error(); 187 | } catch { 188 | addToast({ 189 | type: 'error', 190 | title: 'Erro', 191 | description: 'Erro ao tentar remover item' 192 | }); 193 | } 194 | }; 195 | 196 | const setSumInfoItems = (updatedCartItems: CartItem[]) => { 197 | const cartWithSubtotal = updatedCartItems.map((item) => ({ 198 | ...item, 199 | subTotal: item.product.priceSpecification.price * item.quantity, 200 | discount: item.product.priceSpecification.discount 201 | })); 202 | 203 | let itemsSubTotal = 0; 204 | let itemsDiscount = 0; 205 | 206 | cartWithSubtotal.forEach((item) => { 207 | itemsDiscount += item.discount; 208 | itemsSubTotal += item.subTotal; 209 | }); 210 | 211 | const itemsTotal = itemsSubTotal - itemsDiscount; 212 | 213 | const sumInfoObject = { 214 | ...sumInfo, 215 | itemsSubTotal, 216 | itemsDiscount, 217 | itemsTotal 218 | }; 219 | 220 | setSumInfo(sumInfoObject); 221 | setToLocalStorage(BELEZA_NA_WEB_SUM_INFO, sumInfoObject); 222 | }; 223 | 224 | return ( 225 | 240 | {children} 241 | 242 | ); 243 | } 244 | 245 | export function useCart(): CartContextData { 246 | const context = useContext(CartContext); 247 | return context; 248 | } 249 | -------------------------------------------------------------------------------- /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/home/adrianashikasho/WSL/Projetos/React/test-front/src/index.tsx":"1","/home/adrianashikasho/WSL/Projetos/React/test-front/src/providers/AppProvider.tsx":"2","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/index.js":"3","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/createStore.js":"4","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/rootSaga.js":"5","/home/adrianashikasho/WSL/Projetos/React/test-front/src/config/ReactotronConfig.ts":"6","/home/adrianashikasho/WSL/Projetos/React/test-front/src/services/api.js":"7","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/cart/sagas.js":"8","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/cart/reducer.js":"9","/home/adrianashikasho/WSL/Projetos/React/test-front/src/App.tsx":"10","/home/adrianashikasho/WSL/Projetos/React/test-front/src/styles/global.js":"11","/home/adrianashikasho/WSL/Projetos/React/test-front/src/styles/themes/default.js":"12","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/rootReducer.js":"13","/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Payment/index.tsx":"14","/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Cart/index.tsx":"15","/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Confirmation/index.tsx":"16","/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Item/index.tsx":"17","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/payments/reducer.js":"18","/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/cart/actions.js":"19","/home/adrianashikasho/WSL/Projetos/React/test-front/src/constants/types-reducers.ts":"20","/home/adrianashikasho/WSL/Projetos/React/test-front/src/styles/styles.ts":"21","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Header/index.tsx":"22","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Header/styles.ts":"23","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Button/index.tsx":"24","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Button/styles.ts":"25","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/MainContent/index.tsx":"26","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/MainContent/styles.ts":"27","/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Cart/styles.ts":"28","/home/adrianashikasho/WSL/Projetos/React/test-front/src/helpers/formatCurrency.ts":"29","/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Payment/styles.ts":"30","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Input/index.tsx":"31","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Input/styles.ts":"32","/home/adrianashikasho/WSL/Projetos/React/test-front/src/constants/validation.ts":"33","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Tooltip/index.tsx":"34","/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Tooltip/styles.ts":"35","/home/adrianashikasho/WSL/Projetos/React/test-front/src/helpers/validations.ts":"36"},{"size":831,"mtime":1644411070900,"results":"37","hashOfConfig":"38"},{"size":371,"mtime":1644355029790,"results":"39","hashOfConfig":"38"},{"size":675,"mtime":1644354803740,"results":"40","hashOfConfig":"38"},{"size":391,"mtime":1644354803740,"results":"41","hashOfConfig":"38"},{"size":141,"mtime":1644354937040,"results":"42","hashOfConfig":"38"},{"size":493,"mtime":1644356256310,"results":"43","hashOfConfig":"38"},{"size":529,"mtime":1644358185100,"results":"44","hashOfConfig":"38"},{"size":670,"mtime":1644371407970,"results":"45","hashOfConfig":"38"},{"size":758,"mtime":1644367988090,"results":"46","hashOfConfig":"38"},{"size":728,"mtime":1644412095000,"results":"47","hashOfConfig":"38"},{"size":837,"mtime":1644436489340,"results":"48","hashOfConfig":"38"},{"size":459,"mtime":1644433229440,"results":"49","hashOfConfig":"38"},{"size":180,"mtime":1644369479250,"results":"50","hashOfConfig":"38"},{"size":3550,"mtime":1644436548240,"results":"51","hashOfConfig":"38"},{"size":1409,"mtime":1644437310550,"results":"52","hashOfConfig":"38"},{"size":103,"mtime":1644356753690,"results":"53","hashOfConfig":"38"},{"size":960,"mtime":1644372135910,"results":"54","hashOfConfig":"38"},{"size":762,"mtime":1644369456580,"results":"55","hashOfConfig":"38"},{"size":536,"mtime":1644367166280,"results":"56","hashOfConfig":"38"},{"size":220,"mtime":1644366168960,"results":"57","hashOfConfig":"38"},{"size":348,"mtime":1644409323170,"results":"58","hashOfConfig":"38"},{"size":1291,"mtime":1644410582940,"results":"59","hashOfConfig":"38"},{"size":612,"mtime":1644433282100,"results":"60","hashOfConfig":"38"},{"size":619,"mtime":1644436244820,"results":"61","hashOfConfig":"38"},{"size":1178,"mtime":1644436044650,"results":"62","hashOfConfig":"38"},{"size":142,"mtime":1644411434520,"results":"63","hashOfConfig":"38"},{"size":188,"mtime":1644412313600,"results":"64","hashOfConfig":"38"},{"size":1318,"mtime":1644434067750,"results":"65","hashOfConfig":"38"},{"size":238,"mtime":1644413335990,"results":"66","hashOfConfig":"38"},{"size":1318,"mtime":1644434127810,"results":"67","hashOfConfig":"38"},{"size":2129,"mtime":1644435963430,"results":"68","hashOfConfig":"38"},{"size":2196,"mtime":1644434797800,"results":"69","hashOfConfig":"38"},{"size":125,"mtime":1644366049440,"results":"70","hashOfConfig":"38"},{"size":404,"mtime":1644372956410,"results":"71","hashOfConfig":"38"},{"size":737,"mtime":1644372956410,"results":"72","hashOfConfig":"38"},{"size":389,"mtime":1643483890100,"results":"73","hashOfConfig":"38"},{"filePath":"74","messages":"75","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},"1qy1xiq",{"filePath":"77","messages":"78","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"79","messages":"80","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"82","messages":"83","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"84","messages":"85","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"86","messages":"87","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"88","messages":"89","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"90","messages":"91","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"92","messages":"93","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"94","messages":"95","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"96","messages":"97","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"98","messages":"99","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"100","messages":"101","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"102","messages":"103","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"104","messages":"105","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"106","messages":"107","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"108","messages":"109","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"110"},{"filePath":"111","messages":"112","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"113","messages":"114","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"81"},{"filePath":"115","messages":"116","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"117","messages":"118","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"119","messages":"120","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"121","messages":"122","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"123","messages":"124","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"125","messages":"126","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"127","messages":"128","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"129","messages":"130","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"131","messages":"132","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"133","messages":"134","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"135","messages":"136","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"137","messages":"138","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"139","messages":"140","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"141","messages":"142","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"143","messages":"144","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"145","messages":"146","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},{"filePath":"147","messages":"148","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"76"},"/home/adrianashikasho/WSL/Projetos/React/test-front/src/index.tsx",[],[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/providers/AppProvider.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/index.js",[],[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/createStore.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/rootSaga.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/config/ReactotronConfig.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/services/api.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/cart/sagas.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/cart/reducer.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/App.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/styles/global.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/styles/themes/default.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/rootReducer.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Payment/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Cart/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Confirmation/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Item/index.tsx",[],[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/payments/reducer.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/store/modules/cart/actions.js",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/constants/types-reducers.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/styles/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Header/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Header/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Button/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Button/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/MainContent/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/MainContent/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Cart/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/helpers/formatCurrency.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/pages/Payment/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Input/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Input/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/constants/validation.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Tooltip/index.tsx",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/components/Tooltip/styles.ts",[],"/home/adrianashikasho/WSL/Projetos/React/test-front/src/helpers/validations.ts",[]] --------------------------------------------------------------------------------