392 lines
14 KiB
TypeScript
392 lines
14 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { BrowserRouter, Route, Redirect } from "react-router-dom";
|
|
|
|
import AuthRoute from "../security/AuthRoute";
|
|
import { setSessionToken, removeSessionToken, getSessionToken } from "../security/Token";
|
|
|
|
import Dashboard from "../containers/Dashboard";
|
|
import LoginPage from "../containers/LoginPage";
|
|
import LevelListPage from "../containers/LevelList";
|
|
import LevelPage from "../containers/LevelPage";
|
|
import ReviewPage from "../containers/Review";
|
|
import SummaryPage from "../containers/SummaryPage";
|
|
import WelcomePage from "../pages/intro";
|
|
import RegisterPage from "../containers/Register";
|
|
|
|
import Drawer from "../containers/Drawer";
|
|
|
|
import { BACKEND_URL } from "../config";
|
|
|
|
import { ILevel } from "../models/level";
|
|
import { TopTen } from "../models/learner";
|
|
import { IVocab } from "../models/vocab";
|
|
import { IReviewMetadata, ReviewType } from "../models/review";
|
|
import { IUser } from "../models/user";
|
|
import { IResponse } from "../models/server";
|
|
|
|
interface IProps {
|
|
authenticated: boolean;
|
|
|
|
user: IUser;
|
|
setAuthenticated: (status: boolean) => void;
|
|
setDidLogin: (status: boolean) => void;
|
|
setUser: (user: IUser) => void;
|
|
setLastReview: (meta: IReviewMetadata) => void;
|
|
setUserScoreDelta: (delta: number) => void;
|
|
};
|
|
|
|
// TODO: Replace the sessionStorage with localStorage?
|
|
export default class Application extends React.Component<IProps> {
|
|
componentDidMount() {
|
|
// TODO: When asking the server if our session is still valid, a spinner
|
|
// should be shown
|
|
const token = getSessionToken(window);
|
|
if (token !== null && !this.props.authenticated) {
|
|
this.checkAuthStatus(token).then(user => {
|
|
this.props.setUser(user);
|
|
this.props.setAuthenticated(true);
|
|
}).catch(err => {
|
|
// The token is invalid, so we should remove it
|
|
removeSessionToken(window);
|
|
this.props.setAuthenticated(false);
|
|
});
|
|
}
|
|
}
|
|
|
|
checkAuthStatus = (token: string): Promise<IUser> => {
|
|
return new Promise((res, rej) => {
|
|
fetch(`${BACKEND_URL}/api/user/me`, {
|
|
headers: new Headers({
|
|
"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);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
getLevels = (): Promise<ILevel[]> => {
|
|
return new Promise((res, rej) => {
|
|
fetch(`${BACKEND_URL}/api/levels`, {
|
|
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> => {
|
|
return new Promise((res, rej) => {
|
|
fetch(`${BACKEND_URL}/api/user/lastReview`, {
|
|
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?
|
|
setLastReview = (meta: IReviewMetadata, sm2: any, delta: number) => {
|
|
// Update the state
|
|
this.props.setLastReview(meta);
|
|
this.props.setUserScoreDelta(delta);
|
|
|
|
// Tell the server about the last review
|
|
fetch(`${BACKEND_URL}/api/user/lastReview`, {
|
|
headers: new Headers({
|
|
"Content-Type": "application/json",
|
|
"Token": this.props.user.sessionToken,
|
|
}),
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
meta,
|
|
sm2,
|
|
delta,
|
|
}),
|
|
}).then(resp => resp.json(), err => {
|
|
console.log("Application::setLastReview: POSTing last results failed");
|
|
});
|
|
}
|
|
|
|
getReviewQueue = (): Promise<IVocab[]> => {
|
|
return new Promise((res, rej) => {
|
|
fetch(`${BACKEND_URL}/api/user/queue`, {
|
|
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[]> => {
|
|
// TODO: Deprecate?
|
|
const id = this.props.user.classId;
|
|
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> => {
|
|
return new Promise((res, rej) => {
|
|
fetch(`${BACKEND_URL}/api/user/nextLevel`, {
|
|
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[]> => {
|
|
return new Promise((res, rej) => {
|
|
fetch(`${BACKEND_URL}/api/level/${id}/vocab`, {
|
|
method: "GET",
|
|
headers: new Headers({
|
|
"Content-Type": "application/json",
|
|
"Token": this.props.user.sessionToken,
|
|
}),
|
|
}).then(data => data.json(), err => {
|
|
rej(err);
|
|
}).then((resp: IResponse) => {
|
|
if (resp.error === "0") {
|
|
res(resp.data.vocab);
|
|
} else {
|
|
rej(resp);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
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<any> => {
|
|
return new Promise((res, rej) => {
|
|
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> => {
|
|
return new Promise((res, rej) => {
|
|
fetch(`${BACKEND_URL}/api/login`, {
|
|
method: "POST",
|
|
headers: new Headers({
|
|
"Content-Type": "application/json",
|
|
}),
|
|
body: JSON.stringify({
|
|
// NOTE: We will force HTTPS, so this should not be a
|
|
// problem
|
|
username,
|
|
password,
|
|
}),
|
|
}).then(data => data.json(), err => {
|
|
// The fetch failed
|
|
rej(err);
|
|
}).then((resp: IResponse) => {
|
|
if (resp.error === "0") {
|
|
// Successful login
|
|
this.props.setUser(resp.data);
|
|
this.props.setDidLogin(true);
|
|
setSessionToken(window, resp.data.sessionToken);
|
|
this.props.setAuthenticated(true);
|
|
|
|
res(resp.data);
|
|
} else {
|
|
rej(resp);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
logout = () => {
|
|
// NOTE: No promise, since we don't care about the result
|
|
fetch(`${BACKEND_URL}/api/user/logout`, {
|
|
headers: new Headers({
|
|
"Content-Type": "application/json",
|
|
"Token": this.props.user.sessionToken,
|
|
}),
|
|
method: "GET",
|
|
});
|
|
|
|
// Remove the session locally
|
|
removeSessionToken(window);
|
|
this.props.setAuthenticated(false);
|
|
}
|
|
|
|
// Checks whether the user is logged in
|
|
isAuthenticated = () => {
|
|
return this.props.authenticated;
|
|
}
|
|
|
|
render() {
|
|
// TODO: Show a spinner before mounting the routes, so that we can
|
|
// check if were authenticated before doing any requests
|
|
return <BrowserRouter
|
|
basename="/app/">
|
|
<div className="flex" >
|
|
<Drawer logout={this.logout} />
|
|
<div className="content">
|
|
<Route exact path="/" component={() => <Redirect to="/login" />} />
|
|
<Route exact path="/login" component={() => {
|
|
return <LoginPage login={this.login} />
|
|
}} />
|
|
<AuthRoute
|
|
isAuth={this.isAuthenticated}
|
|
path="/dashboard"
|
|
component={() => {
|
|
return <Dashboard
|
|
getDashboard={this.getDashboard} />
|
|
}} />
|
|
<AuthRoute
|
|
isAuth={this.isAuthenticated}
|
|
path="/welcome"
|
|
component={() => {
|
|
return <WelcomePage
|
|
dontShowAgain={this.introDontShowAgain} />
|
|
}} />
|
|
<AuthRoute
|
|
isAuth={this.isAuthenticated}
|
|
path="/levelList"
|
|
component={() => <LevelListPage
|
|
getLevels={this.getLevels} />} />
|
|
{/*We cannot use AuthRoute here, because match is undefined otherwise*/}
|
|
<Route
|
|
path="/level/:id"
|
|
component={({ match }) => {
|
|
if (this.isAuthenticated()) {
|
|
return <LevelPage
|
|
id={match.params.id}
|
|
levelVocab={this.getLevelVocab}
|
|
setLastReview={this.setLastReview} />;
|
|
} else {
|
|
return <Redirect to="/login" />;
|
|
}
|
|
}} />
|
|
<Route
|
|
path="/register"
|
|
component={() => <RegisterPage />} />
|
|
<Route
|
|
path="/review/level/:id"
|
|
component={({ match }) => {
|
|
if (this.isAuthenticated()) {
|
|
return <ReviewPage
|
|
reviewType={ReviewType.LEVEL}
|
|
updateDoneLevels={this.updateDoneLevels}
|
|
levelId={match.params.id}
|
|
vocabByLevel={this.getLevelVocab}
|
|
setLastReview={this.setLastReview} />;
|
|
} else {
|
|
return <Redirect to="/login" />;
|
|
}
|
|
}} />
|
|
<AuthRoute
|
|
isAuth={this.isAuthenticated}
|
|
path="/review/queue"
|
|
component={() => {
|
|
return <ReviewPage
|
|
reviewType={ReviewType.QUEUE}
|
|
vocabByQueue={this.getReviewQueue}
|
|
setLastReview={this.setLastReview} />;
|
|
}} />
|
|
<AuthRoute
|
|
isAuth={this.isAuthenticated}
|
|
path="/review/summary"
|
|
component={() => {
|
|
return <SummaryPage />
|
|
}} />
|
|
</div>
|
|
</div >
|
|
</BrowserRouter >;
|
|
}
|
|
};
|