diff --git a/backend/src/api/level.ts b/backend/src/api/level.ts index 3a74f65..1a606db 100644 --- a/backend/src/api/level.ts +++ b/backend/src/api/level.ts @@ -21,10 +21,15 @@ levelRouter.get("/:id/vocab", async (req: LRequest, res: Response) => { } const levelId = parseInt(req.params.id); - // TODO: Handle this - // if (levelId === NaN) { - // // Something - // } + if (levelId === NaN) { + res.send({ + error: "400", + data: { + msg: "Invalid level id", + }, + }); + return; + } // Find the level // TODO: if (level) @@ -33,12 +38,9 @@ levelRouter.get("/:id/vocab", async (req: LRequest, res: Response) => { // TODO: This aint safe, boi level: levelId, }); - console.log(level); // Fetch all the vocabulary const vocab = await db.collection("vocabulary").find({ id: { $in: level.vocab } }).toArray(); - console.log(vocab); - res.send({ error: "0", data: { diff --git a/backend/src/api/user.ts b/backend/src/api/user.ts index caa967c..9e20cbb 100644 --- a/backend/src/api/user.ts +++ b/backend/src/api/user.ts @@ -155,12 +155,54 @@ userRouter.get("/nextLevel", async (req: LRequest, res) => { }); // Mark a level as done -userRouter.post("/level/:id", async (req, res) => { - console.log("STUB(post): /user/level/:id"); +userRouter.post("/level/:id", async (req: LRequest, res) => { + // Is everything specified? + if (!req.params || !req.params.id) { + res.send({ + error: "400", + data: { + msg: "No level ID specified", + }, + }); + return; + } + + const id = parseInt(req.params.id); + if (id === NaN) { + res.send({ + error: "400", + data: { + msg: "Invalid level ID", + }, + }); + return; + } + + // TODO: if (user) + const { token, db } = req; + const user = await userFromSession(token, db); + + if (id in user.levels) { + // Nothing + } else { + // The level is new to the user + // Is the new level higher than the "highest" already completed level? + let update = { + levels: user.levels.concat(id), + }; + if (id > Math.max(...user.levels)) { + // Also update the lastLevel attribute + Object.assign(update, { lastLevel: id }); + } + await db.collection("users").updateOne({ + username: user.username, + }, { + $set: update, + }); + } - // TODO: Stub res.send({ - error: "0", + error: "200", data: {}, }); }); @@ -206,4 +248,35 @@ userRouter.get("/dashboard", async (req: LRequest, res) => { }); }); +userRouter.post("/showWelcome", async (req: LRequest, res) => { + const { db, token } = req; + + // Are all arguments specified? + if (!req.body || !("state" in req.body)) { + res.send({ + error: "400", + data: { + msg: "State not specified", + }, + }); + return; + } + + // Find the user + // TODO: if (user) + const user = await userFromSession(token, db); + + await db.collection("users").updateOne({ + username: user.username, + }, { + $set: { + showWelcome: req.body.state, + }, + }); + res.send({ + error: "200", + data: {}, + }); +}); + export default userRouter; diff --git a/backend/src/main.ts b/backend/src/main.ts index 72890ff..7e1339b 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -5,6 +5,8 @@ import * as bodyparser from "body-parser"; 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"; @@ -39,10 +41,9 @@ const assert = require('assert'); app.use(bodyparser.json()); app.options("*", cors()); - app.use((req, res, next) => { + app.use((req: LRequest, res, next) => { // Every route should have access to the database so that // we can easily make calls to it - // @ts-ignore req.db = db; next(); }); diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index dcec68a..904e73f 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -12,9 +12,21 @@ export interface IUserDBModel { wrong: number; }; + // Levels that we have done + levels: number[]; + // The "highest" level the user has done lastLevel: number; queue: number[]; + + // Vocabulary ID -> SM2 Metadata + metadata: { + [key: number]: { + easiness: number; + nextDueDate: number; + consecutiveCorrectAnswers: number; + }; + }; }; export interface IUser { diff --git a/frontend/src/components/app.tsx b/frontend/src/components/app.tsx index 6bfc9c1..ffa1d47 100644 --- a/frontend/src/components/app.tsx +++ b/frontend/src/components/app.tsx @@ -195,6 +195,22 @@ export default class Application extends React.Component { }); } + introDontShowAgain = (): void => { + // NOTE: This is not a promise, as we do not care about any response + // being sent, since we don't need to update any client-side + // 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? getDashboard = (): Promise => { return new Promise((res, rej) => { @@ -216,6 +232,16 @@ export default class Application extends React.Component { }); } + 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 => { return new Promise((res, rej) => { fetch(`${BACKEND_URL}/api/login`, { @@ -282,7 +308,8 @@ export default class Application extends React.Component { isAuth={this.isAuthenticated} path="/welcome" component={() => { - return + return }} /> { return ; } else { return ; diff --git a/frontend/src/pages/intro.tsx b/frontend/src/pages/intro.tsx index 6907155..2920e3e 100644 --- a/frontend/src/pages/intro.tsx +++ b/frontend/src/pages/intro.tsx @@ -9,7 +9,11 @@ import CardContent from "@material-ui/core/CardContent"; import CardActions from "@material-ui/core/CardActions"; import Button from "@material-ui/core/Button"; -export default class IntroPage extends React.Component<{}> { +interface IProps { + dontShowAgain: () => void; +}; + +export default class IntroPage extends React.Component { render() { const small = window.matchMedia("(max-width: 700px)").matches; const cName = small ? "intro-card-xs" : "intro-card-lg"; @@ -68,11 +72,11 @@ export default class IntroPage extends React.Component<{}> { - {/*TODO: Tell the server to not show this page again*/} diff --git a/frontend/src/pages/level.tsx b/frontend/src/pages/level.tsx index 847d148..c6087fb 100644 --- a/frontend/src/pages/level.tsx +++ b/frontend/src/pages/level.tsx @@ -17,6 +17,7 @@ import { IVocab } from "../models/vocab"; interface IProps { id: string; levelVocab: (id: string) => Promise; + updateDoneLevels: (id: string) => void; history: any; @@ -88,6 +89,7 @@ const LevelPageWithRouter = withRouter( const { vocab, lookedAt, id } = this.props; // Only go to the review if all vocabulary item have been looked at if (vocab.length === lookedAt.length) { + this.props.updateDoneLevels(id); this.props.setLoading(true); this.props.history.push(`/review/level/${id}`); } diff --git a/protocol.md b/protocol.md index 69ccdcc..ed5ffdc 100644 --- a/protocol.md +++ b/protocol.md @@ -18,3 +18,6 @@ Sick # 18.09.2018 - Move the whole application from "vanilla React" to React with Redux to avoid issues with components being re-mounted, when the AppBar gets updated + +# 29.09.2018 +- Finish implementing the most needed API endpoints diff --git a/server/nginx.conf b/server/nginx.conf index 17178e9..8ef08ca 100644 --- a/server/nginx.conf +++ b/server/nginx.conf @@ -13,6 +13,10 @@ http { # server_name lateinicus; listen 80 default_server; + # Enable gzip compression + gzip on; + gzip_min_length 256K; + # Reverse Proxy location /api/ { # Seems weird, but it is (Prevent /api/api/)