├── 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 |
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 |
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 |

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 |
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 |
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",[]]
--------------------------------------------------------------------------------