RESSOURCES


DÉVELOPPER VOTRE PREMIÈRE APPLICATION REACT.

👉 Les ressources de la formation « React : Développez votre première application avec TypeScript et les Hooks» se trouvent bien sur cette page. Vous trouverez, comme promis :

L’application de démonstration

Lien vers l’application de démonstration.

La correction du code

Les extraits de code

package.json

{
  "name": "react-pokemons-app",
  "version": "1.0.0",
  "description": "An awesome application to handle some pokemons.",
  "dependencies": {
    "@types/node": "12.11.1",
    "@types/react": "16.9.9",
    "@types/react-dom": "16.9.2",
    "@types/react-router-dom": "^5.1.2",
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "react-router-dom": "^5.1.2",
    "react-scripts": "3.2.0",
    "typescript": "3.6.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react"
  },
  "include": [
    "src"
  ]
}

App.tsx

import React from 'react';
 
const App: React.FC = () => {
 const name: String = 'React';
   
 return (
  <h1>Hello, {name} !</h1>
 )
}
 
export default App;

pokemon.html

<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Pokédex</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root">L'application est en cours de chargement...</div>
  </body>
</html>

models/pokemon.ts

export default class Pokemon {
 // 1. Typage des propiétés d'un pokémon.
 id: number;
 hp: number;
 cp: number;
 name: string;
 picture: string;
 types: Array<string>;
 created: Date;
 
 // 2. Définition des valeurs par défaut des propriétés d'un pokémon.
 constructor(
  id: number,
  hp: number = 100,
  cp: number = 10,
  name: string = 'name',
  picture: string = 'http://...',
  types: Array<string> = ['Normal'],
  created: Date = new Date()
 ) {
  // 3. Initialisation des propiétés d'un pokémons.
  this.id = id;
  this.hp = hp;
  this.cp = cp;
  this.name = name;
  this.picture = picture;
  this.types = types;
  this.created = created;
 }
}

models/mock-pokemon.ts

import Pokemon from './pokemon';
  
export const POKEMONS: Pokemon[] = [
 {
  id: 1,
  name: "Bulbizarre",
  hp: 25,
  cp: 5,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/001.png",
  types: ["Plante", "Poison"],
  created: new Date()
 },
 {
  id: 2,
  name: "Salamèche",
  hp: 28,
  cp: 6,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/004.png",
  types: ["Feu"],
  created: new Date()
 },
 {
  id: 3,
  name: "Carapuce",
  hp: 21,
  cp: 4,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/007.png",
  types: ["Eau"],
  created: new Date()
 },
 {
  id: 4,
  name: "Aspicot",
  hp: 16,
  cp: 2,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/013.png",
  types: ["Insecte", "Poison"],
  created: new Date()
 },
 {
  id: 5,
  name: "Roucool",
  hp: 30,
  cp: 7,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/016.png",
  types: ["Normal", "Vol"],
  created: new Date()
 },
 {
  id: 6,
  name: "Rattata",
  hp: 18,
  cp: 6,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/019.png",
  types: ["Normal"],
  created: new Date()
 },
 {
  id: 7,
  name: "Piafabec",
  hp: 14,
  cp: 5,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/021.png",
  types: ["Normal", "Vol"],
  created: new Date()
 },
 {
  id: 8,
  name: "Abo",
  hp: 16,
  cp: 4,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/023.png",
  types: ["Poison"],
  created: new Date()
 },
 {
  id: 9,
  name: "Pikachu",
  hp: 21,
  cp: 7,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/025.png",
  types: ["Electrik"],
  created: new Date()
 },
 {
  id: 10,
  name: "Sabelette",
  hp: 19,
  cp: 3,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/027.png",
  types: ["Normal"],
  created: new Date()
 },
 {
  id: 11,
  name: "Mélofée",
  hp: 25,
  cp: 5,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/035.png",
  types: ["Fée"],
  created: new Date()
 },
 {
  id: 12,
  name: "Groupix",
  hp: 17,
  cp: 8,
  picture: "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/037.png",
  types: ["Feu"],
  created: new Date()
 }
];
 
export default POKEMONS;

CODE : formatType

  const formatType = (type: string): string => {
    let color: string;
 
    switch (type) {
      case 'Feu': 
        color = 'red lighten-1'; 
        break; 
      case 'Eau': 
        color = 'blue lighten-1'; 
        break; 
      case 'Plante': 
        color = 'green lighten-1'; 
        break; 
      case 'Insecte': 
        color = 'brown lighten-1'; 
        break; 
      case 'Normal': 
        color = 'grey lighten-3'; 
        break; 
      case 'Vol': 
        color = 'blue lighten-3'; 
        break; 
      case 'Poison': 
        color = 'deep-purple accent-1'; 
        break; 
      case 'Fée': 
        color = 'pink lighten-4'; 
        break; 
      case 'Psy': 
        color = 'deep-purple darken-2'; 
        break; 
      case 'Electrik': 
        color = 'lime accent-1'; 
        break; 
      case 'Combat': 
        color = 'deep-orange'; 
        break; 
      default: 
        color = 'grey'; 
        break; 
    }
 
    return `chip ${color}`;
  }

pages/pokemon-list.tsx

import React, { FunctionComponent, useState, useEffect } from 'react';
import Pokemon from '../models/pokemon';
import POKEMONS from '../models/mock-pokemon';
import PokemonCard from '../components/pokemon-card';
 
const PokemonList: FunctionComponent = () => {
  const [pokemons, setPokemons] = useState<Pokemon[]>([]);
 
  useEffect(() => {
    setPokemons(POKEMONS);
  }, []);
 
  return (
    <div>
      <h1 className="center">Pokédex</h1>
      <div className="container"> 
        <div className="row"> 
        {pokemons.map(pokemon => (
          <PokemonCard key={pokemon.id} pokemon={pokemon}/>
        ))}
        </div>
      </div>
    </div> 
  );
}
 
export default PokemonList;

pages/pokemon-card.tsx

import React, { FunctionComponent } from 'react';
import Pokemon from '../models/pokemon';
 
type Props = {
  pokemon: Pokemon
};
 
const PokemonCard: FunctionComponent<Props> = ({pokemon}) => {
   
  return (
    <div className="col s6 m4">
      <div className="card horizontal">
        <div className="card-image"> 
          <img src={pokemon.picture} alt={pokemon.name}/>
        </div>
        <div className="card-stacked">
          <div className="card-content">
            <p>{pokemon.name}</p>
            <p><small>{pokemon.created.toString()}</small></p>
          </div>
        </div>
      </div> 
    </div>
  );
}
 
export default PokemonCard;

pages/pokemon-detail.tsx

import React, { FunctionComponent, useState, useEffect } from 'react';
import { RouteComponentProps, Link } from 'react-router-dom';
import Pokemon from '../models/pokemon';
import POKEMONS from '../models/mock-pokemon';
import formatDate from '../helpers/format-date';
import formatType from '../helpers/format-type';
 
type Params = { id: string };
 
const PokemonsDetail: FunctionComponent<RouteComponentProps<Params>> = ({ match }) => {
   
  const [pokemon, setPokemon] = useState<Pokemon|null>(null);
 
  useEffect(() => {
    POKEMONS.forEach(pokemon => {
      if (match.params.id === pokemon.id.toString()) {
        setPokemon(pokemon);
      }
    })
  }, [match.params.id]);
   
  return (
    <div>
      { pokemon ? (
        <div className="row">
          <div className="col s12 m8 offset-m2"> 
            <h2 className="header center">{ pokemon.name }</h2>
            <div className="card hoverable"> 
              <div className="card-image">
                <img src={pokemon.picture} alt={pokemon.name} style={{width: '250px', margin: '0 auto'}}/>
              </div>
              <div className="card-stacked">
                <div className="card-content">
                  <table className="bordered striped">
                    <tbody>
                      <tr> 
                        <td>Nom</td> 
                        <td><strong>{ pokemon.name }</strong></td> 
                      </tr>
                      <tr> 
                        <td>Points de vie</td> 
                        <td><strong>{ pokemon.hp }</strong></td> 
                      </tr> 
                      <tr> 
                        <td>Dégâts</td> 
                        <td><strong>{ pokemon.cp }</strong></td> 
                      </tr> 
                      <tr> 
                        <td>Types</td> 
                        <td>
                          {pokemon.types.map(type => (
                           <span key={type} className={formatType(type)}>{type}</span>
                          ))}</td> 
                      </tr> 
                      <tr> 
                        <td>Date de création</td> 
                        <td>{formatDate(pokemon.created)}</td> 
                      </tr>
                    </tbody>
                  </table>
                </div>
                <div className="card-action">
                  <Link to="/">Retour</Link>
                </div>
              </div>
            </div>
          </div>
        </div>
      ) : (
        <h4 className="center">Aucun pokémon à afficher !</h4>
      )}
    </div>
  );
}
 
export default PokemonsDetail;

pages/page-not-found.tsx

import React, { FunctionComponent } from 'react';
import { Link } from 'react-router-dom';
 
const PageNotFound: FunctionComponent = () => {
 
  return (
    <div className="center">
      <img src="http://assets.pokemon.com/assets/cms2/img/pokedex/full/035.png" alt="Page non trouvée"/>
      <h1>Hey, cette page n'existe pas !</h1> 
      <Link to="/" className="waves-effect waves-teal btn-flat">
        Retourner à l'accueil
      </Link>
    </div>
  );
}
 
export default PageNotFound;

components/pokemon-form.tsx

import React, { FunctionComponent } from 'react';
import Pokemon from '../models/pokemon';
import formatType from '../helpers/format-type';
 
type Props = {
  pokemon: Pokemon
};
 
const PokemonForm: FunctionComponent<Props> = ({pokemon}) => {
 
  const types: string[] = [
    'Plante', 'Feu', 'Eau', 'Insecte', 'Normal', 'Electrik',
    'Poison', 'Fée', 'Vol', 'Combat', 'Psy'
  ];
  
  return (
    <form>
      <div className="row">
        <div className="col s12 m8 offset-m2">
          <div className="card hoverable"> 
            <div className="card-image">
              <img src={pokemon.picture} alt={pokemon.name} style={{width: '250px', margin: '0 auto'}}/>
            </div>
            <div className="card-stacked">
              <div className="card-content">
                {/* Pokemon name */}
                <div className="form-group">
                  <label htmlFor="name">Nom</label>
                  <input id="name" type="text" className="form-control"></input>
                </div>
                {/* Pokemon hp */}
                <div className="form-group">
                  <label htmlFor="hp">Point de vie</label>
                  <input id="hp" type="number" className="form-control"></input>
                </div>
                {/* Pokemon cp */}
                <div className="form-group">
                  <label htmlFor="cp">Dégâts</label>
                  <input id="cp" type="number" className="form-control"></input>
                </div>
                {/* Pokemon types */}
                <div className="form-group">
                  <label>Types</label>
                  {types.map(type => (
                    <div key={type} style={{marginBottom: '10px'}}>
                      <label>
                        <input id={type} type="checkbox" className="filled-in"></input>
                        <span>
                          <p className={formatType(type)}>{ type }</p>
                        </span>
                      </label>
                    </div>
                  ))}
                </div>
              </div>
              <div className="card-action center">
                {/* Submit button */}
                <button type="submit" className="btn">Valider</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </form>
  );
};
  
export default PokemonForm;

pages/pokemon-edit.tsx

import React, { FunctionComponent, useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import PokemonForm from '../components/pokemon-form';
import Pokemon from '../models/pokemon';
import POKEMONS from '../models/mock-pokemon';

type Params = { id: string };
 
const PokemonEdit: FunctionComponent<RouteComponentProps<Params>> = ({ match }) => {
   
  const [pokemon, setPokemon] = useState<Pokemon|null>(null);
 
  useEffect(() => {
    POKEMONS.forEach(pokemon => {
      if (match.params.id === pokemon.id.toString()) {
        setPokemon(pokemon);
      }
    })
  }, [match.params.id]);
   
  return (
    <div>
      { pokemon ? (
        <div className="row">
            <h2 className="header center">Éditer { pokemon.name }</h2>
            <PokemonForm pokemon={pokemon}></PokemonForm>
        </div>
      ) : (
        <h4 className="center">Aucun pokémon à afficher !</h4>
      )}
    </div>
  );
}
 
export default PokemonEdit;

services/pokemon-service.ts

import Pokemon from "../models/pokemon";

export default class PokemonService {

  static getPokemons(): Promise<Pokemon[]> {
    return fetch('http://localhost:3001/pokemons')
      .then(response => response.json());
  }

  static getPokemon(id: number): Promise<Pokemon|null> {
    return fetch(`http://localhost:3001/pokemons/${id}`)
      .then(response => response.json())
      .then(data => this.isEmpty(data) ? null : data);
  }

  static isEmpty(data: Object): boolean {
    return Object.keys(data).length === 0;
  }
}

models/db.json

{
  "pokemons": [
    {
      "id": 1,
      "name": "Bulbizarre",
      "hp": 25,
      "cp": 5,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/001.png",
      "types": ["Plante", "Poison"]
     },
     {
      "id": 2,
      "name": "Salamèche",
      "hp": 28,
      "cp": 6,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/004.png",
      "types": ["Feu"]
     },
     {
      "id": 3,
      "name": "Carapuce",
      "hp": 21,
      "cp": 4,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/007.png",
      "types": ["Eau"]
     },
     {
      "id": 4,
      "name": "Aspicot",
      "hp": 16,
      "cp": 2,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/013.png",
      "types": ["Insecte", "Poison"]
     },
     {
      "id": 5,
      "name": "Roucool",
      "hp": 30,
      "cp": 7,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/016.png",
      "types": ["Normal", "Vol"]
     },
     {
      "id": 6,
      "name": "Rattata",
      "hp": 18,
      "cp": 6,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/019.png",
      "types": ["Normal"]
     },
     {
      "id": 7,
      "name": "Piafabec",
      "hp": 14,
      "cp": 5,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/021.png",
      "types": ["Normal", "Vol"]
     },
     {
      "id": 8,
      "name": "Abo",
      "hp": 16,
      "cp": 4,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/023.png",
      "types": ["Poison"]
     },
     {
      "id": 9,
      "name": "Pikachu",
      "hp": 21,
      "cp": 7,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/025.png",
      "types": ["Electrik"]
     },
     {
      "id": 10,
      "name": "Sabelette",
      "hp": 19,
      "cp": 3,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/027.png",
      "types": ["Normal"]
     },
     {
      "id": 11,
      "name": "Mélofée",
      "hp": 25,
      "cp": 5,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/035.png",
      "types": ["Fée"]
     },
     {
      "id": 12,
      "name": "Groupix",
      "hp": 17,
      "cp": 8,
      "picture": "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/037.png",
      "types": ["Feu"]
     }
  ]
}

components/pokemon-search.tsx

import React, { FunctionComponent, useState } from 'react';
import { Link } from 'react-router-dom';
import Pokemon from '../models/pokemon';
import PokemonService from '../services/pokemon-service';

const PokemonSearch: FunctionComponent = () => {
 
  const [term, setTerm] = useState<string>('');
  const [pokemons, setPokemons] = useState<Pokemon[]>([]);

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    const term = e.target.value;
    setTerm(term);

    if(term.length <= 1) {
      setPokemons([]);
      return;
    }

    PokemonService.searchPokemon(term).then(pokemons => setPokemons(pokemons));
  }
 
  return (
    <div className="row"> 
    <div className="col s12 m6 offset-m3"> 
      <div className="card"> 
      <div className="card-content"> 
        <div className="input-field"> 
        <input type="text" placeholder="Rechercher un pokémon" value={term} onChange={e => handleInputChange(e)} /> 
        </div> 
        <div className='collection'>
        {pokemons.map((pokemon) => (
          <Link key={pokemon.id} to={`/pokemons/${pokemon.id}`} className="collection-item">
            {pokemon.name}
          </Link>
        ))}
        </div> 
      </div> 
      </div> 
    </div> 
    </div>
  );
}
 
export default PokemonSearch;

components/loader.tsx

import React, { FunctionComponent } from 'react';
 
const Loader: FunctionComponent = () => {
  
  return (
    <div className="preloader-wrapper big active"> 
      <div className="spinner-layer spinner-blue"> 
        <div className="circle-clipper left"> 
          <div className="circle"></div> 
        </div>
        <div className="gap-patch"> 
          <div className="circle"></div> 
        </div><div className="circle-clipper right"> 
          <div className="circle"></div> 
        </div>
      </div> 
   </div> 
  );
}
  
export default Loader;

pages/login.tsx

Récupérer le code initial du composant login.tsx.

Important: Ce fichier doit être récupéré depuis un autre site où est hébergé le code de correction du cours. En effet, dans ce composant j’utilise des emojis ainsi que le symbole « && », qui ne sont pas pris en compte par le plugin de coloration syntaxique utilisé sur ce site. Merci de votre compréhension ! 🙂

PrivateRoute.tsx

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import AuthenticationService from './services/authentication-service';
 
const PrivateRoute = ({ component: Component, ...rest }: any) => (
  <Route {...rest} render={(props) => {
    const isAuthenticated = AuthenticationService.isAuthenticated;
    if (!isAuthenticated) {    
      return <Redirect to={{ pathname: '/login' }} />
    }
 
    return <Component {...props} />
  }} />
);
 
export default PrivateRoute;

services/pokemon-service.ts (version de PRODUCTION)

import Pokemon from "../models/pokemon";
import POKEMONS from "../models/mock-pokemon";
 
export default class PokemonService {
 
  static pokemons:Pokemon[] = POKEMONS;
 
  static isDev = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development');
 
  static getPokemons(): Promise<Pokemon[]> {
    if(this.isDev) {
      return fetch('http://localhost:3001/pokemons')
      .then(response => response.json())
      .catch(error => this.handleError(error));
    }
 
    return new Promise(resolve => {
      resolve(this.pokemons);
    });
  }
 
  static getPokemon(id: number): Promise<Pokemon|null> {
    if(this.isDev) {
      return fetch(`http://localhost:3001/pokemons/${id}`)
      .then(response => response.json())
      .then(data => this.isEmpty(data) ? null : data)
      .catch(error => this.handleError(error));
    }
 
    return new Promise(resolve => {    
      resolve(this.pokemons.find(pokemon => id === pokemon.id));
    }); 
  }
 
  static updatePokemon(pokemon: Pokemon): Promise<Pokemon> {
    if(this.isDev) {
      return fetch(`http://localhost:3001/pokemons/${pokemon.id}`, {
        method: 'PUT',
        body: JSON.stringify(pokemon),
        headers: { 'Content-Type': 'application/json'}
      })
      .then(response => response.json())
      .catch(error => this.handleError(error));
    }
 
    return new Promise(resolve => {
      const { id } = pokemon;
      const index = this.pokemons.findIndex(pokemon => pokemon.id === id);
      this.pokemons[index] = pokemon;
      resolve(pokemon);
    }); 
  }
 
  static deletePokemon(pokemon: Pokemon): Promise<{}> {
    if(this.isDev) {
      return fetch(`http://localhost:3001/pokemons/${pokemon.id}`, {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json'}
      })
      .then(response => response.json())
      .catch(error => this.handleError(error));
    }
 
    return new Promise(resolve => {    
      const { id } = pokemon;
      this.pokemons = this.pokemons.filter(pokemon => pokemon.id !== id);
      resolve({});
    }); 
  }
 
  static addPokemon(pokemon: Pokemon): Promise<Pokemon> {
    pokemon.created = new Date(pokemon.created);
 
    if(this.isDev) {
      return fetch(`http://localhost:3001/pokemons`, {
        method: 'POST',
        body: JSON.stringify(pokemon),
        headers: { 'Content-Type': 'application/json'}
      })
      .then(response => response.json())
      .catch(error => this.handleError(error));
    }
 
    return new Promise(resolve => {    
      this.pokemons.push(pokemon);
      resolve(pokemon);
    }); 
  }
 
  static searchPokemon(term: string): Promise<Pokemon[]> {
    if(this.isDev) {
      return fetch(`http://localhost:3001/pokemons?q=${term}`)
      .then(response => response.json())
      .catch(error => this.handleError(error));
    }
 
    return new Promise(resolve => {    
      const results = this.pokemons.filter(pokemon => pokemon.name.includes(term));
      resolve(results);
    });
 
  }
 
  static isEmpty(data: Object): boolean {
    return Object.keys(data).length === 0;
  }
 
  static handleError(error: Error): void {
    console.error(error);
  }
}