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[]>; setLastReview: (meta: IReviewMetadata, sm2: any) => void; reviewType: ReviewType; history: any; dialogOpen: boolean; loading: boolean; vocab: IVocab[]; current: IReviewCard; metadata: IReviewMetadata; 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; 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; // Mapping: Vocab Id -> Correctly answered private sm2_metadata: any = {}; 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.props; return { wrong: metadata.wrong + wrong, correct: metadata.correct + correct, }; } vocabFromId = (id: number): IVocab | {} => { return this.vocab.find((el) => el.id === this.props.current.id); } // When a vocabulary item has been answered, we need to update // the group's SuperMemo2-data. // We update based on the following State-Machine: (C = Correct; W = Wrong) // Answer | Group | New group // | | state // -------+-------+---------- // C | C | C // C | W | W // W | C | W // W | W | W // (1)C | - | C // (2)W | - | W // @correct: Was the answer given correct? updateGroupSM2 = (correct: boolean) => { const { id } = this.props.current; switch (correct) { case true: // Case (1) if (!(id in this.sm2_metadata)) { this.sm2_metadata[id] = true; break; } switch (this.sm2_metadata[id]) { case true: this.sm2_metadata[id] = true; break; case false: this.sm2_metadata[id] = false; break; } break; case false: // We don't need to explicitly catch case (2), as we set // anything, that was incorrectly answered to false. this.sm2_metadata[id] = false; break; } } 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); // Check if the user's answer was correct if (minDist === 0) { this.updateGroupSM2(true); // TODO: Show it's correct? // Show the next vocab word if (this.reviewQueue.size() === 0) { // Update the metadata this.props.setReview(this.props.current, this.increaseMeta(1, 0)); this.props.setLastReview(this.props.metadata, this.sm2_metadata); this.props.setLoading(true); // Show the drawer button again this.props.drawerButtonState(true); // Go to the summary screen this.props.history.push("/review/summary"); } else { // Increase the vocab this.props.setReview(this.reviewQueue.dequeue(), this.increaseMeta(1, 0)); this.inputRef.value = ""; // TODO(?): Show a snackbar for showing the updated score } } 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"); * } */ // Update the metadata this.updateGroupSM2(false); this.props.setReview(this.props.current, this.increaseMeta(0, 1)); this.props.setPopover(true, "Das war nicht richtig", "red", "white"); } } 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;