
Partager:
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 ❤️
Comment construire une plateforme d'apprentissage avec React, Express et Apollo GraphQL
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 :
Un compte API Vonage.
Un compte Video API de Vonage. Inscrivez-vous gratuitement ici.
Un numéro de téléphone virtuel. Une fois que vous avez votre compte API Vonage voyez comment vous pouvez obtenir un numéro ici.
Une paire de clés et de secrets pour un projet Vonage Video API. Vous pouvez créer un projet à partir de votre page de votre compte Video API de Vonage.
Un compte AWS et une paire de clés et de secrets.
Un godet AWS S3 pour télécharger des fichiers.
Node 12 et NPM 6 installés dans votre système.
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 :
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 :
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 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 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 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 Login
Uploading 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 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 :
Initialiser le client opentok.
Créer la session.
Générer un identifiant pour la session à utiliser dans l'URL. Pour cela, nous utiliserons le paquetage
uuidnpm.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.Générer un jeton pour la session.
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 :
Vérifier qu'un UUID a été fourni.
Recherchez l'appel vidéo dans la base de données.
Initialiser le client opentok.
Utilisez la session pour générer un jeton pour l'étudiant.
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:
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 ❤️
