diff --git a/README.md b/README.md index 4b396901f..4930e2d52 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,32 @@ Quando o usuário clicar em "Adicionar", o formulário deverá ser resetado e o - O canditado deverá realizar um fork deste repositório e submeter o código no mesmo; - O prazo de entrega para este desafio é de 2 (duas) semanas, contando a partir do dia em que o candidato recebeu o email com o link do repositório; - Ao finalizar o desafio, o candidato deverá enviar um email para jobs@clubpetro.com.br contendo o link do seu PR. + +### Baixe e execute o projeto localmente + +**1 -** Clone o projeto e instale as dependências: +``` +$ git clone https://github.com/wagnerGCastro/desafio-react-clubpetro-frontend +$ cd desafio-react-clubpetro-frontend +$ cd lugares +$ yarn +``` + +**2 -** Subir servidor JSON Server +``` +- Abra o terminal, dentro da pasta lugares execute: +$ yarn json-server + ou +$ yarn json-server --watch db.json -p 3065 +``` + +**3 -** Rodar a aplicação no browser +``` +- Abra outro terminal, dentro da pasta lugares execute: +$ yarn start +``` +Abra o navegador e acesse [https://localhost:3033](https://localhost:3033) para visualizar o projeto. + +**4 -** Preview do projeto finalizado e hospedado :) + +Acesse [http://desafio-clubpetro-frontend.wagnercastro.tk/](http://desafio-clubpetro-frontend.wagnercastro.tk/) \ No newline at end of file diff --git a/lugares/.gitignore b/lugares/.gitignore new file mode 100644 index 000000000..4f95daf1f --- /dev/null +++ b/lugares/.gitignore @@ -0,0 +1,38 @@ + +# Add any directories, files, or patterns you don't want to be tracked by version control +#ignorar arquivos do vscode +.vscode/* +!.vscode/.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +\.idea/* + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json + +\.eslintcache diff --git a/lugares/db.json b/lugares/db.json new file mode 100644 index 000000000..1d3fdb754 --- /dev/null +++ b/lugares/db.json @@ -0,0 +1,74 @@ +{ + "lugares": [ + { + "country": "Estados Unidos", + "flag": "https://restcountries.eu/data/usa.svg", + "local": "Disney World", + "meta": "12/2022", + "id": 56 + }, + { + "id": 57, + "country": "Itália", + "flag": "https://restcountries.eu/data/ita.svg", + "local": "Veneza", + "meta": "12/2022" + }, + { + "id": 58, + "country": "México", + "flag": "https://restcountries.eu/data/mex.svg", + "local": "Cancú", + "meta": "05/2022" + }, + { + "country": "França", + "flag": "https://restcountries.eu/data/fra.svg", + "local": "Paris", + "meta": "04/2023", + "id": 59 + }, + { + "country": "Emirados árabes Unidos", + "flag": "https://restcountries.eu/data/are.svg", + "local": "Dubai", + "meta": "01/2024", + "id": 60 + }, + { + "country": "Estados Unidos", + "flag": "https://restcountries.eu/data/usa.svg", + "local": "Filadélfia", + "meta": "12/2022", + "id": 61 + }, + { + "country": "Estados Unidos", + "flag": "https://restcountries.eu/data/usa.svg", + "local": "Nova York", + "meta": "12/2022", + "id": 62 + }, + { + "country": "Brasil", + "flag": "https://restcountries.eu/data/bra.svg", + "local": "Fortaleza", + "meta": "05/2022", + "id": 63 + }, + { + "country": "Bolívia", + "flag": "https://restcountries.eu/data/bol.svg", + "local": "Salar de Uyuni", + "meta": "04/2023", + "id": 64 + }, + { + "country": "Brasil", + "flag": "https://restcountries.eu/data/bra.svg", + "local": "Balneário Camboriu", + "meta": "04/2022", + "id": 65 + } + ] +} \ No newline at end of file diff --git a/lugares/package.json b/lugares/package.json new file mode 100644 index 000000000..39a7b520c --- /dev/null +++ b/lugares/package.json @@ -0,0 +1,59 @@ +{ + "name": "desafio-react-clubpetro-frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "@material-ui/core": "^4.11.2", + "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "^4.0.0-alpha.57", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "@types/jest": "^26.0.15", + "@types/node": "^12.0.0", + "@types/react": "^16.9.53", + "@types/react-dom": "^16.9.8", + "axios": "^0.21.1", + "clsx": "^1.1.1", + "fontsource-roboto": "^4.0.0", + "json-server": "^0.16.3", + "node-sass": "^4.14.1", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-input-mask": "^2.0.4", + "react-paginate": "^7.0.0", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.1", + "typescript": "^4.0.3", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "PORT=3033 react-scripts start", + "build": "react-scripts build", + "json-server": "npx json-server --watch db.json --port=3065", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/react-input-mask": "^3.0.0", + "@types/react-router-dom": "^5.1.7" + } +} diff --git a/lugares/public/favicon.ico b/lugares/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/lugares/public/favicon.ico differ diff --git a/lugares/public/index.html b/lugares/public/index.html new file mode 100644 index 000000000..7715b54a9 --- /dev/null +++ b/lugares/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + ClubPetro Challenge + + + +
+ + + diff --git a/lugares/public/logo32.png b/lugares/public/logo32.png new file mode 100644 index 000000000..30fe87520 Binary files /dev/null and b/lugares/public/logo32.png differ diff --git a/lugares/public/logo512.png b/lugares/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/lugares/public/logo512.png differ diff --git a/lugares/public/manifest.json b/lugares/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/lugares/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/lugares/public/robots.txt b/lugares/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/lugares/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/lugares/public/static/.gitkeep b/lugares/public/static/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/lugares/public/static/lugares-logo.png b/lugares/public/static/lugares-logo.png new file mode 100644 index 000000000..1a6652d31 Binary files /dev/null and b/lugares/public/static/lugares-logo.png differ diff --git a/lugares/public/static/lugares.png b/lugares/public/static/lugares.png new file mode 100644 index 000000000..822d4812f Binary files /dev/null and b/lugares/public/static/lugares.png differ diff --git a/lugares/src/App.tsx b/lugares/src/App.tsx new file mode 100644 index 000000000..82ec11438 --- /dev/null +++ b/lugares/src/App.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { BrowserRouter } from 'react-router-dom'; +import { ThemeProvider } from '@material-ui/core/styles'; +import theme from 'configs/theme'; +import Routes from './routes'; + +const App: React.FC = () => { + return ( + + + + + + ); +} + +export default App; diff --git a/lugares/src/components/CardList.tsx b/lugares/src/components/CardList.tsx new file mode 100644 index 000000000..b405539f6 --- /dev/null +++ b/lugares/src/components/CardList.tsx @@ -0,0 +1,178 @@ +import React from 'react'; +import clsx from 'clsx'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import { + Grid, Card, Box, ButtonGroup, Typography, IconButton, + DialogActions, DialogContent, Button, Dialog +} from '@material-ui/core'; +import EditIcon from '@material-ui/icons/Edit'; +import ClearIcon from '@material-ui/icons/Clear'; + +//---------------------- Interfaces ------------------------// +import { FormDataUpdInterface } from 'pages/Home' + +interface PropsInterface { + onClickEdit(event: React.MouseEvent, id: string | number): void; + formData: Array + onClickRemove(event: React.MouseEvent): void; + dialogConfirmDelete(event: React.MouseEvent, id: string | number): void; + onDialog: boolean; + closeDialog(): void; +} + +//------------------- Custom Material Ui -------------------// +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + overflow: 'hidden', + paddingTop: '50px', + paddingBottom: '30px', + flexGrow: 1, + }, + card: { + height: '245px', + width: '245px', + padding: '5px 10px 8px', + marginTop: '7px', + marginBottom: '7px', + borderRadius: '10px', + boxShadow: "0px 4px 2px 0px rgba(0,0,0,0.2), 0px 1px 1px 0px rgba(0,0,0,0.14), 0px 1px 3px 0px rgba(0,0,0,0.12)", + '&:hover': { + background: '#fefefe' + } + }, + cardHead: { + position: 'relative', + display: 'flex', + justifyContent: 'space-between', + }, + cardIcons: { + position: 'absolute', + right: '-5px', + top: 0 + }, + cardfigure: { + position: 'relative', + margin: '15px 0 10px', + padding: 0 + }, + cardSvg: { + width: '56px', + height: '34px' + }, + cardCountry: { + fontWeight: 700, + fontSize: '1rem', + color: '#4F9419', + textTransform: 'uppercase' + }, + carDesc: { + '& p': { + color: '#000', + fontSize: '1rem', + } + }, + cardLine: { + border: '1px solid #ABABAB' + }, + buttonGroup: { + marginTop: '10px', + padding: 0 + }, + iconButton: { + padding: 0, + minWidth: '30px' + }, + icon: { + color: '#868686', + }, + respContainer: { + [theme.breakpoints.up('lg')]: { + maxWidth: '1346px', + margin: 'auto' + }, + } + }), +); + +const CardList: React.FC = (props) => { + const classes = useStyles(); + + return ( + <> + + + + {props.formData.map((c, idx) => ( + + + + + +
+ lugares-logo +
+ {c.country} +
+
+ + + + props.onClickEdit(event, c.id)} data-id={c.id} className={classes.iconButton}> + + + + props.dialogConfirmDelete(event, c.id)} className={classes.iconButton}> + + + + +
+ +
+ + + + + + Local: {c.local} + + + + + + + Meta: {c.meta} + + + + +
+
+ ))} +
+
+
+ + + + Tem certeza que deseja deletar? + + + + + + + + + ); +} + +export default CardList; diff --git a/lugares/src/components/FormAdd.tsx b/lugares/src/components/FormAdd.tsx new file mode 100644 index 000000000..2bd00d554 --- /dev/null +++ b/lugares/src/components/FormAdd.tsx @@ -0,0 +1,225 @@ +import React from 'react'; +import InputMask from "react-input-mask"; +import clsx from 'clsx'; +import { makeStyles, Theme, withStyles, createStyles } from '@material-ui/core/styles'; +import { + Container, Grid, TextField, Box,Button, FormControl, + FormHelperText, NativeSelect, InputBase +} from '@material-ui/core'; + +//---------------------- Interfaces ------------------------// +import { CountryInterface } from 'pages/Home' +import { FormDataAddInterface } from 'pages/Home' + +interface PropsInterface { + onSubmitAdd(event: React.FormEvent): void; + onChangeFormAdd(event: React.ChangeEvent): void; + onChangeFormDataFlag(event: {target: HTMLInputElement} | React.ChangeEvent): void; + formDataAdd?: FormDataAddInterface; + countryes?: Array + onErrorCountry: boolean; + onErrorLocal: boolean; + onErrorMeta: boolean; +} + +//------------------- Custom Material Ui -------------------// +const BootstrapInput = withStyles((theme: Theme) => + createStyles({ + root: { + 'label + &': { + marginTop: theme.spacing(0), + }, + }, + input: { + borderRadius: 4, + position: 'relative', + backgroundColor: theme.palette.background.paper, + border: '1px solid #ced4da', + fontSize: 16, + color: '#868686', + padding: '12px 10px 13px', + transition: theme.transitions.create(['border-color', 'box-shadow']), + } + }), +)(InputBase); + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + sectionFormAdd: { + background: '#4F9419', + padding: '55px 0 60px', + }, + root: { + '& .MuiOutlinedInput-notchedOutline': { + display: 'none' + }, + '& .MuiFormHelperText-contained': { + marginLleft: '0px', + marginRight: '0px' + } + }, + withoutLabel: { + marginTop: theme.spacing(3), + }, + formControl: { + minWidth: 120, + }, + outlinedInput: { + padding: '0', + borderColor: 'none', + border: '0px', + borderRadius: '7px', + backgroundColor: '#fff', + color: '#868686', + '& :before, & :after, & :hover': { + border: 'none', + borderBottom: 'none !important', + }, + }, + selectEmpty: { + background: '#fff', + margin: theme.spacing(0), + padding:0, + borderColor: 'none', + border: '0px', + borderRadius: '7px', + color: '#868686', + }, + textField: { + width: '100%', + padding: 0, + color: '#fff', + }, + boxGroupInputs: { + display: "flex", + flexWrap: 'wrap' + }, + myButton: { + width: '100%', + marginTop: '19px', + padding: '11.5px', + textTransform: 'initial' + }, + error: { + border: '1px solid #fff817', + borderRadius: '4px' + }, + errorColor: { + color: '#fff817 !important' + }, + respBoxInputs: { + [theme.breakpoints.down('sm')]: { + width: '100%' + }, + }, + }), +); + +const FormAdd: React.FC = (props) => { + const classes = useStyles(); + const { + onSubmitAdd, onChangeFormAdd, onChangeFormDataFlag, formDataAdd, + countryes, onErrorCountry, onErrorLocal, onErrorMeta + } = props; + + return ( +
+ + +
+ + + + + + } + > + + {countryes?.map((c, idx) => ( + + ))} + + { + onErrorCountry ? + ( + *Campo obrigatório) + : null + } + + + + + + + + { + onErrorLocal ? + ( + *Campo obrigatório + ) + : null + } + + + + + + + + {() => } + + { + onErrorMeta ? + ( + *Campo obrigatório
*Data (01 á 12) e Ano (2021 á 2099) +
) + : null} +
+
+ + + + +
+
+
+
+
+ ); +} + +export default FormAdd; + diff --git a/lugares/src/components/FormEdit.tsx b/lugares/src/components/FormEdit.tsx new file mode 100644 index 000000000..c034eda1c --- /dev/null +++ b/lugares/src/components/FormEdit.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import InputMask from "react-input-mask"; +import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles'; +import { + Button, Dialog, FormControl, NativeSelect, + InputBase, FormHelperText, OutlinedInput +} from '@material-ui/core'; + +import MuiDialogContent from '@material-ui/core/DialogContent'; +import MuiDialogActions from '@material-ui/core/DialogActions'; + +//---------------------- Interfaces ------------------------// +import { CountryInterface } from 'pages/Home' +import { FormDataUpdInterface } from 'pages/Home' + +interface PropsInterface { + onSubmitUpdate(event: React.MouseEvent): void; + editFormChange(event: React.ChangeEvent): void; + editFormChangeFlag(event: { target: HTMLInputElement } | React.ChangeEvent): void; + sEditForm?: FormDataUpdInterface; + countries?: Array + handleClickOpen(event: React.MouseEvent): void; + handleClose(event: React.MouseEvent): void; + open: boolean; + sEditErrorCountry: boolean; + sEditErrorLocal: boolean; + sEditErrorMeta: boolean; +} + +//------------------- Custom Material Ui -------------------// +const BootstrapInput = withStyles((theme: Theme) => + createStyles({ + root: { + 'label + &': { + marginTop: theme.spacing(0), + }, + }, + input: { + borderRadius: 4, + position: 'relative', + backgroundColor: theme.palette.background.paper, + border: '1px solid #ced4da', + fontSize: 16, + color: '#868686', + padding: '12px 10px 13px', + transition: theme.transitions.create(['border-color', 'box-shadow']), + } + }), +)(InputBase); + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + margin: '20px', + '& .MuiDialog-paper': { + margin: '0px' + } + }, + formControl: { + width: '100%', + marginTop: theme.spacing(2) + }, + myButton: { + marginTop: theme.spacing(2), + padding: '11.5px', + textTransform: 'initial' + }, + error: { + border: '1px solid #ff1744', + borderRadius: '4px' + } + }) +); + +const DialogContent = withStyles((theme: Theme) => ({ + root: { + padding: theme.spacing(2), + }, +}))(MuiDialogContent); + +const DialogActions = withStyles((theme: Theme) => ({ + root: { + margin: 0, + padding: theme.spacing(2), + }, +}))(MuiDialogActions); +//------------------------- End ----------------------------// + +const FormEdit: React.FC = (props) => { + const classes = useStyles(); + + const { + sEditForm, editFormChangeFlag, editFormChange, handleClose, open, + onSubmitUpdate, countries, sEditErrorCountry, sEditErrorLocal, sEditErrorMeta + } = props; + + return ( + + +
+ + + } + > + + {countries?.map((c, idx) => { + return( + + ) + })} + + {sEditErrorCountry ? (*Campo obrigatório) : null} + + + + + + { + sEditErrorLocal ? + ( + *Campo obrigatório + ) + : null} + + + + + + {() => } + + { + sEditErrorMeta ? + ( + *Campo obrigatório
*Data (01 á 12) e Ano (2021 á 2099) +
) + : null + } +
+
+
+ + + + + +
+ ); +} + +export default FormEdit; diff --git a/lugares/src/components/Topbar.tsx b/lugares/src/components/Topbar.tsx new file mode 100644 index 000000000..6efb95b68 --- /dev/null +++ b/lugares/src/components/Topbar.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import { Container, AppBar, Toolbar } from '@material-ui/core'; + +//------------------- Custom Material Ui -------------------// +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + flexGrow: 1, + }, + toolbar: { + padding: 0, + }, + }), +); + +const Topbar: React.FC = () => { + const classes = useStyles(); + + return ( +
+ + + + logo + + + +
+ ); +} + +export default Topbar; diff --git a/lugares/src/configs/axios.tsx b/lugares/src/configs/axios.tsx new file mode 100644 index 000000000..a68983a96 --- /dev/null +++ b/lugares/src/configs/axios.tsx @@ -0,0 +1,16 @@ +import axios from "axios"; + +const axiosInstance = axios.create({ + baseURL: "http://localhost:3065", + responseType: "json", + // timeout: 20000 +}); + +const apiCountries = axios.create({ + baseURL: 'https://restcountries.eu/rest/v2', +}); + + +export { + axiosInstance, apiCountries +} diff --git a/lugares/src/configs/theme.jsx b/lugares/src/configs/theme.jsx new file mode 100644 index 000000000..ab6a79dd8 --- /dev/null +++ b/lugares/src/configs/theme.jsx @@ -0,0 +1,25 @@ +import { createMuiTheme } from '@material-ui/core/styles'; + +// A custom theme for this app +const theme = createMuiTheme({ + root: { + overflow: 'none' + }, + palette: { + primary: { + // main: '#556cd6', + main: '#006C18', + }, + secondary: { + main: '#19857b', + }, + error: { + main: '#f44336', + }, + background: { + default: '#fff', + }, + }, +}); + +export default theme; diff --git a/lugares/src/helpers/capitalize.tsx b/lugares/src/helpers/capitalize.tsx new file mode 100644 index 000000000..43fa11c4a --- /dev/null +++ b/lugares/src/helpers/capitalize.tsx @@ -0,0 +1,7 @@ + +export default function capitalize(str: string): void | string { + if (typeof str !== 'string') { + return ''; + } + return str.charAt(0).toUpperCase() + str.substr(1); +} diff --git a/lugares/src/helpers/index.tsx b/lugares/src/helpers/index.tsx new file mode 100644 index 000000000..c675ed9ff --- /dev/null +++ b/lugares/src/helpers/index.tsx @@ -0,0 +1,5 @@ +import capitalize from './capitalize' + +export { + capitalize +} \ No newline at end of file diff --git a/lugares/src/index.tsx b/lugares/src/index.tsx new file mode 100644 index 000000000..be9139874 --- /dev/null +++ b/lugares/src/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import CssBaseline from '@material-ui/core/CssBaseline'; + +// Fonts +import 'fontsource-roboto'; + +import App from './App'; + +ReactDOM.render( + + + + , + document.getElementById('root') +); diff --git a/lugares/src/pages/Home.tsx b/lugares/src/pages/Home.tsx new file mode 100644 index 000000000..2ad170248 --- /dev/null +++ b/lugares/src/pages/Home.tsx @@ -0,0 +1,379 @@ +import React, { useState, useEffect } from 'react'; +import { Snackbar } from '@material-ui/core'; +import MuiAlert, { AlertProps } from '@material-ui/lab/Alert'; +import * as service from 'services/api'; +import { capitalize } from 'helpers'; + +// Components +import AppTopbar from 'components/Topbar'; +import FormAdd from 'components/FormAdd'; +import CardList from 'components/CardList'; +import FormEdit from 'components/FormEdit'; + +//---------------------- Interfaces ------------------------// +export interface CountryInterface { + country: string; + flag: string; +} + +export interface FormDataUpdInterface extends FormDataAddInterface { + id: string ; +} + +export interface FormDataAddInterface { + country: string; + flag: string | undefined; + local: string; + meta: string; +} + +interface CountryesFormDataInterface { + countryes: Array +} + +interface SystemNotification { + status: boolean, + message: string, + type: string +} + +const Home: React.FC = () => { + //---------------------- States ------------------------// + var initialState = { + formAdd: { + country: '', + flag: '', + local: '', + meta: '', + }, + formEdit: { + id: '', + country: '', + flag: '', + local: '', + meta: '', + } + } + + // Countryes API + const [countryes, setCountries] = useState<[CountryInterface]>(); + + // Countryes Json Server + const [formData, setFormData] = useState({ countryes: [] }); + + // Add + const [formDataAdd, setFormDataAdd] = useState(initialState.formAdd); + const [onErrorCountry, setOnErrorCountry] = useState(false); + const [onErrorLocal, setOnErrorLocal] = useState(false); + const [onErrorMeta, setOnErrorMeta] = useState(false); + + // Edit + const [sEditForm, setEditForm] = useState(initialState.formEdit) + const [sEditErrorCountry, setEditErrorCountry] = useState(false) + const [sEditErrorLocal, setEditErrorLocal] = useState(false) + const [sEditErrorMeta, setEditErrorMeta] = useState(false) + const [sEditIdDelete, setEditsEditIdDelete] = useState({id_delete: ''}) + + // Notification + const [open, setOpen] = useState(false); + const [onDialog, setOnDialog] = useState(false); + const [openNotification, setOpenNotification] = useState(false); + const [msnNotification, setMsnNotification] = useState({status: false, message: '', type:''}); + + //---------------------- Methods ------------------------// + const handleClickOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + const closeDialog = () => { + setOnDialog(false); + setEditsEditIdDelete({id_delete: ''}) + }; + + async function loadCountries() { + try { + const { data } = await service.getCountryAll(); + let dataFilter: any = []; + + // Order By Alphabet + data.forEach( (t: any) => dataFilter.push({ country: capitalize(t.translations.pt), flag: t.flag })); + dataFilter.sort((a: any, b: any)=> (a.country > b.country ? 1 : -1)); + setCountries(dataFilter) + + } catch(err) { + systemNotification({ + status: true, + message: 'Ops! Erro ao carregar lista de Países', + type: 'error' + }) + } + } + + async function loadLugares() { + try { + const { data } = await service.getLugaresAll(); + setFormData({countryes: data}) + + } catch(err) { + systemNotification({ + status: true, + message: 'Ops! Erro ao carregar lista de lugares', + type: 'error' + }) + } + } + + async function onSubmitAdd(event: React.FormEvent) { + event.preventDefault(); + let target = event.currentTarget; + let dataForm = { ...formDataAdd } + + // Validation + if(!validateFields(dataForm)) return; + + // Set Add + try { + let { data } = await service.create(dataForm); + setFormData({ countryes: [data, ...formData.countryes] }); + setFormDataAdd(initialState.formAdd); + target.country.value = ""; + systemNotification({status: true, message: 'Cadastrado com sucesso', type: 'success'}) + // clear fields + } catch(err) { + systemNotification({status: true, message: 'Ops! Houve erro ao cadastrar', type: 'error'}) + } + } + + const onClickEdit = (event: React.MouseEvent, idItem: string) => { + event.preventDefault(); + // Reseet Errors + resetErrors(); + editFormResetErrors(); + clearSystemNotification(); + + let result = formData.countryes.find( item => item.id === idItem ); + setEditForm({ ...sEditForm, ...result }) + setOpen(true); + } + + async function onSubmitUpdate(event: React.MouseEvent) { + event.preventDefault(); + let form = { ...sEditForm } + // Validation + if(!editFormValidateFields(form)) return; + + try { + // Set update + await service.update(form.id, form); + let updateItem = []; + updateItem = formData.countryes.map(item => (item.id === form.id ? { ...item, ...form } : item)); + setFormData({countryes: updateItem}) + editFormClear(); + setOpen(false); + systemNotification({status: true, message: 'Atualizado com sucesso', type: 'success'}) + + } catch(err) { + systemNotification({status: true, message: 'Ops! Houve erro ao cadastrar', type: 'error'}) + } + } + + async function onClickRemove (event: React.MouseEvent) { + event.preventDefault(); + setOnDialog(false); + + if (! sEditIdDelete.id_delete || sEditIdDelete.id_delete === '') { + alert('ID não existe') + return; + } + + try { + await service.deleteLugaresId(sEditIdDelete.id_delete); + const removedItem = [...formData.countryes].filter(item => item.id !== sEditIdDelete.id_delete); + setFormData({ countryes: removedItem}) + systemNotification({status: true, message: 'Deletado com sucesso', type: 'success'}) + } catch(err) { + systemNotification({status: true, message: 'Ops! houve um erro ao deletar', type: 'error'}) + } + } + + const dialogConfirmDelete = (event: React.MouseEvent, idItem: string ): void => { + event.preventDefault(); + setOnDialog(true); + setEditsEditIdDelete({id_delete: idItem}) + } + + const onChangeFormAdd = (event: { target: HTMLInputElement } ): void => { + let { name, value } = event.target; + let values = { [name]: value } + setFormDataAdd({...formDataAdd, ...values }); + if(name === 'meta') setOnErrorMeta(false); + if(name === 'local') setOnErrorLocal(false); + } + + const onChangeFormDataFlag = (event: React.ChangeEvent): void => { + let { name, value, selectedOptions } = event.target; + let values = { [name]: value }; + + if(selectedOptions[0].dataset.flag !== '') { + let values = { [name]: value , flag: selectedOptions[0].dataset.flag }; + setFormDataAdd({...formDataAdd, ...values }); + } else { + setFormDataAdd({...formDataAdd, ...values }); + } + + setOnErrorCountry(false); + } + + const editFormChangeFlag = (event: React.ChangeEvent): void => { + let { name, value, selectedOptions } = event.target; + let values = { [name]: value }; + + if(selectedOptions[0].dataset.flag !== '') { + let values = { [name]: value , flag: selectedOptions[0].dataset.flag }; + setEditForm({...sEditForm, ...values }); + } else { + setEditForm({...sEditForm, ...values }); + } + + setOnErrorCountry(false); + } + + const editFormChange = (event: { target: HTMLInputElement }) => { + let { name, value } = event.target; + let values = { [name]: value } + + setEditForm({...sEditForm, ...values }); + + if(name === 'meta') setOnErrorMeta(false); + if(name === 'local') setOnErrorLocal(false); + } + + const systemNotification = (v: SystemNotification) => { + let valuesMens = {status: v.status, message: v.message, type: v.type}; + setMsnNotification({ ...msnNotification, ...valuesMens }); + window.setTimeout(() => setOpenNotification(true), 500); + } + + //---------------------- Validates -----------------------// + const validateFields = (elem: any) => { + let count = 0; + + // Format: date YY (01|12) / Any YYYY (2021|2099) + let date_regex = /^(0[1-9]|1[0-2])\/(20[2-9][1-9])$/; + + if (!elem.country.trim()) {setOnErrorCountry(true); count++;} + if (!elem.local.trim()) {setOnErrorLocal(true); count++;} + if (!elem.meta.trim() || !date_regex.test(elem.meta)) {setOnErrorMeta(true); count++;} + if(count >= 1) return false; + + return true + } + + const editFormValidateFields = (elem: any) => { + let count = 0; + + // Format: date YY (01|12) / Any YYYY (2021|2099) + let date_regex = /^(0[1-9]|1[0-2])\/(20[2-9][1-9])$/; + + if (!elem.country.trim()) {setEditErrorCountry(true); count++;} + if (!elem.local.trim()) {setEditErrorLocal(true); count++;} + if (!elem.meta.trim() || !date_regex.test(elem.meta)) {setEditErrorMeta(true); count++;} + if(count >= 1) return false; + + return true + } + + //---------------- (Clean|Reset) States -----------------// + const editFormClear = () => { + setEditForm(initialState.formEdit) + } + + const clearSystemNotification = () => { + setMsnNotification({ status: false, message: '', type: ''}); + setOpenNotification(false) + } + + const resetErrors = () => { + setOnErrorCountry(false); + setOnErrorLocal(false); + setOnErrorMeta(false); + } + + const editFormResetErrors = () => { + setEditErrorCountry(false); + setEditErrorLocal(false); + setEditErrorMeta(false); + } + + const Alert = (props: AlertProps) => { + return ; + } + + const closeNotification = (event?: React.SyntheticEvent, reason?: string) => { + if (reason === 'clickaway') { + return; + } + + setOpenNotification(false); + }; + + //---------------------- Lifecycles------------------------// + useEffect(() => { + loadCountries(); + loadLugares(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + + + + + + + + { + openNotification && msnNotification.status && + + + { msnNotification.message } + + + } + + ); +} + +export default Home; diff --git a/lugares/src/react-app-env.d.ts b/lugares/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/lugares/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/lugares/src/routes/index.tsx b/lugares/src/routes/index.tsx new file mode 100644 index 000000000..b3f227b6c --- /dev/null +++ b/lugares/src/routes/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import Home from 'pages/Home'; + +const Routes: React.FC = () => ( + + + +); + +export default Routes; diff --git a/lugares/src/services/api.tsx b/lugares/src/services/api.tsx new file mode 100644 index 000000000..2fc703e34 --- /dev/null +++ b/lugares/src/services/api.tsx @@ -0,0 +1,50 @@ +import { axiosInstance as request, apiCountries } from '../configs/axios' + +interface CountryPropsInterface { + country: string; + flag?: string; + local: string; + meta: string; +} + +export function getCountryAll() { + return apiCountries.get('/all') +} + +export function getLugaresAll() { + return request({ + url: '/lugares', + method: 'get' + }) +} + +export function getCountryName(country: string) { + return request({ + url: '/name/'+country, + method: 'get' + }) +} + +export function create(data: CountryPropsInterface) { + return request({ + url: '/lugares', + data, + method: 'post' + }) +} + +export function update(id: string, data: CountryPropsInterface) { + return request({ + url: '/lugares/'+id, + data, + method: 'put' + }) +} + +export function deleteLugaresId(id: string) { + return request({ + url: '/lugares/'+id, + method: 'delete' + }) +} + diff --git a/lugares/tsconfig.json b/lugares/tsconfig.json new file mode 100644 index 000000000..972b640ea --- /dev/null +++ b/lugares/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +}