refactor: MONOREPO
This commit is contained in:
158
frontend/src/components/Drawer.tsx
Normal file
158
frontend/src/components/Drawer.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import AppBar from "@material-ui/core/AppBar";
|
||||
import Toolbar from "@material-ui/core/Toolbar";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import SwipeableDrawer from "@material-ui/core/SwipeableDrawer";
|
||||
import List from "@material-ui/core/List";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemIcon from "@material-ui/core/ListItemIcon";
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import Avatar from "@material-ui/core/Avatar";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import SettingsIcon from "@material-ui/icons/Settings";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
import InfoIcon from "@material-ui/icons/Info";
|
||||
import HomeIcon from "@material-ui/icons/Home";
|
||||
import BookIcon from "@material-ui/icons/Book";
|
||||
import ViewWeekIcon from "@material-ui/icons/ViewWeek";
|
||||
|
||||
import { IUser } from "../models/user";
|
||||
|
||||
interface IProps {
|
||||
logout: () => void;
|
||||
|
||||
user: IUser;
|
||||
open: boolean;
|
||||
showButton: boolean;
|
||||
authenticated: boolean;
|
||||
setDrawer: (state: boolean) => void;
|
||||
};
|
||||
|
||||
export default class Drawer extends React.Component<IProps> {
|
||||
openDrawer = () => {
|
||||
this.props.setDrawer(true);
|
||||
}
|
||||
closeDrawer = () => {
|
||||
this.props.setDrawer(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
{
|
||||
(this.props.authenticated && this.props.showButton) ? (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={this.openDrawer}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
}
|
||||
<Typography className="flex" variant="title" color="inherit">
|
||||
Lateinicus
|
||||
</Typography>
|
||||
{
|
||||
this.props.authenticated ? (
|
||||
<Button color="inherit">
|
||||
{`${this.props.user.score} / 200`}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<SwipeableDrawer
|
||||
anchor="left"
|
||||
open={this.props.open}
|
||||
onClose={this.closeDrawer}
|
||||
onOpen={this.openDrawer}>
|
||||
<List component="nav">
|
||||
<ListItem>
|
||||
<Avatar alt="{Username}" style={{ width: 80, height: 80 }} src="https://avatarfiles.alphacoders.com/105/105250.jpg" />
|
||||
<ListItemText primary={"{{ PLACEHOLDER }}"} />
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<PersonIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Profil" />
|
||||
</ListItem>
|
||||
<ListItem button>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Einstellungen" />
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem
|
||||
component={Link}
|
||||
to="/dashboard"
|
||||
onClick={this.closeDrawer}
|
||||
button>
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Dashboard" />
|
||||
</ListItem>
|
||||
<ListItem
|
||||
component={Link}
|
||||
to="/review/queue"
|
||||
onClick={this.closeDrawer}
|
||||
button>
|
||||
<ListItemIcon>
|
||||
<BookIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Vokabeln üben
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
component={Link}
|
||||
to="/levelList"
|
||||
onClick={this.closeDrawer}
|
||||
button>
|
||||
<ListItemIcon>
|
||||
<ViewWeekIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Levelübersicht
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem button onClick={() => {
|
||||
this.closeDrawer();
|
||||
this.props.logout();
|
||||
}}>
|
||||
<ListItemText>
|
||||
Abmelden
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem button onClick={() => window.location = "https://gitlab.com/Polynomdivision/Lateinicus/tree/master"}>
|
||||
<ListItemIcon>
|
||||
<InfoIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Über" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</SwipeableDrawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
38
frontend/src/components/SummaryTable.tsx
Normal file
38
frontend/src/components/SummaryTable.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
|
||||
import { IReviewMetadata } from "../models/review";
|
||||
|
||||
interface IProps {
|
||||
reviewMeta: () => IReviewMetadata;
|
||||
}
|
||||
|
||||
export default class SummaryTable extends React.Component<IProps> {
|
||||
render() {
|
||||
const meta = this.props.reviewMeta();
|
||||
|
||||
return <Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Antworten</TableCell>
|
||||
<TableCell>Anzahl</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Korrekt</TableCell>
|
||||
<TableCell numeric>{meta.correct}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Falsch</TableCell>
|
||||
<TableCell numeric>{meta.wrong}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>;
|
||||
}
|
||||
}
|
||||
318
frontend/src/components/app.tsx
Normal file
318
frontend/src/components/app.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { BrowserRouter, Route, Redirect } from "react-router-dom";
|
||||
|
||||
import AuthRoute from "../security/AuthRoute";
|
||||
import { setSessionToken, removeSessionToken } from "../security/Token";
|
||||
|
||||
import Dashboard from "../pages/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 Drawer from "../containers/Drawer";
|
||||
|
||||
import { BACKEND_URL } from "../config";
|
||||
|
||||
import { ILevel } from "../models/level";
|
||||
import { ILearner } from "../models/learner";
|
||||
import { IVocab, VocabType } from "../models/vocab";
|
||||
import { IReviewMetadata, ReviewType } from "../models/review";
|
||||
import { IUser } from "../models/user";
|
||||
|
||||
interface IProps {
|
||||
authenticated: boolean;
|
||||
|
||||
setAuthenticated: (status: boolean) => void;
|
||||
setUser: (user: IUser) => void;
|
||||
};
|
||||
|
||||
// TODO: Replace the sessionStorage with localStorage?
|
||||
// TODO: Cache API-Calls
|
||||
// TODO: When mounting without a login, check if the sessionToken is still valid
|
||||
export default class Application extends React.Component<IProps> {
|
||||
getLevels(): Promise<ILevel[]> {
|
||||
console.log("STUB: Application::getLevels");
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
// TODO: Actually fetch them from somewhere
|
||||
setTimeout(() => {
|
||||
const levels = [{
|
||||
name: "Der Bauer auf dem Feld",
|
||||
desc: "So fängt alles an: Du bist ein einfacher Bauer und musst dich die Karriereleiter mit deinen freshen Latein-Skills hinaufarbeiten",
|
||||
level: 1,
|
||||
done: true,
|
||||
}, {
|
||||
name: "???",
|
||||
desc: "Warum schreibe ich überhaupt was?dsd dddddddddddddddddddddd",
|
||||
level: 2,
|
||||
done: false,
|
||||
}];
|
||||
|
||||
res(levels);
|
||||
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
getLastReview = (): IReviewMetadata => {
|
||||
console.log("STUB: Application::getLastReview");
|
||||
|
||||
// TODO: Actually fetch this
|
||||
// TODO: Stub
|
||||
return {} as IReviewMetadata;
|
||||
}
|
||||
|
||||
setLastReview = (meta: IReviewMetadata) => {
|
||||
console.log("STUB: Application::setLastReview");
|
||||
|
||||
// TODO: Send this to the server
|
||||
this.setState({
|
||||
lastReview: meta,
|
||||
});
|
||||
}
|
||||
|
||||
getReviewQueue = (): Promise<IVocab[]> => {
|
||||
console.log("STUB: Application::getReviewQueue");
|
||||
|
||||
// TODO: Implement
|
||||
return new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
res([
|
||||
{
|
||||
german: ["Wein"],
|
||||
hint: "Worte auf '-um' sind meistens NeutrUM",
|
||||
type: VocabType.NOMEN,
|
||||
latin: {
|
||||
grundform: "Vinum",
|
||||
genitiv: "Vini",
|
||||
genus: "Neutrum"
|
||||
},
|
||||
id: 0
|
||||
}/* , {
|
||||
* latin: "Vici",
|
||||
* german: "<Wortbedeutung>",
|
||||
* hint: "Wird \"Viki\" und nicht \"Vichi\" ausgesprochen",
|
||||
* mnemonic: "Merk dir das Wort mit Caesars berühmten Worten: \"Veni Vidi Vici\"; Er kam, sah und siegte",
|
||||
* type: VocabType.NOMEN,
|
||||
* id: 2
|
||||
}, {
|
||||
* latin: "fuga",
|
||||
* german: "Flucht",
|
||||
* hint: "Worte auf \"-a\" sind FeminA",
|
||||
* type: VocabType.NOMEN,
|
||||
* id: 3
|
||||
} */
|
||||
]);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
getLearners(): ILearner[] {
|
||||
console.log("STUB: Application::getLearners");
|
||||
|
||||
// TODO: Implement
|
||||
return [{
|
||||
username: "Polynomdivision",
|
||||
level: 5,
|
||||
score: 400,
|
||||
}, {
|
||||
username: "Polynomdivision2",
|
||||
level: 3,
|
||||
score: 500,
|
||||
}, {
|
||||
username: "Der eine Typ",
|
||||
level: 7,
|
||||
score: 100,
|
||||
}];
|
||||
}
|
||||
|
||||
getTopTenLearners(): ILearner[] {
|
||||
console.log("STUB: Application::getTopTenLearners");
|
||||
|
||||
// TODO: Implement
|
||||
return [{
|
||||
username: "Polynomdivision",
|
||||
level: 5,
|
||||
score: 400,
|
||||
}, {
|
||||
username: "Polynomdivision2",
|
||||
level: 3,
|
||||
score: 500,
|
||||
}, {
|
||||
username: "Der eine Typ",
|
||||
level: 7,
|
||||
score: 100,
|
||||
}];
|
||||
}
|
||||
|
||||
getNextLevel(): ILevel {
|
||||
console.log("STUB: Application::getNextLevel");
|
||||
|
||||
// TODO: Actually fetch data
|
||||
return {
|
||||
name: "???",
|
||||
desc: "Warum schreibe ich überhaupt was?dsd dddddddddddddddddddddd",
|
||||
level: 2,
|
||||
done: false,
|
||||
};
|
||||
}
|
||||
|
||||
getLevelVocab(id: number): Promise<IVocab[]> {
|
||||
console.log("STUB: Application::getLevelVocab");
|
||||
|
||||
// TODO: Actually implement this
|
||||
return new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
res([{
|
||||
german: ["Wein"],
|
||||
hint: "Worte auf '-um' sind meistens NeutrUM",
|
||||
type: VocabType.NOMEN,
|
||||
latin: {
|
||||
grundform: "Vinum",
|
||||
genitiv: "Vini",
|
||||
genus: "Neutrum"
|
||||
},
|
||||
id: 0
|
||||
}/* , {
|
||||
* latin: "Vici",
|
||||
* german: "<Wortbedeutung>",
|
||||
* hint: "Wird \"Viki\" und nicht \"Vichi\" ausgesprochen",
|
||||
* mnemonic: "Merk dir das Wort mit Caesars berühmten Worten: \"Veni Vidi Vici\"; Er kam, sah und siegte",
|
||||
* type: VocabType.NOMEN,
|
||||
* id: 2
|
||||
}, {
|
||||
* latin: "fuga",
|
||||
* german: "Flucht",
|
||||
* hint: "Worte auf \"-a\" sind FeminA",
|
||||
* type: VocabType.NOMEN,
|
||||
* id: 3
|
||||
} */]);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
login = (username: string, password: string): Promise<IUser | {}> => {
|
||||
return new Promise((res, rej) => {
|
||||
fetch(`${BACKEND_URL}/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())
|
||||
.then(resp => {
|
||||
if (resp.error === "0") {
|
||||
// Successful login
|
||||
this.props.setUser(resp.data);
|
||||
setSessionToken(window, resp.data.sessionToken);
|
||||
|
||||
res(resp.data);
|
||||
} else {
|
||||
rej({});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logout = () => {
|
||||
// TODO: Tell the server that we're logging ourselves out
|
||||
removeSessionToken(window);
|
||||
this.props.setAuthenticated(false);
|
||||
}
|
||||
|
||||
// Checks whether the user is logged in
|
||||
isAuthenticated = () => {
|
||||
// TODO: Security?
|
||||
// TODO: Implement
|
||||
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
|
||||
nextLevel={this.getNextLevel}
|
||||
getLastReview={this.getLastReview}
|
||||
getTopTen={this.getTopTenLearners} />
|
||||
}} />
|
||||
<AuthRoute
|
||||
isAuth={this.isAuthenticated}
|
||||
path="/welcome"
|
||||
component={() => {
|
||||
return <WelcomePage />
|
||||
}} />
|
||||
<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}
|
||||
drawerButtonState={this.drawerButtonState}
|
||||
setLastReview={this.setLastReview} />;
|
||||
} else {
|
||||
return <Redirect to="/login" />;
|
||||
}
|
||||
}} />
|
||||
<Route
|
||||
path="/review/level/:id"
|
||||
component={({ match }) => {
|
||||
if (this.isAuthenticated()) {
|
||||
return <ReviewPage
|
||||
reviewType={ReviewType.LEVEL}
|
||||
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}
|
||||
drawerButtonState={this.drawerButtonState}
|
||||
setLastReview={this.setLastReview} />;
|
||||
}} />
|
||||
<AuthRoute
|
||||
isAuth={this.isAuthenticated}
|
||||
path="/review/summary"
|
||||
component={() => {
|
||||
return <SummaryPage />
|
||||
}} />
|
||||
</div>
|
||||
</div >
|
||||
</BrowserRouter >;
|
||||
}
|
||||
};
|
||||
7
frontend/src/components/loading.tsx
Normal file
7
frontend/src/components/loading.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
export default class Loader extends React.Component<{}> {
|
||||
render() {
|
||||
return <div className="loader" />;
|
||||
}
|
||||
}
|
||||
73
frontend/src/components/scoreboard.tsx
Normal file
73
frontend/src/components/scoreboard.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
import { ILearner } from "../models/learner";
|
||||
|
||||
interface IProps {
|
||||
topTen: ILearner[];
|
||||
}
|
||||
|
||||
export default class Scoreboard extends React.Component<IProps> {
|
||||
private unique = 0;
|
||||
private nr = 1;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.genId = this.genId.bind(this);
|
||||
this.tableRow = this.tableRow.bind(this);
|
||||
}
|
||||
|
||||
genId() {
|
||||
return "SCOREBOARD" + this.unique++;
|
||||
}
|
||||
|
||||
tableRow(learner: ILearner) {
|
||||
return <TableRow key={this.genId()}>
|
||||
<TableCell>
|
||||
<Typography variant="title" component="b">
|
||||
{this.nr++}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography component="b">{learner.username}</Typography>
|
||||
</TableCell>
|
||||
{/*<TableCell numeric>{learner.level}</TableCell>*/}
|
||||
{/* To make this fit on both mobile and desktop, we don't use
|
||||
numeric, as it would otherwise look weir otherwise look weird */}
|
||||
<TableCell>{learner.score}</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
|
||||
render() {
|
||||
const sortedLearners = this.props.topTen.sort((a, b) => {
|
||||
if (a.score > b.score) {
|
||||
return -1;
|
||||
} else if (a.score < b.score) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return <Table padding="none">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>User</TableCell>
|
||||
{/*<TableCell>Level</TableCell>*/}
|
||||
<TableCell>Punktzahl</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedLearners.map(this.tableRow)}
|
||||
</TableBody>
|
||||
</Table>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user