feat: Implement a review queue
This commit is contained in:
parent
12993570fe
commit
1200e0e3eb
@ -39,12 +39,12 @@ export interface IVocab {
|
|||||||
type: VocabType;
|
type: VocabType;
|
||||||
latin: INomenData | IVerbData | IAdjektivData;
|
latin: INomenData | IVerbData | IAdjektivData;
|
||||||
|
|
||||||
// This number is lesson specific
|
// This number is unique across all vocabulary items
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// What kind of question should be answered
|
// What kind of question should be answered
|
||||||
export enum IReviewQType {
|
export enum ReviewQType {
|
||||||
GERMAN,
|
GERMAN,
|
||||||
|
|
||||||
NOMEN_GENITIV,
|
NOMEN_GENITIV,
|
||||||
@ -63,9 +63,36 @@ export interface IReviewCard {
|
|||||||
// If a question can have multiple answers
|
// If a question can have multiple answers
|
||||||
answers: string[];
|
answers: string[];
|
||||||
|
|
||||||
qtype: IReviewQType;
|
qtype: ReviewQType;
|
||||||
|
|
||||||
|
// Identical to its corresponding IVocab item
|
||||||
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function reviewQTypeToStr(type: ReviewQType): string {
|
||||||
|
switch (type) {
|
||||||
|
case ReviewQType.GERMAN:
|
||||||
|
return "Übersetzung";
|
||||||
|
case ReviewQType.NOMEN_GENITIV:
|
||||||
|
return "Genitiv";
|
||||||
|
case ReviewQType.NOMEN_GENUS:
|
||||||
|
return "Genus";
|
||||||
|
case ReviewQType.ADJ_NOM_A:
|
||||||
|
// TODO
|
||||||
|
return "Nominativ A";
|
||||||
|
case ReviewQType.ADJ_NOM_B:
|
||||||
|
// TODO
|
||||||
|
return "Nominativ B";
|
||||||
|
case ReviewQType.VERB_PRAESENS:
|
||||||
|
return "1. Person Präsens";
|
||||||
|
case ReviewQType.VERB_PERFEKT:
|
||||||
|
return "1. Person Perfekt";
|
||||||
|
case ReviewQType.VERB_PPP:
|
||||||
|
// TODO
|
||||||
|
return "PPP";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Turn a vocabulaty item into a series of questions about the item
|
// Turn a vocabulaty item into a series of questions about the item
|
||||||
export function vocabToReviewCard(vocab: IVocab): IReviewCard[] {
|
export function vocabToReviewCard(vocab: IVocab): IReviewCard[] {
|
||||||
switch (vocab.type) {
|
switch (vocab.type) {
|
||||||
@ -75,24 +102,27 @@ export function vocabToReviewCard(vocab: IVocab): IReviewCard[] {
|
|||||||
// Latin -> German
|
// Latin -> German
|
||||||
question: latin.grundform,
|
question: latin.grundform,
|
||||||
answers: vocab.german,
|
answers: vocab.german,
|
||||||
qtype: IReviewQType.GERMAN,
|
qtype: ReviewQType.GERMAN,
|
||||||
|
id: vocab.id,
|
||||||
}, {
|
}, {
|
||||||
// Latin -> Genitiv
|
// Latin -> Genitiv
|
||||||
question: latin.grundform,
|
question: latin.grundform,
|
||||||
answers: [latin.genitiv],
|
answers: [latin.genitiv],
|
||||||
qtype: IReviewQType.NOMEN_GENITIV,
|
qtype: ReviewQType.NOMEN_GENITIV,
|
||||||
|
id: vocab.id,
|
||||||
}, {
|
}, {
|
||||||
// Latin -> Genus
|
// Latin -> Genus
|
||||||
question: latin.grundform,
|
question: latin.grundform,
|
||||||
answers: [latin.genus],
|
answers: [latin.genus],
|
||||||
qtype: IReviewQType.NOMEN_GENUS,
|
qtype: ReviewQType.NOMEN_GENUS,
|
||||||
|
id: vocab.id,
|
||||||
}];
|
}];
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const typeToPoints = (VocabType type) => {
|
export function typeToPoints(type: VocabType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
// Nomen: 2P + 1 (Wenn richtig)
|
// Nomen: 2P + 1 (Wenn richtig)
|
||||||
//
|
//
|
||||||
|
@ -7,16 +7,17 @@ import Grid from "@material-ui/core/Grid";
|
|||||||
import Button from "@material-ui/core/Button";
|
import Button from "@material-ui/core/Button";
|
||||||
import Typography from "@material-ui/core/Typography";
|
import Typography from "@material-ui/core/Typography";
|
||||||
import Popover from "@material-ui/core/Popover";
|
import Popover from "@material-ui/core/Popover";
|
||||||
import Paper from "@material-ui/core/Paper";
|
|
||||||
|
|
||||||
import { Redirect } from "react-router-dom";
|
import { Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import { IVocab, ReviewMode, VocabType } from "../models/vocab";
|
import { IVocab, IReviewCard, vocabToReviewCard, reviewQTypeToStr } from "../models/vocab";
|
||||||
import { ReviewType } from "../models/review";
|
import { ReviewType } from "../models/review";
|
||||||
|
|
||||||
import { levW } from "../algorithms/levenshtein";
|
import { levW } from "../algorithms/levenshtein";
|
||||||
import { LEVENSHTEIN_MAX_DISTANCE } from "../config";
|
import { LEVENSHTEIN_MAX_DISTANCE } from "../config";
|
||||||
|
|
||||||
|
import { Queue } from "../utils/queue";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
levelId?: number;
|
levelId?: number;
|
||||||
vocabByLevel?: (level: number) => IVocab[];
|
vocabByLevel?: (level: number) => IVocab[];
|
||||||
@ -29,7 +30,7 @@ interface IProps {
|
|||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
input: string;
|
input: string;
|
||||||
current: number;
|
current: IReviewCard;
|
||||||
|
|
||||||
toSummary: boolean;
|
toSummary: boolean;
|
||||||
|
|
||||||
@ -40,23 +41,13 @@ interface IState {
|
|||||||
|
|
||||||
export default class ReviewPage extends React.Component<IProps, IState> {
|
export default class ReviewPage extends React.Component<IProps, IState> {
|
||||||
private vocab: IVocab[] = [];
|
private vocab: IVocab[] = [];
|
||||||
|
private reviewQueue: Queue<IReviewCard> = new Queue();
|
||||||
// Used for positioning the popover
|
// Used for positioning the popover
|
||||||
private buttonRef: HTMLButtonElement;
|
private buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
|
||||||
input: "",
|
|
||||||
current: 0,
|
|
||||||
|
|
||||||
toSummary: false,
|
|
||||||
|
|
||||||
popoverOpen: false,
|
|
||||||
popoverText: "",
|
|
||||||
popoverColor: "red",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hide the drawer button
|
// Hide the drawer button
|
||||||
this.props.drawerButtonState(false);
|
this.props.drawerButtonState(false);
|
||||||
|
|
||||||
@ -80,26 +71,44 @@ export default class ReviewPage extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Turn the vocabulary into IReviewCards and queue them
|
||||||
|
this.vocab.forEach((vocab) => {
|
||||||
|
vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
input: "",
|
||||||
|
current: this.reviewQueue.dequeue(),
|
||||||
|
|
||||||
|
toSummary: false,
|
||||||
|
|
||||||
|
popoverOpen: false,
|
||||||
|
popoverText: "",
|
||||||
|
popoverColor: "red",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVocab = () => {
|
currentVocab = (id: number) => {
|
||||||
return this.vocab[this.state.current];
|
return this.vocab.find((el) => el.id === this.state.current.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkInput = () => {
|
checkInput = () => {
|
||||||
const current = this.currentVocab();
|
|
||||||
|
|
||||||
// Check if the given answer is somewhere in the german words
|
// Check if the given answer is somewhere in the german words
|
||||||
const { input } = this.state;
|
const { input } = this.state;
|
||||||
const german = current.german.map((str) => str.toLowerCase());
|
|
||||||
const dists = german.map((ger) => levW(input.toLowerCase(), ger));
|
// Map all possible answers to lowercase ( => ignore casing)
|
||||||
|
const answers = this.state.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);
|
const minDist = Math.min(...dists);
|
||||||
|
|
||||||
// Check if the user's answer was correct
|
// Check if the user's answer was correct
|
||||||
if (minDist === 0) {
|
if (minDist === 0) {
|
||||||
// TODO: Show it's correct?
|
// TODO: Show it's correct?
|
||||||
// Show the next vocab word
|
// Show the next vocab word
|
||||||
if (this.state.current + 1 >= this.vocab.length) {
|
if (this.reviewQueue.size() === 0) {
|
||||||
// TODO: Set some data that the summary screen will show
|
// TODO: Set some data that the summary screen will show
|
||||||
this.setState({
|
this.setState({
|
||||||
toSummary: true,
|
toSummary: true,
|
||||||
@ -107,7 +116,7 @@ export default class ReviewPage extends React.Component<IProps, IState> {
|
|||||||
} else {
|
} else {
|
||||||
// Increase the vocab
|
// Increase the vocab
|
||||||
this.setState({
|
this.setState({
|
||||||
current: this.state.current + 1,
|
current: this.reviewQueue.dequeue(),
|
||||||
input: "",
|
input: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -128,6 +137,8 @@ export default class ReviewPage extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { question, qtype } = this.state.current;
|
||||||
|
const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`;
|
||||||
return <div>
|
return <div>
|
||||||
{
|
{
|
||||||
this.state.toSummary ? (
|
this.state.toSummary ? (
|
||||||
@ -140,7 +151,7 @@ export default class ReviewPage extends React.Component<IProps, IState> {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container direction="column">
|
<Grid container direction="column">
|
||||||
<Typography variant="display2">
|
<Typography variant="display2">
|
||||||
{this.currentVocab().latin.grundform}
|
{questionTitle}
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
@ -158,11 +169,11 @@ export default class ReviewPage extends React.Component<IProps, IState> {
|
|||||||
open={this.state.popoverOpen}
|
open={this.state.popoverOpen}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: "center",
|
vertical: "center",
|
||||||
horizontal: "left"
|
horizontal: "center"
|
||||||
}}
|
}}
|
||||||
transformOrigin={{
|
transformOrigin={{
|
||||||
vertical: "bottom",
|
vertical: "bottom",
|
||||||
horizontal: "left"
|
horizontal: "center"
|
||||||
}}
|
}}
|
||||||
anchorEl={this.buttonRef}
|
anchorEl={this.buttonRef}
|
||||||
onClose={() => this.setState({
|
onClose={() => this.setState({
|
||||||
@ -175,11 +186,15 @@ export default class ReviewPage extends React.Component<IProps, IState> {
|
|||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Typography variant="button" color="inherit">
|
<Typography
|
||||||
|
variant="button"
|
||||||
|
color="inherit">
|
||||||
{this.state.popoverText}
|
{this.state.popoverText}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Button onClick={this.checkInput} buttonRef={node => this.buttonRef = node}>
|
<Button
|
||||||
|
onClick={this.checkInput}
|
||||||
|
buttonRef={node => this.buttonRef = node}>
|
||||||
Prüfen
|
Prüfen
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
Reference in New Issue
Block a user