refactor: Simplify API calls

API calls can now make with a simple wrapper function.

Additionally, the error code "200" now means success for all API calls.
This commit is contained in:
Alexander Polynomdivision 2018-10-17 18:28:29 +02:00
parent 7de3cedb15
commit 3b7e55d957
14 changed files with 410 additions and 234 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "lateinicusserver", "name": "lateinicusserver",
"version": "1.2.0", "version": "1.3.0",
"description": "The backend server for Lateinicus", "description": "The backend server for Lateinicus",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@ -22,7 +22,8 @@
"body-parser": "1.18.3", "body-parser": "1.18.3",
"cors": "^2.8.4", "cors": "^2.8.4",
"express": "4.16.3", "express": "4.16.3",
"mongodb": "^3.1.6" "mongodb": "^3.1.6",
"profanity-util": "^0.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.4", "@types/cors": "^2.8.4",

276
backend/src/#main.ts# Normal file
View File

@ -0,0 +1,276 @@
import { env, exit } from "process";
// import * as fs from "fs";
import { randomBytes, pbkdf2Sync } from "crypto";
import * as assert from "assert";
import * as express from "express";
import * as cors from "cors";
import * as bodyparser from "body-parser";
//@ts-ignore
import * as profanity from "profanity-util";
import { isAuthenticated, performLogin } from "./security/auth";
import { LRequest } from "./types/express";
import UserRouter from "./api/user";
import ClassRouter from "./api/class";
import LevelRouter from "./api/level";
import { ITrackerDBModel } from "./models/tracker";
const baseRouter = express.Router();
const authRouter = express.Router();
import { MongoClient } from "mongodb";
const user = encodeURIComponent("backend");
const password = encodeURIComponent(env["LATEINICUS_USER_PW"]);
(async function() {
// Load the profanity list
// const list = JSON.parse(fs.readFileSync("/etc/profanity", { encoding: "utf-8" }));
// const profanityFilter = new Filter({
// list,
// });
// Database Name
const dbName = 'lateinicus';
// Connection URL
const url = `mongodb://${user}:${password}@128.1.0.2:27017/?authMechanism=SCRAM-SHA-1&authSource=${dbName}`;
let client: MongoClient;
try {
// Use connect method to connect to the Server
client = await MongoClient.connect(url);
console.log("Connected to MongoDB");
} catch (err) {
console.log(err.stack);
assert(false);
}
const db = client.db(dbName);
console.log("Connected to the database");
const app = express();
app.use(bodyparser.json());
app.options("*", cors());
app.use((req: LRequest, res, next) => {
// Every route should have access to the database so that
// we can easily make calls to it
req.db = db;
next();
});
app.use("/api/level", LevelRouter);
app.use("/api/class", ClassRouter);
app.use("/api/user", UserRouter);
app.post("/api/tracker", async (req, res) => {
// Did we get any data
if (!req.body) {
res.send({
error: "403",
data: {
msg: "No request body provided",
},
});
return;
}
// Did we get all arguments?
if (!("session" in req.body) || !("event" in req.body)) {
res.send({
error: "403",
data: {
msg: "Invalid request",
},
});
return;
}
// Insert it into the database
const tracker: ITrackerDBModel = Object.assign({}, req.body, {
timestamp: Date.now(),
});
await db.collection("tracker").insertOne(tracker);
res.send({
error: "200",
data: {},
});
});
app.get("/api/levels", async (req, res) => {
// TODO: if (levels)
const levels = (await db.collection("levels").find({}, {
// The order in which we send the levels is important, so better
// sort them
sort: {
level: 1,
},
})
.toArray())
.map((el) => {
let tmp = Object.assign({}, el);
delete tmp.vocab;
delete tmp._id;
return tmp;
});
res.send({
error: "200",
data: {
levels,
},
});
});
app.post("/api/register", async (req, res) => {
// Check if any data was sent
if (!req.body) {
res.send({
error: "403",
data: {
msg: `No data sent`,
},
});
return;
}
// Check if we have all we need
const params = ["username", "password", "classId"];
for (let param of params) {
if (!(param in req.body)) {
res.send({
error: "403",
data: {
msg: `${param} not specified!`,
},
});
return;
}
}
const { username, password, classId } = req.body;
// Check if the registration is open for the class Id
// NOTE: This to prevent people from spamming the database
const classes = env["LATEINICUS_CLASSES"].split(",");
if (classes.indexOf(classId) === -1) {
res.send({
error: "403",
data: {
msg: "Class does not exist",
},
});
return;
}
// TODO: Check if the username is profane
// if (profanityFilter.isProfane(username)) {
// res.send({
// error: "451",
// data: {
// msg: "Profane username",
// },
// });
// return;
// }
const matches = profanity.check(username, { substring: true });
if (matches.length > 0) {
res.send({
error: "451",
data: {
msg: "Profane username",
},
});
return;
}
// Check if the user already exists
const checkUser = await db.collection("users").findOne({
username,
});
if (checkUser) {
res.send({
error: "403",
data: {
msg: "User already exists",
},
});
return;
}
const salt = randomBytes(30).toString("hex");
const hash = pbkdf2Sync(password, salt, 50000, 512, "sha512").toString("hex");
const user = {
username,
salt,
hash,
classId,
score: 0,
showWelcome: true,
lastReview: {
correct: 0,
wrong: 0,
},
lastLevel: 0,
levels: [] as number[],
vocabMetadata: {},
};
await db.collection("users").insertOne(user);
res.send({
error: "200",
data: {},
});
});
app.get("/api/health", (req, res) => {
res.send({
error: "200",
data: {
msg: "lol",
},
});
});
app.post("/api/login", async (req, res) => {
// Check if all arguments were sent
const { body } = req;
if (!body || !("username" in body) || !("password" in body)) {
res.send({
error: "400",
data: {
msg: "Username or password not specified",
},
});
return;
}
// Try to log the user in
try {
const userData = await performLogin(body.username, body.password, db);
res.send({
error: "200",
data: userData,
});
} catch (err) {
console.log("Could not resolve login promise!", err);
// If anything was wrong, just tell the client
res.send({
error: "1",
data: {
msg: "Username or password is wrong",
},
});
}
});
const server = app.listen(8080, () => {
console.log("Starting on port 8080");
});
})();

1
backend/src/.#main.ts Symbolic link
View File

@ -0,0 +1 @@
alexander@nishimiya.6541:1539788829

View File

@ -1,11 +1,15 @@
import { dayInNDays } from "../../utils/date";
export interface ISchedulingData { export interface ISchedulingData {
easiness: number; easiness: number;
consecutiveCorrectAnswers: number; consecutiveCorrectAnswers: number;
nextDueDate: number; nextDueDate: number;
}; };
function dateInNDays(n: number): number {
let today = new Date();
today.setDate(today.getDate() + n);
return Date.parse(today.toString());
}
export enum AnswerType { export enum AnswerType {
CORRECT, CORRECT,
WRONG, WRONG,
@ -28,8 +32,8 @@ export function updateSchedulingData(data: ISchedulingData, answer: AnswerType):
data.consecutiveCorrectAnswers + 1 data.consecutiveCorrectAnswers + 1
) : 0; ) : 0;
data.nextDueDate = answer === AnswerType.CORRECT ? ( data.nextDueDate = answer === AnswerType.CORRECT ? (
dayInNDays(6 * Math.pow(data.easiness, data.consecutiveCorrectAnswers - 1)) dateInNDays(6 * Math.pow(data.easiness, data.consecutiveCorrectAnswers - 1))
) : dayInNDays(1); ) : dateInNDays(1);
return data; return data;
} }

View File

@ -49,7 +49,7 @@ levelRouter.get("/:id/vocab", async (req: LRequest, res: Response) => {
// Fetch all the vocabulary // Fetch all the vocabulary
const vocab = await db.collection("vocabulary").find({ id: { $in: level.vocab } }).toArray(); const vocab = await db.collection("vocabulary").find({ id: { $in: level.vocab } }).toArray();
res.send({ res.send({
error: "0", error: "200",
data: { data: {
vocab, vocab,
} }

View File

@ -37,7 +37,7 @@ userRouter.get("/me", async (req: LRequest, res) => {
delete copy.vocabMetadata; delete copy.vocabMetadata;
res.send({ res.send({
error: "0", error: "200",
data: copy, data: copy,
}); });
} else { } else {
@ -55,7 +55,7 @@ userRouter.get("/logout", async (req: LRequest, res) => {
await db.collection("sessions").findOneAndDelete({ token, }); await db.collection("sessions").findOneAndDelete({ token, });
res.send({ res.send({
error: "0", error: "200",
data: {}, data: {},
}); });
}); });
@ -143,9 +143,9 @@ userRouter.get("/queue", async (req: LRequest, res) => {
}); });
res.send({ res.send({
error: "0", error: "200",
data: { data: {
queue: vocab, vocab,
}, },
}); });
}); });
@ -157,17 +157,11 @@ userRouter.get("/lastReview", async (req: LRequest, res) => {
const user = await userFromSession(token, db); const user = await userFromSession(token, db);
res.send({ res.send({
error: "0", error: "200",
data: user.lastReview, data: user.lastReview,
}); });
}); });
function dateInNDays(n: number): number {
let today = new Date();
today.setDate(today.getDate() + n);
return Date.parse(today.toString());
}
userRouter.post("/lastReview", async (req: LRequest, res) => { userRouter.post("/lastReview", async (req: LRequest, res) => {
// Check if we get the needed data // Check if we get the needed data
if (!req.body || !("meta" in req.body) || !("sm2" in req.body) || !("delta" in req.body)) { if (!req.body || !("meta" in req.body) || !("sm2" in req.body) || !("delta" in req.body)) {
@ -228,7 +222,7 @@ userRouter.post("/lastReview", async (req: LRequest, res) => {
}); });
res.send({ res.send({
error: "0", error: "200",
data: {}, data: {},
}); });
}); });
@ -259,7 +253,7 @@ async function getNextLevel(token: string, db: Db): Promise<any> {
userRouter.get("/nextLevel", async (req: LRequest, res) => { userRouter.get("/nextLevel", async (req: LRequest, res) => {
const level = await getNextLevel(req.token, req.db); const level = await getNextLevel(req.token, req.db);
res.send({ res.send({
error: "0", error: "200",
data: level, data: level,
}); });
}); });
@ -307,7 +301,7 @@ userRouter.post("/level/:id", async (req: LRequest, res) => {
}); });
// Convert the level's vocabulary to SM2 metadata // Convert the level's vocabulary to SM2 metadata
let sm2: { [id: number]: ISM2Metadata } = {}; let sm2: { [id: number]: ISchedulingData } = {};
level.vocab.forEach((id: number) => { level.vocab.forEach((id: number) => {
sm2[id] = { sm2[id] = {
easiness: 1.3, easiness: 1.3,

View File

@ -9,7 +9,7 @@ import * as cors from "cors";
import * as bodyparser from "body-parser"; import * as bodyparser from "body-parser";
//@ts-ignore //@ts-ignore
//import * as Filter from "bad-words"; import * as profanity from "profanity-util";
import { isAuthenticated, performLogin } from "./security/auth"; import { isAuthenticated, performLogin } from "./security/auth";
@ -121,7 +121,7 @@ const password = encodeURIComponent(env["LATEINICUS_USER_PW"]);
}); });
res.send({ res.send({
error: "0", error: "200",
data: { data: {
levels, levels,
}, },
@ -178,6 +178,16 @@ const password = encodeURIComponent(env["LATEINICUS_USER_PW"]);
// }); // });
// return; // return;
// } // }
const matches = profanity.check(username, { substring: true });
if (matches.length > 0) {
res.send({
error: "451",
data: {
msg: "Profane username",
},
});
return;
}
// Check if the user already exists // Check if the user already exists
const checkUser = await db.collection("users").findOne({ const checkUser = await db.collection("users").findOne({
@ -221,7 +231,7 @@ const password = encodeURIComponent(env["LATEINICUS_USER_PW"]);
}); });
app.get("/api/health", (req, res) => { app.get("/api/health", (req, res) => {
res.send({ res.send({
error: "0", error: "200",
data: { data: {
msg: "lol", msg: "lol",
}, },
@ -245,7 +255,7 @@ const password = encodeURIComponent(env["LATEINICUS_USER_PW"]);
try { try {
const userData = await performLogin(body.username, body.password, db); const userData = await performLogin(body.username, body.password, db);
res.send({ res.send({
error: "0", error: "200",
data: userData, data: userData,
}); });

View File

@ -16,7 +16,7 @@ services:
ipv4_address: 128.1.0.2 ipv4_address: 128.1.0.2
backend: backend:
image: lateinicus/server:1.2.0 image: lateinicus/server:1.3.0
environment: environment:
- LATEINICUS_USER_PW=abc123 - LATEINICUS_USER_PW=abc123
- LATEINICUS_CLASSES=test - LATEINICUS_CLASSES=test

View File

@ -1,6 +1,6 @@
{ {
"name": "seminarfach", "name": "seminarfach",
"version": "1.2.0", "version": "1.3.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

33
frontend/src/api/call.ts Normal file
View File

@ -0,0 +1,33 @@
import { BACKEND_URL } from "../config.in";
interface IAPIOptions {
token?: string;
body?: any;
method: "post" | "get";
};
export function makeAPICall<T>(endpoint: string, options: IAPIOptions): Promise<T> {
const { token, body, method } = options;
const headers = token !== "" ? ({
"Content-Type": "application/json",
"Token": token,
}) : ({
"Content-Type": "application/json",
});
return new Promise((res, rej) => {
fetch(`${BACKEND_URL}${endpoint}`, {
// WHUT
headers: new Headers(headers),
body: body !== {} ? JSON.stringify(body) : "",
method,
})
.then(resp => resp.json(), err => rej(err))
.then(data => {
if (data.error === "200") {
res(data.data);
} else {
rej(data);
}
});
});
}

View File

@ -17,6 +17,7 @@ import VocabPage from "../containers/VocabPage";
import Drawer from "../containers/Drawer"; import Drawer from "../containers/Drawer";
import { trackAction } from "../api/tracker"; import { trackAction } from "../api/tracker";
import { makeAPICall } from "../api/call";
import { BACKEND_URL } from "../config.in"; import { BACKEND_URL } from "../config.in";
@ -60,76 +61,26 @@ export default class Application extends React.Component<IProps> {
// Track the end of a review // Track the end of a review
trackAction(TrackerEvent.LOG_IN); trackAction(TrackerEvent.LOG_IN);
return new Promise((res, rej) => { return makeAPICall("/api/user/me", {
fetch(`${BACKEND_URL}/api/user/me`, { token: this.props.user.sessionToken,
headers: new Headers({ method: "get",
"Content-Type": "application/json", })
"Token": token,
}),
}).then(resp => resp.json(), err => rej(err))
.then(data => {
if (data.error === "0") {
res(data.data);
} else {
rej(data);
}
});
});
} }
getVocab = (): Promise<IVocab[]> => { getVocab = () => makeAPICall("/api/user/vocab", {
return new Promise((res, rej) => { token: this.props.user.sessionToken,
fetch(`${BACKEND_URL}/api/user/vocab`, { method: "get",
headers: new Headers({ })
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(), err => rej(err))
.then(data => {
if (data.error === "200") {
res(data.data);
} else {
rej(data);
}
});
});
}
getLevels = (): Promise<ILevel[]> => { getLevels = () => makeAPICall("/api/levels", {
return new Promise((res, rej) => { token: this.props.user.sessionToken,
fetch(`${BACKEND_URL}/api/levels`, { method: "get",
headers: new Headers({ });
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(), err => rej(err))
.then(data => {
if (data.error === "0") {
res(data.data.levels);
} else {
rej(data);
}
});
});
}
getLastReview = (): Promise<IReviewMetadata> => { getLastReview = () => makeAPICall("/api/user/lastReview", {
return new Promise((res, rej) => { token: this.props.user.sessionToken,
fetch(`${BACKEND_URL}/api/user/lastReview`, { method: "get",
headers: new Headers({ });
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(), err => rej(err))
.then(data => {
if (data.error === "0") {
res(data.data);
} else {
rej(data);
}
});
});
}
// TODO: Type? // TODO: Type?
setLastReview = (meta: IReviewMetadata, sm2: any, delta: number) => { setLastReview = (meta: IReviewMetadata, sm2: any, delta: number) => {
@ -138,149 +89,55 @@ export default class Application extends React.Component<IProps> {
this.props.setUserScoreDelta(delta); this.props.setUserScoreDelta(delta);
// Tell the server about the last review // Tell the server about the last review
fetch(`${BACKEND_URL}/api/user/lastReview`, { makeAPICall("/api/user/lastReview", {
headers: new Headers({ token: this.props.user.sessionToken,
"Content-Type": "application/json", body: {
"Token": this.props.user.sessionToken,
}),
method: "POST",
body: JSON.stringify({
meta, meta,
sm2, sm2,
delta, delta,
}), },
}).then(resp => resp.json(), err => { method: "post",
console.log("Application::setLastReview: POSTing last results failed");
}); });
// Track the end of a review // Track the end of a review
trackAction(TrackerEvent.FINISH_LEARNING); trackAction(TrackerEvent.FINISH_LEARNING);
} }
getReviewQueue = (): Promise<IVocab[]> => { getReviewQueue = () => makeAPICall("/api/user/queue", {
return new Promise((res, rej) => { token: this.props.user.sessionToken,
fetch(`${BACKEND_URL}/api/user/queue`, { method: "get",
headers: new Headers({ });
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(), err => rej(err))
.then(data => {
if (data.error === "0") {
res(data.data.queue);
} else {
rej(data);
}
});
});
}
getTopTenLearners = (): Promise<TopTen[]> => { getNextLevel = () => makeAPICall("/api/user/nextLevel", {
// TODO: Deprecate? token: this.props.user.sessionToken,
const id = this.props.user.classId; method: "get",
return new Promise((res, rej) => { });
fetch(`${BACKEND_URL}/api/class/${id}/topTen`, {
headers: new Headers({
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(),
err => rej(err))
.then(data => {
if (data.error === "0") {
res(data.data.topTen);
} else {
rej(data);
}
});
});
}
getNextLevel = (): Promise<ILevel> => { getLevelVocab = (id: number) => makeAPICall(`/api/level/${id}/vocab`, {
return new Promise((res, rej) => { token: this.props.user.sessionToken,
fetch(`${BACKEND_URL}/api/user/nextLevel`, { method: "get",
headers: new Headers({ });
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(),
err => rej(err))
.then(data => {
if (data.error === "0") {
res(data.data);
} else {
rej(data);
}
});
});
}
getLevelVocab = (id: number): Promise<IVocab[]> => { // NOTE: This is not a promise, as we do not care about any response
return new Promise((res, rej) => { // being sent, since we don't need to update any client-side
fetch(`${BACKEND_URL}/api/level/${id}/vocab`, { // state.
method: "GET", introDontShowAgain = () => makeAPICall("/api/user/showWelcome", {
headers: new Headers({ token: this.props.user.sessionToken,
"Content-Type": "application/json", body: {
"Token": this.props.user.sessionToken, state: false,
}), },
}).then(data => data.json(), err => { method: "post",
rej(err); });
}).then((resp: IResponse) => {
if (resp.error === "0") {
res(resp.data.vocab);
} else {
rej(resp);
}
});
});
}
introDontShowAgain = (): void => { getDashboard = () => makeAPICall("/api/user/dashboard", {
// NOTE: This is not a promise, as we do not care about any response token: this.props.user.sessionToken,
// being sent, since we don't need to update any client-side method: "get",
// state. });
fetch(`${BACKEND_URL}/api/user/showWelcome`, {
headers: new Headers({
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
method: "POST",
body: JSON.stringify({
state: false,
}),
});
}
// TODO: Type? updateDoneLevels = (id: string) => makeAPICall(`/api/user/level/${id}`, {
getDashboard = (): Promise<any> => { token: this.props.user.sessionToken,
return new Promise((res, rej) => { method: "post",
fetch(`${BACKEND_URL}/api/user/dashboard`, { });
headers: new Headers({
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
})
.then(resp => resp.json(), err => rej(err))
.then(data => {
if (data.error === "200") {
res(data.data);
} else {
console.log("Application::getDashboard: Failed to get dashboard");
rej(data);
}
});
});
}
updateDoneLevels = (id: string): void => {
fetch(`${BACKEND_URL}/api/user/level/${id}`, {
headers: new Headers({
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
method: "POST",
});
}
login = (username: string, password: string): Promise<IUser | IResponse> => { login = (username: string, password: string): Promise<IUser | IResponse> => {
// Track the login // Track the login
@ -302,7 +159,7 @@ export default class Application extends React.Component<IProps> {
// The fetch failed // The fetch failed
rej(err); rej(err);
}).then((resp: IResponse) => { }).then((resp: IResponse) => {
if (resp.error === "0") { if (resp.error === "200") {
// Successful login // Successful login
this.props.setUser(resp.data); this.props.setUser(resp.data);
this.props.setDidLogin(true); this.props.setDidLogin(true);

View File

@ -28,7 +28,7 @@ import { IVocab, VocabType } from "../models/vocab";
interface IProps { interface IProps {
id: string; id: string;
levelVocab: (id: string) => Promise<IVocab[]>; levelVocab: (id: string) => Promise<any>;
history: any; history: any;
@ -64,7 +64,8 @@ const LevelPageWithRouter = withRouter(
this.props.setLoading(true); this.props.setLoading(true);
// TODO: Error handling // TODO: Error handling
this.props.levelVocab(this.props.id).then(vocab => { this.props.levelVocab(this.props.id).then(data => {
const { vocab } = data;
this.props.setVocab(vocab); this.props.setVocab(vocab);
this.props.setCurrentVocab(vocab[0]); this.props.setCurrentVocab(vocab[0]);
this.props.setLookedAt([vocab[0].id]); this.props.setLookedAt([vocab[0].id]);

View File

@ -6,7 +6,6 @@ import Button from "@material-ui/core/Button";
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions'; import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent'; import CardContent from '@material-ui/core/CardContent';
import Paper from "@material-ui/core/Paper";
import Snackbar from "@material-ui/core/Snackbar"; import Snackbar from "@material-ui/core/Snackbar";
import Loader from "../components/loading"; import Loader from "../components/loading";
@ -17,7 +16,7 @@ import { ILevel } from "../models/level";
import { IUser } from "../models/user"; import { IUser } from "../models/user";
interface IProps { interface IProps {
getLevels: () => Promise<ILevel[]>; getLevels: () => Promise<any>;
history: any; history: any;
@ -37,7 +36,7 @@ const LevelListWithRouter = withRouter(
// Fetch the levels // Fetch the levels
this.props.getLevels().then(res => { this.props.getLevels().then(res => {
this.props.setLevels(res); this.props.setLevels(res.levels);
this.props.setLoading(false); this.props.setLoading(false);
}); });
} }

View File

@ -40,8 +40,8 @@ import { Queue } from "../utils/queue";
interface IProps { interface IProps {
levelId?: number; levelId?: number;
vocabByLevel?: (level: number) => Promise<IVocab[]>; vocabByLevel?: (level: number) => Promise<any>;
vocabByQueue?: () => Promise<IVocab[]>; vocabByQueue?: () => Promise<any>;
updateDoneLevels?: (id: string) => void; updateDoneLevels?: (id: string) => void;
setLastReview: (meta: IReviewMetadata, sm2: any, delta: number) => void; setLastReview: (meta: IReviewMetadata, sm2: any, delta: number) => void;
reviewType: ReviewType; reviewType: ReviewType;
@ -110,19 +110,19 @@ const ReviewPageWithRouter = withRouter(
// Track the start of a session // Track the start of a session
trackAction(TrackerEvent.START_LEARNING); trackAction(TrackerEvent.START_LEARNING);
getVocab().then((res: IVocab[]) => { getVocab().then((res: any) => {
// Check if we received any vocabulary // Check if we received any vocabulary
if (res.length === 0) { if (res.vocab.length === 0) {
this.openModal(); this.openModal();
return; return;
} }
// Stop the loading // Stop the loading
this.props.setLoading(false); this.props.setLoading(false);
this.vocab = res; this.vocab = res.vocab;
// Convert the vocab items into review queue cards // Convert the vocab items into review queue cards
res.forEach(vocab => { res.vocab.forEach((vocab: IVocab) => {
// Set the error data for the group // Set the error data for the group
this.error_data[vocab.id] = 0; this.error_data[vocab.id] = 0;