├── .env
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── _redirects
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── components
│ ├── App
│ │ ├── App.css
│ │ ├── App.tsx
│ │ └── index.ts
│ ├── Authentication
│ │ ├── LoginForm.tsx
│ │ └── ProfileCard.tsx
│ ├── Header
│ │ ├── Header.css
│ │ ├── Header.tsx
│ │ └── index.ts
│ └── Products
│ │ ├── ProductForm.tsx
│ │ └── ProductsCRUD.tsx
├── index.css
├── index.tsx
├── react-app-env.d.ts
├── redux
│ ├── Authentication
│ │ ├── Authentication.actions.ts
│ │ └── Authentication.reducer.ts
│ ├── Products
│ │ ├── Products.actions.ts
│ │ └── Products.reducer.ts
│ └── index.ts
├── services
│ ├── Authentication.service.ts
│ └── Products.service.ts
├── shared
│ ├── Button
│ │ ├── Button.css
│ │ ├── Button.tsx
│ │ └── index.ts
│ ├── Container
│ │ ├── Container.css
│ │ ├── Container.tsx
│ │ └── index.ts
│ ├── Form
│ │ ├── Form.scss
│ │ ├── Form.tsx
│ │ └── index.ts
│ ├── Input
│ │ ├── Input.css
│ │ ├── Input.tsx
│ │ └── index.ts
│ └── Table
│ │ ├── Table.mockdata.ts
│ │ ├── Table.scss
│ │ ├── Table.tsx
│ │ └── index.ts
├── utils
│ ├── HOC
│ │ └── withPermission.tsx
│ ├── http.ts
│ ├── organizeDataForTable.ts
│ └── paginate.ts
└── views
│ ├── LoginView.tsx
│ ├── NotFoundView.tsx
│ ├── ProductsView.tsx
│ └── ProfileView.tsx
└── tsconfig.json
/.env:
--------------------------------------------------------------------------------
1 | GENERATE_SOURCEMAP=false
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
Ignição React
4 |
5 |
6 | ## :dart: Objetivo
7 |
8 | Aprenda a desenvolver aplicações front-end com React do zero ao intermediário do jeito certo.
9 |
10 |
11 | ## :hammer_and_wrench: Tecnologias usadas no projeto
12 |
13 | | Nome | Versão | Documentação |
14 | |--|--|--|
15 | | Axios | 0.27.2 | [Acessar](https://axios-http.com/ptbr/docs/intro) |
16 | | Query String | 7.1.1 | [Acessar](https://axios-http.com/ptbr/docs/intro) |
17 | | NodeJS | 16.17.0 | [Acessar](https://nodejs.org/en/) |
18 | | Netlify | --- | [Acessar](https://www.netlify.com/) |
19 | | React | 18.0.2 | [Acessar](https://github.com/facebook/react/blob/main/CHANGELOG.md#1820-june-14-2022) |
20 | | React Redux | 8.0.2 | [Acessar](https://react-redux.js.org/) |
21 | | React Router Dom | 6.4.1 | [Acessar](https://reactrouter.com/en/6.4.1) |
22 | | Redux | 4.2.0 | [Acessar](https://redux.js.org/) |
23 | | Redux Persist | 6.0.0 | [Acessar](https://www.npmjs.com/package/redux-persist) |
24 | | Redux Thunk | 2.4.1 | [Acessar](https://redux.js.org/usage/writing-logic-thunks) |
25 | | Sass | 1.54.4 | [Acessar](https://sass-lang.com/) |
26 | | Sweet Alert2 | 11.4.29 | [Acessar](https://sweetalert2.github.io/) |
27 | | Typescript | 4.8.2 | [Acessar](https://www.typescriptlang.org/) |
28 |
29 | ## :rocket: Executando o projeto
30 |
31 | No diretório do projeto, você pode executar:
32 |
33 | ```npm start```
34 | Executa o aplicativo no modo de desenvolvimento.
35 | Abra http://localhost:3000 para visualizá-lo no navegador.
36 |
37 | A página será recarregada se você fizer edições.
38 | Você também verá erros de lint no console.
39 |
40 | ```npm test```
41 | Inicia o executor de teste no modo de relógio interativo.
42 | Consulte a seção sobre como executar testes para obter mais informações.
43 |
44 | ```npm run build```
45 | Compila o aplicativo para produção na pasta de compilação.
46 | Ele agrupa corretamente o React no modo de produção e otimiza a compilação para o melhor desempenho.
47 |
48 | A compilação é minificada e os nomes dos arquivos incluem os hashes.
49 | Seu aplicativo está pronto para ser implantado!
50 |
51 | ```npm run eject```
52 | Se você não estiver satisfeito com a ferramenta de compilação e as opções de configuração, poderá ejetar a qualquer momento. Este comando removerá a dependência de compilação única do seu projeto.
53 |
54 | Em vez disso, ele copiará todos os arquivos de configuração e as dependências transitivas (webpack, Babel, ESLint, etc) diretamente em seu projeto para que você tenha controle total sobre eles. Todos os comandos, exceto ejetar, ainda funcionarão, mas apontarão para os scripts copiados para que você possa ajustá-los. Neste momento você está por sua conta.
55 |
56 | Você não precisa usar ejetar. O conjunto de recursos com curadoria é adequado para implantações pequenas e médias, e você não deve se sentir obrigado a usar esse recurso. No entanto, entendemos que essa ferramenta não seria útil se você não pudesse personalizá-la quando estiver pronto para isso.
57 |
58 | ## :desktop_computer: Padronização de código
59 |
60 | - [Eslint](https://eslint.org/)
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alga-stock",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.5.0",
8 | "@testing-library/user-event": "^7.2.1",
9 | "@types/jest": "^24.9.1",
10 | "@types/node": "^16.11.56",
11 | "@types/react": "^18.0.18",
12 | "@types/react-dom": "^18.0.6",
13 | "axios": "^0.27.2",
14 | "query-string": "^7.1.1",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "react-redux": "^8.0.2",
18 | "react-router-dom": "^6.4.1",
19 | "react-scripts": "^5.0.1",
20 | "redux": "^4.2.0",
21 | "redux-persist": "^6.0.0",
22 | "redux-thunk": "^2.4.1",
23 | "sass": "^1.54.4",
24 | "sweetalert2": "^11.4.29",
25 | "typescript": "^4.8.2"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": "react-app",
35 | "rules": {
36 | "react-hooks/exhaustive-deps": "off",
37 | "import/no-anonymous-default-export": [
38 | "error",
39 | {
40 | "allowAnonymousFunction": true
41 | }
42 | ]
43 | }
44 | },
45 | "browserslist": {
46 | "production": [
47 | ">0.2%",
48 | "not dead",
49 | "not op_mini all"
50 | ],
51 | "development": [
52 | "last 1 chrome version",
53 | "last 1 firefox version",
54 | "last 1 safari version"
55 | ]
56 | },
57 | "devDependencies": {
58 | "@types/react-redux": "^7.1.24",
59 | "@types/react-router-dom": "^5.3.3"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algaworks/algastock/3e62ec57e2691a24d1027a745a78bbcdd7878566/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algaworks/algastock/3e62ec57e2691a24d1027a745a78bbcdd7878566/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algaworks/algastock/3e62ec57e2691a24d1027a745a78bbcdd7878566/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
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 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/components/App/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/algaworks/algastock/3e62ec57e2691a24d1027a745a78bbcdd7878566/src/components/App/App.css
--------------------------------------------------------------------------------
/src/components/App/App.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React from 'react';
3 | import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
4 | import './App.css';
5 | import ProductsView from '../../views/ProductsView';
6 | import NotFoundView from '../../views/NotFoundView';
7 | import LoginView from '../../views/LoginView';
8 | import ProfileView from '../../views/ProfileView';
9 |
10 |
11 | function App() {
12 | return (
13 |
14 |
15 |
16 | } />
17 | }>
18 |
19 |
20 | } />
21 | } />
22 | } />
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default App;
30 |
--------------------------------------------------------------------------------
/src/components/App/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './App'
--------------------------------------------------------------------------------
/src/components/Authentication/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React, { useState } from 'react'
3 | import Form from '../../shared/Form'
4 | import Input from '../../shared/Input'
5 | import Button from '../../shared/Button'
6 | import Swal from 'sweetalert2'
7 | import { useDispatch } from 'react-redux'
8 | import { login } from '../../redux/Authentication/Authentication.actions'
9 | import { useNavigate } from 'react-router-dom'
10 |
11 | const LoginForm = () => {
12 | const dispatch = useDispatch()
13 | const [form, setForm] = useState({
14 | user: '',
15 | pass: ''
16 | })
17 |
18 | const navigate = useNavigate()
19 |
20 | const handleLogin = async () => {
21 | try {
22 | await dispatch(login(form))
23 | navigate('/')
24 | } catch(err) {
25 | Swal.fire(
26 | 'Error',
27 | err.response?.data?.message || err.message,
28 | 'error'
29 | )
30 | }
31 | }
32 |
33 | const handleInputChange = (event: React.ChangeEvent) => {
34 | const { value, name } = event.target
35 |
36 | setForm({
37 | ...form,
38 | [name]: value
39 | })
40 | }
41 |
42 | return
61 | }
62 |
63 | export default LoginForm
--------------------------------------------------------------------------------
/src/components/Authentication/ProfileCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Form from '../../shared/Form'
3 | import Input from '../../shared/Input'
4 |
5 | export interface User {
6 | name: string
7 | email: string
8 | }
9 |
10 | declare interface ProfileCardProps {
11 | user: User
12 | }
13 |
14 | const ProfileCard: React.FC = (props) => {
15 | return
16 |
28 |
29 | }
30 |
31 | export default ProfileCard
32 |
--------------------------------------------------------------------------------
/src/components/Header/Header.css:
--------------------------------------------------------------------------------
1 | header.AppHeader {
2 | background-color: #09f;
3 | color: #fff;
4 | height: 80px;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | padding: 10px;
10 | box-shadow: 0 3px 6px rgba(0,0,0,.2);
11 | }
12 |
13 | header.AppHeader h1 {
14 | font-size: 1.2em;
15 | margin-bottom: 7px;
16 | }
17 |
18 | header.AppHeader span {
19 | cursor: pointer;
20 | }
--------------------------------------------------------------------------------
/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Header.css'
3 | import { RootState } from '../../redux'
4 | import { connect, useDispatch } from 'react-redux'
5 | import { Product } from '../../shared/Table/Table.mockdata'
6 | import { User } from '../../services/Authentication.service'
7 | import { useNavigate } from 'react-router-dom'
8 | import { logout } from '../../redux/Authentication/Authentication.actions'
9 | import Swal from 'sweetalert2'
10 |
11 | declare interface HeaderProps {
12 | title: string
13 | firstProduct: Product
14 | profile?: User
15 | }
16 |
17 | const Header: React.FC = (props) => {
18 | const dispatch = useDispatch()
19 | const navigate = useNavigate()
20 |
21 | const isLoggedIn = !!props.profile?._id
22 |
23 | const askToLogout = () => {
24 | Swal
25 | .fire({
26 | title: 'Are you sure?',
27 | icon: 'warning',
28 | showCancelButton: true,
29 | confirmButtonColor: '#09f',
30 | cancelButtonColor: '#d33'
31 | })
32 | .then(({ value }) => value && dispatch(logout()))
33 | }
34 |
35 | const handleLoginLogout = () => {
36 | isLoggedIn
37 | ? askToLogout()
38 | : navigate('/login')
39 | }
40 |
41 | return
51 | }
52 |
53 | const mapStateToProps = (state: RootState) => ({
54 | firstProduct: state.products[0],
55 | profile: state.authentication.profile
56 | })
57 |
58 | export default connect(mapStateToProps)(Header)
--------------------------------------------------------------------------------
/src/components/Header/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Header'
2 |
--------------------------------------------------------------------------------
/src/components/Products/ProductForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 |
3 | import Form from '../../shared/Form'
4 | import Input from '../../shared/Input'
5 | import Button from '../../shared/Button'
6 | import { Product } from '../../shared/Table/Table.mockdata'
7 | import withPermission from '../../utils/HOC/withPermission'
8 |
9 | declare interface InitialFormState {
10 | _id?: string
11 | name: string
12 | price: string
13 | stock: string
14 | }
15 |
16 | export interface ProductCreator {
17 | name: string
18 | price: number
19 | stock: number
20 | }
21 |
22 | declare interface ProductFormProps {
23 | form?: Product
24 | onSubmit?: (product: ProductCreator) => void
25 | onUpdate?: (product: Product) => void
26 | }
27 |
28 | const ProductForm: React.FC = (props) => {
29 | const initialFormState: InitialFormState = props.form
30 | ? {
31 | _id: props.form._id,
32 | name: props.form.name,
33 | price: String(props.form.price),
34 | stock: String(props.form.stock),
35 | }
36 | : {
37 | name: '',
38 | price: '',
39 | stock: ''
40 | }
41 |
42 | const [form, setForm] = useState(initialFormState)
43 |
44 | useEffect(() => {
45 | setForm(initialFormState)
46 | // eslint-disable-next-line
47 | }, [props.form])
48 |
49 | const handleInputChange = (event: React.ChangeEvent) => {
50 | const { value, name } = event.target
51 |
52 | setForm({
53 | ...form,
54 | [name]: value
55 | })
56 | }
57 |
58 | const updateProduct = (product: InitialFormState) => {
59 | const productDto = {
60 | _id: String(product._id),
61 | name: String(product.name),
62 | price: parseFloat(product.price),
63 | stock: Number(product.stock)
64 | }
65 |
66 | props.onUpdate &&
67 | props.onUpdate(productDto)
68 | }
69 |
70 | const createProduct = (product: InitialFormState) => {
71 | const productDto = {
72 | name: String(product.name),
73 | price: parseFloat(product.price),
74 | stock: Number(product.stock)
75 | }
76 |
77 | props.onSubmit &&
78 | props.onSubmit(productDto)
79 | }
80 |
81 | const handleFormSubmit = () => {
82 | form._id
83 | ? updateProduct(form)
84 | : createProduct(form)
85 |
86 | setForm(initialFormState)
87 | }
88 |
89 | return
125 | }
126 |
127 | export default withPermission(['customer', 'admin'])(ProductForm)
--------------------------------------------------------------------------------
/src/components/Products/ProductsCRUD.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React, { useState, useEffect } from 'react'
3 | import Table, { TableHeader } from '../../shared/Table'
4 | import { Product } from '../../shared/Table/Table.mockdata'
5 | import ProductForm, { ProductCreator } from './ProductForm'
6 | import Swal from 'sweetalert2'
7 | import { connect, useDispatch } from 'react-redux'
8 | import * as ProductsAction from '../../redux/Products/Products.actions'
9 | import { RootState, ThunkDispatch } from '../../redux'
10 | import { useLocation, useNavigate, useParams } from 'react-router-dom'
11 |
12 | const headers: TableHeader[] = [
13 | { key: 'name', value: 'Product' },
14 | { key: 'price', value: 'Price', right: true },
15 | { key: 'stock', value: 'Available Stock', right: true }
16 | ]
17 |
18 | declare interface ProductsCRUDProps {
19 | products: Product[]
20 | }
21 |
22 | const ProductsCRUD: React.FC = (props) => {
23 | const dispatch: ThunkDispatch = useDispatch()
24 | const params = useParams<{ id?: string }>()
25 | const navigate = useNavigate()
26 | const location = useLocation()
27 | const showErrorAlert =
28 | (err: Error) => Swal.fire('Oops!', err.message, 'error')
29 |
30 | const [updatingProduct, setUpdatingProduct] = useState(undefined)
31 |
32 | useEffect(() => {
33 | setUpdatingProduct(
34 | params.id
35 | ? props.products.find(product => product._id === params.id)
36 | : undefined
37 | )
38 | }, [params, props.products])
39 |
40 | useEffect(() => {
41 | fetchData()
42 | // eslint-disable-next-line
43 | }, [])
44 |
45 | async function fetchData() {
46 | dispatch(ProductsAction.getProducts())
47 | .catch(showErrorAlert)
48 | }
49 |
50 | const handleProductSubmit = async (product: ProductCreator) => {
51 | dispatch(ProductsAction.insertNewProduct(product))
52 | .catch(showErrorAlert)
53 | }
54 |
55 | const handleProductUpdate = async (newProduct: Product) => {
56 | dispatch(ProductsAction.updateProduct(newProduct))
57 | .then(() => setUpdatingProduct(undefined))
58 | .catch(showErrorAlert)
59 | }
60 |
61 | const deleteProduct = async (id: string) => {
62 | dispatch(ProductsAction.deleteProduct(id))
63 | .then(() => {
64 | Swal.fire('Uhul!', 'Product successfully deleted', 'success')
65 | })
66 | .catch(showErrorAlert)
67 | }
68 |
69 | const handleProductDelete = (product: Product) => {
70 | Swal
71 | .fire({
72 | title: 'Are you sure?',
73 | text: "You won't be able to revert this!",
74 | icon: 'warning',
75 | showCancelButton: true,
76 | confirmButtonColor: '#09f',
77 | cancelButtonColor: '#d33',
78 | confirmButtonText: `Yes, delete ${product.name}!`
79 | })
80 | .then(({ value }) => value && deleteProduct(product._id))
81 | }
82 |
83 | const handleProductDetail = (product: Product) => {
84 | Swal.fire(
85 | 'Product details',
86 | `${product.name} costs $${product.price} and we have ${product.stock} available in stock.`,
87 | 'info'
88 | )
89 | }
90 |
91 | return <>
92 | {
99 | navigate({
100 | pathname: `/products/${product._id}`,
101 | search: location.search
102 | })
103 | }}
104 | itemsPerPage={3}
105 | />
106 |
107 |
112 | >
113 | }
114 |
115 | const mapStateToProps = (state: RootState) => ({
116 | products: state.products
117 | })
118 |
119 | export default connect(mapStateToProps)(ProductsCRUD)
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap');
2 |
3 | * {
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | font-family: Lato, sans-serif;
8 | }
9 |
10 | .Container {
11 | margin: 10px auto;
12 | max-width: 480px;
13 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './components/App';
5 | import { Provider } from 'react-redux'
6 | import { store, persistor } from './redux'
7 | import { PersistGate } from 'redux-persist/integration/react'
8 |
9 | const root = ReactDOM.createRoot(
10 | document.getElementById('root') as HTMLElement
11 | );
12 |
13 | root.render(
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/redux/Authentication/Authentication.actions.ts:
--------------------------------------------------------------------------------
1 | import { Thunk } from "..";
2 | import {
3 | signInUser
4 | } from "../../services/Authentication.service";
5 |
6 | declare interface Credentials {
7 | user: string
8 | pass: string
9 | }
10 |
11 | export const login =
12 | ({ user, pass }: Credentials): Thunk =>
13 | async (dispatch) => {
14 | const loggedInUser = await signInUser(user, pass)
15 | dispatch({
16 | type: 'AUTHENTICATION_LOGIN',
17 | payload: loggedInUser
18 | })
19 | }
20 |
21 | export const logout = () => ({
22 | type: 'AUTHENTICATION_LOGOUT'
23 | })
--------------------------------------------------------------------------------
/src/redux/Authentication/Authentication.reducer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | User
3 | } from "../../services/Authentication.service";
4 | import { Action } from "..";
5 |
6 | declare interface AuthenticationState {
7 | profile?: User
8 | }
9 | export default function (
10 | state: AuthenticationState = {},
11 | action: Action
12 | ): AuthenticationState {
13 | switch (action.type) {
14 | case 'AUTHENTICATION_LOGIN':
15 | return { profile: action.payload }
16 |
17 | case 'AUTHENTICATION_LOGOUT':
18 | return {}
19 |
20 | default:
21 | return state
22 | }
23 | }
--------------------------------------------------------------------------------
/src/redux/Products/Products.actions.ts:
--------------------------------------------------------------------------------
1 | import { Thunk } from ".."
2 | import { ProductCreator } from "../../components/Products/ProductForm"
3 | import {
4 | getAllProducts,
5 | updateSingleProduct,
6 | createSingleProduct,
7 | deleteSingleProduct
8 | } from "../../services/Products.service"
9 | import { Product } from "../../shared/Table/Table.mockdata"
10 |
11 | export const updateProduct =
12 | (newProduct: Product): Thunk =>
13 | async (dispatch) => {
14 | await updateSingleProduct(newProduct)
15 | dispatch(getProducts())
16 | }
17 |
18 | export const getProducts =
19 | (): Thunk =>
20 | async (dispatch) => {
21 | const products = await getAllProducts()
22 | console.log('fetched')
23 | dispatch({
24 | type: 'FETCH_PRODUCTS',
25 | payload: products
26 | })
27 | }
28 |
29 | export const insertNewProduct =
30 | (product: ProductCreator): Thunk =>
31 | async (dispatch) => {
32 | await createSingleProduct(product)
33 | dispatch(getProducts())
34 | }
35 |
36 | export const deleteProduct =
37 | (productId: string): Thunk =>
38 | async (dispatch) => {
39 | await deleteSingleProduct(productId)
40 | dispatch(getProducts())
41 | }
--------------------------------------------------------------------------------
/src/redux/Products/Products.reducer.ts:
--------------------------------------------------------------------------------
1 | import { Product } from "../../shared/Table/Table.mockdata"
2 | import { Action } from ".."
3 |
4 | export default function (state: Product[] = [], action: Action): Product[] {
5 | switch (action.type) {
6 | case 'FETCH_PRODUCTS':
7 | return [...action.payload]
8 | default:
9 | return state
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/redux/index.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import {
3 | legacy_createStore as createStore,
4 | combineReducers,
5 | compose,
6 | applyMiddleware
7 | } from 'redux'
8 | import thunk, { ThunkAction } from 'redux-thunk'
9 | import Products from './Products/Products.reducer'
10 | import { persistReducer, persistStore } from 'redux-persist'
11 | import storage from 'redux-persist/lib/storage'
12 | import Authentication from './Authentication/Authentication.reducer'
13 |
14 | const reducers = combineReducers({
15 | products: Products,
16 | authentication: Authentication
17 | })
18 |
19 | const persistedReducer = persistReducer({
20 | key: 'algastock',
21 | storage,
22 | blacklist: ['products']
23 | }, reducers)
24 |
25 | const enhancers = [
26 | applyMiddleware(thunk),
27 | // @ts-ignore
28 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
29 | ].filter(e => e)
30 |
31 | const store = createStore(
32 | persistedReducer,
33 | compose(...enhancers)
34 | )
35 |
36 | const persistor = persistStore(store)
37 |
38 | export interface Action {
39 | type: string
40 | payload?: T
41 | }
42 |
43 | export type RootState = ReturnType
44 |
45 | export type Thunk =
46 | ThunkAction>
47 |
48 | export type ThunkDispatch = (thunk: Thunk) => Promise
49 |
50 | export { store, persistor }
51 |
52 |
--------------------------------------------------------------------------------
/src/services/Authentication.service.ts:
--------------------------------------------------------------------------------
1 | import http from "../utils/http";
2 |
3 | export interface User {
4 | _id: string
5 | user: string
6 | email: string
7 | role: 'admin' | 'customer'
8 | token: string
9 | createdAt: string
10 | updatedAt: string
11 | }
12 |
13 | export const signInUser = (user: string, pass: string) =>
14 | http
15 | .post('/authentication/login', { user, pass })
16 | .then(res => res.data)
--------------------------------------------------------------------------------
/src/services/Products.service.ts:
--------------------------------------------------------------------------------
1 | import http from '../utils/http'
2 | import { Product } from '../shared/Table/Table.mockdata'
3 | import { ProductCreator } from '../components/Products/ProductForm'
4 |
5 | export const getAllProducts = () =>
6 | http
7 | .get('/products')
8 | .then(res => res.data)
9 |
10 | export const createSingleProduct = (product: ProductCreator) =>
11 | http
12 | .post('/products', product)
13 |
14 | export const updateSingleProduct = ({ _id, name, price, stock }: Product) =>
15 | http
16 | .patch(`/products/${_id}`, {
17 | ...(name && { name }),
18 | ...(price && { price }),
19 | ...(stock && { stock }),
20 | })
21 |
22 | export const deleteSingleProduct = (id: string) =>
23 | http
24 | .delete(`/products/${id}`)
--------------------------------------------------------------------------------
/src/shared/Button/Button.css:
--------------------------------------------------------------------------------
1 | button.AppButton {
2 | background-color: #09f;
3 | color: #fff;
4 | border-radius: 5px;
5 | box-shadow: 0 3px 6px;
6 | border: none;
7 | padding: 7px 10px;
8 |
9 | transition: .25s ease;
10 | cursor: pointer;
11 | }
12 |
13 | button.AppButton:hover {
14 | background-color: #07f;
15 | box-shadow: 0 3px 6px rgba(0,0,0,.2);
16 | transform: translateY(-3px);
17 | }
--------------------------------------------------------------------------------
/src/shared/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Button.css'
3 |
4 | declare interface ButtonProps {
5 | content?: string
6 | onClick?: () => void
7 | appendIcon?: JSX.Element
8 | children: string
9 | }
10 |
11 | const Button: React.FC = (props) => {
12 | return
19 | }
20 |
21 | export default Button
22 |
--------------------------------------------------------------------------------
/src/shared/Button/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Button'
2 |
--------------------------------------------------------------------------------
/src/shared/Container/Container.css:
--------------------------------------------------------------------------------
1 | div.AppContainer {
2 | margin: 10px auto;
3 | max-width: 640px;
4 | }
--------------------------------------------------------------------------------
/src/shared/Container/Container.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Container.css'
3 |
4 | const Container: React.FC<{ children: JSX.Element | JSX.Element[]}> = (props) => {
5 | return
6 | { props.children }
7 |
8 | }
9 |
10 | export default Container
11 |
--------------------------------------------------------------------------------
/src/shared/Container/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Container'
2 |
--------------------------------------------------------------------------------
/src/shared/Form/Form.scss:
--------------------------------------------------------------------------------
1 | .AppForm {
2 | margin: 10px 0;
3 | background-color: #fff;
4 | border-radius: 5px;
5 | box-shadow: 0 3px 6px rgba(#000, .1);
6 | padding: 20px;
7 |
8 | > *:not(:last-child) {
9 | margin-bottom: 20px;
10 | }
11 |
12 | .Title {
13 | font-size: .85em;
14 | background-color: #09f;
15 | color: #fff;
16 | margin: -20px;
17 | margin-bottom: 20px;
18 | padding: 15px 20px;
19 | border-top-right-radius: 5px;
20 | border-top-left-radius: 5px;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/shared/Form/Form.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Form.scss'
3 |
4 | declare interface FormProps {
5 | title?: string
6 | onSubmit?: (event: React.FormEvent) => void
7 | children: JSX.Element | JSX.Element[]
8 | }
9 |
10 | const Form: React.FC = (props) => {
11 | const preventedSubmit = (event: React.FormEvent) => {
12 | event.preventDefault()
13 | props.onSubmit && props.onSubmit(event)
14 | }
15 |
16 | return
27 | }
28 |
29 | export default Form
30 |
--------------------------------------------------------------------------------
/src/shared/Form/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Form'
--------------------------------------------------------------------------------
/src/shared/Input/Input.css:
--------------------------------------------------------------------------------
1 | .AppInput {
2 | height: 45px;
3 | }
4 |
5 | .AppInput label {
6 | display: flex;
7 | flex-direction: column;
8 | }
9 |
10 | .AppInput label span {
11 | text-transform: uppercase;
12 | color: #09f;
13 | font-size: .8em;
14 | height: 15px;
15 | letter-spacing: .75px;
16 | }
17 |
18 | .AppInput label input {
19 | border: none;
20 | border-bottom: 1px solid #ccc;
21 | border-radius: 0; /* fix edge's default border-radius */
22 | height: 30px;
23 | color: #222;
24 | }
25 |
26 | .AppInput label input:focus {
27 | border-color: #09f;
28 | outline: none;
29 | }
30 |
31 | .AppInput label input::placeholder {
32 | color: #ccc;
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/shared/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Input.css'
3 |
4 | declare interface InputProps extends React.InputHTMLAttributes {
5 | label: string
6 | }
7 |
8 | const Input: React.FC = (props) => {
9 | return
10 |
16 |
17 | }
18 |
19 | export default Input
--------------------------------------------------------------------------------
/src/shared/Input/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Input'
--------------------------------------------------------------------------------
/src/shared/Table/Table.mockdata.ts:
--------------------------------------------------------------------------------
1 | export interface Product {
2 | _id: string
3 | name: string
4 | price: number
5 | stock: number
6 | createdAt?: string
7 | updatedAt?: string
8 | }
9 |
10 | const Products: Product[] = [
11 | {
12 | _id: '1',
13 | name: 'Cookie',
14 | price: 1.25,
15 | stock: 20
16 | },
17 | {
18 | _id: '2',
19 | name: 'Milk 1l',
20 | price: 0.99,
21 | stock: 10
22 | },
23 | {
24 | _id: '3',
25 | name: 'Detergent',
26 | price: 0.75,
27 | stock: 65
28 | },
29 | {
30 | _id: '4',
31 | name: 'Water 1l',
32 | price: 0.30,
33 | stock: 150
34 | }
35 | ]
36 |
37 | export default Products
38 |
--------------------------------------------------------------------------------
/src/shared/Table/Table.scss:
--------------------------------------------------------------------------------
1 | table.AppTable {
2 | width: 100%;
3 | box-shadow: 0 3px 6px rgba(#000, .1);
4 | border-collapse: collapse;
5 | border-radius: 5px;
6 | overflow: hidden;
7 |
8 | thead {
9 | background-color: #09f;
10 |
11 | tr th {
12 | color: #fff;
13 | text-align: left;
14 | padding: 7px 10px;
15 | font-size: .8em;
16 | font-weight: 400;
17 |
18 | &.right {
19 | text-align: right;
20 | }
21 | }
22 | }
23 |
24 | tbody {
25 | tr {
26 | &:hover {
27 | background-color: rgba(#000, .05);
28 | }
29 | td {
30 | padding: 7px 10px;
31 | color: #222;
32 |
33 | &.right {
34 | text-align: right;
35 | }
36 | }
37 | }
38 | }
39 |
40 | .actions {
41 | > *:not(:last-child) {
42 | margin-right: 5px;
43 | }
44 | }
45 | }
46 |
47 | .Table__pagination {
48 | display: flex;
49 | justify-content: flex-end;
50 | margin-top: 7px;
51 |
52 | a {
53 | width: 25px;
54 | height: 25px;
55 |
56 | background-color: #fff;
57 | border: 1px solid #ccc;
58 |
59 | color: #09f;
60 |
61 | border-radius: 5px;
62 |
63 | display: flex;
64 | justify-content: center;
65 | align-items: center;
66 |
67 | text-decoration: none;
68 | font-size: 12px;
69 |
70 | &:not(:last-child) {
71 | margin-right: 5px;
72 | }
73 |
74 | transition: .25s ease;
75 | &:hover {
76 | background-color: #09f;
77 | color: #fff;
78 | border-color: #09f;
79 | box-shadow: 0 3px 6px rgba(#000, .2);
80 | transform: translateY(-3px);
81 | }
82 |
83 | &.selected {
84 | color: #222;
85 | opacity: 0.5;
86 | cursor: default;
87 | pointer-events: none;
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/shared/Table/Table.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Table.scss'
3 | import organizeData from '../../utils/organizeDataForTable'
4 | import Button from '../Button'
5 | import { NavLink, useLocation } from 'react-router-dom'
6 | import { parse } from 'query-string'
7 | import paginate from '../../utils/paginate'
8 | export interface TableHeader {
9 | key: string
10 | value: string
11 | right?: boolean
12 | }
13 | declare interface TableProps {
14 | headers: TableHeader[]
15 | data: any[]
16 |
17 | enableActions?: boolean
18 |
19 | itemsPerPage?: number
20 |
21 | onDelete?: (item : any) => void
22 | onDetail?: (item : any) => void
23 | onEdit?: (item : any) => void
24 | }
25 |
26 | const Table: React.FC = (props) => {
27 | const itemsPerPage = props.itemsPerPage || 5
28 | const location = useLocation()
29 |
30 | const page = parseInt(parse(location.search).page as string) || 1
31 | const [organizedData, indexedHeaders] = organizeData(props.data, props.headers)
32 | const paginatedData = paginate(organizedData, itemsPerPage, page)
33 | const totalPages = Math.ceil(organizedData.length / itemsPerPage)
34 | return <>
35 |
36 |
37 |
38 | {
39 | props.headers.map(header =>
40 |
44 | {header.value}
45 | |
46 | )
47 | }
48 | {
49 | props.enableActions
50 | &&
51 | Actions
52 | |
53 | }
54 |
55 |
56 |
57 | {
58 | paginatedData.map((row, i) => {
59 | return
60 | {
61 | Object
62 | .keys(row)
63 | .map((item, i) =>
64 | item !== '$original'
65 | ?
69 | { row[item] }
70 | |
71 | : null
72 | )
73 | }
74 |
75 | {
76 | props.enableActions
77 | &&
78 | {
79 | props.onEdit &&
80 |
85 | }
86 | {
87 | props.onDetail &&
88 |
93 | }
94 | {
95 | props.onDelete &&
96 |
101 | }
102 | |
103 | }
104 |
105 | })
106 | }
107 |
108 |
109 |
110 | {
111 | Array(totalPages)
112 | .fill('')
113 | .map((_, i) => {
114 | return page === i + 1 ? "selected" : ""}
117 | to={{
118 | pathname: location.pathname,
119 | search: `?page=${i + 1}`
120 | }}>
121 | { i + 1 }
122 |
123 | })
124 | }
125 |
126 | >
127 | }
128 |
129 | export default Table
130 |
--------------------------------------------------------------------------------
/src/shared/Table/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from './Table'
2 | export * from './Table'
--------------------------------------------------------------------------------
/src/utils/HOC/withPermission.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react'
2 | import { useSelector } from 'react-redux'
3 | import { RootState } from '../../redux'
4 | import { redirect as redirectPage } from 'react-router-dom'
5 |
6 | type Role = 'admin' | 'customer' | undefined
7 |
8 | const withPermission =
9 | (roles: Role[], redirect = '') =>
10 | (Component: FC) =>
11 | (props: any) => {
12 | const auth = useSelector((state: RootState) => ({
13 | profile: state.authentication.profile
14 | }))
15 |
16 | return roles.includes(auth.profile?.role)
17 | ?
18 | : redirect
19 | ? redirectPage(redirect)
20 | : null
21 | }
22 |
23 | export default withPermission
--------------------------------------------------------------------------------
/src/utils/http.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { store } from '../redux'
3 |
4 | const http = axios.create({
5 | baseURL: 'http://localhost:3024',
6 | })
7 |
8 | http.interceptors.request.use((config) => {
9 | const token = store.getState().authentication.profile?.token
10 |
11 | if (token)
12 | config!.headers!.Authorization = `Bearer ${token}`
13 |
14 | return config
15 | })
16 |
17 | export default http
18 |
--------------------------------------------------------------------------------
/src/utils/organizeDataForTable.ts:
--------------------------------------------------------------------------------
1 | import { TableHeader } from "../shared/Table"
2 |
3 | type IndexedHeaders = {
4 | [key: string]: TableHeader
5 | }
6 |
7 | type OrganizedItem = {
8 | [key: string]: any
9 | }
10 |
11 | export default function organizeData (data: any[], headers: TableHeader[]):
12 | [OrganizedItem[], IndexedHeaders] {
13 | const indexedHeaders: IndexedHeaders = {}
14 |
15 | headers.forEach(header => {
16 | indexedHeaders[header.key] = {
17 | ...header
18 | }
19 | })
20 |
21 | const headerKeysInOrder = Object.keys(indexedHeaders)
22 |
23 | const organizedData = data.map(item => {
24 | const organizedItem: OrganizedItem = {}
25 |
26 | headerKeysInOrder.forEach(key => {
27 | organizedItem[key] = item[key]
28 | })
29 |
30 | organizedItem.$original = item
31 |
32 | return organizedItem
33 | })
34 |
35 | return [organizedData, indexedHeaders]
36 | }
37 |
--------------------------------------------------------------------------------
/src/utils/paginate.ts:
--------------------------------------------------------------------------------
1 | export default function paginate(
2 | array: any[],
3 | itemsPerPage: number,
4 | actualPage: number
5 | ) {
6 | return array.slice(
7 | (actualPage - 1) * itemsPerPage,
8 | actualPage * itemsPerPage
9 | )
10 | }
--------------------------------------------------------------------------------
/src/views/LoginView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import LoginForm from '../components/Authentication/LoginForm'
3 |
4 | const LoginView = () => {
5 | return
15 | }
16 |
17 | export default LoginView
18 |
--------------------------------------------------------------------------------
/src/views/NotFoundView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | const NotFoundView = () => {
5 | return
13 |
404
17 |
Não encontrado
18 |
24 | Voltar para a Home
25 |
26 |
27 | }
28 |
29 | export default NotFoundView
--------------------------------------------------------------------------------
/src/views/ProductsView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import Header from '../components/Header'
4 | import Container from '../shared/Container'
5 | import ProductsCRUD from '../components/Products/ProductsCRUD'
6 |
7 | const HomeView = () => {
8 | return <>
9 |
10 |
11 |
12 |
13 | >
14 | }
15 |
16 | export default HomeView
--------------------------------------------------------------------------------
/src/views/ProfileView.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React from 'react'
3 | import { connect } from 'react-redux'
4 | import ProfileCard, { User } from '../components/Authentication/ProfileCard'
5 | import Header from '../components/Header'
6 | import Container from '../shared/Container'
7 | import withPermission from '../utils/HOC/withPermission'
8 |
9 | declare interface ProfileViewProps {
10 | user: User
11 | }
12 |
13 | const ProfileView: React.FC = (props) => {
14 | return <>
15 |
16 |
17 |
23 |
24 | >
25 | }
26 |
27 | const mapStateToProps = () => ({
28 | user: {
29 | name: 'Daniel Bonifacio',
30 | email: 'daniel.bonifacio@algaworks.com'
31 | }
32 | })
33 |
34 | export default connect(mapStateToProps)(
35 | withPermission(['admin', 'customer'], '/')(ProfileView)
36 | )
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react"
21 | },
22 | "include": [
23 | "src"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------