
Compartilhar:
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 ❤️
Como criar uma plataforma de aprendizagem com React, Express e Apollo GraphQL
Tempo de leitura: 18 minutos
2020 foi um ano atípico para todos nós. Muitos setores tiveram que “repensar” a forma como conduzem seus negócios, e é provável que essas estratégias não sejam temporárias, mas sim duradouras.
Uma dessas mudanças é a forma como aprendemos. Muitas escolas, universidades e instituições de ensino em todo o mundo têm registrado um aumento nos serviços à distância, muitas vezes recorrendo a soluções privadas para oferecê-los.
Hoje vamos ver como é possível criar nossa própria plataforma de aprendizagem com recursos de Video e áudio, notificações por SMS e autenticação sem senha.
Pré-requisitos
Para compilar e executar o aplicativo, você precisará dos seguintes recursos:
Um account da API da Vonage.
Uma conta da Video API da Vonage. Cadastre-se gratuitamente aqui.
Um número de telefone virtual. Assim que você tiver sua conta da API da Vonage veja aqui como obter um número.
Um par de chave e segredo para um projeto da Video API da Vonage. Você pode criar um projeto a partir da sua página da sua conta da Video API da Vonage.
Um account na AWS e um par de chave e segredo.
Um bucket do AWS S3 para enviar arquivos.
O Node 12 e o NPM 6 estão instalados no seu sistema.
O que vamos construir
Vamos desenvolver um aplicativo web que permita aos professores criar aulas instantâneas em Video ou áudio, das quais os alunos possam participar apenas com o link. Os professores poderão criar uma lista de alunos, identificados por seus números de telefone, e, posteriormente, enviar a eles o link para a chamada por SMS.
O professor também pode criar tarefas. Posteriormente, os alunos podem se identificar por meio de autenticação sem senha e enviar arquivos que poderão ser revisados pelo professor.
Para simplificar as coisas e economizar tempo, alguns recursos, como a autenticação (login e logout) e um banco de dados propriamente dito, foram deixados de lado. Em vez disso, todas as páginas estão disponíveis publicamente e os dados serão armazenados na memória por meio de matrizes de JavaScript.
Se você tiver interesse em experimentar o produto final por conta própria, criei um repositório no GitHub que você pode clonar localmente. O repositório contém uma final pasta onde você pode ver o exemplo finalizado e uma starter outra — com React, Express e Apollo GraphQL já pré-configurados — que você pode usar para acompanhar e construir o projeto passo a passo.
O código de demonstração está dividido em uma server pasta que contém um servidor Apollo GraphQL com Expresse uma client pasta que contém um aplicativo React . O código do back-end está escrito em JavaScript puro, enquanto o front-end usa TypeScript; assim, se você não estiver familiarizado com as diferenças entre as duas linguagens, poderá compará-las lado a lado.
Antes de começar, certifique-se de acessar cada pasta e instalar as dependências usando o npm, conforme mostrado abaixo:
Você também precisa configurar os segredos. Na client pasta, basta renomear o .env arquivo para .env.local.
Na server pasta, por outro lado, renomeie app.envs o arquivo para .env você também precisa substituir os valores dos espaços reservados no arquivo pelas suas próprias chaves da AWS, nome do bucket do S3, chaves da Vonage e número virtual da Vonage.
Se você quiser executar o produto final, abra duas janelas de terminal separadas e use o npm para iniciar os dois aplicativos, conforme mostrado abaixo:
Uma janela do navegador será aberta automaticamente e você verá o aplicativo em ação. A primeira coisa que você verá é uma janela com um botão que permite criar uma turma. Antes de começar, acesse a Students página e crie alguns alunos usando números de telefone válidos.
The Students Page
Agora, volte à tela principal clicando no título do aplicativo. Em seguida, crie uma nova aula.
Após alguns segundos, você estará em uma sessão da Video API da Vonage. Usando a lista de alunos, você pode enviar a eles uma notificação por SMS para que possam participar da aula simplesmente clicando no Invite botão.
Starting a class
Agora, digamos que você queira criar uma tarefa em que os alunos tenham que enviar um documento em PDF. Você pode fazer isso de forma que não seja necessário que eles tenham um Account propriamente dito, mas que possam se autenticar simplesmente usando o celular.
Para isso, acesse a Homeworks página e crie um novo Tarefa definindo uma descrição. Em seguida, como aluno, clique no Upload link.
Creating an Assignment
Para enviar o arquivo, o aluno precisa informar o mesmo número de telefone que foi usado pelo professor no momento da criação. Um código de verificação será enviado para esse número e, após informá-lo no aplicativo, o aluno poderá enviar o arquivo.
Passwordless Login
Uploading a File
O professor pode ver os arquivos que cada aluno enviou para cada tarefa clicando no UUID gerado automaticamente para a tarefa.
Seeing assignments
Familiarizando-se com o código inicial
Se você quiser acompanhar, mas não estiver familiarizado com algumas das tecnologias utilizadas aqui, não se preocupe. Nesta seção, descreveremos brevemente o que são essas tecnologias, como elas são configuradas no código inicial e forneceremos alguns links úteis para que você possa obter mais informações. Se você já é um especialista em GraphQL e React, pode pular esta seção e ir direto para criar classes, embora talvez valha a pena ler mesmo assim para entender como essas partes se encaixam no código de demonstração.
Apollo GraphQL
O GraphQL oferece uma linguagem de consulta e um ambiente de execução para consultar dados de um servidor (geralmente de várias fontes). Ele permite descrever os dados de forma clara e dá ao cliente a capacidade de solicitar exatamente o que precisa.
O Apollo GraphQL é uma implementação do GraphQL que segue os padrões do setor. Ele oferece bibliotecas para servidor e cliente que permitem combinar e utilizar facilmente bancos de dados, APIs e microsserviços em um único grafo.
A pasta do servidor é composta por um servidor GraphQL baseado no Express. A configuração está no server/index.js arquivo. Os elementos mais importantes da configuração são as definições de tipos e os resolvedores.
As definições de tipos são onde o GraphQL descreve os dados que um cliente pode consumir. Isso é feito por meio de tipos. As definições de tipos são configuradas no server/src/typeDefs.js arquivo. Abaixo estão alguns exemplos dos tipos para o código de demonstração:
type Student {
phoneNumber: String!
firstName: String!
lastName: String!
}
type Homework {
uuid: String!
description: String!
}Os tipos mais importantes são os Query e Mutation , que, na verdade, definem quais “consultas” e “mutações” um cliente pode realizar com os dados.
A seguir estão as consultas e mutações definidas para o código de demonstração:
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
}O que é ótimo no GraphQL é que você define o comportamento dessas consultas e mutações usando seu próprio código personalizado, o que permite recuperar informações de vários bancos de dados, APIs REST ou até mesmo de outros servidores GraphQL. Esse código personalizado que você cria é conhecido como resolvers.
No código de demonstração, os resolvedores são atribuídos a cada consulta e mutação no server/src/resolvers.js arquivo, enquanto as funções de resolução propriamente ditas estão localizadas na server/src/graphql pasta. Atualmente, os resolvedores estão lançando apenas uma NOT_IMPLEMENTED exceção, mas vamos mudar isso ao longo deste artigo.
const saveHomework = async (_, { description }, __, ___) => {
throw new Error(NOT_IMPLEMENTED);
};O Apollo também oferece uma biblioteca para código do lado do cliente que permite que você utilize facilmente os dados do servidor. Ele mantém um cache para que o cliente não precise solicitar dados ao servidor caso eles já existam.
Se você quiser saber mais sobre GraphQL e Apollo GraphQL, pode conferir os links a seguir:
Reagir
O React é uma biblioteca de JavaScript para a criação de interfaces de usuário por meio de uma abordagem baseada em componentes. Cada componente pode ser reutilizado e mantém seu próprio estado, que atualiza automaticamente a interface de usuário quando é alterado.
Este projeto utiliza componentes funcionais, que oferecem uma maneira simples, porém poderosa, de criar componentes React. Ele também utiliza hooks para fornecer funcionalidades adicionais, como estado e comunicação com o servidor.
O código de demonstração apresenta um aplicativo React básico escrito em TypeScript. Ele utiliza a biblioteca Apollo Client para se conectar ao servidor e também para fornecer um cache para armazenar os dados recuperados do servidor.
Todo o aplicativo está encapsulado no ApolloProvider, o que permite o acesso ao seu contexto em todos os componentes.
ReactDOM.render(
<ApolloProvider client={client}>
<Pages />
</ApolloProvider>,
document.getElementById('root')
);Se você quiser saber mais sobre o React e sua integração com o servidor Apollo, confira os links a seguir:
Criação de classes
Ok, se você quiser acompanhar, é hora de colocar a mão na massa. Abra seu editor de código favorito e acesse a starter pasta. A primeira coisa que faremos é adicionar a capacidade de criar novas classes.
Como nosso código está dividido entre o código do servidor e o do navegador, faz sentido começar a configurar o código do backend antes de trabalhar no que o usuário verá. Então, vamos começar criando uma mutação GraphQL que crie uma sessão no serviço da Video API do Vonage.
Criação do serviço da Video API da Vonage e do Resolver
Para criar uma sessão de áudio/vídeo na Video API da Vonage, usaremos o opentok pacote, que já está instalado. A primeira coisa que precisamos fazer é inicializar o cliente, passando a chave da API e o par de segredos.
No server/src/services/vonage/videoApi.js arquivo, vamos preencher a initializeOpentok função. Vamos retornar uma instância singleton da opentok variável; isso garantirá que a mesma instância seja retornada sempre que chamarmos a função. Observe como estamos importando a chave e o segredo que definimos anteriormente como variáveis de ambiente usando o apiKey e apiSecret valores de um ../../utils/envs .
const { vonageVideoApiKey : apiKey, vonageVideoApiSecret : apiSecret } = require('../../util/envs');
let opentok = null;
...
const initializeOpentok = () => {
opentok = opentok ? opentok : new OpenTok(apiKey, apiSecret);
}O próximo passo é criar a sessão propriamente dita. Para isso, usaremos a opentok.createSession função. Essa função recebe um objeto que define a sessão como routed. Uma routed sessão significa que usaremos os servidores de mídia da Vonage, o que permite reduzir o uso de largura de banda em sessões com vários participantes e também nos permite habilitar recursos avançados, como gravações e interconexão SIP.
// server/src/services/vonage/videoApi.js
...
const opentokSessionArgs = {
mediaMode: 'routed',
};
...
const createSession = () => {
return new Promise((resolve, reject) => {
opentok.createSession(opentokSessionArgs, (err, session) => {
if (err) {
reject(err);
} else {
resolve(session);
}
});
});
};Por fim, vamos adicionar uma função para gerar tokens JWT, que serão usados para autenticar usuários no contexto de uma sessão e também para definir permissões.
// server/src/services/vonage/videoApi.js
...
const generateToken = (sessionId) => {
return opentok.generateToken(sessionId);
}
...Agora que já temos a funcionalidade pronta, só falta disponibilizá-la aos clientes. Para isso, vamos criar um par de mutações que o cliente React possa utilizar, a fim de permitir que os professores criem sessões e que os alunos participem delas.
Vamos abrir o server/src/graphql/videoApi.js arquivo e preencher os resolvedores de placeholders.
Para criar sessões, estas são as etapas que seguiremos:
Inicialize o cliente Opentok.
Crie a sessão.
Gerar um ID para a sessão, a ser usado como parte da URL. Para isso, usaremos o
uuidpacote npm.Salve a sessão em um armazenamento persistente. Para simplificar, vamos armazenar os dados na memória usando matrizes definidas em
server/src/services/db/index.js, mas, em uma aplicação real, faz mais sentido usar um banco de dados propriamente dito.Gerar um token para a sessão.
Retorne os dados, respeitando o formato definido na definição do tipo para a resposta da mutação.
// 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);
}
};
...A mutação para iniciar a sessão, juntamente com o tipo de resposta, já está definida em server/src/typeDefs.js.
// server/src/typeDefs.js
...
type SessionResponse {
uuid: String!
token: String!
session: String!
apiKey: String!
}
...
type Mutation {
...
startVideocallSession: SessionResponse
...
}A função do resolvedor também já está atribuída. Podemos ver isso no server/src/resolver.js arquivo:
// 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,
...
},
};Em seguida, precisamos criar uma função de resolução que permita aos alunos ingressarem em uma sessão já criada. Para isso, seguiremos estas etapas:
Verifique se um UUID foi fornecido.
Procure a videochamada no banco de dados.
Inicialize o cliente Opentok.
Use a sessão para gerar um token para o aluno.
Retornar os dados, respeitando o formato definido na definição do tipo para a mutação.
// 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,
...
},
};Assim como na função anterior, o resolvedor já está vinculado à definição do tipo. A única diferença é que, desta vez, em vez de uma mutação, trata-se de uma consulta.
// 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,
...
},
};Agora estamos prontos para criar a interface do usuário.
Incorporando a interface do usuário
Primeiro, vamos criar alguns componentes do React. Dentro da client/src/components/ pasta, crie uma nova Videocall pasta.
Agora crie um arquivo chamado Room.tsx dentro da pasta recém-criada. Esse é o componente que hospedará a sessão.
Para criar o componente, usaremos o opentok-react pacote do npm. O componente receberá uma uuid propriedade que será usada na consulta para recuperar as informações sobre a sessão.
// 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;A seguir, vamos adicionar um botão para criar a sessão. Aqui, vamos explorar um recurso poderoso do Apollo Client: o cache.
Atualmente, o componente Room tenta recuperar os detalhes da sessão do servidor com base no UUID de uma sessão já criada.
Como também obtemos esses mesmos detalhes ao criar a sessão, não faz sentido fazer uma segunda solicitação ao ingressar. Em vez disso, vamos gravá-los no cache para que o componente Room possa obtê-los de lá e não precise fazer uma nova solicitação ao servidor.
Crie um StartButton.tsx arquivo e preencha-o da seguinte forma:
// 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;Antes de começarmos a adicionar as páginas, vamos criar um index.tsx arquivo em client/src/components/Videocall que irá expor ambos os componentes sob a mesma importação:
// src/components/Videocall/index.tsx
export { default as StartButton } from './StartButton';
export { default as Room } from './Room';Agora basta criar uma nova página em client/src/pages/ com o nome VideoSession.tsx, e, em seguida, adicione o componente Room. Observe que não precisamos especificar o Room arquivo, mas apenas importá-lo no nível da pasta. Isso é possível graças ao index.tsx arquivo que acabamos de adicionar
// 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;Em seguida, adicione a rota VideoSession no src/pages/index.tsx arquivo:
// 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>
);
}
...Por fim, adicione o botão à src/pages/Home.tsx página:
// src/pages/Home.tsx
...
import { StartButton } from '../components/Videocall';
const Inicio = () => {
return (
<div>
<StartButton />
</div>
);
};
... Criação de uma lista de alunos
O próximo passo é permitir que um professor crie uma lista de alunos. A ideia é que, quando uma chamada for iniciada, o professor possa consultar a lista e enviar notificações por SMS aos alunos para convidá-los a participar da chamada.
Assim como fizemos com as classes, começaremos realizando as mutações e consultas necessárias no servidor GraphQL. Em seguida, adicionaremos a interface do usuário.
Configurando mutações e consultas
Vamos começar a trabalhar no código do servidor, permitindo que um professor crie um aluno. Para simplificar, vamos armazenar os alunos em um array, mas, em uma aplicação real, faria mais sentido usar um banco de dados.
Abra o server/src/graphql/student.js arquivo e preencha as funções do resolvedor da seguinte maneira:
// 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;
};A seguir, vamos adicionar a “mágica” da Vonage para enviar notificações. Para isso, usaremos o @vonage/server-sdk pacote npm, que já vem pré-instalado e inicializado como uma instância singleton no server/src/services/vonage/vonage.js arquivo:
// 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
}Abra o server/src/services/vonage/sms.js arquivo e preencha a sendSms função da seguinte maneira:
// 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);
}
}
});
});
};
... Incorporando a interface do usuário
Primeiro, vamos criar alguns componentes que reutilizaremos mais tarde ao criar alunos e convidá-los para uma sessão de Video.
Crie uma nova pasta na pasta client/src/components chamada Students, e dentro dela crie mais três arquivos: index.tsx, StudentForm.tsx e StudentsList.tsx.
Ao criar o formulário, adotaremos uma abordagem semelhante à utilizada na criação de uma classe, em que, após chamar a mutação que cria o aluno no servidor, também atualizamos o cache local para evitar solicitações subsequentes ao servidor.
Para o formulário propriamente dito, usaremos componentes controlados para que seus valores sejam gerenciados pelo estado do React. Como estamos usando componentes funcionais, utilizaremos o hook useState para fornecer um estado ao formulário.
Preencha o StudentForm.tsx arquivo da seguinte maneira:
// 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;Ao criar a lista de alunos, vamos adicionar uma actions propriedade que será uma matriz de “ações” que podem ser aplicadas a um aluno.
Para cada ação, adicionaremos um botão na tabela, na coluna “Ações”, que acionará uma função personalizada. Pense em ações como “editar”, “excluir” ou “desativar”. Mais tarde, usaremos essa propriedade para “convidar” um aluno para uma aula.
Preencha o StudentsList.tsx arquivo da seguinte maneira:
// 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;Agora, vamos expor os dois componentes recém-criados no index.tsx da seguinte maneira:
// client/src/components/Students/index.tsx
export {default as StudentsList} from './StudentsList';
export {default as StudentForm} from './StudentForm';E agora vamos criar a client/src/pages/StudentPage.tsx página e, em seguida, adicionar a rota no client/src/page/index.tsx index. Observe como estamos importando tanto StudentForm e StudentsList do mesmo namespace. (Mais uma vez, obrigado, 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>
);
}
...Agora já devemos conseguir criar novos alunos e visualizá-los na lista.
Convidando alunos
A ideia de ter alunos é poder convidá-los para uma chamada. Lembra-se da actions propriedade de que falamos anteriormente? É aqui que esse recurso vai se destacar, pois nos permitirá oferecer essa funcionalidade à lista de alunos, ao mesmo tempo em que podemos reutilizar o mesmo componente que criamos antes.
Vamos criar um novo componente chamado Attendees.tsx em client/src/components/Videocall/. Nesse novo componente, vamos criar uma ação personalizada que acionará a inviteStudent mutação.
// 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;Adicione também o componente recém-criado ao índice do Videocall:
// src/components/Videocall/index.tsx
...
export { default as Attendees } from './Attendees';E, por fim, adicione o componente “Attendees” à página “VideoSession”:
// src/pages/VideoSession.tsx
...
import { Room, Attendees } from '../components/Videocall';
...
const VideoSession = () => {
...
return (
<>
...
<Attendees />
</>
);
};Agora crie alguns alunos usando números de telefone válidos, inicie uma aula e clique no botão “convidar” para convidá-los.
Criação e envio de tarefas
A etapa final da nossa demonstração é permitir que os alunos enviem suas tarefas. Para garantir que possamos identificar a quem pertence um arquivo de lição de casa, usaremos o login sem senha com base no número de telefone utilizado no cadastro do aluno.
Configurar mutações e consultas
A primeira coisa que precisamos fazer é permitir a criação de tarefas e arquivos de tarefas. Também precisamos dar aos usuários a possibilidade de enviar arquivos. Para isso, usaremos um bucket do S3 com solicitações POST pré-assinadas.
Vamos começar com os resolvedores para criar e recuperar tarefas e arquivos de tarefas. Abra o server/src/graphql/homework.js arquivo, na pasta server, e preencha os resolvedores da seguinte forma:
// 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);
};
...A seguir, vamos adicionar uma mutação para pré-assinar uma solicitação POST que poderá ser usada posteriormente no código do lado do cliente para enviar o arquivo para o S3. Para isso, vamos usar o aws-sdk pacote npm. O serviço já está configurado em 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);
}
});
});
};
...Então, tudo o que precisamos fazer é, na verdade, utilizar o serviço em uma nova mutação. Abra o server/src/graphql/s3.js arquivo e preencha a presignDocument função de resolução da seguinte maneira:
// 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);
}
};
...
Agora é hora de configurar a “mágica” do Vonage para a autenticação sem senha. Para isso, usaremos a Verify API. Primeiro, vamos criar o serviço. Abra o server/src/services/vonage/verify.js arquivo e preencha o verifyRequest e checkCode da seguinte maneira:
// 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);
}
}
});
})
}
...Por fim, vamos expor esses serviços por meio do GraphQL. Abra o server/src/graphql/vonage.js arquivo e preencha o verifyRequestResolver e checkCodeResolver da seguinte maneira:
// 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,
};
}
};
... Criar componentes e páginas do React
Vamos começar criando um formulário para criar tarefas de casa e uma tabela simples para listá-las.
Crie uma Homeworks pasta dentro de client/src/components e, em seguida, crie HomeworkForm.tsx e HomeworkList.tsx dentro dela. Preencha o primeiro arquivo da seguinte forma para criar o formulário:
// 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;Em seguida, preencha o HomeworkList.tsx arquivo da seguinte maneira para criar uma tabela simples que liste os trabalhos de casa criados. Observe que também estamos definindo alguns Links nas colunas “Identificador” e “Ação”. Esses links permitirão que o professor revise os arquivos de uma determinada tarefa e que os alunos enviem os arquivos propriamente ditos.
Vamos trabalhar nas páginas que esses links abrirão daqui a pouco.
// 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;Agora, vamos expor os componentes recém-criados criando um index.tsx arquivo em client/src/components/Homeworks com o seguinte conteúdo:
// client/src/components/Homeworks/index.tsx
export {default as HomeworkList} from './HomeworkList';
export {default as HomeworkForm} from './HomeworkForm';Em seguida, crie o HomeworksPage.tsx a seguir client/src/pages/ da seguinte maneira:
// client/src/pages/HomeworksPage.tsx
import React from 'react';
import { HomeworkForm, HomeworkList } from '../components/Homeworks';
const HomeworksPage = () => {
return (
<>
<HomeworkForm />
<HomeworkList />
</>
)
}
export default HomeworksPage;E não se esqueça de adicioná-lo ao index.tsx arquivo na mesma pasta:
// 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>
);
} Implementação de um login sem senha
Para o login sem senha, vamos criar dois novos componentes: um que servirá como página de login e outro que conterá o formulário que os alunos verão após a autenticação.
Criar PasswordlessLogin.tsx e HomeworkFileForm.tsx em client/src/components/Homeworks.
Primeiro, vamos nos concentrar na criação do formulário de login. Para isso, nosso componente definirá uma mutação para criar uma solicitação de verificação e outra para realizar a verificação propriamente dita.
A interface do usuário consistirá em uma caixa de texto para inserir o número de telefone e um botão para iniciar a solicitação. Após o requestId tenha sido retornado com sucesso pelo servidor, queremos exibir um campo de texto adicional para inserir o código e um botão para verificação.
Preencha o PasswordlessLogin.tsx arquivo da seguinte maneira:
// 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;Em seguida, crie o formulário para fazer o upload do arquivo. Esse formulário será um pouco diferente dos que criamos anteriormente neste tutorial, pois será um formulário não controlado. Além disso, é preciso seguir algumas etapas adicionais para fazer o upload do arquivo para o S3 antes de chamar a função `mutate`.
Preencha o HomeworkFileForm.tsx da seguinte maneira:
// 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;Agora vamos criar uma página que exibirá um componente diferente dependendo se o aluno está conectado ou não. Crie o client/src/pages/AddHomeworkFilePage.tsx arquivo e preencha-o da seguinte forma:
// 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;Mais uma vez, não se esqueça de adicionar a página recém-criada à index.tsx na mesma pasta:
// 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>
);
}
... Criação de uma lista dos arquivos de tarefas de casa
A última coisa que precisamos fazer é permitir que o professor verifique os arquivos dos deveres de casa que os alunos enviaram. Para isso, basta criarmos um HomeworkFileList componente semelhante aos que acabamos de criar para Alunos e Tarefas.
Crie um novo client/src/components/Homeworks/HomeworkFileList.tsx e preencha-o da seguinte forma para criar a lista de arquivos de lição de casa:
// 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;Por fim, crie o ListHomeworkFilesPage.tsx arquivo conforme client/src/pages conforme mostrado abaixo:
// 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;E, pela última vez, não se esqueça de adicionar a rota ao index.tsx arquivo na mesma pasta:
// 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>
);
}
... Conclusão
E é isso! Espero que este post tenha dado a vocês uma ideia do que podem fazer para se adaptar à “nova normalidade” e de como as novidades incríveis que estão sendo desenvolvidas na Vonage podem ajudá-los a alcançar isso.
Compartilhar:
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 ❤️