https://d226lax1qjow5r.cloudfront.net/blog/blogposts/how-to-build-a-learning-platform-using-vonage-javascript-typescript-react-express-and-apollo-graphql/blog_remote-learning-platform_1200x600.jpg

Comment construire une plateforme d'apprentissage avec React, Express et Apollo GraphQL

Publié le December 15, 2020

Temps de lecture : 19 minutes

2020 a été une année atypique pour nous tous. De nombreux secteurs ont dû "repenser" leur façon de travailler et il y a fort à parier que ces stratégies ne sont pas temporaires, mais qu'elles sont là pour durer.

L'un de ces changements concerne notre façon d'apprendre. De nombreuses écoles, universités et académies dans le monde ont connu une augmentation des services à distance, s'appuyant souvent sur des solutions privées pour les fournir.

Aujourd'hui, nous allons voir comment il est possible de construire notre propre plateforme d'apprentissage avec des capacités vidéo/audio, des notifications SMS et une authentification sans mot de passe.

Conditions préalables

Pour construire et exécuter l'application, vous aurez besoin des ressources suivantes :

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Ce que nous allons construire

Nous allons développer une application web qui permet aux enseignants de créer des classes vidéo/audio instantanées qu'un étudiant peut rejoindre avec un simple lien. Les enseignants pourront créer une liste d'étudiants, identifiés par leur numéro de téléphone, et pourront ensuite leur envoyer le lien pour l'appel par SMS.

L'enseignant peut également créer des devoirs. Les étudiants peuvent ensuite s'identifier à l'aide d'une authentification sans mot de passe et télécharger des fichiers qui peuvent être examinés ultérieurement par l'enseignant.

Dans un souci de simplicité et d'efficacité, certaines fonctionnalités, telles que l'authentification (connexion et déconnexion) et une véritable base de données, ont été laissées de côté. Au lieu de cela, toutes les pages sont accessibles au public et les données sont stockées en mémoire à l'aide de tableaux JavaScript.

Si vous souhaitez expérimenter le produit final par vous-même, j'ai créé un dépôt Github que vous pouvez cloner localement. Ce dépôt contient un dossier final où vous pouvez voir l'exemple fini, et un dossier starter avec React, Express et Apollo GraphQL déjà préconfigurés, que vous pouvez utiliser pour le suivre et le construire étape par étape.

Le code de démonstration est divisé en un server qui contient un fichier Apollo GraphQL d'Apollo avec Expresset un client qui contient un serveur React de base. Le code du backend est écrit en JavaScript simple tandis que le frontend utilise TypeScript, de cette façon si vous n'êtes pas familier avec les différences entre ces deux, vous pouvez les comparer côte à côte.

Avant de commencer, assurez-vous d'aller dans chaque dossier et d'installer les dépendances à l'aide de npm, comme indiqué ci-dessous :

cd server/ npm install cd ../client npm install

Vous devez également configurer les secrets. Dans le dossier client il suffit de renommer le fichier .env en .env.local.

Dans le dossier server en revanche, renommez le fichier app.envs en .env vous devez également remplacer les valeurs de remplacement du fichier par vos propres clés AWS, nom de Bucket S3, clés Vonage et numéro virtuel Vonage.

Si vous souhaitez exécuter le produit fini, ouvrez deux fenêtres de terminal distinctes et utilisez npm pour démarrer les deux applications, comme indiqué ci-dessous :

# In Terminal 1 start the server in development mode cd server/ npm run dev # In terminal 2 use the already-configured react-scripts script to run the react application cd client/ npm start

Une fenêtre de navigateur s'ouvrira automatiquement et vous y verrez l'application en action. La première chose que vous voyez est une fenêtre avec un bouton qui vous permet de créer une classe. Avant d'entrer dans le vif du sujet, rendez-vous sur la page Students et créez quelques étudiants en utilisant des numéros de téléphone valides.

The Students PageThe Students Page

Retournez à l'écran principal en cliquant sur le titre de l'application. Ensuite, commencez une nouvelle classe.

Après quelques secondes, vous serez dans une session Video API de Vonage. En utilisant la liste des étudiants, vous pouvez envoyer aux étudiants une notification par SMS afin qu'ils puissent rejoindre la classe en cliquant simplement sur le bouton Invite pour qu'ils se joignent à la classe.

Starting a classStarting a class

Imaginons que vous souhaitiez créer un devoir pour lequel les élèves doivent télécharger un document PDF. Vous pouvez le faire de manière à ce qu'il ne soit pas nécessaire qu'ils aient un Account, mais qu'ils puissent s'authentifier en utilisant leur téléphone.

Pour ce faire, rendez-vous sur la page Homeworks et créez un nouveau devoir en définissant une description. Ensuite, en tant qu'élève, cliquez sur le lien Upload lien.

Creating an AssignmentCreating an Assignment

Pour télécharger le fichier, l'étudiant doit fournir le même numéro de téléphone que celui utilisé par l'enseignant au moment de la création. Un code de vérification sera envoyé au numéro de téléphone et, après l'avoir fourni à l'application, l'étudiant pourra télécharger le fichier.

Passwordless LoginPasswordless Login

Uploading a FileUploading a File

L'enseignant peut voir les fichiers que chaque étudiant a téléchargés par devoir en cliquant sur l'UUID généré automatiquement pour le devoir.

Seeing assignmentsSeeing assignments

Se familiariser avec le code de départ

Si vous souhaitez suivre le processus mais que vous n'êtes pas familier avec certaines des technologies utilisées ici, nous vous couvrons. Dans cette section, nous décrirons brièvement ce qu'elles sont, comment elles sont configurées dans le code de démarrage, et nous fournirons quelques liens utiles pour que vous puissiez obtenir plus d'informations. Si vous êtes déjà un pro de GraphQL et React, vous pouvez sauter cette section et aller directement à créer des classesbien que vous puissiez vouloir la lire quand même pour savoir comment ces pièces s'emboîtent dans le code de démonstration.

Apollo GraphQL

GraphQL fournit un langage d'interrogation et un moteur d'exécution pour l'interrogation de données à partir d'un serveur (généralement à partir de sources multiples). Il permet de décrire clairement les données et donne au client la possibilité de demander exactement ce dont il a besoin.

Apollo GraphQL est une implémentation standard de GraphQL. Elle fournit des bibliothèques serveur et client qui vous permettent de combiner et de consommer facilement des bases de données, des API et des microservices dans un seul graphe.

Le dossier serveur est composé d'un serveur GraphQL alimenté par Express. La configuration se trouve dans le fichier server/index.js (en anglais). Les éléments les plus importants de la configuration sont les définitions de type et les résolveurs.

La définition des types est l'endroit où GraphQL décrit les données qu'un client peut consommer. Cela se fait à l'aide de types. Les définitions de type sont configurées dans le fichier server/src/typeDefs.js dans le fichier Vous trouverez ci-dessous quelques exemples de types pour le code de démonstration :

type Student {
  phoneNumber: String!
  firstName: String!
  lastName: String!
}

type Homework {
  uuid: String!
  description: String!
}

Les types les plus importants sont les Query et Mutation qui exposent les "requêtes" et les "mutations" qu'un client peut effectuer avec les données.

Vous trouverez ci-dessous les requêtes et les mutations définies pour le code de démonstration :

type Query {
  homeworks: [Homework]
  homework(uuid: String): Homework
  homeworkFiles(uuid: String): [HomeworkFile] 
  sessionDetails(uuid: String) : SessionResponse
  students: [Student]
  student(phoneNumber: String): Student
}

type Mutation {
  addHomeworkFile(url: String!, uuid: String!, token: String!): HomeworkFile
  checkCode(requestId: String!, code: String!, number: String!): CheckCodeResponse
  inviteStudent(phoneNumber: String!, url: String!): OperationResponse
  presignDocument(fileName: String!, isPublic: Boolean, token: String!): String!
  saveHomework(id: Int, description: String!): Homework
  saveStudent(
    id: Int
    phoneNumber: String!
    firstName: String!
    lastName: String!
  ): Student
  startVideocallSession: SessionResponse
  verifyRequest(number: String!): VerifyRequestResponse
}

L'intérêt de GraphQL est que vous définissez le comportement de ces requêtes et mutations à l'aide de votre propre code personnalisé, ce qui vous permet d'extraire les informations de plusieurs bases de données, d'API REST ou même d'autres serveurs GraphQL. Ce code personnalisé que vous créez est connu sous le nom de resolvers.

Dans le code de démonstration, les résolveurs sont assignés à chaque requête et mutation dans le fichier server/src/resolvers.js tandis que les fonctions de résolution proprement dites sont situées dans le dossier server/src/graphql . Actuellement, les résolveurs ne lancent qu'une NOT_IMPLEMENTED mais nous allons changer cela tout au long de cet article.

const saveHomework = async (_, { description }, __, ___) => {
  throw new Error(NOT_IMPLEMENTED);
};

Apollo fournit également une bibliothèque pour le code côté client qui vous permet de consommer facilement les données du serveur. Elle maintient un cache afin que le client n'ait pas à demander des données au serveur si elles existent déjà.

Si vous souhaitez en savoir plus sur GraphQL et Apollo Graphql, vous pouvez consulter les liens suivants :

Réagir

React est une bibliothèque JavaScript qui permet de construire des interfaces utilisateur en utilisant une approche basée sur les composants. Chaque composant peut être réutilisé et conserve son propre état qui met automatiquement à jour l'interface utilisateur en cas de modification.

Ce projet utilise des composants fonctionnels qui fournissent un moyen simple mais puissant d'écrire des composants React. Il utilise également des crochets pour fournir des fonctionnalités supplémentaires telles que l'état et la communication avec le serveur.

Le code de démonstration présente une application React de base écrite en TypeScript. Elle utilise la bibliothèque Apollo Client pour se connecter au serveur et fournir un cache pour stocker les données récupérées sur le serveur.

L'ensemble de l'application est enveloppée dans l'ApolloProvider qui permet l'accès à son contexte à travers tous les composants.

ReactDOM.render(
  <ApolloProvider client={client}>
    <Pages />
  </ApolloProvider>,
  document.getElementById('root')
);

Si vous souhaitez en savoir plus sur React et son intégration avec le serveur Apollo, vous pouvez consulter les liens suivants :

Création de classes

Ok, si vous voulez suivre, il est temps de se salir les mains. Prenez votre éditeur de code préféré et ouvrez le dossier starter et ouvrez le dossier La première chose que nous allons faire est d'ajouter la possibilité de créer de nouvelles classes.

Puisque notre code est divisé en code serveur et code navigateur, il est logique de commencer à configurer le code du backend avant de travailler sur ce que l'utilisateur verra. Commençons donc par une mutation GraphQL qui crée une session dans le service Video API de Vonage.

Création du service Video API de Vonage et du résolveur

Pour créer une session audio/vidéo dans l'API Video de Vonage, nous utiliserons le paquetage opentok qui est déjà installé. La première chose à faire est d'initialiser le client en lui transmettant la clé API et la paire de secrets.

Dans le fichier server/src/services/vonage/videoApi.js remplissons la fonction initializeOpentok . Nous allons retourner une instance singleton de la variable opentok ce qui garantira que la même instance sera renvoyée à chaque fois que nous appellerons la fonction. Notez que nous importons la clé et le secret que nous avons définis précédemment en tant que variable d'environnement en utilisant les balises apiKey et apiSecret d'un fichier ../../utils/envs déjà configuré.

const { vonageVideoApiKey : apiKey, vonageVideoApiSecret : apiSecret } = require('../../util/envs');

let opentok = null;
...
const initializeOpentok = () => {
  opentok = opentok ? opentok : new OpenTok(apiKey, apiSecret);
}

L'étape suivante consiste à créer la session. Pour ce faire, nous utiliserons la fonction opentok.createSession pour ce faire. Cette fonction reçoit un objet qui définit la session en tant que routed. A routed signifie que nous utiliserons les serveurs de médias de Vonage, ce qui permet de réduire l'utilisation de la bande passante dans les sessions multipartites et d'activer des fonctions avancées telles que les enregistrements et l'interconnexion SIP.

// server/src/services/vonage/videoApi.js
...
const opentokSessionArgs = {
  mediaMode: 'routed',
};
...
const createSession = () => {
  return new Promise((resolve, reject) => {
    opentok.createSession(opentokSessionArgs, (err, session) => {
      if (err) {
        reject(err);
      } else {
        resolve(session);
      }
    });
  });
};

Enfin, nous ajouterons une fonction permettant de générer des jetons JWT qui seront utilisés pour authentifier les utilisateurs dans le contexte d'une session et pour définir les autorisations.

// server/src/services/vonage/videoApi.js
...
const generateToken = (sessionId) => {
  return opentok.generateToken(sessionId);
}
...

Maintenant que la fonctionnalité est en place, il ne reste plus qu'à l'exposer aux clients. Pour ce faire, nous allons créer une paire de mutations que le client React peut consommer afin de permettre aux enseignants de créer des sessions et aux étudiants de les rejoindre.

Ouvrons le fichier server/src/graphql/videoApi.js et remplissons les résolveurs d'espaces réservés.

Pour créer des sessions, voici les étapes à suivre :

  1. Initialiser le client opentok.

  2. Créer la session.

  3. Générer un identifiant pour la session à utiliser dans l'URL. Pour cela, nous utiliserons le paquetage uuid npm.

  4. Sauvegarder la session dans une mémoire persistante. Pour simplifier les choses, nous stockerons les données en mémoire à l'aide de tableaux définis dans server/src/services/db/index.jsmais dans une application réelle, une base de données réelle est plus judicieuse.

  5. Générer un jeton pour la session.

  6. Renvoie les données en respectant le format défini dans la définition du type de réponse à la mutation.

// server/src/graphql/videoApi.js
...
const startSession = async (_, __, ___, ____) => {
  try {
    // initialize opentok
    initializeOpentok();

    // create the session
    const session = await createSession();

    // generate an id
    const uuid = uuidv4();

    // save the session
    videocalls.push({
      uuid,
      sessionId: session.sessionId,
    });

    // generate a token with moderator privileges
    const token = session.generateToken({
      role: 'moderator',
      data: `role=moderator`,
    });

    // return date honoring the format for SessionResponse type
    return {
      uuid,
      token,
      session: session.sessionId,
      apiKey,
    };
  } catch (e) {
    console.error('An error occurredocurred when creating opentok session', e);
  }
};
...

La mutation pour le démarrage de la session, ainsi que le type de réponse, sont déjà définis à l'adresse suivante server/src/typeDefs.js.

// server/src/typeDefs.js
...
type SessionResponse {
  uuid: String!
  token: String!
  session: String!
  apiKey: String!
}
...
type Mutation {
  ...
  startVideocallSession: SessionResponse
  ...
}

La fonction de résolveur est également déjà attribuée. Nous pouvons le voir dans le fichier server/src/resolver.js fichier :

// server/src/resolvers.js
const {
  ...,
  // here we reference the startSession function we have just created
  videoApi: { joinSession, startSession },
} = require('./graphql');
...
const resolvers = {
  ...
  Mutation: {
    ...
    // here we assign that function as the resolver for the startVideocallSession mutation
    startVideocallSession: startSession,
    ...
  },
};

Ensuite, nous devons créer une fonction de résolution qui permet aux étudiants de rejoindre une session déjà créée. Pour ce faire, voici les étapes à suivre :

  1. Vérifier qu'un UUID a été fourni.

  2. Recherchez l'appel vidéo dans la base de données.

  3. Initialiser le client opentok.

  4. Utilisez la session pour générer un jeton pour l'étudiant.

  5. Renvoyer les données en respectant le format défini dans la définition du type de la mutation.

// server/src/resolvers.js
const {
  ...,
  // here we reference the startSession function we have just created
  videoApi: { joinSession, startSession },
} = require('./graphql');
...
const resolvers = {
  ...
  Mutation: {
    ...
    // here we assign that function as the resolver for the startVideocallSession mutation
    startVideocallSession: startSession,
    ...
  },
};

Comme pour la fonction précédente, le résolveur est déjà connecté à la définition du type. La seule différence est que cette fois-ci, au lieu d'une mutation, il s'agit d'une requête.

// server/src/typeDefs.js
type Query {
  ...
  sessionDetails(uuid: String) : SessionResponse
  ...
}

// server/src/resolvers.js
const {
...
  videoApi: { joinSession, startSession },
} = require('./graphql');
...
const resolvers = {
  Query: {
    sessionDetails: joinSession,
  ...
  },
};

Nous sommes maintenant prêts à construire l'interface utilisateur.

Ajout de l'interface utilisateur

Tout d'abord, créons quelques composants React. Dans le dossier client/src/components/ créez un nouveau dossier Videocall dans le dossier.

Créez maintenant un fichier nommé Room.tsx dans le dossier nouvellement créé. Il s'agit du composant qui hébergera la session.

Pour construire le composant, nous utiliserons le paquetage opentok-react npm. Le composant recevra une propriété uuid qui sera utilisée dans la requête pour récupérer les informations sur la session.

// client/src/components/Videocall/Room.tsx
import React from 'react';
import { useQuery } from '@apollo/client';
import { GET_START_CALL_SETTINGS } from '../../data/queries';
import { OTSession, OTPublisher, OTStreams, OTSubscriber } from 'opentok-react';

const Room = (props: any) => {
  // get the uuid from props
  const { uuid } = props;

  // make a query to the server (or not??? we'll talk about this later)
  const { data, loading, error } = useQuery(GET_START_CALL_SETTINGS, {
    variables: { uuid },
  });

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error!</p>;
  }

  // after the query is complete, get the session details...
  const { apiKey, session, token } = data.sessionDetails;

  // ... and pass them to opentok-react
  return (
    <OTSession apiKey={apiKey} sessionId={session} token={token}>
      <OTPublisher />
      <OTStreams>
        <OTSubscriber />
      </OTStreams>
    </OTSession>
  );
};

export default Room;

Ensuite, ajoutons un bouton pour créer la session. Ici, nous allons explorer une fonctionnalité puissante du client Apollo : le cache.

Actuellement, le composant Room tente de récupérer les détails de la session sur le serveur en se basant sur l'UUID d'une session déjà créée.

Étant donné que nous obtenons les mêmes informations lors de la création de la session, il n'est pas utile d'effectuer une deuxième demande lors de l'adhésion. Au lieu de cela, nous l'écrire dans le cache afin que le composant Room puisse l'obtenir à partir de là et n'ait pas à faire une nouvelle demande au serveur.

Créez un fichier StartButton.tsx et le remplir comme suit :

// client/src/components/Videocall/StartButton.tsx
import { useMutation } from '@apollo/client';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { START_VIDEOCALL_SESSION } from '../../data/mutations';
import { GET_START_CALL_SETTINGS } from '../../data/queries';

const startCallButton = {
  padding: '10pt',
  borderRadius: '3px',
  border: '0px',
};

const StartButton = () => {
  // we use the useMutation hook to create a mutate function
  const [startSession] = useMutation(START_VIDEOCALL_SESSION, {
    // the update() function allows to run code after running the mutation
    update(client, { data: { startVideocallSession } }) {
      // here we write the resulting data into the cache
      client.writeQuery({
        query: GET_START_CALL_SETTINGS,
        data: {
          sessionDetails: startVideocallSession
        },
        variables: {
          uuid: startVideocallSession.uuid
        }
      });
    },
    onCompleted({startVideocallSession}) {
      // after creating the session we move to a different page
      history.push(`/session/${startVideocallSession.uuid}`);
    } 
  });
  const history = useHistory();

  // we render the button and call the mutate function on click
  return (
    <div>
      <button
        style={startCallButton}
        onClick={() => {
          startSession();
        }}
      >
        Start Call
      </button>
    </div>
  );
};

export default StartButton;

Avant d'ajouter les pages, créons un fichier index.tsx sous client/src/components/Videocall qui exposera les deux composants sous le même import :

// src/components/Videocall/index.tsx
export { default as StartButton } from './StartButton';
export { default as Room } from './Room';

Il suffit maintenant de créer une nouvelle page sous client/src/pages/ nommée VideoSession.tsxet d'y ajouter le composant Room. Notez que nous n'avons pas besoin de spécifier le fichier Room mais que nous l'importons simplement au niveau du dossier. C'est grâce au fichier index.tsx que nous venons d'ajouter

// src/pages/VideoSession.tsx
import React from 'react';
import { useParams } from 'react-router-dom';
import { Room } from '../components/Videocall';
import { VideoSessionParams } from '../models';

const VideoSession = () => {
  // get the uuid from the url
  const { uuid } = useParams<VideoSessionParams>();
  
  // render the Room component and pass the UUID property
  return (
    <>
      <p>Joining to session {uuid}</p>
      <Room uuid={uuid} />
    </>
  );
};

export default VideoSession;

Ensuite, ajoutez la route VideoSession dans le fichier src/pages/index.tsx dans le fichier

// src/pages/index.tsx
...
import VideoSession from './VideoSession';

function Pages() {
  return (
    <Router>
      <Navigation />
      <div id="roots" className="p-2">
        <Route path="/session/:uuid" exact component={VideoSession} />
        <Route path="/" exact component={Inicio} />
      </div>
    </Router>
  );
}
...

Enfin, ajoutez le bouton à la page src/pages/Home.tsx page :

// src/pages/Home.tsx
...
import { StartButton } from '../components/Videocall';

const Inicio = () => {
  return (
    <div>
      <StartButton />
    </div>
  );
};
...

Création d'une liste d'étudiants

L'étape suivante consiste à permettre à un enseignant de créer une liste d'étudiants. L'idée est que lorsqu'un appel est lancé, l'enseignant peut consulter la liste et envoyer des notifications SMS aux étudiants pour les inviter à l'appel.

Comme pour les classes, nous commencerons par effectuer les mutations et les requêtes nécessaires dans le serveur GraphQL. Ensuite, nous ajouterons l'interface utilisateur.

Configuration des mutations et des requêtes

Commençons par travailler sur le code du serveur en permettant à un enseignant de créer un élève. Pour simplifier les choses, nous stockerons les étudiants dans un tableau, mais dans une application réelle, une base de données serait plus judicieuse.

Ouvrez le fichier server/src/graphql/student.js et remplissez les fonctions du résolveur comme suit :

// server/src/graphql/student.js
// import the "database" service
const { students } = require('../services/db');
...
const saveStudent = async (_, student, __, ___) => {
  try {
    // push the new student into the array
    students.push(student);

    // return the newly created student
    return student;
  } catch (err) {
    console.error('Error while trying to create student', err);
    throw new Error(INTERNAL_ERROR);
  }
};

const getStudents = (_, __, ___, ____) => {
  // return all the students
  return students;
};

Ensuite, ajoutons la magie Vonage pour envoyer des notifications. Pour ce faire, nous utiliserons le paquetage @vonage/server-sdk qui est déjà préinstallé et initialisé en tant qu'instance singleton dans le fichier server/src/services/vonage/vonage.js en tant qu'instance singleton :

// server/src/services/vonage/vonage.js
// import the npm package
const Vonage = require('@vonage/server-sdk');
// import the Vonage credentials from the environment variables
const { vonageApiKey : apiKey, vonageApiSecret : apiSecret } = require('../../util/envs');

// define the Vonage client
let instance = null;

const getVonageClient = () => {
  // if the client is not already define then initialize it
  if (!instance) {
    instance = new Vonage({
      apiKey,
      apiSecret
    });
  }

  // return the client
  return instance
}

// export the function
module.exports = {
  getVonageClient
}

Ouvrir le server/src/services/vonage/sms.js et remplir la fonction sendSms comme suit :

// server/src/services/vonage/sms.js
...
const sendSms = (to, text) => {
  return new Promise((resolve, reject) => {
    // get the Vonage client
    const vonageClient = getVonageClient();
    // get the Virtual Phone Number used to send the sms
    const from = vonageSenderNumber;

    // Call the sendSms method
    vonageClient.message.sendSms(from, to, text, (err, responseData) => {
      if (err) {
        reject(false);
      } else {
        if (responseData.messages[0]['status'] === '0') {
          console.log('Message sent successfully.');
          resolve(true)
        } else {
          console.log(
            `Message failed with error: ${responseData.messages[0]['error-text']}`
          );
          reject(false);
        }
      }
    });
  });
};
...

Ajout de l'interface utilisateur

Tout d'abord, créons quelques composants que nous réutiliserons ultérieurement pour créer des étudiants et les inviter à une session vidéo.

Créez un nouveau dossier sous client/src/components nommé Studentset à l'intérieur de celui-ci, créez trois fichiers supplémentaires : index.tsx, StudentForm.tsx et StudentsList.tsx.

Lors de la création du formulaire, nous adopterons une approche similaire à celle utilisée lors de la création d'une classe. En effet, après avoir appelé la mutation qui crée l'étudiant dans le serveur, nous mettons également à jour le cache local afin d'éviter les requêtes ultérieures au serveur.

Pour le formulaire proprement dit, nous utiliserons composants contrôlés afin que ses valeurs soient gérées par l'état de React. Puisque nous utilisons des composants fonctionnels, nous utiliserons le hook useState hook pour fournir un état au formulaire.

Remplissez le fichier StudentForm.tsx comme suit :

// client/src/components/Students/StudentForm.tsx
import { useMutation } from '@apollo/client';
import React, { useState } from 'react';
import { ADD_STUDENT } from '../../data/mutations';
import { GET_STUDENTS } from '../../data/queries';
import { Student, StudentData, StudentVars } from '../../models';

const StudentForm = () => {
  // set the state for the form
  const [phoneNumber, setPhoneNumber] = useState('');
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  // create the mutate function for adding student
  const [addStudent] = useMutation<{ saveStudent: Student }, StudentVars>(
    ADD_STUDENT,
    {
      onCompleted() { // set the onCompleted function to reset the state
        setPhoneNumber('');
        setFirstName('');
        setLastName('');
      },
      update(cache, { data }) { // set the update function to update local cache
        const existingStudentsdata = cache.readQuery<StudentData>({
          query: GET_STUDENTS,
        });
        cache.writeQuery({
          query: GET_STUDENTS,
          data: {
            students: [
              ...(existingStudentsdata?.students as Student[]),
              data?.saveStudent,
            ],
          },
        });
      },
    }
  );

  // render the form
  return (
    <form
      className="form-inline"
      onSubmit={(e) => {
        e.preventDefault();
        phoneNumber &&
          firstName &&
          lastName &&
          addStudent({
            variables: {
              phoneNumber,
              firstName,
              lastName,
            },
          });
      }}
    >
      <label htmlFor="phone">
        Phone Number
      </label>
      <input
        type="text"
        id="phone"
        name="phone"
        value={phoneNumber}
        onChange={(e) => setPhoneNumber(e.target.value)}
      />

      <label htmlFor="firstName">
        First Name
      </label>
      <input
        type="text"
        id="firstName"
        name="firstName"
        value={firstName}
        onChange={(e) => setFirstName(e.target.value)}
      />

      <label htmlFor="lastName">
        Last Name
      </label>
      <input
        type="text"
        id="lastName"
        name="lastName"
        value={lastName}
        onChange={(e) => setLastName(e.target.value)}
      />

      <button type="submit">
        Submit
      </button>
    </form>
  );
};

export default StudentForm;

Lors de la création de la liste des étudiants, nous ajouterons une propriété actions qui sera un tableau d'"actions" pouvant être appliquées à un étudiant.

Pour chaque action, nous ajouterons un bouton dans le tableau, dans la colonne "Actions", qui déclenchera une fonction personnalisée. Pensez à des actions telles que "modifier", "supprimer" ou "désactiver". Nous utiliserons ultérieurement cette propriété pour "inviter" un étudiant à un cours.

Remplissez le fichier StudentsList.tsx comme suit :

// client/src/components/Students/StudentsList.tsx
import React from 'react';
import { useQuery } from '@apollo/client';
import { GET_STUDENTS } from '../../data/queries';
import { Student, StudentListProps } from '../../models';

const StudentsList = ({ actions = [] }: StudentListProps) => {
  // create the query
  const { data, loading, error } = useQuery(GET_STUDENTS);

  // render the table
  return (
    <>
      {loading && <p>Loading...</p>}
      {error && <p>Error!</p>}
      {data && (
        <table>
          <thead>
          <tr>
            <th>Phone Number</th>
            <th>First Name</th>
            <th>Last Name</th>
            {actions.length > 0 && <th>Actions</th>}
          </tr>
          </thead>
          <tbody>
          {data.students.map((student: Student) => {
            return (
              <tr key={student.phoneNumber}>
                <td>{student.phoneNumber}</td>
                <td>{student.firstName}</td>
                <td>{student.lastName}</td>
                <td>
                  {actions.map((action) => (
                    <button key={action.actionName} onClick={() => action.onAction(student)}>
                      {action.actionName}
                    </button>
                  ))}
                </td>
              </tr>
            );
          })}
          </tbody>
        </table>
      )}
    </>
  );
};

export default StudentsList;

Exposons à présent les deux composants nouvellement créés dans le fichier index.tsx comme suit :

// client/src/components/Students/index.tsx
export {default as StudentsList} from './StudentsList';
export {default as StudentForm} from './StudentForm';

Et maintenant, créons la page client/src/pages/StudentPage.tsx et ajoutons la route dans l'index client/src/page/index.tsx index. Notez que nous importons à la fois StudentForm et StudentsList du même espace de noms. (Merci encore, index.tsx!)

// client/src/pages/StudentPage.tsx
import React from 'react';
import { StudentsList, StudentForm } from '../components/Students';

const Students = () => {
  return (
    <>
      <h1>Students</h1>
      <StudentForm />
      <StudentsList />
    </>
  );
};

export default Students;

// client/src/pages/index.tsx
...
import StudentPage from './StudentPage';

function Pages() {
  return (
    <Router>
      <Navigation />
      <div id="roots" className="p-2">
        ...
        <Route path="/students" exact component={StudentPage} />
      </div>
    </Router>
  );
}
...

Nous devrions maintenant être en mesure de créer de nouveaux étudiants et de les afficher dans la liste.

Inviter les étudiants

L'idée d'avoir des étudiants est de pouvoir les inviter à un appel. Vous vous souvenez de la propriété actions dont nous avons parlé plus tôt ? C'est ici que cette fonctionnalité va briller, car elle va nous permettre de fournir cette fonctionnalité à la liste des étudiants tout en nous permettant de réutiliser le même composant que celui que nous avons créé précédemment.

Créons un nouveau composant appelé Attendees.tsx sous client/src/components/Videocall/. Dans ce nouveau composant, nous créerons une action personnalisée qui déclenchera la inviteStudent mutation.

// client/src/components/Videocall/Attendees.tsx
import { useMutation } from '@apollo/client';
import React from 'react';
import { useLocation } from 'react-router';
import { INVITE_STUDENT } from '../../data/mutations';
import { Student, StudentListAction } from '../../models';
import { StudentsList } from '../Students';

const Attendees = () => {
  // create the mutate function
  const [inviteStudent] = useMutation(INVITE_STUDENT)
  // get the session url
  const location = useLocation();

  // create the custom action in an array
  const actions = new Array<StudentListAction>(
    {
      // set a name
      actionName: 'Invite',
      // set the action
      onAction: (student: Student) => {
        inviteStudent({
          variables: {
            phoneNumber: student.phoneNumber,
            url: window.location.origin + location.pathname
          }
        })
      }
    }
  )

  // render the StudentsList and pass the action
  return (
    <>
      <StudentsList actions={actions}/>
    </>
  )
}

export default Attendees;

Ajoutez également le composant nouvellement créé à l'index Videocall :

// src/components/Videocall/index.tsx
...
export { default as Attendees } from './Attendees';

Enfin, ajoutez le composant Attendees à la page VideoSession :

// src/pages/VideoSession.tsx
...
import { Room, Attendees } from '../components/Videocall';
...

const VideoSession = () => {
  ...
  
  return (
    <>
      ...
      <Attendees />
    </>
  );
};

Créez maintenant quelques étudiants en utilisant des numéros de téléphone valides, démarrez une classe et cliquez sur le bouton "inviter" pour les inviter.

Création et envoi d'affectations

La dernière étape de notre démonstration consiste à permettre aux étudiants d'envoyer des devoirs. Pour nous assurer que nous sommes en mesure d'identifier à quel étudiant appartient un fichier de devoirs, nous utiliserons une connexion sans mot de passe basée sur le numéro de téléphone utilisé pour enregistrer l'étudiant.

Mise en place des mutations et des requêtes

La première chose à faire est de permettre la création de devoirs et de fichiers de devoirs. Nous devons également donner aux utilisateurs la possibilité de télécharger des fichiers. Pour ce faire, nous utiliserons un seau S3 avec des requêtes POST présignées.

Commençons par les résolveurs pour la création et la récupération des devoirs et des fichiers de devoirs. Ouvrez le fichier server/src/graphql/homework.js sous serveret remplissez les résolveurs comme suit :

// src/graphql/homework.js
...
const saveHomework = async (_, { description }, __, ___) => {
  const uuid = uuidv4();

  try {
    const homework = {
      uuid,
      description
    }
    homeworks.push(homework);

    return homework;
  } catch (err) {
    console.error('Error while trying to create homework', err);
    throw new Error(INTERNAL_ERROR);
  }
};

const getHomeworks = (_, __, ___, ____) => {
  return homeworks;
};

const getHomework = (_, { uuid }, __, ___) => {
  const [homework] = homeworks.filter((homework) => homework.uuid === uuid);
  return homework;
};

const addHomeworkFile = async (_, { url, uuid, token }, __, ___) => {
  // This token comes from the passwordless login
  if (!token) {
    throw new Error(NOT_AUTHENTICATED);
  }

  try {
    const decodedToken = jwt.verify(token, accessTokenSecret);
    const [student] = students.filter((student) => student.phoneNumber === decodedToken.phoneNumber)
    const [homework] = homeworks.filter((homework) => homework.uuid === uuid);
    const homeworkFile = {
      url,
      student,
      homework,
    };
    homeworkFiles.push(homeworkFile);

    return homeworkFile;
  } catch (err) {
    console.log('An error occurredocurred when trying to save homework file', err);
    throw new Error(INTERNAL_ERROR);
  }
};

const getHomeworkFiles = (_, { uuid }, __, ___) => {
  return homeworkFiles.filter(homeworkFile => homeworkFile.homework.uuid === uuid);
};
...

Ensuite, ajoutons une mutation pour pré-signer une requête POST qui peut être utilisée plus tard dans le code côté client pour télécharger le fichier sur S3. Pour ce faire, nous utilisons le paquetage aws-sdk npm. Le service est déjà configuré dans server/src/services/aws/s3.js.

// server/src/services/aws/s3.js
...
const presignedPostDocument = (keyName, isPublic = false) => {
  const acl = isPublic ? 'public-read' : 'private';
  return new Promise((resolve, reject) => {
    const params = {
      Bucket: s3Bucket,
      Fields: {
        Key: keyName,
      },
      Expires: 300,
      Conditions: [
        ['content-length-range', 0, 5242880],
        ['eq', '$Content-Type', 'application/pdf'],
        { acl },
      ],
    };
    s3.createPresignedPost(params, (err, data) => {
      if (err) {
        reject('Error while creating presigned post', err);
      } else {
        resolve(data);
      }
    });
  });
};
...

Il ne nous reste donc plus qu'à consommer le service dans une nouvelle mutation. Ouvrez le fichier server/src/graphql/s3.js et remplir la fonction presignDocument comme suit :

// server/src/graphql/s3.js
...
const presignDocument = async (_, { fileName, isPublic, token }, __, ___) => {
  // This token comes from the passwordless login
  if (!token) {
    throw new Error(NOT_AUTHENTICATED);
  }

  try {
    // identify the student
    const data = jwt.verify(token, accessTokenSecret);
    console.info(
      `Student with id ${data.id} is presigning filename ${fileName}`
    );
    // presign the post requests
    const uploadData = await s3.presignedPostDocument(fileName, isPublic);
    // return the response as a stringified JSON
    return JSON.stringify(uploadData);
  } catch (err) {
    console.log('An error ocurred when presigning document:', err);
    throw new Error(INTERNAL_ERROR);
  }
};
...

Il est maintenant temps de configurer la magie Vonage pour l'authentification sans mot de passe. Pour ce faire, nous allons utiliser l'API Verify. Tout d'abord, créons le service. Ouvrez le fichier server/src/services/vonage/verify.js et remplissez les champs verifyRequest et checkCode comme suit :

// server/src/services/vonage/verify.js
...
const verifyRequest = (number) => {
  return new Promise((resolve, reject) => {
    // get the Vonage client
    const vonageClient = getVonageClient();
    const brand = 'Vonage APIs';

    // Create a verification request for the given number
    vonageClient.verify.request({number, brand}, (err, result) => {
      if (err) {
        reject(false);
      } else {
        // return the request id which will be used when verifying the code
        resolve(result.request_id);
      }
    });
  });
};

const checkCode = (code, request_id) => {
  return new Promise((resolve, reject) => {
    // get the Vonage client
    const vonageClient = getVonageClient();
    
    // here pass both the request id and the code sent by the student
    vonageClient.verify.check({
      request_id,
      code
    }, (err, result) => {
      if (err) {
        reject(false);
      } else {
        // if code is correct we authenticate the student
        if (result.status === '0') {
          resolve(true);
        } else {
          reject(false);
        }
      }
    });
  })
}
...

Enfin, exposons ces services via GraphQL. Ouvrez le fichier server/src/graphql/vonage.js et remplissons les champs verifyRequestResolver et checkCodeResolver comme suit :

// server/src/graphql/vonage.js
...
const verifyRequestResolver = async (_, { number }, __, ___) => {
  try {
    const requestId = await verifyRequest(number);
    return {
      requestId,
    };
  } catch (err) {
    console.error(err);
    throw new Error(INTERNAL_ERROR);
  }
};

const checkCodeResolver = async (_, { requestId, code, number }, __, ___) => {
  try {
    const result = await checkCode(code, requestId);

    // if verification is successful, we return a JWT token
    if (result) {
      const [student] = students.filter(
        (student) => student.phoneNumber === number
      );

      // create the token
      const token = jwt.sign(
        {
          phoneNumber: student.phoneNumber,
        },
        accessTokenSecret,
        {
          expiresIn: '15min',
        }
      );

      return {
        token,
      };
    } else {
      return {
        token: null,
      };
    }
  } catch (err) {
    console.error('An error ocurred when trying to check code', err);
    return {
      token: null,
    };
  }
};
...

Créer des composants et des pages React

Commençons par créer un formulaire pour la création des devoirs et un tableau simple pour les répertorier.

Créer un dossier Homeworks sous client/src/components puis créez HomeworkForm.tsx et HomeworkList.tsx à l'intérieur de celui-ci. Remplissez le premier fichier comme suit pour créer le formulaire :

// client/src/components/Homeworks/HomeworkForm.tsx
import { useMutation } from '@apollo/client';
import React, { useState } from 'react';
import { Homework } from '../../models';
import { ADD_HOMEWORK } from '../../data/mutations';
import { GET_HOMEWORKS } from '../../data/queries';

const HomeworkForm = () => {
  // declare the state for the controlled form
  const [description, setDescription] = useState('');

  // setup the mutate function that creates the homework
  const [addHomework] = useMutation<
    { saveHomework: Homework },
    { description: string }
  >(ADD_HOMEWORK, {
    // on completion, reset the state
    onCompleted() {
      setDescription('');
    },
    // after creating the homework, add it to the local cache
    update(cache, { data }) {
      const existingHomeworksData = cache.readQuery<{ homeworks: Homework[] }>({
        query: GET_HOMEWORKS,
      });
      cache.writeQuery({
        query: GET_HOMEWORKS,
        data: {
          homeworks: [
            ...(existingHomeworksData?.homeworks as Homework[]),
            data?.saveHomework,
          ],
        },
      });
    },
  });

  return (
    <>
      <form
        className="form-inline"
        onSubmit={(e) => {
          // when submit the form call the mutate function
          e.preventDefault();
          description &&
            addHomework({
              variables: {
                description,
              },
            });
        }}
      >
        <label htmlFor="description">
          Description
        </label>
        <input
          type="text"
          id="description"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
        />

        <button type="submit">
          Submit
        </button>
      </form>
    </>
  );
};

export default HomeworkForm;

Ensuite, il faut remplir le fichier HomeworkList.tsx comme suit pour créer un tableau simple qui répertorie les devoirs créés. Notez que nous définissons également quelques Links sous les colonnes "Identifiant" et "Action". Ces liens permettront à un enseignant de consulter les fichiers d'un devoir donné et aux étudiants de télécharger les fichiers.

Nous travaillerons sur les pages que ces liens ouvriront dans un instant.

// client/src/components/Homeworks/HomeworkList.tsx
import React from 'react';
import { useQuery } from '@apollo/client';
import { Link } from 'react-router-dom';
import { GET_HOMEWORKS } from '../../data/queries';
import { Homework } from '../../models';

const HomeworkList = () => {
  // query the list of homeworks
  const { data, loading, error } = useQuery(GET_HOMEWORKS);

  // render the component with the retrieved homework
  return (
    <>
      {loading && <p>Loading...</p>}
      {error && <p>Error!</p>}
      {data && (
        <table>
          <thead>
            <tr>
              <th>Identifier</th>
              <th>Description</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {data.homeworks.map((homework: Homework) => {
              return (
                <tr key={homework.uuid}>
                  <td>
                    <Link to={`/homeworks/${homework.uuid}/list`}>
                      {homework.uuid}
                    </Link>
                  </td>
                  <td>{homework.description}</td>
                  <td>
                    <Link to={`/homeworks/${homework.uuid}/upload`}>
                      Upload
                    </Link>
                  </td>
                </tr>
              )
            })}
          </tbody>
        </table>
      )}
    </>
  )
}

export default HomeworkList;

Maintenant, exposons les composants nouvellement créés en créant un fichier index.tsx sous client/src/components/Homeworks avec le contenu suivant :

// client/src/components/Homeworks/index.tsx
export {default as HomeworkList} from './HomeworkList';
export {default as HomeworkForm} from './HomeworkForm';

Créez ensuite le fichier HomeworksPage.tsx sous client/src/pages/ comme suit :

// client/src/pages/HomeworksPage.tsx
import React from 'react';
import { HomeworkForm, HomeworkList } from '../components/Homeworks';

const HomeworksPage = () => {
  return (
    <>
      <HomeworkForm />
      <HomeworkList />
    </>
  )
}

export default HomeworksPage;

Et n'oubliez pas de l'ajouter au fichier index.tsx dans le même dossier :

// client/src/pages/index.tsx
...
import HomeworksPage from './HomeworksPage';

function Pages() {
  return (
    <Router>
      <Navigation />
      <div id="roots" className="p-2">
        ...
        <Route path="/homeworks" exact component={HomeworksPage} />
        <Route path="/" exact component={Home} />
      </div>
    </Router>
  );
}

Mise en œuvre d'une connexion sans mot de passe

Pour la connexion sans mot de passe, créons deux nouveaux composants : un qui servira de page de connexion et un autre qui contiendra le formulaire que les élèves verront après s'être authentifiés.

Créer PasswordlessLogin.tsx et HomeworkFileForm.tsx sous client/src/components/Homeworks.

Tout d'abord, concentrons-nous sur la création du formulaire de connexion. Pour ce faire, notre composant définira une mutation pour la création d'une demande de vérification et une autre pour la vérification proprement dite.

L'interface utilisateur se compose d'une zone de texte qui demande le numéro de téléphone et d'un bouton pour lancer la demande. Après que le serveur a renvoyé un requestId a été renvoyé avec succès par le serveur, nous voulons afficher un champ de texte supplémentaire pour saisir le code et un bouton pour la vérification.

Remplissez le fichier PasswordlessLogin.tsx comme suit :

// client/src/components/Homeworks/PasswordlessLogin.tsx
import { useMutation } from '@apollo/client';
import React, { useState } from 'react';
import { VERIFY_REQUEST, CHECK_CODE } from '../../data/mutations';

// the component receives a custom onLogin function that runs after a student has successfullysuccesfully authenticated
const PasswordlessLogin = ({
  onLogin,
}: {
  onLogin: (token: String) => void;
}) => {
  // setup the state for the controlled form
  const [number, setNumber] = useState('');
  const [code, setCode] = useState('');
  const [requestId, setRequestId] = useState<string | null>(null);

  // setup mutate functions for both request a verification and check the code
  const [verifyRequest] = useMutation<{
    verifyRequest: { requestId: string };
    verifyRequestVars: { number: string };
  }>(VERIFY_REQUEST, {
    // after getting the request id add it to the state
    onCompleted(data: { verifyRequest: { requestId: string } }) {
      setRequestId(data.verifyRequest.requestId);
    },
  });
  const [checkCode] = useMutation<{
    checkCode: { token: string };
    checkCodeVars: { requestId: string; code: string; number: string };
  }>(CHECK_CODE, {
    // after authenticating call the custom onLogin function
    onCompleted(data: { checkCode: { token: string } }) {
      if (data.checkCode.token) {
        onLogin(data.checkCode.token);
      }
    },
  });

  return (
    <>
      <form
        onSubmit={(e) => {
          // on submit check the code
          e.preventDefault();
          number && code && requestId && checkCode({
            variables: {
              requestId,
              code,
              number
            }
          });
        }}
      >
        <div className="form-row">
          <div className="col">
            <label htmlFor="number">Phone Number</label>
            <input
              type="text"
              className="form-control"
              id="number"
              placeholder="Enter phone number"
              value={number}
              onChange={(e) => setNumber(e.target.value)}
            />
          </div>
          <div className="col">
            <button
              onClick={(e) => {
                // when clicking this button initiate the verification
                number &&
                  verifyRequest({
                    variables: {
                      number,
                    },
                  });
              }}
            >
              Request code
            </button>
          </div>
        </div>
        {requestId && ( // only show the elements below if a requestId has been successfullysuccesfully returned by the server
          <>
            <div className="form-group">
              <label htmlFor="code">Code</label>
              <input
                type="text"
                className="form-control"
                id="code"
                placeholder="Enter one time code"
                value={code}
                onChange={(e) => setCode(e.target.value)}
              />
            </div>
            <button type="submit" className="btn btn-primary">
              Submit
            </button>
          </>
        )}
      </form>
    </>
  );
};

export default PasswordlessLogin;

Ensuite, créez le formulaire de téléchargement du fichier. Ce formulaire sera légèrement différent de ceux que nous avons construits précédemment dans ce tutoriel car il s'agira d'un formulaire non contrôlé. En outre, des mesures supplémentaires doivent être prises pour télécharger le fichier vers S3 avant d'appeler la fonction de mutation.

Remplissez le HomeworkFileForm.tsx comme suit :

// client/src/component/HomeworkFileForm.tsx
import React from 'react';
import { useMutation, useQuery } from '@apollo/client';
import {
  ADD_HOMEWORK_FILE,
  PRESIGN_HOMEWORK_FILE_UPLOAD,
} from '../../data/mutations';
import { GET_HOMEWORK } from '../../data/queries';
import { Homework } from '../../models';

// this components receives the uuid of the homework and the authentication token as properties
const HomeworkFileForm = ({ uuid, token }: { uuid: string; token: string }) => {
  // create a reference of the html element 
  let homeworkFileRef: HTMLInputElement;

  // use the uuid to retrieve the information of the homework
  const { data, loading, error } = useQuery<
    { homework: Homework },
    { uuid: string }
  >(GET_HOMEWORK, {
    variables: {
      uuid
    }
  });

  // setup the mutate functions for presiging the file and for adding the reference to the database
  const [presignHomeworkFileUpload] = useMutation(PRESIGN_HOMEWORK_FILE_UPLOAD);
  const [addHomeworkFile] = useMutation(ADD_HOMEWORK_FILE);

  render the component
  return (
    <>
      {loading && <p>Loading...</p>}
      {error && <p>Error!</p>}
      {data && (
        <>
          <p>Description: {data.homework.description}</p>
          <form
            onSubmit={async (e) => {
              e.preventDefault();

              try {
                // get the file from the html element reference
                const file =
                  homeworkFileRef &&
                  homeworkFileRef.files &&
                  homeworkFileRef.files[0];

                // make sure a file was provided
                if (!file) {
                  throw new Error('file is not defined');
                }

                // get the presign informatio from the server
                const { data } = await presignHomeworkFileUpload({
                  variables: {
                    fileName: `homeworks/uuid/${Date.now()}`,
                    isPublic: true,
                    token,
                  },
                });

                // parse the stringified JSON
                const imageData = JSON.parse(data.presignDocument);

                // create a form programatically for sending the file to S3
                const formData = new FormData();

                // add the required headers
                formData.append('Content-Type', file?.type);
                formData.append('acl', 'public-read');

                // add the signing information
                Object.keys(imageData.fields).forEach((key) => {
                  formData.append(key, imageData.fields[key]);
                });

                // and finally add the file
                formData.append('file', file);

                // use fetch to send a POST requests to S3
                const result = await fetch(imageData.url, {
                  method: 'POST',
                  body: formData,
                });

                // if the file was uploaded sucessfully then add the file information to the database
                if (result.status >= 200 && result.status <= 299) {
                  addHomeworkFile({
                    variables: {
                      url: imageData.url + '/' + imageData.fields.Key,
                      uuid,
                      token,
                    },
                  });
                }
              } catch (err) {
                console.error('An error ocurred', err);
              }
            }}
          >
            <input
              id="homeworkFile"
              type="file"
              name="homeworkFile"
              ref={(node: HTMLInputElement) => (homeworkFileRef = node)}
            />
            <button type="submit">Send</button>
          </form>
        </>
      )}
    </>
  );
};

export default HomeworkFileForm;

Créons maintenant une page qui affichera un composant différent selon que l'étudiant s'est connecté ou non. Créez le fichier client/src/pages/AddHomeworkFilePage.tsx et le remplir comme suit :

// client/src/pages/AddHomeworkFilePage.tsx
import React, {useState} from 'react';
import { useParams } from 'react-router-dom';
import { PasswordlessLogin, HomeworkFileForm } from '../components/Homeworks';

const AddHomeworkFilePage = () => {
  // setup the state for storing the token
  const [ token, setToken ] = useState<string | null>(null);
  // get the homework uuid from the url
  const { uuid } = useParams<{ uuid: string }>();

  // if a token exists show the homework form, if not show the login form
  return token ? <HomeworkFileForm token={token as string} uuid={uuid} /> : <PasswordlessLogin onLogin={(token) => setToken(token as string)} />
}

export default AddHomeworkFilePage;

Là encore, n'oubliez pas d'ajouter la page nouvellement créée au dossier index.tsx dans le même dossier :

// client/src/pages/index.tsx
...
import AddHomeworkFilePage from './AddHomeworkFilePage';

function Pages() {
  return (
    <Router>
      <Navigation />
      <div id="roots" className="p-2">
        ...
        <Route path="/homeworks/:uuid/upload" exact component={AddHomeworkFilePage} />
       ...
      </div>
    </Router>
  );
}
...

Création d'une liste des fichiers de devoirs

La dernière chose que nous devons faire est de permettre à l'enseignant de vérifier les fichiers de devoirs envoyés par les étudiants. Pour ce faire, nous allons simplement créer un composant HomeworkFileList similaire à ceux que nous venons de créer pour les étudiants et les devoirs.

Créez un nouveau client/src/components/Homeworks/HomeworkFileList.tsx et remplissez-le comme suit pour créer la liste des fichiers de devoirs :

// client/src/components/Homeworks/HomeworkFileList.tsx
import React from 'react';
import { useQuery } from '@apollo/client';
import { GET_HOMEWORK_FILES } from '../../data/queries';
import { HomeworkFile } from '../../models';

// the component get the uuid of the homework as property
const HomeworkFileList = ({ uuid }: { uuid: string }) => {
  // retrieve the submitted files for the given homework
  const { data, loading, error } = useQuery<
    { homeworkFiles: HomeworkFile[] },
    { uuid: string }
  >(GET_HOMEWORK_FILES, {
    variables: {
      uuid,
    },
  });

  // render the html elements
  return (
    <>
      {loading && <p>Loading...</p>}
      {error && <p>Error!</p>}
      {data && (
        <>
          <h1>Homework Files</h1>
          <p>{data.homeworkFiles.length > 0 && data.homeworkFiles[0].homework.description}</p>
          <table>
            <thead>
              <tr>
                <th>Phone Number</th>
                <th>Student Name</th>
                <th>Link</th>
              </tr>
            </thead>
            <tbody>
              {data.homeworkFiles.map((homeworkFile) => {
                return (
                  <tr key={homeworkFile.student.phoneNumber}>
                    <td>{homeworkFile.student.phoneNumber}</td>
                    <td>
                      {homeworkFile.student.firstName}
                      {homeworkFile.student.lastName}
                    </td>
                    <td>
                      <a
                        target="_blank"
                        rel="noopener noreferrer"
                        href={homeworkFile.url}
                      >
                        {homeworkFile.url}
                      </a>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </>
      )}
    </>
  );
};

export default HomeworkFileList;

Enfin, créez le fichier ListHomeworkFilesPage.tsx sous client/src/pages comme indiqué ci-dessous :

// client/src/pages/ListHomeworkFilesPage.tsx
import React from 'react';
import { useParams } from 'react-router-dom';
import { HomeworkFileList } from '../components/Homeworks';

const ListHomeworkFilesPage = () => {
  const { uuid } = useParams<{uuid: string}>();

  return <HomeworkFileList uuid={uuid} />
}

export default ListHomeworkFilesPage;

Et pour la dernière fois, n'oubliez pas d'ajouter la route au fichier index.tsx dans le même dossier :

// client/src/pages/index.tsx
...
import ListHomeworkFilesPage from './ListHomeworkFilesPage';

function Pages() {
  return (
    <Router>
      <Navigation />
      <div id="roots" className="p-2">
        ...
        <Route path="/homeworks/:uuid/list" exact component={ListHomeworkFilesPage} />
        ...
      </div>
    </Router>
  );
}
...

Conclusion

Et c'est tout ! Nous espérons que ce billet vous a donné une idée de ce que vous pouvez faire pour vous adapter à la " nouvelle normalité " et de la façon dont les outils géniaux développés par Vonage peuvent vous aider à y parvenir.

Partager:

https://a.storyblok.com/f/270183/250x250/20ed4402f3/hector-zelaya.png
Hector Zelaya

Héctor is a Computer Systems Engineer based in El Salvador. He works on DevOps, technical writing, and QA. He has plenty of experience with web applications and cloud services. When he is not in front of the computer he loves playing music, videogames and going on adventures with the love of his life: his wife ❤️