feat: Implement updating the done levels
This commit is contained in:
parent
e6e7505383
commit
ab0c75331d
@ -21,10 +21,15 @@ levelRouter.get("/:id/vocab", async (req: LRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const levelId = parseInt(req.params.id);
|
const levelId = parseInt(req.params.id);
|
||||||
// TODO: Handle this
|
if (levelId === NaN) {
|
||||||
// if (levelId === NaN) {
|
res.send({
|
||||||
// // Something
|
error: "400",
|
||||||
// }
|
data: {
|
||||||
|
msg: "Invalid level id",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the level
|
// Find the level
|
||||||
// TODO: if (level)
|
// TODO: if (level)
|
||||||
@ -33,12 +38,9 @@ levelRouter.get("/:id/vocab", async (req: LRequest, res: Response) => {
|
|||||||
// TODO: This aint safe, boi
|
// TODO: This aint safe, boi
|
||||||
level: levelId,
|
level: levelId,
|
||||||
});
|
});
|
||||||
console.log(level);
|
|
||||||
|
|
||||||
// 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();
|
||||||
console.log(vocab);
|
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
error: "0",
|
error: "0",
|
||||||
data: {
|
data: {
|
||||||
|
@ -155,12 +155,54 @@ userRouter.get("/nextLevel", async (req: LRequest, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mark a level as done
|
// Mark a level as done
|
||||||
userRouter.post("/level/:id", async (req, res) => {
|
userRouter.post("/level/:id", async (req: LRequest, res) => {
|
||||||
console.log("STUB(post): /user/level/:id");
|
// Is everything specified?
|
||||||
|
if (!req.params || !req.params.id) {
|
||||||
// TODO: Stub
|
|
||||||
res.send({
|
res.send({
|
||||||
error: "0",
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
error: "200",
|
||||||
data: {},
|
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;
|
export default userRouter;
|
||||||
|
@ -5,6 +5,8 @@ import * as bodyparser from "body-parser";
|
|||||||
|
|
||||||
import { isAuthenticated, performLogin } from "./security/auth";
|
import { isAuthenticated, performLogin } from "./security/auth";
|
||||||
|
|
||||||
|
import { LRequest } from "./types/express";
|
||||||
|
|
||||||
import UserRouter from "./api/user";
|
import UserRouter from "./api/user";
|
||||||
import ClassRouter from "./api/class";
|
import ClassRouter from "./api/class";
|
||||||
import LevelRouter from "./api/level";
|
import LevelRouter from "./api/level";
|
||||||
@ -39,10 +41,9 @@ const assert = require('assert');
|
|||||||
app.use(bodyparser.json());
|
app.use(bodyparser.json());
|
||||||
app.options("*", cors());
|
app.options("*", cors());
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req: LRequest, res, next) => {
|
||||||
// Every route should have access to the database so that
|
// Every route should have access to the database so that
|
||||||
// we can easily make calls to it
|
// we can easily make calls to it
|
||||||
// @ts-ignore
|
|
||||||
req.db = db;
|
req.db = db;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -12,9 +12,21 @@ export interface IUserDBModel {
|
|||||||
wrong: number;
|
wrong: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Levels that we have done
|
||||||
|
levels: number[];
|
||||||
|
// The "highest" level the user has done
|
||||||
lastLevel: number;
|
lastLevel: number;
|
||||||
|
|
||||||
queue: number[];
|
queue: number[];
|
||||||
|
|
||||||
|
// Vocabulary ID -> SM2 Metadata
|
||||||
|
metadata: {
|
||||||
|
[key: number]: {
|
||||||
|
easiness: number;
|
||||||
|
nextDueDate: number;
|
||||||
|
consecutiveCorrectAnswers: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
|
@ -195,6 +195,22 @@ export default class Application extends React.Component<IProps> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?
|
// TODO: Type?
|
||||||
getDashboard = (): Promise<any> => {
|
getDashboard = (): Promise<any> => {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
@ -216,6 +232,16 @@ export default class Application extends React.Component<IProps> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> => {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
fetch(`${BACKEND_URL}/api/login`, {
|
fetch(`${BACKEND_URL}/api/login`, {
|
||||||
@ -282,7 +308,8 @@ export default class Application extends React.Component<IProps> {
|
|||||||
isAuth={this.isAuthenticated}
|
isAuth={this.isAuthenticated}
|
||||||
path="/welcome"
|
path="/welcome"
|
||||||
component={() => {
|
component={() => {
|
||||||
return <WelcomePage />
|
return <WelcomePage
|
||||||
|
dontShowAgain={this.introDontShowAgain} />
|
||||||
}} />
|
}} />
|
||||||
<AuthRoute
|
<AuthRoute
|
||||||
isAuth={this.isAuthenticated}
|
isAuth={this.isAuthenticated}
|
||||||
@ -297,7 +324,7 @@ export default class Application extends React.Component<IProps> {
|
|||||||
return <LevelPage
|
return <LevelPage
|
||||||
id={match.params.id}
|
id={match.params.id}
|
||||||
levelVocab={this.getLevelVocab}
|
levelVocab={this.getLevelVocab}
|
||||||
drawerButtonState={this.drawerButtonState}
|
updateDoneLevels={this.updateDoneLevels}
|
||||||
setLastReview={this.setLastReview} />;
|
setLastReview={this.setLastReview} />;
|
||||||
} else {
|
} else {
|
||||||
return <Redirect to="/login" />;
|
return <Redirect to="/login" />;
|
||||||
|
@ -9,7 +9,11 @@ import CardContent from "@material-ui/core/CardContent";
|
|||||||
import CardActions from "@material-ui/core/CardActions";
|
import CardActions from "@material-ui/core/CardActions";
|
||||||
import Button from "@material-ui/core/Button";
|
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<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const small = window.matchMedia("(max-width: 700px)").matches;
|
const small = window.matchMedia("(max-width: 700px)").matches;
|
||||||
const cName = small ? "intro-card-xs" : "intro-card-lg";
|
const cName = small ? "intro-card-xs" : "intro-card-lg";
|
||||||
@ -68,11 +72,11 @@ export default class IntroPage extends React.Component<{}> {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
{/*TODO: Tell the server to not show this page again*/}
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/dashboard">
|
to="/dashboard"
|
||||||
|
onClick={this.props.dontShowAgain}>
|
||||||
Lass uns loslegen
|
Lass uns loslegen
|
||||||
</Button>
|
</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -17,6 +17,7 @@ import { IVocab } from "../models/vocab";
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
id: string;
|
id: string;
|
||||||
levelVocab: (id: string) => Promise<IVocab[]>;
|
levelVocab: (id: string) => Promise<IVocab[]>;
|
||||||
|
updateDoneLevels: (id: string) => void;
|
||||||
|
|
||||||
history: any;
|
history: any;
|
||||||
|
|
||||||
@ -88,6 +89,7 @@ const LevelPageWithRouter = withRouter(
|
|||||||
const { vocab, lookedAt, id } = this.props;
|
const { vocab, lookedAt, id } = this.props;
|
||||||
// Only go to the review if all vocabulary item have been looked at
|
// Only go to the review if all vocabulary item have been looked at
|
||||||
if (vocab.length === lookedAt.length) {
|
if (vocab.length === lookedAt.length) {
|
||||||
|
this.props.updateDoneLevels(id);
|
||||||
this.props.setLoading(true);
|
this.props.setLoading(true);
|
||||||
this.props.history.push(`/review/level/${id}`);
|
this.props.history.push(`/review/level/${id}`);
|
||||||
}
|
}
|
||||||
|
@ -18,3 +18,6 @@ Sick
|
|||||||
# 18.09.2018
|
# 18.09.2018
|
||||||
- Move the whole application from "vanilla React" to React with Redux to avoid issues with components
|
- Move the whole application from "vanilla React" to React with Redux to avoid issues with components
|
||||||
being re-mounted, when the AppBar gets updated
|
being re-mounted, when the AppBar gets updated
|
||||||
|
|
||||||
|
# 29.09.2018
|
||||||
|
- Finish implementing the most needed API endpoints
|
||||||
|
@ -13,6 +13,10 @@ http {
|
|||||||
# server_name lateinicus;
|
# server_name lateinicus;
|
||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
|
|
||||||
|
# Enable gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 256K;
|
||||||
|
|
||||||
# Reverse Proxy
|
# Reverse Proxy
|
||||||
location /api/ {
|
location /api/ {
|
||||||
# Seems weird, but it is (Prevent /api/api/)
|
# Seems weird, but it is (Prevent /api/api/)
|
||||||
|
Reference in New Issue
Block a user