feat: Partially transition Review to Redux

This commit is contained in:
Alexander Polynomdivision 2018-09-19 16:39:02 +02:00
parent d94fa63ac7
commit 882ca5a9e3
6 changed files with 272 additions and 84 deletions

View File

@ -1,5 +1,7 @@
import { IVocab } from "../models/vocab"; import { IVocab } from "../models/vocab";
import { IUser } from "../models/user"; import { IUser } from "../models/user";
import { ILevel } from "../models/level";
import { IReviewMetadata, IReviewCard } from "../models/review";
export const SET_DRAWER = "SET_DRAWER"; export const SET_DRAWER = "SET_DRAWER";
export function setDrawer(state: boolean) { export function setDrawer(state: boolean) {
@ -89,3 +91,54 @@ export function setLevelLoading(state: boolean) {
state, state,
}; };
}; };
export const SET_LEVELS = "SET_LEVELS";
export function setLevels(levels: ILevel[]) {
return {
type: SET_LEVELS,
levels,
};
};
export const REVIEW_SET_POPOVER = "REVIEW_SET_POPOVER";
export function setReviewPopover(state: boolean, text: string, color: string) {
return {
type: REVIEW_SET_POPOVER,
state,
text,
color,
};
};
export const REVIEW_SET_SUMMARY = "REVIEW_SET_SUMMARY";
export function setReviewSummary(state: boolean) {
return {
type: REVIEW_SET_SUMMARY,
state,
};
};
export const REVIEW_SET_LOADING = "REVIEW_SET_LOADING";
export function setReviewLoading(state: boolean) {
return {
type: REVIEW_SET_LOADING,
state,
};
};
export const SET_LAST_REVIEW = "SET_LAST_REVIEW";
export function setLastReview(metadata: IReviewMetadata) {
return {
type: SET_LAST_REVIEW,
metadata,
};
};
export const SET_REVIEW = "SET_REVIEW";
export function setReview(current: IReviewCard, meta: IReviewMetadata) {
return {
type: SET_REVIEW,
current,
meta,
};
};

View File

@ -6,9 +6,9 @@ import AuthRoute from "../security/AuthRoute";
import Dashboard from "../pages/dashboard"; import Dashboard from "../pages/dashboard";
import LoginPage from "../containers/LoginPage"; import LoginPage from "../containers/LoginPage";
import LevelListPage from "../pages/levelList"; import LevelListPage from "../containers/LevelList";
import LevelPage from "../containers/LevelPage"; import LevelPage from "../containers/LevelPage";
import ReviewPage from "../pages/review"; import ReviewPage from "../containers/Review";
import SummaryPage from "../pages/summary"; import SummaryPage from "../pages/summary";
import WelcomePage from "../pages/intro"; import WelcomePage from "../pages/intro";
@ -258,8 +258,7 @@ export default class Application extends React.Component<IProps> {
<AuthRoute <AuthRoute
isAuth={this.isAuthenticated} isAuth={this.isAuthenticated}
path="/levelList" path="/levelList"
component={() => <LevelListPage component={() => <LevelListPage />} />
levels={this.getLevels()} />} />
{/*We cannot use AuthRoute here, because match is undefined otherwise*/} {/*We cannot use AuthRoute here, because match is undefined otherwise*/}
<Route <Route
path="/level/:id" path="/level/:id"
@ -282,7 +281,6 @@ export default class Application extends React.Component<IProps> {
reviewType={ReviewType.LEVEL} reviewType={ReviewType.LEVEL}
levelId={match.params.id} levelId={match.params.id}
vocabByLevel={this.getLevelVocab} vocabByLevel={this.getLevelVocab}
drawerButtonState={this.drawerButtonState}
setLastReview={this.setLastReview} />; setLastReview={this.setLastReview} />;
} else { } else {
return <Redirect to="/login" />; return <Redirect to="/login" />;

View File

@ -0,0 +1,14 @@
import { connect } from "react-redux";
import LevelListPage from "../pages/levelList";
const mapStateToProps = state => {
return {
levels: state.levels,
};
};
const mapDispatchToProps = dispatch => { return {} };
const LevelListContainer = connect(mapStateToProps,
mapDispatchToProps)(LevelListPage);
export default LevelListContainer;

35
src/containers/Review.ts Normal file
View File

@ -0,0 +1,35 @@
import { connect } from "react-redux";
import {
setDrawerButton, setReviewPopover, setReviewSummary, setLastReview,
setReview, setReviewLoading
} from "../actions";
import { IReviewMetadata } from "../models/review";
import { IVocab } from "../models/vocab";
import ReviewPage from "../pages/review";
const mapStateToProps = state => {
return {
metadata: state.review.metadata,
vocab: state.review.vocab,
current: state.review.current,
popoverOpen: state.review.popoverOpen,
popoverText: state.review.popoverText,
popoverColor: state.review.popoverColor,
loading: state.review.loading,
};
};
const mapDispatchToProps = dispatch => {
return {
drawerButtonState: (state: boolean) => dispatch(setDrawerButton(state)),
setPopover: (state: boolean, text: string, color: string) => dispatch(setReviewPopover(state, text, color)),
setSummary: (state: boolean) => dispatch(setReviewSummary(state)),
setReview: (current: IVocab, meta: IReviewMetadata) => dispatch(setReview(current, meta)),
setLoading: (state: boolean) => dispatch(setReviewLoading(state)),
};
};
const ReviewContainer = connect(mapStateToProps,
mapDispatchToProps)(ReviewPage);
export default ReviewContainer;

View File

@ -8,6 +8,8 @@ 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 LinearProgress from "@material-ui/core/LinearProgress"; import LinearProgress from "@material-ui/core/LinearProgress";
import CircularProgress from "@material-ui/core/CircularProgress";
import Paper from "@material-ui/core/Paper";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
@ -21,34 +23,34 @@ import { Queue } from "../utils/queue";
interface IProps { interface IProps {
levelId?: number; levelId?: number;
vocabByLevel?: (level: number) => IVocab[]; vocabByLevel?: (level: number) => Promise<IVocab[]>;
vocabByQueue?: () => IVocab[]; vocabByQueue?: () => Promise<IVocab[]>;
setLastReview: (meta: IReviewMetadata) => void;
reviewType: ReviewType; reviewType: ReviewType;
drawerButtonState: (state: boolean) => void; loading: boolean;
} vocab: IVocab[];
interface IState {
input: string;
current: IReviewCard; current: IReviewCard;
metadata: IReviewMetadata;
toSummary: boolean; toSummary: boolean;
popoverOpen: boolean; popoverOpen: boolean;
popoverText: string; popoverText: string;
popoverColor: string; popoverColor: string;
setSummary: (state: boolean) => void;
setPopover: (state: boolean, text: string, color: string) => void;
drawerButtonState: (state: boolean) => void;
setLastReview: (meta: IReviewMetadata) => void;
setReview: (curent: IReviewCard, meta: IReviewMetadata) => void;
setLoading: (state: boolean) => void;
} }
export default class ReviewPage extends React.Component<IProps, IState> { export default class ReviewPage extends React.Component<IProps> {
private vocab: IVocab[] = []; private vocab: IVocab[] = [];
private reviewQueue: Queue<IReviewCard> = undefined; private reviewQueue: Queue<IReviewCard> = new Queue();
// Used for positioning the popover // Used for positioning the popover
private buttonRef: HTMLButtonElement; private buttonRef: HTMLButtonElement;
private inputRef: HTMLInputElement;
private metadata: IReviewMetadata = { correct: 0, wrong: 0 };
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -56,6 +58,17 @@ export default class ReviewPage extends React.Component<IProps, IState> {
// Hide the drawer button // Hide the drawer button
this.props.drawerButtonState(false); 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 // Get the correct vocabulary
const { reviewType, vocabByLevel, levelId, vocabByQueue } = this.props; const { reviewType, vocabByLevel, levelId, vocabByQueue } = this.props;
switch (reviewType) { switch (reviewType) {
@ -63,7 +76,10 @@ export default class ReviewPage extends React.Component<IProps, IState> {
if (!vocabByLevel || !levelId) { if (!vocabByLevel || !levelId) {
alert("[ReviewPage::constructor] vocabByLevel or levelId undefined"); alert("[ReviewPage::constructor] vocabByLevel or levelId undefined");
} else { } else {
this.vocab = vocabByLevel(levelId); vocabByLevel(levelId).then(res => {
this.vocab = res;
vocToQueue();
});
} }
break; break;
@ -71,38 +87,18 @@ export default class ReviewPage extends React.Component<IProps, IState> {
if (!vocabByQueue) { if (!vocabByQueue) {
alert("[ReviewPage::constructor] vocabByQueue undefined"); alert("[ReviewPage::constructor] vocabByQueue undefined");
} else { } else {
this.vocab = vocabByQueue(); vocabByQueue().then(res => {
this.vocab = res;
vocToQueue();
});
} }
break; break;
} }
// Turn the vocabulary into IReviewCards and queue them
if (!this.reviewQueue) {
this.reviewQueue = new Queue();
this.vocab.forEach((vocab) => {
vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue);
});
}
this.state = {
input: "",
current: this.reviewQueue.dequeue(),
metadata: {
correct: 0,
wrong: 0,
},
toSummary: false,
popoverOpen: false,
popoverText: "",
popoverColor: "red",
};
} }
increaseMeta = (correct: number, wrong: number): IReviewMetadata => { increaseMeta = (correct: number, wrong: number): IReviewMetadata => {
const { metadata } = this.state; const { metadata } = this;
return { return {
wrong: metadata.wrong + wrong, wrong: metadata.wrong + wrong,
@ -111,21 +107,21 @@ export default class ReviewPage extends React.Component<IProps, IState> {
} }
vocabFromId = (id: number) => { vocabFromId = (id: number) => {
return this.vocab.find((el) => el.id === this.state.current.id); return this.vocab.find((el) => el.id === this.props.current.id);
} }
checkInput = () => { checkInput = () => {
// 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.inputRef.value || "";
// Map all possible answers to lowercase ( => ignore casing) // Map all possible answers to lowercase ( => ignore casing)
const answers = this.state.current.answers.map((el) => el.toLowerCase()); const answers = this.props.current.answers.map((el) => el.toLowerCase());
// Calculate the distances to all possible answers // Calculate the distances to all possible answers
const dists = answers.map((el) => levW(input.toLowerCase(), el)); const dists = answers.map((el) => levW(input.toLowerCase(), el));
// Find the lowest distance // Find the lowest distance
const minDist = Math.min(...dists); const minDist = Math.min(...dists);
console.log(this.reviewQueue.size()); console.log("Review Queue size:", this.reviewQueue.size());
// Check if the user's answer was correct // Check if the user's answer was correct
if (minDist === 0) { if (minDist === 0) {
@ -133,57 +129,76 @@ export default class ReviewPage extends React.Component<IProps, IState> {
// Show the next vocab word // Show the next vocab word
if (this.reviewQueue.size() === 0) { if (this.reviewQueue.size() === 0) {
// Go to the summary screen // Go to the summary screen
this.setState({ this.props.setLastReview(this.metadata);
toSummary: true, this.props.setSummary(true);
}, () => {
// Update the "Last Review" data
this.props.setLastReview(this.state.metadata);
});
} else { } else {
// Increase the vocab // Increase the vocab
this.setState({ this.props.setReview(this.reviewQueue.dequeue(), this.increaseMeta(1, 0));
current: this.reviewQueue.dequeue(), this.inputRef.value = "";
input: "",
// Add one correct answer
metadata: this.increaseMeta(1, 0),
});
} }
} else if (minDist <= LEVENSHTEIN_MAX_DISTANCE) { } else if (minDist <= LEVENSHTEIN_MAX_DISTANCE) {
// TODO: Show a hint // TODO: Show a hint
console.log("Partially correct"); console.log("Partially correct");
} else { } else {
// Find the IVocab item // Find the IVocab item
const vocab = this.vocabFromId(this.state.current.id); const vocab = this.vocabFromId(this.props.current.id);
if (vocab) { if (vocab) {
// Re-Add the vocabulary item to the review queue // Re-Add the vocabulary item to the review queue
// TODO: Only re-add when it when it's not re-queued // TODO: Only re-add when it when it's not re-queued
vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue); // vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue);
} else { } else {
console.log("[ReviewPage::checkInput] Could not find IVocab item for wrong IReviewCard"); console.log("[ReviewPage::checkInput] Could not find IVocab item for wrong IReviewCard");
} }
this.setState({ this.props.setPopover(true, "Das war nicht richtig", "red");
popoverOpen: true,
popoverText: "Das war nicht richtig",
popoverColor: "red",
// TODO: Or maybe don't reset the text
input: "",
metadata: this.increaseMeta(0, 1),
});
} }
// TODO(?): Show a snackbar for showing the updated score // TODO(?): Show a snackbar for showing the updated score
} }
render() { render() {
const { question, qtype } = this.state.current; if (this.props.loading) {
return <div>
{/*
* This would be the case when the user presses the "to
* review" button. That is because we need the state of loading
* to be true, when this page gets called
* TODO:?
*/}
{
this.props.toSummary ? (
<Redirect to="/review/summary" />
) : undefined
}
<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)})`; const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`;
// TODO: // TODO:
const progress = 50; const progress = 50;
return <div> return <div>
{ {
this.state.toSummary ? ( this.props.toSummary ? (
<Redirect to="/review/summary" /> <Redirect to="/review/summary" />
) : undefined ) : undefined
} }
@ -198,10 +213,7 @@ export default class ReviewPage extends React.Component<IProps, IState> {
<TextField <TextField
margin="normal" margin="normal"
fullWidth={true} fullWidth={true}
value={this.state.input} inputRef={node => this.inputRef = node}
onChange={(ev) => this.setState({
input: ev.target.value,
})}
onKeyPress={(ev) => { onKeyPress={(ev) => {
// Allow checking of the answer by pressing Enter // Allow checking of the answer by pressing Enter
if (ev.key === "Enter") if (ev.key === "Enter")
@ -211,7 +223,7 @@ export default class ReviewPage extends React.Component<IProps, IState> {
variant="determinate" variant="determinate"
value={progress} /> value={progress} />
<Popover <Popover
open={this.state.popoverOpen} open={this.props.popoverOpen}
anchorOrigin={{ anchorOrigin={{
vertical: "center", vertical: "center",
horizontal: "center" horizontal: "center"
@ -221,12 +233,10 @@ export default class ReviewPage extends React.Component<IProps, IState> {
horizontal: "center" horizontal: "center"
}} }}
anchorEl={this.buttonRef} anchorEl={this.buttonRef}
onClose={() => this.setState({ onClose={() => this.props.setPopover(false, "", "")}
popoverOpen: false,
})}
PaperProps={{ PaperProps={{
style: { style: {
backgroundColor: this.state.popoverColor, backgroundColor: this.props.popoverColor,
padding: 10, padding: 10,
color: "white" color: "white"
} }
@ -234,7 +244,7 @@ export default class ReviewPage extends React.Component<IProps, IState> {
<Typography <Typography
variant="button" variant="button"
color="inherit"> color="inherit">
{this.state.popoverText} {this.props.popoverText}
</Typography> </Typography>
</Popover> </Popover>
<Button <Button

View File

@ -1,8 +1,10 @@
import * as Actions from "../actions"; import * as Actions from "../actions";
import { ILearner } from "../models/learner"; import { ILearner } from "../models/learner";
import { ILevel } from "../models/level";
import { IUser } from "../models/user"; import { IUser } from "../models/user";
import { IVocab } from "../models/vocab"; import { IVocab } from "../models/vocab";
import { IReviewCard, IReviewMetadata } from "../models/review";
interface IState { interface IState {
drawer: boolean; drawer: boolean;
@ -12,6 +14,9 @@ interface IState {
// TODO: Rework this // TODO: Rework this
user: IUser | {}, user: IUser | {},
// All available levels
levels: ILevel[];
login: { login: {
loading: boolean; loading: boolean;
snackMsg: string; snackMsg: string;
@ -26,7 +31,22 @@ interface IState {
loading: boolean; loading: boolean;
}; };
review: {
current: IReviewCard;
loading: boolean;
vocab: IVocab[];
metadata: IReviewMetadata;
toSummary: boolean;
popoverOpen: boolean;
popoverText: string;
popoverColor: string;
};
topTen: ILearner[]; topTen: ILearner[];
lastReview: any;
}; };
const initialState: IState = { const initialState: IState = {
@ -46,6 +66,8 @@ const initialState: IState = {
snackOpen: false, snackOpen: false,
}, },
levels: [],
level: { level: {
currentVocab: {} as IVocab, currentVocab: {} as IVocab,
lookedAt: [0], lookedAt: [0],
@ -54,6 +76,23 @@ const initialState: IState = {
loading: true, loading: true,
}, },
review: {
current: {} as IReviewCard,
loading: true,
vocab: [],
metadata: {} as IReviewMetadata,
toSummary: false,
popoverOpen: false,
popoverText: "",
popoverColor: "",
},
lastReview: {
correct: 0,
wrong: 0,
},
// The top ten // The top ten
topTen: [], topTen: [],
}; };
@ -125,7 +164,46 @@ export function LateinicusApp(state: IState = initialState, action: any) {
loading: action.state, loading: action.state,
}), }),
}); });
case Actions.SET_LEVELS:
return Object.assign({}, state, {
levels: action.levels,
});
case Actions.REVIEW_SET_POPOVER:
return Object.assign({}, state, {
review: Object.assign({}, state.review, {
popoverText: action.text,
popoverOpen: action.state,
popoverColor: action.color,
}),
});
case Actions.SET_REVIEW:
return Object.assign({}, state, {
review: Object.assign({}, state.review, {
current: action.current,
metadata: action.meta,
}),
});
case Actions.SET_LAST_REVIEW:
return Object.assign({}, state, {
lastReview: action.metadata,
});
case Actions.REVIEW_SET_LOADING:
return Object.assign({}, state, {
review: Object.assign({}, state.review, {
loading: action.state,
}),
});
case Actions.REVIEW_SET_SUMMARY:
return Object.assign({}, state, {
review: Object.assign({}, state.review, {
toSummary: action.state,
}),
});
default: default:
if (action.type) {
console.log("Reducer not implemented:", action.type);
}
return state; return state;
} }
}; };