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

Wie man eine Lernplattform mit React, Express und Apollo GraphQL erstellt

Zuletzt aktualisiert am December 15, 2020

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:

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:

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

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):

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

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 PageThe 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 classStarting 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 AssignmentCreating 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 LoginPasswordless Login

Uploading a FileUploading 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 assignmentsSeeing 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:

  1. Initialisieren Sie den opentok-Client.

  2. Erstellen Sie die Sitzung.

  3. Erzeugen Sie eine ID für die Sitzung, die als Teil der URL verwendet werden soll. Hierfür verwenden wir das uuid npm-Paket.

  4. 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.jsdefiniert sind, aber in einer realen Anwendung ist eine echte Datenbank sinnvoller.

  5. Erzeugen Sie ein Token für die Sitzung.

  6. 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:

  1. Prüfen Sie, ob eine UUID angegeben wurde.

  2. Suchen Sie den Videoaufruf in der Datenbank.

  3. Initialisieren Sie den opentok-Client.

  4. Verwenden Sie die Sitzung, um ein Token für den Schüler zu erstellen.

  5. 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.tsxund 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 serverund 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:

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

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