Comment créer un trombinoscope avec React, Firebase et G Suite en moins de 30 minutes

Au temps d’antan, quand le temps justement ne manquait pas, nous lisions des livres pour apprendre beaucoup de choses. Le faire aujourd’hui pour apprendre à coder serait infructueux  . Vous ne serez jamais aussi efficace qu’en vous lançant dans un projet et en apprenant par besoin. Ce tutoriel se veut être un compromis entre assez d’exhaustivité pour que vous n’ayez besoin d’aucun prérequis et beaucoup de concision pour que ce ne soit pas plus qu’un environnement favorable à l’apprentissage. A terme vous aurez un intranet qui liste les employés de votre entreprise et beaucoup de connaissances à améliore et à approfondir.

 

Prérequis :

  • node >= 8,
  • IDE (VS Code, WebStorm…)
  • Un compte GSuite

 

 

Créez l’application et testez-la

Ouvrez un terminal, allez dans le répertoire où vous voulez créer votre application et exécutez :

npx create-react-app trombi

Vous avez créé votre application sous le nom « trombi »

Allez dans le dossier de votre application :

cd trombi

Pour la tester en mode développement, exécutez :

npm start

Si vous voulez tester des composants spécifiques, il vous faudra faire appel à ces composants dans le fichier index.js de votre dossier src.

Pour faire un build en mode production, exécutez :

npm run build

Nous en aurons besoin pour le déploiement.

 

 

Ajoutez votre application à Firebase

 Firebase peut être intégré à n’importe quel moment à l’application.

  1. Dans l’interface de Firebase (https://console.firebase.google.com/) créez un projet (Add project).
  2. Installez firebase-tools :
    npm install -g firebase-tools
  3. Exécuter firebase init dans votre application pour la connecter au projet :

Etape 1 : Cliquez sur Entrer. Les features dont on a besoin (Database et Hosting) sont sélectionnées par défaut

Etape 2 : Choisissez le projet que vous avez créé et cliquez sur entrer

Etape 3 : Gardez les paramètres par défaut et cliquez sur entrer.

Etape 4 : On utilisera le dossier build pour le déploiement. Entrez « build ».

Etape 5 : Entrez « y ». Notre application sera une SPA.

Etape 6 : Entrez « n ». Ceci permettra de garder le fichier « index.html » généré par le build.

 

L’application est maintenant connectée au projet Firebase , il vous reste une dernière chose à faire avant de déployer. Dans le fichier “firebase.json” créé, ajoutez le contenu suivant :

{
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ],
    "headers": [
      {"source": "/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}]}
    ]
  }
}

Vous aurez à la fin du déploiement l’url de votre application web.
Vous serez amenés à modifier l’application, pensez donc à redéployer (npm run build puis firebase deploy ) dès que vous avez une version stable pour que vos changements soient rapportés en production.

 

 

Connectez votre application à la base de données de Firebase

Créez un fichier firebase.js dans votre dossier src.

firebase.js

import firebase from 'firebase';

require('firebase/firestore');


const config = {
    apiKey: 'Key',
    authDomain: 'domain',
    databaseURL: 'url',
    projectId: 'pId',
    storageBucket: 'bucket',
    messagingSenderId: 'mId',
};

firebase.initializeApp(config);
export var fdb = firebase.firestore();


export const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
    hd: 'yourdomain.com',
    prompt: 'select_account',
});
provider.addScope('https://www.googleapis.com/auth/admin.directory.user.readonly');
provider.addScope('email');

export const auth = firebase.auth();
export default firebase;

Ouvrez votre projet dans la console de Firebase et cliquez sur « Add Firebase to your web app ». Remplacez dans firebase.js la variable « config » par celle de votre application et « yourdomain.com » par votre propre domaine.

 

 

Connecter l’application à l’API de Google

Pour connecter l’application à l’API de Google, créez une clé pour l’application Firebase depuis Google Cloud Platform.

  1. Autorisez l’accès aux API, si ce n’est pas déjà fait, depuis la console admin de G Suite (https://admin.google.com, Security ->API Reference : cocher « Enable API Access »)
  2. Autorisez votre application à utiliser une API spécifique (Security -> Show more -> Advanced settings -> Manage API client access) avec un ID correspondant à la clé que vous avez générée et le scope à : https://www.googleapis.com/auth/admin.directory.user.readonly

Cette API permettra à l’application de charger la liste des utilisateurs de votre domaine en utilisant un access token généré suite à l’authentification de l’un de ces utilisateurs.

 

 

Installez les bibliothèques utilisées

Nous aurons besoin de certaines bibliothèques qu’il serait bon d’installer et d’en comprendre l’utilité avant d’aller plus loin :

  • Redux
npm install --save react-redux

Utilité : Certaines variables sont indispensables au fonctionnement de plusieurs parties indépendantes de l’application, par exemple une variable qui permettrait de savoir si l’utilisateur est authentifié ou non. Nous aurons donc recours à redux pour, grossièrement, permettre l’accès à ces variables et leur modification dans toute l’application. Nous connectons les composants aux variables du store de redux avec mapStateToProps et les actions qui permettent d’agir sur ces variables avec mapDispatchToProps.

 

  • Redux-saga
npm install --save redux-saga

Utilité : La récupération des données depuis GSuite et Firebase étant asynchrone, elle ne peut être faite que par le moyen d’un middleware. Vous pouvez utiliser redux-thunk ou redux-saga, ce dernier est moins simple à mettre en place mais présente plus d’avantages.

 

  • React-router
npm install --save react-router

Utilité: Permet de naviguer entre les différentes vue de l’application et de changer l’url en fonction.

 

  • Material-UI
npm install @material-ui/core

Utilité : Nous utilisons tout au long du projet la bibliothèque Material UI pour deux raisons : un bon rendu et beaucoup de flexibilité.

 

 

Ajoutez les actions et les reducers

Nous ne nous attarderons pas sur les détails de fonctionnement de redux. Nous présentons une façon d’utiliser redux et redux-saga pour gérer les variables globales de notre application.

src
├──actions
├──reducers
└──store

 

actions
├── accessTokenStorage.js
├── refreshTokenStorage.js
├── TMproperties.js
└── index.js

accessTokenStorage.js

const ACCESS_TOKEN_KEY = 'access_token';

const get = () => (
    window.localStorage.getItem(ACCESS_TOKEN_KEY)
);

const set = (accessToken) => (
    window.localStorage.setItem(ACCESS_TOKEN_KEY, accessToken)
);

const clear = () => (
    window.localStorage.removeItem(ACCESS_TOKEN_KEY)
);

export default {
    get,
    set,
    clear,
};

 

refreshTokenStorage.js

const REFRESH_TOKEN_KEY = 'refresh_token';

const get = () => (
    window.localStorage.getItem(REFRESH_TOKEN_KEY)
);

const set = (refreshToken) => (
    window.localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken)
);

const clear = () => (
    window.localStorage.removeItem(REFRESH_TOKEN_KEY)
);

export default {
    get,
    set,
    clear,
};

Ces deux fichiers permettent de définir des fonctions pour enregistrer et récupérer l’accessToken et le refreshToken localement sans avoir à passer par les variables du store de redux.

 

TMproperties.js (Fonctions utilisées pour la conversion des données de Firebase et de GSuite.)

export function experience(titlexp) {
    switch (titlexp) {
        case '0':
            return 'Intern';
        case '1':
            return 'Rookie';
        case '2':
            return 'Pro';
        default:
            return '--';
    }
}

export function practices(pract) {
    var L1 = '', L2 = '', L3 = '';
    for (let p in pract) {
        if (pract.hasOwnProperty(p)) {
            switch (pract[p]) {
                case '1':
                    L1 = L1.concat(' ', p, ',');
                    break;
                case '2':
                    L2 = L2.concat(' ', p, ',');
                    break;
                case '3':
                    L3 = L3.concat(' ', p, ',');
                    break;
                default:
                    break;
            }
        }
    }
    {
        (L1 !== '') ? pract.l1 = L1.replace(/.$/, ' ') : pract.l1 = '--';
    }
    {
        (L2 !== '') ? pract.l2 = L2.replace(/.$/, ' ') : pract.l2 = '--';
    }
    {
        (L3 !== '') ? pract.l3 = L3.replace(/.$/, ' ') : pract.l3 = '--';
    }
}

export function gProperties(tm) {
    var familyname, firstname, picture, phone, mail;
    {
        (tm.name.familyName) ? familyname = tm.name.familyName : familyname = '--';
    }
    {
        (tm.name.givenName) ? firstname = tm.name.givenName : firstname = '--';
    }
    {
        (tm.thumbnailPhotoUrl) ? picture = tm.thumbnailPhotoUrl: picture = null;
    }
    {
        (tm.phones) ? phone = '+212 ' + tm.phones[0].value.slice(-9) : phone = '--';
    }
    {
        (tm.primaryEmail) ? mail = tm.primaryEmail : mail = '--';
    }
    var object;
    object = {
        'familyname': familyname,
        'firstname': firstname,
        'picture': picture,
        'phone': phone,
        'mail': mail,
    };
    return object;
}

export function convertMS(ms) {
    ms = Number(ms);
    var m, y;
    m = Math.floor(ms / (1000 * 3600 * 24 * 30));
    y = Math.floor(ms / (1000 * 3600 * 24 * 30 * 12));
    m = m % 12;
    return String(y) + '+' + String(m);
}

export function retrieve(fsdata, mail) {
    var pract = {};
    var titlexp = '';
    var tenure;
    for (var el in fsdata) {
        if (fsdata[el].Mail === mail) {
            for (var attr in fsdata[el]) {
                if (['React', 'React Native', 'Python', 'Hadoop', 'Angular', 'Spark', 'Ionic', 'Node', 'Java', 'Nifi', 'Machine learning'].includes(attr)) {
                    pract[attr] = fsdata[el][attr];
                }
            }
            {
                (fsdata[el].hasOwnProperty('Career')) ? titlexp = fsdata[el].Career : null;
            }
            {
                if (fsdata[el].hasOwnProperty('StartDate')) {
                    var datenow = Date.now();
                    var datethen = Date.parse(fsdata[el].StartDate);
                    tenure = convertMS(datenow - datethen);
                } else {
                    tenure = '__';
                }
            }
            practices(pract);
            titlexp = experience(titlexp);
            var obj = {
                'tenure': tenure,
                'titlexp': titlexp,
                'L1': pract.l1,
                'L2': pract.l2,
                'L3': pract.l3,
            };
            return obj;
        }
    }
    return null;
}

export function retrieveVisible(fsdata, pract = '', title = '') {
    var list = [];
    if (pract === '' && title === '') {
        for (var el in fsdata) {
            list.push(fsdata[el].Mail);
        }
    } else {
        for (var el in fsdata) {
            var prop = retrieve(fsdata, fsdata[el].Mail);
            if (pract !== '' && (prop.L1.includes(pract) || prop.L2.includes(pract) || prop.L3.includes(pract))) {
                list.push(fsdata[el].Mail);
            }
            if (title !== '' && prop.titlexp === experience(title)) {
                list.push(fsdata[el].Mail);
            }
        }
    }
    return list;
}

Ces fonctions dépendent fortement de votre modèle de données enregistrées dans la base de données de Firebase, vous aurez donc probablement à les remplacer par d’autres. Si vous voulez utiliser celles-ci, dans la console de Firebase :

  1. Allez dans Develop > Database,
  2. Cliquez sur Cloud Firestore,
  3. Ajoutez une collection ( Add collection) sous le nom users,
  4. Ajoutez des documents sous des noms différents (par exemple 1, 2, 3, …) et pour chaque document ajoutez les champs suivants (Add field) avec les valeurs que vous voulez :
    • (Field) Mail = (Type) string
    • (Field) StartDate = (Type) timestamp
    • (Field) Name = (Type) string
    • (Field) Career = (Type) string  : la valeur doit être entre 0 et 2  (cf. la fonction experience)
    • Ajoutez un champ de type string pour chaque practice que vous voulez ajouter avec une valeur entre 1 et 3 (cf. la fonction fretrieve)

Les données de Firebase sont liées à celles de GSuite par la variable Mail.

 

index.js (Les vraies actions.)

import { all, call, put, takeEvery } from 'redux-saga/effects';
import firebase, { auth, fdb, provider } from '../../firebase.js';
import accessTokenStorage from './accessTokenStorage';
import refreshTokenStorage from './refreshTokenStorage';

export function tmHaveFailed(bool) {
    return {
        type: 'TM_HAVE_FAILED',
        haveFailed: bool,
    };
}

export function tmAreLoading(bool) {
    return {
        type: 'TM_ARE_LOADING',
        areLoading: bool,
    };
}

export function tmFetchedData(team) {
    return {
        type: 'TM_FETCHED_DATA',
        team: team,
    };
}

export function fsData(fsdata) {
    return {
        type: 'FS_DATA',
        fsdata: fsdata,
    };
}

export function wrongUser(bool) {
    return {
        type: 'WRONG_USER',
        wrongUser: bool,
    };
}

export function logIn() {
    return {
        type: 'LOGIN',
    };
}

export function logOut() {
    return {
        type: 'LOGOUT',
    };
}

export function tmList() {
    return {
        type: 'TM_LIST',
    };
}

export function loggedIn(bool) {
    return {
        type: 'USER_LOG',
        logged: bool,
    };
}

export function* watchAll() {
    yield all([
        takeEvery('LOGIN', login),
        takeEvery('TM_LIST', fetchData),
        takeEvery('TM_FETCHED_DATA', retrieveFsData),
        takeEvery('LOGOUT', logout),
    ]);
}


export function* login() {
    try {
        const userdata = yield call(() => {
            return auth.signInWithPopup(provider);
        });
        if (userdata.user.email.replace(/.*@/, '') === 'yourdomain.com') {
            yield put(wrongUser(false));
            yield put(loggedIn(true));
            yield call(accessTokenStorage.set, 'Bearer ' + userdata.credential.accessToken);
            yield call(refreshTokenStorage.set, userdata.user.refreshToken);
        }
        else {
            yield put(wrongUser(true));
            var User = yield call(() => {
                return firebase.auth().currentUser;
            });
            yield call(() => {
                return User.delete();
            });
            yield call(() => {
                return auth.signOut();
            });
        }
    } catch (error) {
        console.log('login failed')
    }
}

export function* fetchData() {
    try {
        yield put(tmAreLoading(true));
        const data = yield call(() => {
            return fetch('https://www.googleapis.com/admin/directory/v1/users?domain=yourdomain.com&viewType=domain_public&projection=full&maxResults=500', {
                method: 'get',
                headers: {
                    'Authorization': accessTokenStorage.get(),
                    'grant_type': 'refresh_token',
                    'refresh_token': refreshTokenStorage.get(),
                },
            })
                .then(res => res.json());
        });
        yield put(tmAreLoading(false));
        yield put(tmFetchedData(data.users));
    } catch (error) {
        yield put(tmHaveFailed(true));
        console.log('fetch error', error);
    }

}

export function* logout() {
    try {
        yield call(() => {
            return auth.signOut();
        });
        yield put(loggedIn(false));
        yield put(wrongUser(false));
        const array = [];
        yield put(tmFetchedData(array));
        yield call(accessTokenStorage.clear);
        yield call(refreshTokenStorage.clear);
    } catch (error) {
        console.log('couldn\'t log out', error);
    }
}

export function* retrieveFsData() {
    try {
        const snap = yield call(() => {
            return fdb.collection('users').get();
        });
        const fsdata = snap.docs.reduce((res, item) => ({...res, [item.id]: item.data()}), {});
        yield put(fsData(fsdata));
    } catch (error) {
        console.log('error', error);
    }
}

Remplacez « yourdomain.com » par votre propre domaine dans la quatrième ligne de la fonction fetchData et dans la cinquième ligne de la fonction login pour limiter l’authentification aux utilisateurs de votre domaine.

reducers
├── index.js
└── reducers.js

 

index.js (Exportation des variables du store utilisées par les composants)

import { combineReducers } from 'redux';
import { fbdata, fsdata, team, tmHaveFailed, tmAreLoading, loggedIn, loginHasFailed, wrongUser } from './team';

export default combineReducers({
    team,
    tmHaveFailed,
    loginHasFailed,
    tmAreLoading,
    loggedIn,
    wrongUser,
    fbdata,
    fsdata,
});

 

team.js (Ensemble des actions reducers)

export function tmHaveFailed(state = false, action) {
    switch (action.type) {
        case 'TM_HAVE_FAILED':
            return action.haveFailed;
        default:
            return state;
    }
}

export function tmAreLoading(state = false, action) {
    switch (action.type) {
        case 'TM_ARE_LOADING':
            return action.areLoading;
        default:
            return state;
    }
}

export function team(state = [], action) {
    switch (action.type) {
        case 'TM_FETCHED_DATA':
            return action.team;
        default:
            return state;
    }
}

export function fbdata(state = {}, action) {
    switch (action.type) {
        case 'FB_DATA':
            return action.fbdata;
        default:
            return state;
    }
}

export function fsdata(state = {}, action) {
    switch (action.type) {
        case 'FS_DATA':
            return action.fsdata;
        default:
            return state;
    }
}

export function wrongUser(state = false, action) {
    switch (action.type) {
        case 'WRONG_USER':
            return action.wrongUser;
        default:
            return state;
    }
}

export function loggedIn(state = false, action) {
    switch (action.type) {
        case 'USER_LOG':
            return action.logged;
        default:
            return state;
    }
}

export function loginHasFailed(state = false, action) {
    switch (action.type) {
        case 'LOGIN_HAS_FAILED':
            return action.loginFailed;
        default:
            return state;
    }
}

 

store
└── index.js

 

index.js

import createHistory from 'history/createBrowserHistory';
import { applyMiddleware, createStore } from 'redux';
import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import createSagaMiddleware from 'redux-saga';
import { watchAll } from '../actions';
import rootReducer from '../reducers';

const sagaMiddleware = createSagaMiddleware();

function configureStore(rootReducer, initialState) {
    return createStore(
        rootReducer,
        initialState,
        applyMiddleware(sagaMiddleware),
    );
}

export const history = createHistory();


const persistConfig = {
    key: 'root',
    storage: storage,
    whitelist: ['loggedIn', 'team', 'fsdata'],
};

export const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore(persistedReducer);
sagaMiddleware.run(watchAll);

export const persistor = persistStore(store);

 

 

Créez les composants

On aura besoin de cinq composants :

App.js : contiendra la barre de navigation et les différentes routes. C’est le seul composant auquel on fera appel directement dans index.js

Appbar.js : la barre de navigation

Failed.js : permettra de gérer les authentifications non autorisées

Home.js : la page d’accueil de l’application

TMList.js : la liste des employés

TMCard.js : le modèle de carte d’un employé

 

Dans le dossier src:

components
├──App.js
├──Appbar.js
├──Failed.js
├──Home.js
├──TMCard.js
└──TMList.js

 

  • Cartes

Nous utilisons les cartes de Material UI. Utilisez l’un des modèles disponibles et personnalisez vos cartes en fonction des informations que vous voulez y mettre. Vous pouvez aussi ajouter un bouton qui permettra d’agrandir la carte ou d’avoir accès au profil de l’employé.

https://material-ui-next.com/demos/cards/

TMCard.js

import Avatar from 'material-ui/Avatar';
import Button from 'material-ui/Button';
import Card, { CardActions, CardContent, CardHeader } from 'material-ui/Card';
import Grid from 'material-ui/Grid';
import { withStyles } from 'material-ui/styles';
import Typography from 'material-ui/Typography';
import React from 'react';

const styles = () => ({
    card: {
        margin: 20,
        overflowWrap: 'break-word',
    },
    media: {
        height: 194,
    },
    actions: {
        float: 'right',
    },
    button: {
        marginLeft: 'auto',
    },
    bigAvatar: {
        width: 100,
        height: 100,
    },
    tmnames: {
        paddingBottom: 0,
    },
    content: {
        marginTop: 40,
        marginBottom: 0,
    },
});

class TMCard extends React.Component {
    render() {
        const {classes} = this.props;
        const email = 'mailto:' + this.props.variables.mail + '?';

        return (

            <div>
                <Card className={classes.card}>
                    <Grid container spacing={40}>
                        <Grid item xs={8} sm={4}>
                            <CardHeader className={classes.tmnames}
                                        avatar={this.props.variables.picture ?
                                            <Avatar aria-label="Team member" alt="H" src={this.props.variables.picture}
                                                    className={classes.bigAvatar}/>
                                            :
                                            <Avatar aria-label="Team member"
                                                    className={classes.bigAvatar}>{this.props.variables.firstname}</Avatar>}

                                        title={
                                            <div>
                                                <b>{this.props.variables.firstname + ' ' + this.props.variables.familyname}</b>

                                                <p><b> Title:</b> {this.props.firebasedata.titlexp}</p>
                                                {this.props.firebasedata.tenure ?
                                                    <p><b> Tenure:</b> {this.props.firebasedata.tenure} </p>
                                                    :
                                                    null}
                                            </div>
                                        }
                            />
                        </Grid>
                        <Grid item xs={16} sm={8}>
                            <CardContent className={classes.content}>
                                <Grid container spacing={40}>
                                    <Grid item xs={12} sm={6} className={classes.root}>
                                        <Typography paragraph gutterBottom>
                                            <b>E-mail: </b><a href={email} target="_top">{this.props.variables.mail}</a>
                                        </Typography>
                                        <Typography paragraph gutterBottom>
                                            <b>Mobile: </b>{this.props.variables.phone}
                                        </Typography>
                                    </Grid>
                                    <Grid item xs={12} sm={6} className={classes.root}>
                                        <div>
                                            <Typography paragraph gutterBottom>
                                                <b>Practices</b><br/>
                                                L1: {this.props.firebasedata.L1}<br/>
                                                L2: {this.props.firebasedata.L2}<br/>
                                                L3: {this.props.firebasedata.L3}<br/>
                                            </Typography>
                                        </div>
                                    </Grid>
                                </Grid>
                            </CardContent>

                        </Grid>
                    </Grid>
                </Card>
            </div>
        );
    }
}


export default withStyles(styles)(TMCard);

 

  • Liste

La présentation de la liste dépend de la forme des cartes. Elle regroupe l’ensemble des cartes des employés et affecte les bonnes variables aux différentes cartes.

TMList.js

import Grid from 'material-ui/Grid';
import { withStyles } from 'material-ui/styles';
import Typography from 'material-ui/Typography';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { tmList } from '../actions';
import { retrieve, retrieveVisible, gProperties } from '../actions/TMproperties.js';
import TMCard from './TMCard.js';

const styles = {
    flex: {
        flex: 1,
        fontWeight: 'bold',
        color: '#ffffff',
        marginLeft: 20,
    },
    root: {
        flexGrow: 1,
    },
    gridtm: {
        marginTop: 100,
    },
    box: {
        marginLeft: 20,
        marginRight: 20,
        marginTop: 10,
        maxWidth: 300,
    },
    form: {
        display: 'inline',
    },
};


export class TMList extends Component {
    handleChangeTitle = (event) => {
        this.setState({title: event.target.value}, () => {
            var list = retrieveVisible(this.props.fsdata, this.state.practice, this.state.title);
            this.setState({list: list});
        });
    };
    handleChangePractice = (event) => {
        this.setState({practice: event.target.value}, () => {
            var list = retrieveVisible(this.props.fsdata, this.state.practice, this.state.title);
            this.setState({list: list});
        });
    };
    constructor(props) {
        super(props);
        this.state = {
            practice: '',
            title: '',
            list: ['initialized'],
        };
    }
    componentDidMount() {
        this.props.TMlist();
    }

    render() {
        const {classes} = this.props;

        if (this.props.hasFailed) {
            return <p className={classes.gridtm}>Sorry! There was an error loading the list</p>;
        }
        if (this.props.isLoading) {
            return <p className={classes.gridtm}>Loading…</p>;
        }
        return (
            <div className={classes.gridtm}>
                <div>
                    <div>
                        <select id="practiceSelector"
                                name="practice"
                                value={this.state.practice}
                                onChange={this.handleChangePractice}
                        >
                            <option value="">Select practice:</option>
                            <option value="React">React</option>
                            <option value="Angular">Angular</option>
                            <option value="Ionic">Ionic</option>
                        </select>
                    </div>
                    <div>
                        <select id="titleSelector"
                                name="title"
                                value={this.state.title}
                                onChange={this.handleChangeTitle}
                        >
                            <option value="">Select title:</option>
                            <option value="0">Intern</option>
                            <option value="1">Rookie</option>
                            <option value="2">Pro</option>
                        </select>
                    </div>
                </div>
                <div id='list'>
                    {this.props.team.map(function (tm) {
                        if (this.state.list.indexOf('initialized') === 0) {
                            return (
                                <div key={this.props.team.indexOf(tm)}>
                                    {(retrieve(this.props.fsdata, tm.primaryEmail)) ?
                                        <Grid tm xs={12} key={tm.id}>
                                            <TMCard variables={gProperties(tm)}
                                                    firebasedata={retrieve(this.props.fsdata, tm.primaryEmail)}/>
                                        </Grid> : null}
                                </div>
                            );
                        } else {
                            if (this.state.list.indexOf(tm.primaryEmail) >= 0) {
                                return (
                                    <Grid tm xs={12} key={tm.id}>
                                        <TMCard key={tm.id} variables={gProperties(tm)}
                                                firebasedata={retrieve(this.props.fsdata, tm.primaryEmail)}/>
                                    </Grid>
                                );
                            }
                        }
                    }, this)}
                </div>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    return {
        team: state.team,
        hasFailed: state.tmHaveFailed,
        isLoading: state.tmAreLoading,
        fbdata: state.fbdata,
        fsdata: state.fsdata,
    };
};

const mapDispatchToProps = {
    TMlist: tmList,
};

export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(TMList));

Pensez à ajouter du css pour les filtres qui par défaut ne sont pas forcément jolis à voir.

Nous avons choisi deux champs (« practices » et « title ») pour filtrer la liste des employés. La variable « list » change donc selon les filtres selectionés. Quand la page est chargé la première fois, tous les membres sont affichés sans qu’il y ait besoin de les ajouter à la variable « list ».

La fonction fretrieve permet de sélectionner parmi les données de firebase celles qui correspondent à une adresse mail spécifique. La fonction Gproperties permet d’avoir les données de Gsuite dont on a besoin. La fonction TMList permet de déclencher une autre fonction responsable de la récupération des données.

A terme, votre liste devrait ressembler à ça:

 

  • La barre de navigation

Nous utilisons la barre de navigation de Material UI. Cette barre de navigation sera le seul élément présent dans toutes les pages de l’application, nous y avons donc ajouté un bouton de déconnexion qui apparait quand un utilisateur est authentifié.

https://material-ui-next.com/demos/app-bar/

Appbar.js

import AppBar from 'material-ui/AppBar';
import Button from 'material-ui/Button';
import { withStyles } from 'material-ui/styles';
import Toolbar from 'material-ui/Toolbar';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import newlogo from '../../newlogo.png';
import { logOut } from '../actions';

const styles = {
    root: {
        flexGrow: 1,
        marginBottom: 30,
    },
    bar: {
        backgroundColor: '#333333',
        position: 'fixed',
        top: 0,
        height: 80,
    },
    flex: {
        flex: 1,
        fontWeight: 'lighter',
        color: '#ffffff',
        paddingTop: 20,
        marginLeft: 100,
    },
    log: {
        position: 'absolute',
        right: 10,
        top: 20,
        fontWeight: 'lighter',
        color: '#fff',
        padding: 15,
        border: '1px solid currentColor',
        '&:hover': {
            backgroundColor: '#d14e4b',
        },
    },
    logo: {
        marginTop: 20,
        height: 40,
    },
};

class MenuAppBar extends React.Component {
    logout = () => {
        this.props.LogOut();
    };
    constructor() {
        super();
        this.logout = this.logout.bind(this);
    };

    render() {
        const {classes} = this.props;

        return (
            <div className={classes.root}>
                <AppBar position="static" className={classes.bar}>
                    <Toolbar>
                        <img src={newlogo} alt=" " className={classes.logo}/>
                        {(this.props.isLoggedIn) ?
                            <Button className={classes.log} onClick={this.logout}>Logout</Button>
                            :
                            null
                        }
                    </Toolbar>
                </AppBar>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    return {
        isLoggedIn: state.loggedIn,
    };
};
const mapDispatchToProps = dispatch => bindActionCreators({
    LogOut: logOut,
}, dispatch);


MenuAppBar.propTypes = {
    classes: PropTypes.object.isRequired,
};

export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(MenuAppBar));

 

  • Les routes

App.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Route } from 'react-router-dom';
import Redirect from 'react-router-dom/es/Redirect';
import MenuAppBar from './Appbar.js';
import Failed from './Failed.js';
import Home from './Home.js';
import TMList from './TMList.js';

const PrivateRoute = ({component: Component, ...rest, condition}) => (
    <Route {...rest} render={(props) => (
        condition === true
            ? <Component {...props} />
            : null
    )}/>
);

class Page extends Component {
    render() {
        return (
            <div>
                <div>
                    <MenuAppBar/>
                </div>
                <main>
                    <Route path='/login' component={Home}/>
                    {(!this.props.isLoggedIn && !this.props.wrongUser) ? <Redirect from='/' to='/login'/> : null}
                    <PrivateRoute path='/list' component={TMList} condition={this.props.isLoggedIn}/>
                    {(this.props.isLoggedIn) ? <Redirect from='/' to='/list'/> : null}
                    <PrivateRoute path='/fail' component={Failed} condition={this.props.wrongUser}/>
                    {(this.props.wrongUser) ? <Redirect from='/' to='/fail'/> : null}
                </main>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
    return {
        isLoggedIn: state.loggedIn,
        wrongUser: state.wrongUser,
    };
};

export default connect(mapStateToProps, null, null, {pure: false})(Page);

La constante PrivateRoute est une solution pour limiter l’accès à certaines pages selon les utilisateurs authentifiés.

 

  • La page d’accueil

Il n’y a pas plus simple, nous ajoutons en dessous d’un mot de bienvenue un bouton d’authentification. Par exemple:

Home.js

import Button from 'material-ui/Button';
import { withStyles } from 'material-ui/styles';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import '../../css/Home.css';
import { logIn } from '../actions';


const styles = {
    log: {
        color: '#fff',
        marginTop: 50,
        paddingTop: 15,
        paddingBottom: 15,
        paddingLeft: 30,
        paddingRight: 30,
        border: '1px solid currentColor',
        '&:hover': {
            backgroundColor: '#d14e4b',
        },
        fontSize: 30,
    },
};

class Home extends Component {
    login = () => {
        this.props.logIn();
    };

    constructor() {
        super();
        this.login = this.login.bind(this);
    };

    render() {
        const {classes} = this.props;
        return (
            <div className="Home">
                Connecte-toi pour avoir accès au trombinoscope&nbsp;!<br/>
                {(!this.props.isLoggedIn) ?
                    <Button className={classes.log} onClick={this.login}>Login</Button>
                    :
                    null
                }
            </div>

        );
    }
}

const mapStateToProps = (state) => {
    return {
        isLoggedIn: state.loggedIn,
        token: state.Token,
    };
};
const mapDispatchToProps = dispatch => bindActionCreators({
    logIn,
}, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Home));

 

Home.css

.Home {
    margin-top: 200px;
    text-align: center;
    font-size: 40px;
    font-weight: lighter;
    color: #ffffff;
    width: 50%;
    margin-left: 25%;
}

.Link {
    text-decoration: none !important;
}

 

  • La page d’erreur

Un simple message pour expliquer que l’utilisateur n’est pas autorisé à avoir accès au trombinoscope. par exemple:

Failed.js

import Button from 'material-ui/Button';
import { withStyles } from 'material-ui/styles/index';
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import '../../css/Home.css';

const styles = {
    log: {
        color: '#fff',
        marginTop: 50,
        paddingTop: 15,
        paddingBottom: 15,
        paddingLeft: 30,
        paddingRight: 30,
        border: '1px solid currentColor',
        '&:hover': {
            backgroundColor: '#d14e4b',
        },
        fontSize: 30,
    },
};

class Failed extends Component {
    render() {
        const {classes} = this.props;
        return (
            <div className="Home">
                Vous n'êtes pas autorisé à accéder à ce site.
                <br/>
                <Link className="Link" to="/login"><Button className={classes.log}>Go back</Button></Link>

            </div>
        );
    }
}
export default (withStyles(styles)(Failed));

 

Pour rendre correctement les composants que vous venez de créer, vous devez faire appel au store dans votre fichier src/index.js.

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import { PersistGate } from 'redux-persist/integration/react';
import './css/index.css';
import Page from './js/components/App.js';
import { history, persistor, store } from './js/store/index.js';


const target = document.querySelector('#root');

ReactDOM.render(
    <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
            <ConnectedRouter history={history}>
                <div>
                    <Page/>
                </div>
            </ConnectedRouter>
        </PersistGate>
    </Provider>,
    target,
);

index.css

body {
    margin: 0;
    padding: 0;
    font-family: sans-serif;
    background-color: #a6a6a6;
}

 

Voilà, il ne vous reste plus qu’à nettoyer et supprimer ce dont vous n’avez pas besoin.

 

Conclusion

Nous avons un code prêt à être déployé mais une application qui présente encore beaucoup de possibilités d’amélioration. Prenez-donc le temps d’optimiser le code et d’approfondir parmi les notions abordées celles qui sont nouvelles pour vous.

 

One Reply to “Comment créer un trombinoscope avec React, Firebase et G Suite en moins de 30 minutes”

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*