refactor: MONOREPO
This commit is contained in:
74
frontend/src/pages/dashboard.tsx
Normal file
74
frontend/src/pages/dashboard.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
|
||||
import Scoreboard from "../components/scoreboard";
|
||||
import SummaryTable from "../components/SummaryTable";
|
||||
|
||||
import { ILevel } from "../models/level";
|
||||
import { ILearner } from "../models/learner";
|
||||
import { IReviewMetadata } from "../models/review";
|
||||
|
||||
interface IProps {
|
||||
nextLevel: () => ILevel;
|
||||
getLastReview: () => IReviewMetadata;
|
||||
getTopTen: () => ILearner[];
|
||||
}
|
||||
|
||||
export default class Dashboard extends React.Component<IProps> {
|
||||
render() {
|
||||
const small = window.matchMedia("(max-width: 700px)").matches;
|
||||
const direction = small ? "column" : "row";
|
||||
|
||||
const level = this.props.nextLevel();
|
||||
|
||||
return <div>
|
||||
<Grid container direction={direction} spacing={16}>
|
||||
<Grid item lg={4}>
|
||||
<Paper className="paper">
|
||||
<Typography variant="title">{`Level ${level.level}`}</Typography>
|
||||
<Typography variant="title" component="p">{level.name}</Typography>
|
||||
<br />
|
||||
<Typography component="p">
|
||||
{level.desc}
|
||||
</Typography>
|
||||
<Button
|
||||
component={Link}
|
||||
to={`/level/${level.level}`}
|
||||
className="lesson-card-btn">
|
||||
Zum Level
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item lg={4}>
|
||||
<Paper className="paper">
|
||||
<Typography variant="title" component="p">
|
||||
Rangliste: Top 10
|
||||
</Typography>
|
||||
|
||||
<Scoreboard topTen={this.props.getTopTen()} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item lg={4}>
|
||||
<Paper className="paper">
|
||||
<Typography variant="title">
|
||||
Letzte Wiederholung
|
||||
</Typography>
|
||||
<SummaryTable reviewMeta={this.props.getLastReview} />
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to="/review/queue">
|
||||
Vokabeln üben
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
83
frontend/src/pages/intro.tsx
Normal file
83
frontend/src/pages/intro.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Card from "@material-ui/core/Card";
|
||||
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<{}> {
|
||||
render() {
|
||||
const small = window.matchMedia("(max-width: 700px)").matches;
|
||||
const cName = small ? "intro-card-xs" : "intro-card-lg";
|
||||
return <div>
|
||||
<Grid container justify="center">
|
||||
<Card className={cName}>
|
||||
<CardContent>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="title">
|
||||
Wilkommen bei Lateinicus!
|
||||
</Typography>
|
||||
|
||||
<div className="vert-spacer" />
|
||||
<Typography
|
||||
className="intro-subheading"
|
||||
component="p"
|
||||
variant="subheading">
|
||||
Was ist Lateinicus?
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
component="p">
|
||||
Lateinicus ist eine experimentelle Lernanwendung für Lateinvokabeln. Mit Hilfe dieser
|
||||
Anwendung wollen wir die Frage beantworten, ob "Gamification" tatsächlich effektiver
|
||||
ist als klassisches Lernen mit Vokabelheft.
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
component="p">
|
||||
Um die Effektivität zu bewerten wird vor und nach Verwendung von Lateinicus
|
||||
ein Vokabeltest geschrieben, welche von uns zum Zwecke der Datenerhebung benutzt
|
||||
werden.
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
component="p">
|
||||
Zudem erfassen wir Daten darüber, wann und wie lange gelernt wird, damit wir diese
|
||||
Werte auch in unser Fazit mit einfließen lassen können.
|
||||
</Typography>
|
||||
|
||||
<div className="vert-spacer" />
|
||||
<Typography
|
||||
className="intro-subheading"
|
||||
component="p"
|
||||
variant="subheading">
|
||||
Was ist Gamification?
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body1">
|
||||
Gamification ist ein Konzept, bei welchem eine Tätigkeit, wie hier das Lernen von
|
||||
Lateinvokabeln, in ein Spiel umgewandelt wird.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
{/*TODO: Tell the server to not show this page again*/}
|
||||
<Button
|
||||
fullWidth={true}
|
||||
component={Link}
|
||||
to="/dashboard">
|
||||
Lass uns loslegen
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>;
|
||||
}
|
||||
};
|
||||
187
frontend/src/pages/level.tsx
Normal file
187
frontend/src/pages/level.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import * as React from "react";
|
||||
|
||||
import List from "@material-ui/core/List";
|
||||
import ListItem from "@material-ui/core/ListItem";
|
||||
import ListItemText from "@material-ui/core/ListItemText";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
import { IVocab } from "../models/vocab";
|
||||
|
||||
interface IProps {
|
||||
id: string;
|
||||
levelVocab: (id: string) => Promise<IVocab[]>;
|
||||
|
||||
history: any;
|
||||
|
||||
loading: boolean;
|
||||
setLoading: (state: boolean) => void;
|
||||
vocab: IVocab[];
|
||||
setVocab: (vocab: IVocab[]) => void;
|
||||
setLookedAt: (ids: number[]) => void;
|
||||
setCurrentVocab: (vocab: IVocab) => void;
|
||||
drawerButtonState: (state: boolean) => void;
|
||||
currentVocab: IVocab;
|
||||
lookedAt: number[];
|
||||
};
|
||||
|
||||
const LevelPageWithRouter = withRouter(
|
||||
class LevelPage extends React.Component<IProps> {
|
||||
private uid = 0;
|
||||
// To prevent React from redrawing the vocabulary list and prematurely
|
||||
// cancelling the animation
|
||||
private uids: { [key: string]: string } = {};
|
||||
|
||||
componentDidMount() {
|
||||
// Hide the drawer
|
||||
this.props.drawerButtonState(false);
|
||||
|
||||
// Fetch the vocabulary
|
||||
this.props.setLoading(true);
|
||||
|
||||
// TODO: Error handling
|
||||
this.props.levelVocab(this.props.id).then(vocab => {
|
||||
this.props.setVocab(vocab);
|
||||
this.props.setCurrentVocab(vocab[0]);
|
||||
this.props.setLoading(false);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
genUID = (vocab: IVocab): string => {
|
||||
const { grundform } = vocab.latin;
|
||||
if (grundform in this.uids) {
|
||||
return this.uids[grundform];
|
||||
} else {
|
||||
this.uids[grundform] = "LEVELPAGE" + this.uid++;
|
||||
return this.uids[grundform];
|
||||
}
|
||||
}
|
||||
|
||||
renderVocabListItem = (vocab: IVocab): any => {
|
||||
// Check if the vocab was already looked at
|
||||
const lookedAt = this.props.lookedAt.find((el) => el === vocab.id) || vocab.id === 0;
|
||||
|
||||
return <ListItem button key={this.genUID(vocab)} onClick={() => {
|
||||
// Prevent the user from using too much memory by always clicking on the elements
|
||||
// Show the clicked at vocab word
|
||||
|
||||
this.props.setCurrentVocab(vocab);
|
||||
this.props.setLookedAt(lookedAt ? (
|
||||
this.props.lookedAt
|
||||
) : this.props.lookedAt.concat(vocab.id));
|
||||
}}>
|
||||
<ListItemText>
|
||||
{`${vocab.latin.grundform} ${lookedAt ? "✔" : ""}`}
|
||||
</ListItemText>
|
||||
</ListItem>;
|
||||
}
|
||||
|
||||
toReview = () => {
|
||||
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.setLoading(true);
|
||||
this.props.history.push(`/review/level/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
return <div>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}>
|
||||
<Grid item xs={12}>
|
||||
<Paper className="paper">
|
||||
<Grid container direction="column" spacing={8}>
|
||||
<CircularProgress />
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
const { currentVocab } = this.props;
|
||||
return <div>
|
||||
<Grid container direction="row">
|
||||
<Grid item xs={3}>
|
||||
<List>
|
||||
{this.props.vocab
|
||||
.map(this.renderVocabListItem)}
|
||||
<ListItem button onClick={this.toReview}>
|
||||
<ListItemText>
|
||||
Zur Übung
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid item lg={7} xs={9}>
|
||||
<Grid container direction="column">
|
||||
<Grid item style={{ margin: 12 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="headline" component="h2">
|
||||
{currentVocab.latin.grundform}
|
||||
</Typography>
|
||||
<Typography gutterBottom variant="headline" component="h3">
|
||||
{currentVocab.german.join(", ")}
|
||||
</Typography>
|
||||
{
|
||||
currentVocab.hint ? (
|
||||
<div style={{
|
||||
border: "dashed",
|
||||
borderColor: "red",
|
||||
padding: 12,
|
||||
}}>
|
||||
<Typography variant="subheading" component="p">
|
||||
<b>Tipp:</b>
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{currentVocab.hint}
|
||||
</Typography>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
{
|
||||
currentVocab.mnemonic ? (
|
||||
<div style={{
|
||||
border: "dashed",
|
||||
borderColor: "#f1c40f",
|
||||
marginTop: 12,
|
||||
padding: 12,
|
||||
}}>
|
||||
<Typography variant="subheading" component="p">
|
||||
<b>Eselsbrücke:</b>
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{currentVocab.mnemonic}
|
||||
</Typography>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
</CardContent>
|
||||
{/*TODO: Maybe "next" and "prev" buttons?*/}
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
);
|
||||
export default LevelPageWithRouter;
|
||||
91
frontend/src/pages/levelList.tsx
Normal file
91
frontend/src/pages/levelList.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { ILevel } from "../models/level";
|
||||
|
||||
interface IProps {
|
||||
getLevels: () => Promise<ILevel[]>;
|
||||
|
||||
setLevels: (levels: ILevel[]) => void;
|
||||
setLoading: (state: boolean) => void;
|
||||
loading: boolean;
|
||||
levels: ILevel[];
|
||||
}
|
||||
|
||||
export default class Dashboard extends React.Component<IProps> {
|
||||
componentDidMount() {
|
||||
this.props.setLoading(true);
|
||||
|
||||
// Fetch the levels
|
||||
this.props.getLevels().then(res => {
|
||||
this.props.setLevels(res);
|
||||
this.props.setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
return <div>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}>
|
||||
<Grid item xs={12}>
|
||||
<Paper className="paper">
|
||||
<Grid container direction="column" spacing={8}>
|
||||
<CircularProgress />
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
const small = window.matchMedia("(max-width: 700px)").matches;
|
||||
const cName = small ? "lesson-card-xs" : "lesson-card-lg";
|
||||
|
||||
let key = 0;
|
||||
const levelToCard = (level: ILevel) => {
|
||||
return <Grid item key={key++}>
|
||||
<Card style={{
|
||||
width: small ? window.width - 32 : "300px"
|
||||
}}>
|
||||
<CardContent className={cName}>
|
||||
<Typography variant="title">{`Level ${level.level}`}</Typography>
|
||||
<Typography variant="title" component="p">{level.name}</Typography>
|
||||
<br />
|
||||
<Typography component="p">
|
||||
{level.desc}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
<Button
|
||||
component={Link}
|
||||
to={`/level/${level.level}`}
|
||||
className="lesson-card-btn">
|
||||
Zum Level
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>;
|
||||
};
|
||||
|
||||
return <Grid container spacing={16} direction="row">
|
||||
{this.props.levels.map(levelToCard)}
|
||||
</Grid>
|
||||
}
|
||||
};
|
||||
109
frontend/src/pages/login.tsx
Normal file
109
frontend/src/pages/login.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import LinearProgress from "@material-ui/core/LinearProgress";
|
||||
import Snackbar from "@material-ui/core/Snackbar";
|
||||
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
import { IUser } from "../models/user";
|
||||
|
||||
interface IProps {
|
||||
login: (username: string, password: string) => Promise<IUser | {}>;
|
||||
authenticated: boolean;
|
||||
history: any;
|
||||
|
||||
setLoading: (state: boolean) => void;
|
||||
setSnackbar: (state: boolean, msg: string) => void;
|
||||
loading: boolean;
|
||||
snackOpen: boolean;
|
||||
snackMsg: string;
|
||||
}
|
||||
|
||||
const LoginPageWithRouter = withRouter(
|
||||
class LoginPage extends React.Component<IProps> {
|
||||
private usernameRef: any = undefined;
|
||||
private passwordRef: any = undefined;
|
||||
|
||||
performLogin = () => {
|
||||
this.props.setLoading(true);
|
||||
|
||||
const username = this.usernameRef.value || "";
|
||||
const password = this.passwordRef.value || "";
|
||||
this.props.login(username, password).then((res: IUser) => {
|
||||
if (res.showWelcome) {
|
||||
// If the user logs in for the first time, a welcome
|
||||
// screen should be shown
|
||||
this.props.history.push("/welcome");
|
||||
} else {
|
||||
this.props.history.push("/dashboard");
|
||||
}
|
||||
}, (err) => {
|
||||
this.props.setLoading(false);
|
||||
this.props.setSnackbar(true, "Failed to log in");
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// If we're already authenticated, we can skip the login page
|
||||
if (this.props.authenticated) {
|
||||
this.props.history.push("/dashboard");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}>
|
||||
<Grid item xs={12}>
|
||||
<Paper className="paper">
|
||||
<Typography variant="title">Login</Typography>
|
||||
<Grid container direction="column" spacing={8}>
|
||||
<Grid item>
|
||||
<TextField
|
||||
label="Username"
|
||||
inputRef={node => this.usernameRef = node} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
label="Passwort"
|
||||
type="password"
|
||||
inputRef={node => this.passwordRef = node} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="login-btn"
|
||||
onClick={() => this.performLogin()}>
|
||||
Login
|
||||
</Button>
|
||||
{
|
||||
this.props.loading ? (
|
||||
<LinearProgress />
|
||||
) : undefined
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Snackbar
|
||||
open={this.props.snackOpen}
|
||||
onClose={() => this.props.setSnackbar(false, "")}
|
||||
message={this.props.snackMsg}
|
||||
autoHideDuration={6000} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
);
|
||||
export default LoginPageWithRouter;
|
||||
243
frontend/src/pages/review.tsx
Normal file
243
frontend/src/pages/review.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Popover from "@material-ui/core/Popover";
|
||||
import LinearProgress from "@material-ui/core/LinearProgress";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
import { IVocab, IReviewCard, vocabToReviewCard, reviewQTypeToStr } from "../models/vocab";
|
||||
import { ReviewType, IReviewMetadata } from "../models/review";
|
||||
|
||||
import { levW } from "../algorithms/levenshtein";
|
||||
import { LEVENSHTEIN_MAX_DISTANCE } from "../config";
|
||||
|
||||
import { Queue } from "../utils/queue";
|
||||
|
||||
interface IProps {
|
||||
levelId?: number;
|
||||
vocabByLevel?: (level: number) => Promise<IVocab[]>;
|
||||
vocabByQueue?: () => Promise<IVocab[]>;
|
||||
reviewType: ReviewType;
|
||||
|
||||
history: any;
|
||||
|
||||
loading: boolean;
|
||||
vocab: IVocab[];
|
||||
current: IReviewCard;
|
||||
popoverOpen: boolean;
|
||||
popoverText: string;
|
||||
popoverColor: string;
|
||||
popoverTextColor: string;
|
||||
|
||||
setSummary: (state: boolean) => void;
|
||||
setPopover: (state: boolean, text: string, color: string, textColor: string) => void;
|
||||
drawerButtonState: (state: boolean) => void;
|
||||
setLastReview: (meta: IReviewMetadata) => void;
|
||||
setReview: (curent: IReviewCard, meta: IReviewMetadata) => void;
|
||||
setLoading: (state: boolean) => void;
|
||||
}
|
||||
|
||||
const ReviewPageWithRouter = withRouter(
|
||||
class ReviewPage extends React.Component<IProps> {
|
||||
private vocab: IVocab[] = [];
|
||||
private reviewQueue: Queue<IReviewCard> = new Queue();
|
||||
// Used for positioning the popover
|
||||
private buttonRef: HTMLButtonElement;
|
||||
private inputRef: HTMLInputElement;
|
||||
private metadata: IReviewMetadata = { correct: 0, wrong: 0 };
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
// Hide the drawer button
|
||||
this.props.drawerButtonState(false);
|
||||
|
||||
const vocToQueue = () => {
|
||||
this.vocab.forEach((vocab) => {
|
||||
vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue);
|
||||
});
|
||||
this.props.setReview(this.reviewQueue.dequeue(), {
|
||||
correct: 0,
|
||||
wrong: 0,
|
||||
});
|
||||
this.props.setLoading(false);
|
||||
};
|
||||
|
||||
// Get the correct vocabulary
|
||||
const { reviewType, vocabByLevel, levelId, vocabByQueue } = this.props;
|
||||
|
||||
// Just to make TSC shut up
|
||||
const noopPromise = () => {
|
||||
return new Promise<IVocab[]>((res, rej) => {
|
||||
rej([]);
|
||||
});
|
||||
};
|
||||
const vocabByLevelW = vocabByLevel || noopPromise;
|
||||
const vocabByQueueW = vocabByQueue || noopPromise;
|
||||
const getVocab = {
|
||||
[ReviewType.LEVEL]: () => vocabByLevelW(levelId),
|
||||
[ReviewType.QUEUE]: () => vocabByQueueW(),
|
||||
}[reviewType];
|
||||
|
||||
getVocab().then((res: IVocab[]) => {
|
||||
this.vocab = res;
|
||||
vocToQueue();
|
||||
});
|
||||
}
|
||||
|
||||
increaseMeta = (correct: number, wrong: number): IReviewMetadata => {
|
||||
const { metadata } = this;
|
||||
|
||||
return {
|
||||
wrong: metadata.wrong + wrong,
|
||||
correct: metadata.correct + correct,
|
||||
};
|
||||
}
|
||||
|
||||
vocabFromId = (id: number): IVocab | {} => {
|
||||
return this.vocab.find((el) => el.id === this.props.current.id);
|
||||
}
|
||||
|
||||
checkInput = () => {
|
||||
// Check if the given answer is somewhere in the german words
|
||||
const input = this.inputRef.value || "";
|
||||
|
||||
// Map all possible answers to lowercase ( => ignore casing)
|
||||
const answers = this.props.current.answers.map((el) => el.toLowerCase());
|
||||
// Calculate the distances to all possible answers
|
||||
const dists = answers.map((el) => levW(input.toLowerCase(), el));
|
||||
// Find the lowest distance
|
||||
const minDist = Math.min(...dists);
|
||||
|
||||
console.log("Review Queue size:", this.reviewQueue.size());
|
||||
|
||||
// Check if the user's answer was correct
|
||||
if (minDist === 0) {
|
||||
// TODO: Show it's correct?
|
||||
// Show the next vocab word
|
||||
if (this.reviewQueue.size() === 0) {
|
||||
// Go to the summary screen
|
||||
this.props.setLastReview(this.metadata);
|
||||
this.props.setLoading(true);
|
||||
this.props.history.push("/review/summary");
|
||||
} else {
|
||||
// Increase the vocab
|
||||
this.props.setReview(this.reviewQueue.dequeue(), this.increaseMeta(1, 0));
|
||||
this.inputRef.value = "";
|
||||
}
|
||||
} else if (minDist <= LEVENSHTEIN_MAX_DISTANCE) {
|
||||
this.props.setPopover(true, "Das war fast richtig", "yellow", "black");
|
||||
} else {
|
||||
// Find the IVocab item
|
||||
const vocab = this.vocabFromId(this.props.current.id);
|
||||
if (vocab) {
|
||||
// Re-Add the vocabulary item to the review queue
|
||||
// TODO: Only re-add when it when it's not re-queued
|
||||
// vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue);
|
||||
} else {
|
||||
console.log("[ReviewPage::checkInput] Could not find IVocab item for wrong IReviewCard");
|
||||
}
|
||||
|
||||
this.props.setPopover(true, "Das war nicht richtig", "red", "white");
|
||||
}
|
||||
|
||||
// TODO(?): Show a snackbar for showing the updated score
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
return <div>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}>
|
||||
<Grid item xs={12}>
|
||||
<Paper className="paper">
|
||||
<Grid container direction="column" spacing={8}>
|
||||
<CircularProgress />
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
const { question, qtype } = this.props.current;
|
||||
const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`;
|
||||
// TODO:
|
||||
const progress = 50;
|
||||
|
||||
return <div>
|
||||
<Grid container justify="center">
|
||||
<Grid item style={{ width: "100%" }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container direction="column">
|
||||
<Typography variant="display2">
|
||||
{questionTitle}
|
||||
</Typography>
|
||||
<TextField
|
||||
margin="normal"
|
||||
fullWidth={true}
|
||||
inputRef={node => this.inputRef = node}
|
||||
onKeyPress={(ev) => {
|
||||
// Allow checking of the answer by pressing Enter
|
||||
if (ev.key === "Enter")
|
||||
this.checkInput();
|
||||
}} />
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress} />
|
||||
<Popover
|
||||
open={this.props.popoverOpen}
|
||||
anchorOrigin={{
|
||||
vertical: "center",
|
||||
horizontal: "center"
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center"
|
||||
}}
|
||||
anchorEl={this.buttonRef}
|
||||
onClose={() => this.props.setPopover(false, "", "", "")}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: this.props.popoverColor,
|
||||
padding: 10,
|
||||
color: this.props.popoverTextColor,
|
||||
}
|
||||
}}>
|
||||
<Typography
|
||||
variant="button"
|
||||
color="inherit">
|
||||
{this.props.popoverText}
|
||||
</Typography>
|
||||
</Popover>
|
||||
<Button
|
||||
onClick={this.checkInput}
|
||||
buttonRef={node => this.buttonRef = node}>
|
||||
Prüfen
|
||||
</Button>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
);
|
||||
export default ReviewPageWithRouter;
|
||||
56
frontend/src/pages/summary.tsx
Normal file
56
frontend/src/pages/summary.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import SummaryTable from "../components/SummaryTable";
|
||||
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
import { IReviewMetadata } from "../models/review";
|
||||
|
||||
interface IProps {
|
||||
history: any;
|
||||
|
||||
reviewMeta: IReviewMetadata;
|
||||
setDrawerButton: (state: boolean) => void;
|
||||
}
|
||||
|
||||
// TODO: This stays at the default value
|
||||
const SummaryPageWithRouter = withRouter(
|
||||
class SummaryPage extends React.Component<IProps> {
|
||||
toDashboard = () => {
|
||||
// Show the drawer button
|
||||
this.props.setDrawerButton(true);
|
||||
|
||||
// Go to the dashboard
|
||||
this.props.history.push("/dashboard");
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}>
|
||||
<Grid item xs={12}>
|
||||
<Paper className="paper">
|
||||
<Typography variant="title">Zusammenfassung</Typography>
|
||||
<Grid container direction="column">
|
||||
<SummaryTable reviewMeta={() => this.props.reviewMeta} />
|
||||
<Button onClick={this.toDashboard}>
|
||||
Zum Dashboard
|
||||
</Button>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
);
|
||||
export default SummaryPageWithRouter;
|
||||
Reference in New Issue
Block a user