├── .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
43 | 50 | 57 | 60 |
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 |
17 | 22 | 27 |
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
42 |

{ props.title }

43 |
44 | 45 | { 46 | isLoggedIn ? 'Logout' : 'Login' 47 | } 48 | 49 |
50 |
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
90 | 98 | 109 | 119 | 124 |
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
20 | { 21 | props.title &&
22 | { props.title } 23 |
24 | } 25 | { props.children } 26 | 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 | 46 | ) 47 | } 48 | { 49 | props.enableActions 50 | && 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 | ? 71 | : null 72 | ) 73 | } 74 | 75 | { 76 | props.enableActions 77 | && 103 | } 104 | 105 | }) 106 | } 107 | 108 |
44 | {header.value} 45 | 51 | Actions 52 |
69 | { row[item] } 70 | 78 | { 79 | props.onEdit && 80 | 85 | } 86 | { 87 | props.onDetail && 88 | 93 | } 94 | { 95 | props.onDelete && 96 | 101 | } 102 |
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
11 |
12 | 13 |
14 |
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 |
21 | 22 |
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 | --------------------------------------------------------------------------------