├── learn-react-cover.jpg ├── src ├── components │ ├── pokemon-card.css │ ├── loader.tsx │ ├── pokemon-search.tsx │ ├── pokemon-card.tsx │ └── pokemon-form.tsx ├── index.tsx ├── helpers │ ├── format-date.ts │ └── format-type.ts ├── services │ ├── authentication-service.ts │ └── pokemon-service.ts ├── PrivateRoute.tsx ├── pages │ ├── page-not-found.tsx │ ├── pokemon-add.tsx │ ├── pokemon-edit.tsx │ ├── pokemon-list.tsx │ ├── pokemon-detail.tsx │ └── login.tsx ├── models │ ├── pokemon.ts │ ├── mock-pokemon.ts │ └── db.json └── App.tsx ├── README.md ├── .gitignore ├── public └── index.html ├── tsconfig.json └── package.json /learn-react-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeursenior/react-pokemons-app/HEAD/learn-react-cover.jpg -------------------------------------------------------------------------------- /src/components/pokemon-card.css: -------------------------------------------------------------------------------- 1 | .horizontal { 2 | border: solid 4px #f5f5f5; 3 | height: 200px; 4 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); -------------------------------------------------------------------------------- /src/helpers/format-date.ts: -------------------------------------------------------------------------------- 1 | const formatDate = (date: Date = new Date()): string => { 2 | return `${date.getDate()}/${date.getMonth()+1}/${date.getFullYear()}`; 3 | } 4 | 5 | export default formatDate; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apprendre React: Développez facilement votre première application avec TypeScript ! 2 | *Ce dépôt Github contient le code de la correction de la formation "Apprendre React".* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # typescript 10 | react-app-env.d.ts 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* -------------------------------------------------------------------------------- /src/services/authentication-service.ts: -------------------------------------------------------------------------------- 1 | export default class AuthenticationService { 2 | 3 | static isAuthenticated:boolean = false; 4 | 5 | static login(username: string, password: string): Promise { 6 | const isAuthenticated = (username === 'pikachu' && password === 'pikachu'); 7 | 8 | return new Promise(resolve => { 9 | setTimeout(() => { 10 | this.isAuthenticated = isAuthenticated; 11 | resolve(isAuthenticated); 12 | }, 1000); 13 | }); 14 | } 15 | } -------------------------------------------------------------------------------- /src/PrivateRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import AuthenticationService from './services/authentication-service'; 4 | 5 | const PrivateRoute = ({ component: Component, ...rest }: any) => ( 6 | { 7 | const isAuthenticated = AuthenticationService.isAuthenticated; 8 | if (!isAuthenticated) { 9 | return 10 | } 11 | 12 | return 13 | }} /> 14 | ); 15 | 16 | export default PrivateRoute; -------------------------------------------------------------------------------- /src/pages/page-not-found.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const PageNotFound: FunctionComponent = () => { 5 | 6 | return ( 7 |
8 | Page non trouvée 9 |

Hey, cette page n'existe pas !

10 | 11 | Retourner à l'accueil 12 | 13 |
14 | ); 15 | } 16 | 17 | export default PageNotFound; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pokédex 7 | 8 | 9 | 10 | 11 | 12 |
L'application est en cours de chargement...
13 | 14 | -------------------------------------------------------------------------------- /src/pages/pokemon-add.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from 'react'; 2 | import PokemonForm from '../components/pokemon-form'; 3 | import Pokemon from '../models/pokemon'; 4 | 5 | const PokemonAdd: FunctionComponent = () => { 6 | 7 | const [id] = useState(new Date().getTime()); 8 | const [pokemon] = useState(new Pokemon(id)); 9 | 10 | return ( 11 |
12 |

Ajouter un pokémon

13 | 14 |
15 | ); 16 | } 17 | 18 | export default PokemonAdd; -------------------------------------------------------------------------------- /src/components/loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | 3 | const Loader: FunctionComponent = () => { 4 | 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | 21 | export default Loader; -------------------------------------------------------------------------------- /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 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/models/pokemon.ts: -------------------------------------------------------------------------------- 1 | export default class Pokemon { 2 | id: number; 3 | hp: number; 4 | cp: number; 5 | name: string; 6 | picture: string; 7 | types: Array; 8 | created: Date; 9 | 10 | constructor( 11 | id: number, 12 | hp: number = 100, 13 | cp: number = 10, 14 | name: string = '...', 15 | picture: string = 'https://assets.pokemon.com/assets/cms2/img/pokedex/detail/XXX.png', 16 | types: Array = ['Normal'], 17 | created: Date = new Date() 18 | ) { 19 | this.id = id; 20 | this.hp = hp; 21 | this.cp = cp; 22 | this.name = name; 23 | this.picture = picture; 24 | this.types = types; 25 | this.created = created; 26 | } 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pokemons-app", 3 | "version": "1.0.0", 4 | "description": "An awesome application to handle some pokemons.", 5 | "dependencies": { 6 | "@types/node": "12.11.1", 7 | "@types/react": "16.9.9", 8 | "@types/react-dom": "16.9.2", 9 | "@types/react-router-dom": "^5.1.2", 10 | "react": "^17.0.0", 11 | "react-dom": "^17.0.0", 12 | "react-router-dom": "^5.1.2", 13 | "react-scripts": "^4.0.0", 14 | "typescript": "3.6.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "start:api": "json-server --watch src/models/db.json --port=3001 --delay=500", 19 | "build": "react-scripts build" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/format-type.ts: -------------------------------------------------------------------------------- 1 | const formatType = (type: string): string => { 2 | let color: string; 3 | 4 | switch (type) { 5 | case 'Feu': 6 | color = 'red lighten-1'; 7 | break; 8 | case 'Eau': 9 | color = 'blue lighten-1'; 10 | break; 11 | case 'Plante': 12 | color = 'green lighten-1'; 13 | break; 14 | case 'Insecte': 15 | color = 'brown lighten-1'; 16 | break; 17 | case 'Normal': 18 | color = 'grey lighten-3'; 19 | break; 20 | case 'Vol': 21 | color = 'blue lighten-3'; 22 | break; 23 | case 'Poison': 24 | color = 'deep-purple accent-1'; 25 | break; 26 | case 'Fée': 27 | color = 'pink lighten-4'; 28 | break; 29 | case 'Psy': 30 | color = 'deep-purple darken-2'; 31 | break; 32 | case 'Electrik': 33 | color = 'lime accent-1'; 34 | break; 35 | case 'Combat': 36 | color = 'deep-orange'; 37 | break; 38 | default: 39 | color = 'grey'; 40 | break; 41 | } 42 | 43 | return `chip ${color}`; 44 | } 45 | 46 | export default formatType; -------------------------------------------------------------------------------- /src/pages/pokemon-edit.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState, useEffect } from 'react'; 2 | import { RouteComponentProps } from 'react-router-dom'; 3 | import PokemonForm from '../components/pokemon-form'; 4 | import Pokemon from '../models/pokemon'; 5 | import PokemonService from '../services/pokemon-service'; 6 | import Loader from '../components/loader'; 7 | 8 | 9 | type Params = { id: string }; 10 | 11 | const PokemonEdit: FunctionComponent> = ({ match }) => { 12 | 13 | const [pokemon, setPokemon] = useState(null); 14 | 15 | useEffect(() => { 16 | PokemonService.getPokemon(+match.params.id).then(pokemon => setPokemon(pokemon)); 17 | }, [match.params.id]); 18 | 19 | return ( 20 |
21 | { pokemon ? ( 22 |
23 |

Éditer { pokemon.name }

24 | 25 |
26 | ) : ( 27 |

28 | )} 29 |
30 | ); 31 | } 32 | 33 | export default PokemonEdit; -------------------------------------------------------------------------------- /src/pages/pokemon-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState, useEffect } from 'react'; 2 | import Pokemon from '../models/pokemon'; 3 | import PokemonCard from '../components/pokemon-card'; 4 | import PokemonService from '../services/pokemon-service'; 5 | import { Link } from 'react-router-dom'; 6 | import PokemonSearch from '../components/pokemon-search'; 7 | 8 | const PokemonList: FunctionComponent = () => { 9 | const [pokemons, setPokemons] = useState([]); 10 | 11 | useEffect(() => { 12 | PokemonService.getPokemons().then(pokemons => setPokemons(pokemons)); 13 | }, []); 14 | 15 | return ( 16 |
17 |

Pokédex

18 |
19 |
20 | 21 | {pokemons.map(pokemon => ( 22 | 23 | ))} 24 |
25 |
26 | 29 | add 30 | 31 |
32 | ); 33 | } 34 | 35 | export default PokemonList; -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; 3 | import PokemonsList from './pages/pokemon-list'; 4 | import PokemonsDetail from './pages/pokemon-detail'; 5 | import PokemonEdit from './pages/pokemon-edit'; 6 | import PokemonAdd from './pages/pokemon-add'; 7 | import PageNotFound from './pages/page-not-found'; 8 | import Login from './pages/login'; 9 | import PrivateRoute from './PrivateRoute'; 10 | 11 | const App: FunctionComponent = () => { 12 | 13 | return ( 14 | 15 |
16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | ); 33 | } 34 | 35 | export default App; -------------------------------------------------------------------------------- /src/components/pokemon-search.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Pokemon from '../models/pokemon'; 4 | import PokemonService from '../services/pokemon-service'; 5 | 6 | const PokemonSearch: FunctionComponent = () => { 7 | 8 | const [term, setTerm] = useState(''); 9 | const [pokemons, setPokemons] = useState([]); 10 | 11 | const handleInputChange = (e: React.ChangeEvent): void => { 12 | const term = e.target.value; 13 | setTerm(term); 14 | 15 | if(term.length <= 1) { 16 | setPokemons([]); 17 | return; 18 | } 19 | 20 | PokemonService.searchPokemon(term).then(pokemons => setPokemons(pokemons)); 21 | } 22 | 23 | return ( 24 |
25 |
26 |
27 |
28 |
29 | handleInputChange(e)} /> 30 |
31 |
32 | {pokemons.map((pokemon) => ( 33 | 34 | {pokemon.name} 35 | 36 | ))} 37 |
38 |
39 |
40 |
41 |
42 | ); 43 | } 44 | 45 | export default PokemonSearch; -------------------------------------------------------------------------------- /src/components/pokemon-card.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import Pokemon from '../models/pokemon'; 4 | import formatDate from '../helpers/format-date'; 5 | import formatType from '../helpers/format-type'; 6 | import './pokemon-card.css'; 7 | 8 | type Props = { 9 | pokemon: Pokemon, 10 | borderColor?: string 11 | }; 12 | 13 | const PokemonCard: FunctionComponent = ({pokemon, borderColor = '#009688'}) => { 14 | 15 | const [color, setColor] = useState(); 16 | const history = useHistory(); 17 | 18 | const showBorder = () => { 19 | setColor(borderColor); 20 | }; 21 | 22 | const hideBorder = () => { 23 | setColor('#f5f5f5'); 24 | }; 25 | 26 | const goToPokemon = (id: number) => { 27 | history.push(`/pokemons/${id}`); 28 | } 29 | 30 | return ( 31 |
goToPokemon(pokemon.id)}> 32 |
33 |
34 | {pokemon.name}/ 35 |
36 |
37 |
38 |

{pokemon.name}

39 |

{formatDate(pokemon.created)}

40 | {pokemon.types.map(type => ( 41 | {type} 42 | ))} 43 |
44 |
45 |
46 |
47 | ); 48 | } 49 | 50 | export default PokemonCard; -------------------------------------------------------------------------------- /src/models/mock-pokemon.ts: -------------------------------------------------------------------------------- 1 | import Pokemon from './pokemon'; 2 | 3 | export const POKEMONS: Pokemon[] = [ 4 | { 5 | id: 1, 6 | name: "Bulbizarre", 7 | hp: 25, 8 | cp: 5, 9 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/001.png", 10 | types: ["Plante", "Poison"], 11 | created: new Date() 12 | }, 13 | { 14 | id: 2, 15 | name: "Salamèche", 16 | hp: 28, 17 | cp: 6, 18 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/004.png", 19 | types: ["Feu"], 20 | created: new Date() 21 | }, 22 | { 23 | id: 3, 24 | name: "Carapuce", 25 | hp: 21, 26 | cp: 4, 27 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/007.png", 28 | types: ["Eau"], 29 | created: new Date() 30 | }, 31 | { 32 | id: 4, 33 | name: "Aspicot", 34 | hp: 16, 35 | cp: 2, 36 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/013.png", 37 | types: ["Insecte", "Poison"], 38 | created: new Date() 39 | }, 40 | { 41 | id: 5, 42 | name: "Roucool", 43 | hp: 30, 44 | cp: 7, 45 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/016.png", 46 | types: ["Normal", "Vol"], 47 | created: new Date() 48 | }, 49 | { 50 | id: 6, 51 | name: "Rattata", 52 | hp: 18, 53 | cp: 6, 54 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/019.png", 55 | types: ["Normal"], 56 | created: new Date() 57 | }, 58 | { 59 | id: 7, 60 | name: "Piafabec", 61 | hp: 14, 62 | cp: 5, 63 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/021.png", 64 | types: ["Normal", "Vol"], 65 | created: new Date() 66 | }, 67 | { 68 | id: 8, 69 | name: "Abo", 70 | hp: 16, 71 | cp: 4, 72 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/023.png", 73 | types: ["Poison"], 74 | created: new Date() 75 | }, 76 | { 77 | id: 9, 78 | name: "Pikachu", 79 | hp: 21, 80 | cp: 7, 81 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/025.png", 82 | types: ["Electrik"], 83 | created: new Date() 84 | }, 85 | { 86 | id: 10, 87 | name: "Sabelette", 88 | hp: 19, 89 | cp: 3, 90 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/027.png", 91 | types: ["Normal"], 92 | created: new Date() 93 | }, 94 | { 95 | id: 11, 96 | name: "Mélofée", 97 | hp: 25, 98 | cp: 5, 99 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/035.png", 100 | types: ["Fée"], 101 | created: new Date() 102 | }, 103 | { 104 | id: 12, 105 | name: "Groupix", 106 | hp: 17, 107 | cp: 8, 108 | picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/037.png", 109 | types: ["Feu"], 110 | created: new Date() 111 | } 112 | ]; 113 | 114 | export default POKEMONS; -------------------------------------------------------------------------------- /src/models/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "pokemons": [ 3 | { 4 | "id": 1, 5 | "name": "Bulbizarre", 6 | "hp": 25, 7 | "cp": 5, 8 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/001.png", 9 | "types": ["Plante", "Poison"] 10 | }, 11 | { 12 | "id": 2, 13 | "name": "Salamèche", 14 | "hp": 28, 15 | "cp": 6, 16 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/004.png", 17 | "types": ["Feu"] 18 | }, 19 | { 20 | "id": 3, 21 | "name": "Carapuce", 22 | "hp": 21, 23 | "cp": 4, 24 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/007.png", 25 | "types": ["Eau"] 26 | }, 27 | { 28 | "id": 4, 29 | "name": "Aspicot", 30 | "hp": 16, 31 | "cp": 2, 32 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/013.png", 33 | "types": ["Insecte", "Poison"] 34 | }, 35 | { 36 | "id": 5, 37 | "name": "Roucool", 38 | "hp": 30, 39 | "cp": 7, 40 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/016.png", 41 | "types": ["Normal", "Vol"] 42 | }, 43 | { 44 | "id": 6, 45 | "name": "Rattata", 46 | "hp": 18, 47 | "cp": 6, 48 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/019.png", 49 | "types": ["Normal"] 50 | }, 51 | { 52 | "id": 7, 53 | "name": "Piafabec", 54 | "hp": 14, 55 | "cp": 5, 56 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/021.png", 57 | "types": ["Normal", "Vol"] 58 | }, 59 | { 60 | "id": 8, 61 | "name": "Abo", 62 | "hp": 16, 63 | "cp": 4, 64 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/023.png", 65 | "types": ["Poison"] 66 | }, 67 | { 68 | "id": 9, 69 | "name": "Pikachu", 70 | "hp": 21, 71 | "cp": 7, 72 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/025.png", 73 | "types": ["Electrik"] 74 | }, 75 | { 76 | "id": 10, 77 | "name": "Sabelette", 78 | "hp": 19, 79 | "cp": 3, 80 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/027.png", 81 | "types": ["Normal"] 82 | }, 83 | { 84 | "id": 11, 85 | "name": "Mélofée", 86 | "hp": 25, 87 | "cp": 5, 88 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/035.png", 89 | "types": ["Fée"] 90 | }, 91 | { 92 | "id": 12, 93 | "name": "Groupix", 94 | "hp": 17, 95 | "cp": 8, 96 | "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/037.png", 97 | "types": ["Feu"] 98 | } 99 | ] 100 | } -------------------------------------------------------------------------------- /src/pages/pokemon-detail.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState, useEffect } from 'react'; 2 | import { RouteComponentProps, Link } from 'react-router-dom'; 3 | import Pokemon from '../models/pokemon'; 4 | import formatDate from '../helpers/format-date'; 5 | import formatType from '../helpers/format-type'; 6 | import PokemonService from '../services/pokemon-service'; 7 | import Loader from '../components/loader'; 8 | 9 | type Params = { id: string }; 10 | 11 | const PokemonsDetail: FunctionComponent> = ({ match }) => { 12 | 13 | const [pokemon, setPokemon] = useState(null); 14 | 15 | useEffect(() => { 16 | PokemonService.getPokemon(+match.params.id).then(pokemon => setPokemon(pokemon)); 17 | }, [match.params.id]); 18 | 19 | return ( 20 |
21 | { pokemon ? ( 22 |
23 |
24 |

{ pokemon.name }

25 |
26 |
27 | {pokemon.name} 28 | edit 29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
Nom{ pokemon.name }
Points de vie{ pokemon.hp }
Dégâts{ pokemon.cp }
Types 49 | {pokemon.types.map(type => ( 50 | {type} 51 | ))}
Date de création{formatDate(pokemon.created)}
59 |
60 |
61 | Retour 62 |
63 |
64 |
65 |
66 |
67 | ) : ( 68 |

69 | )} 70 |
71 | ); 72 | } 73 | 74 | export default PokemonsDetail; -------------------------------------------------------------------------------- /src/services/pokemon-service.ts: -------------------------------------------------------------------------------- 1 | import Pokemon from "../models/pokemon"; 2 | import POKEMONS from "../models/mock-pokemon"; 3 | 4 | export default class PokemonService { 5 | 6 | static pokemons:Pokemon[] = POKEMONS; 7 | 8 | static isDev = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development'); 9 | 10 | static getPokemons(): Promise { 11 | if(this.isDev) { 12 | return fetch('http://localhost:3001/pokemons') 13 | .then(response => response.json()) 14 | .catch(error => this.handleError(error)); 15 | } 16 | 17 | return new Promise(resolve => { 18 | resolve(this.pokemons); 19 | }); 20 | } 21 | 22 | static getPokemon(id: number): Promise { 23 | if(this.isDev) { 24 | return fetch(`http://localhost:3001/pokemons/${id}`) 25 | .then(response => response.json()) 26 | .then(data => this.isEmpty(data) ? null : data) 27 | .catch(error => this.handleError(error)); 28 | } 29 | 30 | return new Promise(resolve => { 31 | resolve(this.pokemons.find(pokemon => id === pokemon.id)); 32 | }); 33 | } 34 | 35 | static updatePokemon(pokemon: Pokemon): Promise { 36 | if(this.isDev) { 37 | return fetch(`http://localhost:3001/pokemons/${pokemon.id}`, { 38 | method: 'PUT', 39 | body: JSON.stringify(pokemon), 40 | headers: { 'Content-Type': 'application/json'} 41 | }) 42 | .then(response => response.json()) 43 | .catch(error => this.handleError(error)); 44 | } 45 | 46 | return new Promise(resolve => { 47 | const { id } = pokemon; 48 | const index = this.pokemons.findIndex(pokemon => pokemon.id === id); 49 | this.pokemons[index] = pokemon; 50 | resolve(pokemon); 51 | }); 52 | } 53 | 54 | static deletePokemon(pokemon: Pokemon): Promise<{}> { 55 | if(this.isDev) { 56 | return fetch(`http://localhost:3001/pokemons/${pokemon.id}`, { 57 | method: 'DELETE', 58 | headers: { 'Content-Type': 'application/json'} 59 | }) 60 | .then(response => response.json()) 61 | .catch(error => this.handleError(error)); 62 | } 63 | 64 | return new Promise(resolve => { 65 | const { id } = pokemon; 66 | this.pokemons = this.pokemons.filter(pokemon => pokemon.id !== id); 67 | resolve({}); 68 | }); 69 | } 70 | 71 | static addPokemon(pokemon: Pokemon): Promise { 72 | delete pokemon.created; 73 | 74 | if(this.isDev) { 75 | return fetch(`http://localhost:3001/pokemons`, { 76 | method: 'POST', 77 | body: JSON.stringify(pokemon), 78 | headers: { 'Content-Type': 'application/json'} 79 | }) 80 | .then(response => response.json()) 81 | .catch(error => this.handleError(error)); 82 | } 83 | 84 | return new Promise(resolve => { 85 | this.pokemons.push(pokemon); 86 | resolve(pokemon); 87 | }); 88 | } 89 | 90 | static searchPokemon(term: string): Promise { 91 | if(this.isDev) { 92 | return fetch(`http://localhost:3001/pokemons?q=${term}`) 93 | .then(response => response.json()) 94 | .catch(error => this.handleError(error)); 95 | } 96 | 97 | return new Promise(resolve => { 98 | const results = this.pokemons.filter(pokemon => pokemon.name.includes(term)); 99 | resolve(results); 100 | }); 101 | 102 | } 103 | 104 | static isEmpty(data: Object): boolean { 105 | return Object.keys(data).length === 0; 106 | } 107 | 108 | static handleError(error: Error): void { 109 | console.error(error); 110 | } 111 | } -------------------------------------------------------------------------------- /src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import AuthenticationService from '../services/authentication-service'; 4 | 5 | type Field = { 6 | value?: any, 7 | error?: string, 8 | isValid?: boolean 9 | }; 10 | 11 | type Form = { 12 | username: Field, 13 | password: Field 14 | } 15 | 16 | const Login: FunctionComponent = () => { 17 | 18 | const history = useHistory(); 19 | 20 | const [form, setForm] = useState
({ 21 | username: { value: '' }, 22 | password: { value: '' }, 23 | }); 24 | 25 | const [message, setMessage] = useState('Vous êtes déconnecté. (pikachu / pikachu)'); 26 | 27 | const handleInputChange = (e: React.ChangeEvent): void => { 28 | const fieldName: string = e.target.name; 29 | const fieldValue: string = e.target.value; 30 | const newField: Field = { [fieldName]: { value: fieldValue } }; 31 | 32 | setForm({ ...form, ...newField}); 33 | } 34 | 35 | const validateForm = () => { 36 | let newForm: Form = form; 37 | 38 | // Validator username 39 | if(form.username.value.length < 3) { 40 | const errorMsg: string = 'Votre prénom doit faire au moins 3 caractères de long.'; 41 | const newField: Field = { value: form.username.value, error: errorMsg, isValid: false }; 42 | newForm = { ...newForm, ...{ username: newField } }; 43 | } else { 44 | const newField: Field = { value: form.username.value, error: '', isValid: true }; 45 | newForm = { ...newForm, ...{ username: newField } }; 46 | } 47 | 48 | // Validator password 49 | if(form.password.value.length < 6) { 50 | const errorMsg: string = 'Votre mot de passe doit faire au moins 6 caractères de long.'; 51 | const newField: Field = {value: form.password.value, error: errorMsg, isValid: false}; 52 | newForm = { ...newForm, ...{ password: newField } }; 53 | } else { 54 | const newField: Field = { value: form.password.value, error: '', isValid: true }; 55 | newForm = { ...newForm, ...{ password: newField } }; 56 | } 57 | 58 | setForm(newForm); 59 | 60 | return newForm.username.isValid && newForm.password.isValid; 61 | } 62 | 63 | const handleSubmit = (e: React.FormEvent) => { 64 | e.preventDefault(); 65 | const isFormValid = validateForm(); 66 | if(isFormValid) { 67 | setMessage('👉 Tentative de connexion en cours ...'); 68 | AuthenticationService.login(form.username.value, form.password.value).then(isAuthenticated => { 69 | if(!isAuthenticated) { 70 | setMessage('🔐 Identifiant ou mot de passe incorrect.'); 71 | return; 72 | } 73 | 74 | history.push('/pokemons'); 75 | 76 | }); 77 | } 78 | } 79 | 80 | return ( 81 | handleSubmit(e)}> 82 |
83 |
84 |
85 |
86 |
87 | {/* Form message */} 88 | {message &&
89 |
90 | {message} 91 |
92 |
} 93 | {/* Field username */} 94 |
95 | 96 | handleInputChange(e)}> 97 | {/* error */} 98 | {form.username.error && 99 |
100 | {form.username.error} 101 |
} 102 |
103 | {/* Field password */} 104 |
105 | 106 | handleInputChange(e)}> 107 | {/* error */} 108 | {form.password.error && 109 |
110 | {form.password.error} 111 |
} 112 |
113 |
114 |
115 | {/* Submit button */} 116 | 117 |
118 |
119 |
120 |
121 |
122 | 123 | ); 124 | }; 125 | 126 | export default Login; -------------------------------------------------------------------------------- /src/components/pokemon-form.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import Pokemon from '../models/pokemon'; 4 | import formatType from '../helpers/format-type'; 5 | import PokemonService from '../services/pokemon-service'; 6 | 7 | type Props = { 8 | pokemon: Pokemon, 9 | isEditForm: boolean 10 | }; 11 | 12 | type Field = { 13 | value?: any, 14 | error?: string, 15 | isValid?: boolean 16 | }; 17 | 18 | type Form = { 19 | picture: Field, 20 | name: Field, 21 | hp: Field, 22 | cp: Field, 23 | types: Field 24 | } 25 | 26 | const PokemonForm: FunctionComponent = ({pokemon, isEditForm}) => { 27 | 28 | const history = useHistory(); 29 | 30 | const [form, setForm] = useState
({ 31 | picture: { value: pokemon.picture }, 32 | name: { value: pokemon.name, isValid: true }, 33 | hp: { value: pokemon.hp, isValid: true }, 34 | cp: { value: pokemon.cp, isValid: true }, 35 | types: { value: pokemon.types, isValid: true } 36 | }); 37 | 38 | const types: string[] = [ 39 | 'Plante', 'Feu', 'Eau', 'Insecte', 'Normal', 'Electrik', 40 | 'Poison', 'Fée', 'Vol', 'Combat', 'Psy' 41 | ]; 42 | 43 | const hasType = (type: string): boolean => { 44 | return form.types.value.includes(type); 45 | } 46 | 47 | const selectType = (type: string, e: React.ChangeEvent): void => { 48 | const checked = e.target.checked; 49 | let newField: Field; 50 | 51 | if(checked) { 52 | // Si l'utilisateur coche un type, à l'ajoute à la liste des types du pokémon. 53 | const newTypes: string[] = form.types.value.concat([type]); 54 | newField = { value: newTypes }; 55 | } else { 56 | // Si l'utilisateur décoche un type, on le retire de la liste des types du pokémon. 57 | const newTypes: string[] = form.types.value.filter((currentType: string) => currentType !== type); 58 | newField = { value: newTypes }; 59 | } 60 | 61 | setForm({...form, ...{ types: newField }}); 62 | } 63 | 64 | const handleInputChange = (e: React.ChangeEvent): void => { 65 | const fieldName: string = e.target.name; 66 | const fieldValue: string = e.target.value; 67 | const newField: Field = { [fieldName]: { value: fieldValue } }; 68 | 69 | setForm({ ...form, ...newField}); 70 | } 71 | 72 | const validateForm = () => { 73 | let newForm: Form = form; 74 | 75 | // Validator url 76 | if(isAddForm()) { 77 | 78 | const start = "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/"; 79 | const end = ".png"; 80 | 81 | if(!form.picture.value.startsWith(start) || !form.picture.value.endsWith(end)) { 82 | const errorMsg: string = 'L\'url n\'est pas valide.'; 83 | const newField: Field = { value: form.picture.value, error: errorMsg, isValid: false }; 84 | newForm = { ...newForm, ...{ picture: newField } }; 85 | } else { 86 | const newField: Field = { value: form.picture.value, error: '', isValid: true }; 87 | newForm = { ...newForm, ...{ picture: newField } }; 88 | } 89 | } 90 | 91 | // Validator name 92 | if(!/^[a-zA-Zàéè ]{3,25}$/.test(form.name.value)) { 93 | const errorMsg: string = 'Le nom du pokémon est requis (1-25).'; 94 | const newField: Field = { value: form.name.value, error: errorMsg, isValid: false }; 95 | newForm = { ...newForm, ...{ name: newField } }; 96 | } else { 97 | const newField: Field = { value: form.name.value, error: '', isValid: true }; 98 | newForm = { ...newForm, ...{ name: newField } }; 99 | } 100 | 101 | // Validator hp 102 | if(!/^[0-9]{1,3}$/.test(form.hp.value)) { 103 | const errorMsg: string = 'Les points de vie du pokémon sont compris entre 0 et 999.'; 104 | const newField: Field = {value: form.hp.value, error: errorMsg, isValid: false}; 105 | newForm = { ...newForm, ...{ hp: newField } }; 106 | } else { 107 | const newField: Field = { value: form.hp.value, error: '', isValid: true }; 108 | newForm = { ...newForm, ...{ hp: newField } }; 109 | } 110 | 111 | // Validator cp 112 | if(!/^[0-9]{1,2}$/.test(form.cp.value)) { 113 | const errorMsg: string = 'Les dégâts du pokémon sont compris entre 0 et 99'; 114 | const newField: Field = {value: form.cp.value, error: errorMsg, isValid: false}; 115 | newForm = { ...newForm, ...{ cp: newField } }; 116 | } else { 117 | const newField: Field = { value: form.cp.value, error: '', isValid: true }; 118 | newForm = { ...newForm, ...{ cp: newField } }; 119 | } 120 | 121 | setForm(newForm); 122 | return newForm.name.isValid && newForm.hp.isValid && newForm.cp.isValid; 123 | } 124 | 125 | const isTypesValid = (type: string): boolean => { 126 | // Cas n°1: Le pokémon a un seul type, qui correspond au type passé en paramètre. 127 | // Dans ce cas on revoie false, car l'utilisateur ne doit pas pouvoir décoché ce type (sinon le pokémon aurait 0 type, ce qui est interdit) 128 | if (form.types.value.length === 1 && hasType(type)) { 129 | return false; 130 | } 131 | 132 | // Cas n°1: Le pokémon a au moins 3 types. 133 | // Dans ce cas il faut empêcher à l'utilisateur de cocher un nouveau type, mais pas de décocher les types existants. 134 | if (form.types.value.length >= 3 && !hasType(type)) { 135 | return false; 136 | } 137 | 138 | // Après avoir passé les deux tests ci-dessus, on renvoie 'true', 139 | // c'est-à-dire que l'on autorise l'utilisateur à cocher ou décocher un nouveau type. 140 | return true; 141 | } 142 | 143 | const handleSubmit = (e: React.FormEvent) => { 144 | e.preventDefault(); 145 | const isFormValid = validateForm(); 146 | if(isFormValid) { 147 | pokemon.picture = form.picture.value; 148 | pokemon.name = form.name.value; 149 | pokemon.hp = form.hp.value; 150 | pokemon.cp = form.cp.value; 151 | pokemon.types = form.types.value; 152 | isEditForm ? updatePokemon() : addPokemon(); 153 | } 154 | } 155 | 156 | const deletePokemon = () => { 157 | PokemonService.deletePokemon(pokemon).then(() => history.push(`/pokemons`)); 158 | } 159 | 160 | const isAddForm = (): boolean => { 161 | return !isEditForm; 162 | } 163 | 164 | const addPokemon = () => { 165 | PokemonService.addPokemon(pokemon).then(() => history.push(`/pokemons`)); 166 | } 167 | 168 | const updatePokemon = () => { 169 | PokemonService.updatePokemon(pokemon).then(() => history.push(`/pokemons/${pokemon.id}`)); 170 | } 171 | 172 | return ( 173 | handleSubmit(e)}> 174 |
175 |
176 |
177 | {isEditForm && ( 178 |
179 | {pokemon.name} 180 | 181 | delete 182 | 183 |
184 | )} 185 |
186 |
187 | {/* Pokemon picture */} 188 | {isAddForm() && ( 189 |
190 | 191 | handleInputChange(e)}> 192 | {/* error */} 193 | {form.picture.error && 194 |
195 | {form.picture.error} 196 |
} 197 |
198 | )} 199 | {/* Pokemon name */} 200 |
201 | 202 | handleInputChange(e)}> 203 | {/* error */} 204 | {form.name.error && 205 |
206 | {form.name.error} 207 |
} 208 |
209 | {/* Pokemon hp */} 210 |
211 | 212 | handleInputChange(e)}> 213 | {/* error */} 214 | {form.hp.error && 215 |
216 | {form.hp.error} 217 |
} 218 |
219 | {/* Pokemon cp */} 220 |
221 | 222 | handleInputChange(e)}> 223 | {/* error */} 224 | {form.cp.error && 225 |
226 | {form.cp.error} 227 |
} 228 |
229 | {/* Pokemon types */} 230 |
231 | 232 | {types.map(type => ( 233 |
234 | 240 |
241 | ))} 242 |
243 |
244 |
245 | {/* Submit button */} 246 | 247 |
248 |
249 |
250 |
251 |
252 | 253 | ); 254 | }; 255 | 256 | export default PokemonForm; 257 | --------------------------------------------------------------------------------