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) => (
+
+
+
+
+
+
+
+
+ {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}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
+
+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 (
+
+ );
+}
+
+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 (
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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"
+ ]
+}