
Share:
)
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 ❤️
Wie man eine Lernplattform mit React, Express und Apollo GraphQL erstellt
Lesedauer: 18 Minuten
2020 war für uns alle ein atypisches Jahr. Viele Branchen mussten die Art und Weise, wie sie ihr Geschäft betreiben, "überdenken", und die Chancen stehen gut, dass diese Strategien nicht nur vorübergehend sind, sondern auf Dauer Bestand haben werden.
Eine dieser Veränderungen betrifft die Art und Weise, wie wir lernen. Viele Schulen, Universitäten und Akademien auf der ganzen Welt haben einen Anstieg der Ferndienstleistungen zu verzeichnen, wobei sie sich häufig auf private Lösungen für deren Bereitstellung verlassen.
Heute sehen wir uns an, wie wir unsere eigene Lernplattform mit Video- und Audiofunktionen, SMS-Benachrichtigungen und passwortloser Authentifizierung aufbauen können.
Voraussetzungen
Um die Anwendung zu erstellen und auszuführen, benötigen Sie die folgenden Ressourcen:
Ein Vonage API-Konto.
Ein Vonage Video API-Konto. Melden Sie sich hier kostenlos an.
Eine virtuelle Rufnummer. Sobald Sie Ihr Vonage API-Konto haben sehen Sie hier, wie Sie eine Nummer erhalten können.
Ein Paar aus Schlüssel und Geheimnis für ein Vonage Video API-Projekt. Sie können ein Projekt von Ihrer Vonage Video-API-Kontoseite erstellen..
Ein AWS-Konto und ein Schlüssel-Geheimnis-Paar.
Ein AWS S3-Bucket zum Hochladen von Dateien.
Node 12 und NPM 6 sind in Ihrem System installiert.
Vonage API-Konto
Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.
Was wir bauen werden
Wir werden eine Webanwendung entwickeln, die es Lehrkräften ermöglicht, sofortige Video-/Audiokurse zu erstellen, an denen ein Schüler mit einem Link teilnehmen kann. Die Lehrer können eine Liste von Schülern erstellen, die durch ihre Telefonnummern identifiziert werden, und ihnen später den Link für den Anruf per SMS schicken.
Der Lehrer kann auch Aufgaben erstellen. Die Schüler können sich später mit einer passwortlosen Authentifizierung identifizieren und Dateien hochladen, die später von der Lehrkraft überprüft werden können.
Um die Dinge einfach und zeitsparend zu halten, wurden einige Funktionen wie Authentifizierung (An- und Abmeldung) und eine echte Datenbank weggelassen. Stattdessen sind alle Seiten öffentlich zugänglich, und die Daten werden mithilfe von JavaScript-Arrays im Speicher abgelegt.
Wenn Sie daran interessiert sind, das Endprodukt selbst zu erleben, habe ich ein Github-Repository das Sie lokal klonen können. Das Repository hat einen final
Ordner, in dem Sie das fertige Beispiel sehen können, und einen starter
in dem React, Express und Apollo GraphQL bereits vorkonfiguriert sind, und den Sie verwenden können, um das Beispiel Schritt für Schritt zu erstellen.
Der Demo-Code ist unterteilt in einen server
Ordner, der eine Apollo GraphQL Server mit Expressenthält, und einen client
Ordner, der einen grundlegenden React Anwendung enthält. Der Backend-Code ist in einfachem JavaScript geschrieben, während das Frontend TypeScript verwendet. Wenn Sie also mit den Unterschieden zwischen diesen beiden nicht vertraut sind, können Sie sie nebeneinander vergleichen.
Bevor Sie beginnen, stellen Sie sicher, dass Sie in jeden Ordner gehen und die Abhängigkeiten mit npm installieren, wie unten gezeigt:
Sie müssen auch Geheimnisse konfigurieren. Im Ordner client
Ordner müssen Sie lediglich die Datei .env
Datei in .env.local
.
Im Ordner server
Ordner hingegen benennen Sie die app.envs
Datei in .env
müssen Sie auch die Platzhalterwerte in der Datei durch Ihre eigenen AWS-Schlüssel, den Namen des S3-Buckets, die Vonage-Schlüssel und die virtuelle Vonage-Nummer ersetzen.
Wenn Sie das fertige Produkt ausführen möchten, öffnen Sie zwei separate Terminalfenster und verwenden Sie npm, um beide Anwendungen zu starten (siehe unten):
Es öffnet sich automatisch ein Browserfenster, in dem Sie die Anwendung in Aktion sehen können. Das erste, was Sie sehen, ist ein Fenster mit einer Schaltfläche, mit der Sie eine Klasse erstellen können. Bevor Sie loslegen, gehen Sie auf die Seite Students
Seite und legen Sie einige Schüler mit gültigen Telefonnummern an.
The Students Page
Kehren Sie nun zum Hauptbildschirm zurück, indem Sie auf den Titel der Anwendung klicken. Beginnen Sie nun eine neue Klasse.
Nach ein paar Sekunden befinden Sie sich in einer Vonage Video API-Sitzung. Mit Hilfe der Schülerliste können Sie den Schülern eine SMS-Benachrichtigung senden, damit sie der Klasse beitreten können, indem sie einfach auf die Schaltfläche Invite
Schaltfläche.
Starting a class
Nehmen wir an, Sie möchten eine Aufgabe erstellen, bei der die Schüler ein PDF-Dokument hochladen müssen. Sie können das so machen, dass die Schüler kein eigenes Konto haben müssen, sondern sich einfach mit ihrem Telefon authentifizieren können.
Gehen Sie dazu auf die Seite Homeworks
Seite und erstellen Sie eine neue Hausaufgabe, indem Sie eine Beschreibung eingeben. Klicken Sie dann als Schüler auf den Upload
Link.
Creating an Assignment
Um die Datei hochzuladen, muss der Schüler dieselbe Telefonnummer angeben, die der Lehrer bei der Erstellung verwendet hat. An die Telefonnummer wird ein Verifizierungscode gesendet, den der Schüler in die Anwendung eingeben muss, damit er die Datei hochladen kann.
Passwordless Login
Uploading a File
Der Lehrer kann die Dateien sehen, die jeder Schüler pro Aufgabe hochgeladen hat, indem er auf die automatisch generierte UUID der Hausaufgabe klickt.
Seeing assignments
Vertraut werden mit dem Startcode
Wenn Sie mitmachen möchten, aber mit einigen der hier verwendeten Technologien nicht vertraut sind, haben wir für Sie das Richtige. In diesem Abschnitt werden wir kurz beschreiben, um welche Technologien es sich handelt, wie sie im Startcode konfiguriert sind, und einige nützliche Links angeben, damit Sie weitere Informationen erhalten können. Wenn Sie bereits ein Profi in Sachen GraphQL und React sind, können Sie diesen Abschnitt überspringen und direkt zu Klassen erstellengehen, obwohl Sie ihn vielleicht trotzdem lesen wollen, um zu wissen, wie diese Teile im Demo-Code zusammenpassen.
Apollo GraphQL
GraphQL bietet eine Abfragesprache und eine Laufzeitumgebung für die Abfrage von Daten von einem Server (in der Regel aus mehreren Quellen). Sie ermöglicht eine klare Beschreibung der Daten und gibt dem Client die Möglichkeit, genau das abzufragen, was er benötigt.
Apollo GraphQL ist eine Industriestandard-Implementierung von GraphQL. Sie bietet Server- und Client-Bibliotheken, mit denen Sie Datenbanken, APIs und Microservices einfach in einem einzigen Graphen kombinieren und konsumieren können.
Der Server-Ordner besteht aus einem GraphQL-Server, der von Express betrieben wird. Die Konfiguration befindet sich in der server/index.js
Datei. Die wichtigsten Teile der Konfiguration sind die Type Definitions und Resolver.
In den Typdefinitionen beschreibt GraphQL die Daten, die ein Client konsumieren kann. Dies geschieht mit Hilfe von Typen. Type Definitions werden in der server/src/typeDefs.js
Datei konfiguriert. Nachfolgend finden Sie einige Beispiele für die Typen für den Democode:
type Student {
phoneNumber: String!
firstName: String!
lastName: String!
}
type Homework {
uuid: String!
description: String!
}
Die wichtigsten Typen sind die Query
und Mutation
Typen, die angeben, welche "Abfragen" und "Mutationen" ein Client mit den Daten durchführen kann.
Nachfolgend finden Sie die für den Democode definierten Abfragen und Mutationen:
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
}
Das Schöne an GraphQL ist, dass Sie das Verhalten dieser Abfragen und Mutationen mithilfe Ihres eigenen benutzerdefinierten Codes definieren, wodurch Sie die Informationen von mehreren Datenbanken, REST-APIs oder sogar anderen GraphQL-Servern abrufen können. Der von Ihnen erstellte benutzerdefinierte Code ist bekannt als resolvers
.
Im Democode werden jeder Abfrage und Mutation in der Datei server/src/resolvers.js
zugewiesen, während sich die eigentlichen Resolverfunktionen im Ordner server/src/graphql
Ordner befinden. Momentan werfen die Resolver nur eine NOT_IMPLEMENTED
Ausnahme, aber wir werden das im Laufe dieses Artikels ändern.
const saveHomework = async (_, { description }, __, ___) => {
throw new Error(NOT_IMPLEMENTED);
};
Apollo bietet auch eine Bibliothek für clientseitigen Code, mit der Sie Daten vom Server einfach abrufen können. Sie unterhält einen Cache, so dass der Client keine Daten vom Server anfordern muss, wenn diese bereits vorhanden sind.
Wenn Sie mehr über GraphQL und Apollo Graphql erfahren möchten, können Sie die folgenden Links aufrufen:
Reagieren Sie
React ist eine JavaScript-Bibliothek zur Erstellung von Benutzeroberflächen mit einem komponentenbasierten Ansatz. Jede Komponente kann wiederverwendet werden und verwaltet ihren eigenen Zustand, der die Benutzeroberfläche bei Änderungen automatisch aktualisiert.
Dieses Projekt verwendet funktionale Komponenten, die eine einfache und dennoch leistungsstarke Möglichkeit bieten, React-Komponenten zu schreiben. Es verwendet auch Hooks, um zusätzliche Funktionen wie Zustand und Kommunikation mit dem Server bereitzustellen.
Der Demo-Code zeigt eine einfache, in TypeScript geschriebene React-Anwendung. Sie nutzt die Apollo-Client-Bibliothek, um sich mit dem Server zu verbinden und um einen Cache für die Speicherung der vom Server abgerufenen Daten bereitzustellen.
Die gesamte Anwendung ist im ApolloProvider verpackt, der den Zugriff auf seinen Kontext über alle Komponenten hinweg ermöglicht.
ReactDOM.render(
<ApolloProvider client={client}>
<Pages />
</ApolloProvider>,
document.getElementById('root')
);
Wenn Sie mehr über React und seine Integration mit dem Apollo-Server erfahren möchten, können Sie die folgenden Links aufrufen:
Klassen erstellen
Ok, wenn Sie mitmachen wollen, wird es Zeit, sich die Hände schmutzig zu machen. Nehmen Sie Ihren bevorzugten Code-Editor und öffnen Sie den starter
Ordner. Als erstes werden wir die Möglichkeit hinzufügen, neue Klassen zu erstellen.
Da wir unseren Code in Server- und Browsercode aufgeteilt haben, ist es sinnvoll, mit der Einrichtung des Backend-Codes zu beginnen, bevor wir daran arbeiten, was der Benutzer sehen wird. Beginnen wir also mit der Erstellung einer GraphQL-Mutation, die eine Sitzung im Vonage Video API Service erstellt.
Erstellen des Vonage Video-API-Dienstes und des Resolvers
Um eine Audio/Video-Sitzung in der Vonage Video API zu erstellen, verwenden wir das opentok
Paket, das bereits installiert ist. Als erstes müssen wir den Client initialisieren, indem wir den API-Schlüssel und das geheime Paar übergeben.
In der server/src/services/vonage/videoApi.js
Datei fügen wir die initializeOpentok
Funktion. Wir geben eine Singleton-Instanz der opentok
zurück, um sicherzustellen, dass bei jedem Aufruf der Funktion dieselbe Instanz zurückgegeben wird. Beachten Sie, dass wir den Schlüssel und das Geheimnis, die wir zuvor als Umgebungsvariable definiert haben, mit den Optionen apiKey
und apiSecret
Werte aus einer bereits konfigurierten ../../utils/envs
Datei.
const { vonageVideoApiKey : apiKey, vonageVideoApiSecret : apiSecret } = require('../../util/envs');
let opentok = null;
...
const initializeOpentok = () => {
opentok = opentok ? opentok : new OpenTok(apiKey, apiSecret);
}
Der nächste Schritt besteht darin, die Sitzung tatsächlich zu erstellen. Hierfür verwenden wir die opentok.createSession
Funktion. Diese Funktion empfängt ein Objekt, das die Sitzung als routed
. A routed
Sitzung bedeutet, dass wir die Medienserver von Vonage verwenden werden, was eine geringere Bandbreitennutzung in Mehrparteien-Sitzungen ermöglicht und uns auch erlaubt, erweiterte Funktionen wie Aufzeichnungen und SIP-Zusammenschaltung zu aktivieren.
// 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);
}
});
});
};
Schließlich fügen wir eine Funktion zur Erzeugung von JWT-Tokens hinzu, die zur Authentifizierung von Benutzern im Rahmen einer Sitzung und zum Festlegen von Berechtigungen verwendet werden.
// server/src/services/vonage/videoApi.js
...
const generateToken = (sessionId) => {
return opentok.generateToken(sessionId);
}
...
Nun, da wir die Funktionalität eingerichtet haben, müssen wir sie nur noch den Clients zur Verfügung stellen. Dazu erstellen wir zwei Mutationen, die der React-Client nutzen kann, um Lehrern die Erstellung von Sitzungen und Schülern die Teilnahme an diesen zu ermöglichen.
Öffnen wir die server/src/graphql/videoApi.js
Datei und füllen die Platzhalterauflöser aus.
Bei der Erstellung von Sitzungen gehen wir folgendermaßen vor:
Initialisieren Sie den opentok-Client.
Erstellen Sie die Sitzung.
Erzeugen Sie eine ID für die Sitzung, die als Teil der URL verwendet werden soll. Hierfür verwenden wir das
uuid
npm-Paket.Speichern Sie die Sitzung im permanenten Speicher. Der Einfachheit halber werden wir die Dinge im Speicher mit Hilfe von Arrays speichern, die in
server/src/services/db/index.js
definiert sind, aber in einer realen Anwendung ist eine echte Datenbank sinnvoller.Erzeugen Sie ein Token für die Sitzung.
Rückgabe der Daten unter Beachtung des in der Typdefinition für die Mutationsantwort festgelegten Formats.
// 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);
}
};
...
Die Mutation zum Starten der Sitzung ist zusammen mit dem Antworttyp bereits definiert unter server/src/typeDefs.js
.
// server/src/typeDefs.js
...
type SessionResponse {
uuid: String!
token: String!
session: String!
apiKey: String!
}
...
type Mutation {
...
startVideocallSession: SessionResponse
...
}
Auch die Auflösungsfunktion ist bereits zugewiesen. Wir können dies in der server/src/resolver.js
Datei:
// 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,
...
},
};
Als Nächstes müssen wir eine Auflösungsfunktion erstellen, die es Schülern ermöglicht, einer bereits erstellten Sitzung beizutreten. Dazu führen wir die folgenden Schritte aus:
Prüfen Sie, ob eine UUID angegeben wurde.
Suchen Sie den Videoaufruf in der Datenbank.
Initialisieren Sie den opentok-Client.
Verwenden Sie die Sitzung, um ein Token für den Schüler zu erstellen.
Rückgabe von Daten unter Beachtung des in der Typdefinition für die Mutation festgelegten Formats.
// 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,
...
},
};
Wie bei der vorherigen Funktion ist der Resolver bereits mit der Typdefinition verbunden. Der einzige Unterschied besteht darin, dass es sich dieses Mal nicht um eine Mutation, sondern um eine Abfrage handelt.
// 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,
...
},
};
Jetzt sind wir bereit, die Benutzeroberfläche zu erstellen.
Hinzufügen der Benutzeroberfläche
Zunächst erstellen wir ein paar React-Komponenten. Innerhalb des Ordners client/src/components/
Ordners erstellen wir einen neuen Videocall
Ordner.
Erstellen Sie nun eine Datei namens Room.tsx
innerhalb des neu erstellten Ordners. Dies ist die Komponente, die die Sitzung hosten wird.
Um die Komponente zu bauen, verwenden wir das opentok-react
npm-Paket. Die Komponente erhält eine uuid
Eigenschaft, die in der Abfrage verwendet wird, um die Informationen über die Sitzung abzurufen.
// 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;
Als nächstes fügen wir eine Schaltfläche zum Erstellen der Sitzung hinzu. Hier werden wir eine leistungsstarke Funktion des Apollo-Clients erkunden: den Cache.
Derzeit versucht die Raumkomponente, die Sitzungsdetails vom Server auf der Grundlage der UUID einer bereits erstellten Sitzung abzurufen.
Da wir dieselben Angaben auch beim Erstellen der Sitzung erhalten, ist es nicht sinnvoll, beim Beitritt eine zweite Anfrage zu stellen. Stattdessen werden wir schreiben wir sie in den Cache so dass die Raumkomponente sie von dort abrufen kann und keine neue Anfrage an den Server stellen muss.
Erstellen Sie eine StartButton.tsx
Datei und füllen Sie sie wie folgt:
// 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;
Bevor wir mit dem Hinzufügen der Seiten beginnen, erstellen wir eine index.tsx
Datei unter client/src/components/Videocall
die beide Komponenten unter demselben Import ausweist:
// src/components/Videocall/index.tsx
export { default as StartButton } from './StartButton';
export { default as Room } from './Room';
Erstellen Sie nun einfach eine neue Seite unter client/src/pages/
namens VideoSession.tsx
und fügen Sie dann die Komponente Room hinzu. Beachten Sie, dass wir die Datei nicht angeben müssen Room
Datei angeben, sondern sie einfach auf Ordnerebene importieren. Dies ist dank der index.tsx
Datei, die wir gerade hinzugefügt haben
// 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;
Als nächstes fügen Sie die VideoSession-Route in der src/pages/index.tsx
Datei hinzu:
// 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>
);
}
...
Fügen Sie schließlich die Schaltfläche auf der src/pages/Home.tsx
Seite hinzu:
// src/pages/Home.tsx
...
import { StartButton } from '../components/Videocall';
const Inicio = () => {
return (
<div>
<StartButton />
</div>
);
};
...
Erstellen einer Liste von Schülern
Der nächste Schritt besteht darin, dass ein Lehrer eine Liste von Schülern erstellen kann. Der Gedanke dahinter ist, dass der Lehrer, wenn ein Anruf gestartet wird, die Liste überprüfen und SMS-Benachrichtigungen an die Schüler senden kann, um sie zum Anruf einzuladen.
Wie bei den Klassen beginnen wir damit, die erforderlichen Mutationen und Abfragen im GraphQL-Server vorzunehmen. Dann fügen wir die Benutzeroberfläche hinzu.
Mutationen und Abfragen einrichten
Beginnen wir mit der Arbeit am Servercode, indem wir einem Lehrer erlauben, einen Schüler anzulegen. Um die Dinge einfach zu halten, werden wir die Schüler in einem Array speichern, aber in einer realen Anwendung wäre eine Datenbank sinnvoller.
Öffnen Sie die server/src/graphql/student.js
Datei und füllen Sie die Auflösungsfunktionen wie folgt aus:
// 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;
};
Als Nächstes fügen wir die Vonage-Magie zum Senden von Benachrichtigungen hinzu. Hierfür verwenden wir das @vonage/server-sdk
npm-Paket, das bereits vorinstalliert ist und als Singleton-Instanz in der server/src/services/vonage/vonage.js
Datei initialisiert ist:
// 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
}
Öffnen Sie die server/src/services/vonage/sms.js
Datei und füllen Sie die sendSms
Funktion wie folgt:
// 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);
}
}
});
});
};
...
Hinzufügen der Benutzeroberfläche
Lassen Sie uns zunächst einige Komponenten erstellen, die wir später bei der Erstellung von Schülern und der Einladung zu einer Videositzung wiederverwenden werden.
Erstellen Sie einen neuen Ordner unter client/src/components
mit dem Namen Students
, und erstellen Sie darin drei weitere Dateien: index.tsx
, StudentForm.tsx
und StudentsList.tsx
.
Bei der Erstellung des Formulars gehen wir ähnlich vor wie bei der Erstellung einer Klasse: Nach dem Aufruf der Mutation, die den Schüler auf dem Server erstellt, aktualisieren wir auch den lokalen Cache, um spätere Anfragen an den Server zu vermeiden.
Für das eigentliche Formular werden wir verwenden kontrollierte Komponenten verwenden, so dass ihre Werte von Reacts Status verwaltet werden. Da wir funktionale Komponenten verwenden, werden wir den useState-Haken um dem Formular einen Zustand zu geben.
Füllen Sie die StudentForm.tsx
Datei wie folgt:
// 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;
Bei der Erstellung der Liste der Schüler fügen wir eine actions
Eigenschaft hinzu, die ein Array von "Aktionen" darstellt, die auf einen Schüler angewendet werden können.
Für jede Aktion wird in der Tabelle unter der Spalte "Aktionen" eine Schaltfläche hinzugefügt, die eine benutzerdefinierte Funktion auslöst. Denken Sie dabei an Aktionen wie "Bearbeiten", "Löschen" oder "Deaktivieren". Später werden wir diese Eigenschaft verwenden, um einen Schüler zu einem Kurs "einzuladen".
Füllen Sie die StudentsList.tsx
Datei wie folgt:
// 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;
Lassen Sie uns nun die beiden neu erstellten Komponenten in der index.tsx
wie folgt:
// client/src/components/Students/index.tsx
export {default as StudentsList} from './StudentsList';
export {default as StudentForm} from './StudentForm';
Und nun erstellen wir die client/src/pages/StudentPage.tsx
Seite, und fügen Sie dann die Route in den client/src/page/index.tsx
Index ein. Beachten Sie, dass wir sowohl die StudentForm
und StudentsList
Komponenten aus demselben Namespace importieren. (Nochmals vielen Dank, 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>
);
}
...
Wir sollten nun in der Lage sein, neue Schüler zu erstellen und sie in der Liste anzuzeigen.
Studenten einladen
Der Sinn von Studenten ist es, sie zu einem Gespräch einladen zu können. Erinnern Sie sich an die actions
Eigenschaft, über die wir vorhin gesprochen haben? Hier kommt diese Eigenschaft zum Tragen, denn sie ermöglicht es uns, diese Funktionalität für die Liste der Studenten bereitzustellen und gleichzeitig dieselbe Komponente wiederzuverwenden, die wir zuvor erstellt haben.
Erstellen wir eine neue Komponente namens Attendees.tsx
unter client/src/components/Videocall/
. In dieser neuen Komponente werden wir eine benutzerdefinierte Aktion erstellen, die die inviteStudent
Mutation auslöst.
// 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;
Fügen Sie die neu erstellte Komponente auch in den Videocall-Index ein:
// src/components/Videocall/index.tsx
...
export { default as Attendees } from './Attendees';
Fügen Sie schließlich der Seite VideoSession die Komponente Attendees hinzu:
// src/pages/VideoSession.tsx
...
import { Room, Attendees } from '../components/Videocall';
...
const VideoSession = () => {
...
return (
<>
...
<Attendees />
</>
);
};
Legen Sie nun ein paar Schüler mit gültigen Telefonnummern an, starten Sie eine Klasse und klicken Sie auf die Schaltfläche "Einladen", um sie einzuladen.
Erstellen und Versenden von Zuweisungen
Der letzte Schritt in unserer Demo besteht darin, den Schülern die Möglichkeit zu geben, Hausaufgaben zu senden. Um sicherzustellen, dass wir in der Lage sind, den Schüler zu identifizieren, zu dem eine Hausaufgabe gehört, verwenden wir ein passwortloses Login, das auf der Telefonnummer basiert, die zur Registrierung des Schülers verwendet wurde.
Mutationen und Abfragen einrichten
Als erstes müssen wir die Erstellung von Hausaufgaben und Hausaufgaben-Dateien ermöglichen. Außerdem müssen wir den Benutzern die Möglichkeit geben, Dateien hochzuladen. Für letzteres werden wir einen S3-Bucket mit vordefinierten POST-Anfragen verwenden.
Beginnen wir mit den Resolvern zum Erstellen und Abrufen von Hausaufgaben und Hausaufgaben-Dateien. Öffnen Sie die server/src/graphql/homework.js
Datei, unter server
und füllen Sie die Resolver wie folgt aus:
// 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);
};
...
Als Nächstes fügen wir eine Mutation zum Vorsignieren einer POST-Anforderung hinzu, die später im clientseitigen Code zum Hochladen der Datei in S3 verwendet werden kann. Hierfür verwenden wir das aws-sdk
npm-Paket. Der Dienst ist bereits konfiguriert in 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);
}
});
});
};
...
Alles, was wir also tun müssen, ist, den Dienst in einer neuen Mutation zu nutzen. Öffnen Sie die server/src/graphql/s3.js
Datei, und füllen Sie die presignDocument
Resolver-Funktion wie folgt:
// 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);
}
};
...
Nun ist es an der Zeit, die Vonage Magie für die passwortlose Authentifizierung einzurichten. Dazu werden wir die Verify API verwenden. Lassen Sie uns zunächst den Dienst erstellen. Öffnen Sie die server/src/services/vonage/verify.js
Datei, und füllen Sie die Felder verifyRequest
und checkCode
Funktionen wie folgt:
// 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);
}
}
});
})
}
...
Zum Schluss stellen wir diese Dienste über GraphQL dar. Öffnen Sie die server/src/graphql/vonage.js
Datei und füllen Sie die Felder verifyRequestResolver
und checkCodeResolver
Auflösungsfunktionen wie folgt:
// 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,
};
}
};
...
React-Komponenten und -Seiten erstellen
Beginnen wir mit der Erstellung eines Formulars für die Erstellung von Hausaufgaben und einer einfachen Tabelle, um diese aufzulisten.
Erstellen Sie einen Homeworks
Ordner unter client/src/components
und erstellen Sie dann HomeworkForm.tsx
und HomeworkList.tsx
innerhalb dieses Ordners. Füllen Sie die erste Datei wie folgt aus, um das Formular zu erstellen:
// 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;
Und dann füllen Sie die HomeworkList.tsx
Datei wie folgt, um eine einfache Tabelle zu erstellen, die die erstellten Hausaufgaben auflistet. Beachten Sie, dass wir auch eine Reihe von Links
unter den Spalten "Identifier" "Action". Über diese Links kann eine Lehrkraft die Dateien einer bestimmten Hausaufgabe einsehen und die Schüler können die Dateien hochladen.
Wir werden die Seiten, die diese Links öffnen werden, in Kürze bearbeiten.
// 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;
Lassen Sie uns nun die neu erstellten Komponenten offenlegen, indem wir eine index.tsx
Datei unter client/src/components/Homeworks
mit dem folgenden Inhalt:
// client/src/components/Homeworks/index.tsx
export {default as HomeworkList} from './HomeworkList';
export {default as HomeworkForm} from './HomeworkForm';
Erstellen Sie dann die HomeworksPage.tsx
unter client/src/pages/
wie folgt:
// client/src/pages/HomeworksPage.tsx
import React from 'react';
import { HomeworkForm, HomeworkList } from '../components/Homeworks';
const HomeworksPage = () => {
return (
<>
<HomeworkForm />
<HomeworkList />
</>
)
}
export default HomeworksPage;
Und vergessen Sie nicht, ihn in die index.tsx
Datei im selben Ordner hinzuzufügen:
// 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>
);
}
Implementieren einer passwortlosen Anmeldung
Für die passwortlose Anmeldung erstellen wir zwei neue Komponenten: eine, die als Anmeldeseite dient, und eine weitere, die das Formular enthält, das die Schüler nach der Authentifizierung sehen werden.
Erstellen Sie PasswordlessLogin.tsx
und HomeworkFileForm.tsx
unter client/src/components/Homeworks
.
Konzentrieren wir uns zunächst auf die Erstellung des Anmeldeformulars. Dazu wird unsere Komponente eine Mutation für die Erstellung einer Verifizierungsanfrage und eine weitere für die eigentliche Verifizierung definieren.
Die Benutzeroberfläche besteht aus einem Textfeld, in dem die Telefonnummer abgefragt wird, und einer Schaltfläche zum Auslösen der Anfrage. Nachdem ein requestId
erfolgreich vom Server zurückgegeben wurde, wollen wir ein zusätzliches Textfeld zur Eingabe des Codes und eine Schaltfläche zur Verifizierung anzeigen.
Füllen Sie die PasswordlessLogin.tsx
Datei wie folgt:
// 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;
Als nächstes erstellen Sie das Formular für das Hochladen der Datei. Dieses Formular unterscheidet sich geringfügig von den Formularen, die wir zuvor in diesem Tutorial erstellt haben, da es ein unkontrolliertes Formular sein wird. Außerdem müssen einige zusätzliche Schritte unternommen werden, um die Datei vor dem Aufruf der Mutate-Funktion in S3 hochzuladen.
Füllen Sie die HomeworkFileForm.tsx
wie folgt:
// 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;
Lassen Sie uns nun eine Seite erstellen, die eine andere Komponente anzeigt, je nachdem, ob sich der Schüler angemeldet hat oder nicht. Erstellen Sie die client/src/pages/AddHomeworkFilePage.tsx
Datei und füllen Sie sie wie folgt aus:
// 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;
Vergessen Sie auch hier nicht, die neu erstellte Seite in den Ordner index.tsx
im gleichen Ordner hinzuzufügen:
// 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>
);
}
...
Erstellen einer Liste der Hausaufgaben-Dateien
Als Letztes müssen wir dem Lehrer ermöglichen, die von den Schülern eingereichten Hausaufgaben zu überprüfen. Zu diesem Zweck erstellen wir einfach eine HomeworkFileList
Komponente zu erstellen, die derjenigen ähnelt, die wir gerade für Schüler und Hausaufgaben erstellt haben.
Erstellen Sie eine neue client/src/components/Homeworks/HomeworkFileList.tsx
und füllen Sie es wie folgt, um die Liste der Hausaufgaben zu erstellen:
// 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;
Zum Schluss erstellen Sie die ListHomeworkFilesPage.tsx
Datei unter client/src/pages
wie unten gezeigt:
// 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;
Und zum letzten Mal, vergessen Sie nicht, die Route in die index.tsx
Datei im selben Ordner hinzuzufügen:
// 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>
);
}
...
Schlussfolgerung
Und das war's! Ich hoffe, dieser Beitrag hat Ihnen eine Vorstellung davon vermittelt, was Sie tun können, um sich an die "neue Normalität" anzupassen, und wie die coolen Dinge, die bei Vonage entwickelt werden, Ihnen dabei helfen können.
Share:
)
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 ❤️