From 882ca5a9e3827332ef5aebe78c6de11c3c9fdab6 Mon Sep 17 00:00:00 2001 From: Alexander Polynomdivision Date: Wed, 19 Sep 2018 16:39:02 +0200 Subject: [PATCH] feat: Partially transition Review to Redux --- src/actions/index.ts | 53 ++++++++++++ src/components/app.tsx | 8 +- src/containers/LevelList.ts | 14 +++ src/containers/Review.ts | 35 ++++++++ src/pages/review.tsx | 168 +++++++++++++++++++----------------- src/reducers/index.ts | 78 +++++++++++++++++ 6 files changed, 272 insertions(+), 84 deletions(-) create mode 100644 src/containers/LevelList.ts create mode 100644 src/containers/Review.ts diff --git a/src/actions/index.ts b/src/actions/index.ts index a234dd3..dd2ed67 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,5 +1,7 @@ import { IVocab } from "../models/vocab"; import { IUser } from "../models/user"; +import { ILevel } from "../models/level"; +import { IReviewMetadata, IReviewCard } from "../models/review"; export const SET_DRAWER = "SET_DRAWER"; export function setDrawer(state: boolean) { @@ -89,3 +91,54 @@ export function setLevelLoading(state: boolean) { 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, + }; +}; diff --git a/src/components/app.tsx b/src/components/app.tsx index 6caaa38..9b149ae 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -6,9 +6,9 @@ import AuthRoute from "../security/AuthRoute"; import Dashboard from "../pages/dashboard"; import LoginPage from "../containers/LoginPage"; -import LevelListPage from "../pages/levelList"; +import LevelListPage from "../containers/LevelList"; import LevelPage from "../containers/LevelPage"; -import ReviewPage from "../pages/review"; +import ReviewPage from "../containers/Review"; import SummaryPage from "../pages/summary"; import WelcomePage from "../pages/intro"; @@ -258,8 +258,7 @@ export default class Application extends React.Component { } /> + component={() => } /> {/*We cannot use AuthRoute here, because match is undefined otherwise*/} { reviewType={ReviewType.LEVEL} levelId={match.params.id} vocabByLevel={this.getLevelVocab} - drawerButtonState={this.drawerButtonState} setLastReview={this.setLastReview} />; } else { return ; diff --git a/src/containers/LevelList.ts b/src/containers/LevelList.ts new file mode 100644 index 0000000..4f138b4 --- /dev/null +++ b/src/containers/LevelList.ts @@ -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; diff --git a/src/containers/Review.ts b/src/containers/Review.ts new file mode 100644 index 0000000..495bde4 --- /dev/null +++ b/src/containers/Review.ts @@ -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; diff --git a/src/pages/review.tsx b/src/pages/review.tsx index c9cf2aa..5529642 100644 --- a/src/pages/review.tsx +++ b/src/pages/review.tsx @@ -8,6 +8,8 @@ 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 { Redirect } from "react-router-dom"; @@ -21,34 +23,34 @@ import { Queue } from "../utils/queue"; interface IProps { levelId?: number; - vocabByLevel?: (level: number) => IVocab[]; - vocabByQueue?: () => IVocab[]; - - setLastReview: (meta: IReviewMetadata) => void; + vocabByLevel?: (level: number) => Promise; + vocabByQueue?: () => Promise; reviewType: ReviewType; - drawerButtonState: (state: boolean) => void; -} - -interface IState { - input: string; + loading: boolean; + vocab: IVocab[]; current: IReviewCard; - - metadata: IReviewMetadata; - toSummary: boolean; - popoverOpen: boolean; popoverText: 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 { +export default class ReviewPage extends React.Component { private vocab: IVocab[] = []; - private reviewQueue: Queue = undefined; + private reviewQueue: Queue = 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); @@ -56,6 +58,17 @@ export default class ReviewPage extends React.Component { // 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; switch (reviewType) { @@ -63,7 +76,10 @@ export default class ReviewPage extends React.Component { if (!vocabByLevel || !levelId) { alert("[ReviewPage::constructor] vocabByLevel or levelId undefined"); } else { - this.vocab = vocabByLevel(levelId); + vocabByLevel(levelId).then(res => { + this.vocab = res; + vocToQueue(); + }); } break; @@ -71,38 +87,18 @@ export default class ReviewPage extends React.Component { if (!vocabByQueue) { alert("[ReviewPage::constructor] vocabByQueue undefined"); } else { - this.vocab = vocabByQueue(); + vocabByQueue().then(res => { + this.vocab = res; + vocToQueue(); + }); } 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 => { - const { metadata } = this.state; + const { metadata } = this; return { wrong: metadata.wrong + wrong, @@ -111,21 +107,21 @@ export default class ReviewPage extends React.Component { } 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 = () => { // 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) - 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 const dists = answers.map((el) => levW(input.toLowerCase(), el)); // Find the lowest distance 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 if (minDist === 0) { @@ -133,57 +129,76 @@ export default class ReviewPage extends React.Component { // Show the next vocab word if (this.reviewQueue.size() === 0) { // Go to the summary screen - this.setState({ - toSummary: true, - }, () => { - // Update the "Last Review" data - this.props.setLastReview(this.state.metadata); - }); + this.props.setLastReview(this.metadata); + this.props.setSummary(true); } else { // Increase the vocab - this.setState({ - current: this.reviewQueue.dequeue(), - input: "", - // Add one correct answer - metadata: this.increaseMeta(1, 0), - }); + this.props.setReview(this.reviewQueue.dequeue(), this.increaseMeta(1, 0)); + this.inputRef.value = ""; } } else if (minDist <= LEVENSHTEIN_MAX_DISTANCE) { // TODO: Show a hint console.log("Partially correct"); } else { // Find the IVocab item - const vocab = this.vocabFromId(this.state.current.id); + 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); + // vocabToReviewCard(vocab).forEach(this.reviewQueue.enqueue); } else { console.log("[ReviewPage::checkInput] Could not find IVocab item for wrong IReviewCard"); } - this.setState({ - popoverOpen: true, - popoverText: "Das war nicht richtig", - popoverColor: "red", - // TODO: Or maybe don't reset the text - input: "", - metadata: this.increaseMeta(0, 1), - }); + this.props.setPopover(true, "Das war nicht richtig", "red"); } // TODO(?): Show a snackbar for showing the updated score } render() { - const { question, qtype } = this.state.current; + if (this.props.loading) { + return
+ {/* + * 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 ? ( + + ) : undefined + } + + + + + + + + + + + +
; + } + + + const { question, qtype } = this.props.current; const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`; // TODO: const progress = 50; return
{ - this.state.toSummary ? ( + this.props.toSummary ? ( ) : undefined } @@ -198,10 +213,7 @@ export default class ReviewPage extends React.Component { this.setState({ - input: ev.target.value, - })} + inputRef={node => this.inputRef = node} onKeyPress={(ev) => { // Allow checking of the answer by pressing Enter if (ev.key === "Enter") @@ -211,7 +223,7 @@ export default class ReviewPage extends React.Component { variant="determinate" value={progress} /> { horizontal: "center" }} anchorEl={this.buttonRef} - onClose={() => this.setState({ - popoverOpen: false, - })} + onClose={() => this.props.setPopover(false, "", "")} PaperProps={{ style: { - backgroundColor: this.state.popoverColor, + backgroundColor: this.props.popoverColor, padding: 10, color: "white" } @@ -234,7 +244,7 @@ export default class ReviewPage extends React.Component { - {this.state.popoverText} + {this.props.popoverText}