This repository has been archived on 2022-03-12. You can view files and clone it, but cannot push or open issues or pull requests.
Lateinicus/frontend/src/pages/review.tsx
2018-09-24 16:16:33 +02:00

308 lines
12 KiB
TypeScript

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 Tooltip from "@material-ui/core/Tooltip";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import CloseIcon from "@material-ui/icons/Close";
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;
dialogOpen: boolean;
loading: boolean;
vocab: IVocab[];
current: IReviewCard;
popoverOpen: boolean;
popoverText: string;
popoverColor: string;
popoverTextColor: string;
setReviewDialog: (state: boolean) => void;
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();
});
}
openDialog = () => {
this.props.setReviewDialog(true);
}
closeDialog = () => {
this.props.setReviewDialog(false);
}
cancelReview = () => {
this.closeDialog();
// Show the drawer button again
this.props.drawerButtonState(true);
this.props.history.push("/dashboard");
}
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);
// Show the drawer button again
this.props.drawerButtonState(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">
<center>
<Typography variant="display2">
{questionTitle}
</Typography>
</center>
<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>
<Tooltip
title="Abbrechen"
placement="top">
<Button
variant="fab"
color="primary"
className="review-fab"
onClick={this.openDialog}>
<CloseIcon />
</Button>
</Tooltip>
<Dialog
open={this.props.dialogOpen}
onClose={this.closeDialog}>
<DialogTitle>Willst du die Wiederholung abbrechen?</DialogTitle>
<DialogContent>
<DialogContentText>
Wenn du jetzt abbricht, dann geht dein in dieser Wiederholung gesammelte Fortschritt
verloren.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={this.closeDialog}>
Zurück zur Wiederholung
</Button>
<Button
onClick={this.cancelReview}>
Abbrechen
</Button>
</DialogActions>
</Dialog>
</div>;
}
}
);
export default ReviewPageWithRouter;