From 3136c92687ee4c8af3aa7adfe6c098c4c2c9ad8f Mon Sep 17 00:00:00 2001 From: Alexander Polynomdivision Date: Tue, 18 Sep 2018 20:20:26 +0200 Subject: [PATCH] refactor: Move the LevelPage to Redux --- src/actions/index.ts | 43 +++++++++++++++++ src/components/app.tsx | 57 ++++++++++++----------- src/containers/LevelPage.ts | 37 +++++++++++++++ src/pages/level.tsx | 93 ++++++++++++++++++++++++++----------- src/reducers/index.ts | 52 ++++++++++++++++++++- 5 files changed, 227 insertions(+), 55 deletions(-) create mode 100644 src/containers/LevelPage.ts diff --git a/src/actions/index.ts b/src/actions/index.ts index 613868a..a234dd3 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,3 +1,6 @@ +import { IVocab } from "../models/vocab"; +import { IUser } from "../models/user"; + export const SET_DRAWER = "SET_DRAWER"; export function setDrawer(state: boolean) { return { @@ -46,3 +49,43 @@ export function setUser(user: IUser) { user, }; } + +export const LEVEL_SET_REVIEW = "LEVEL_SET_REVIEW"; +export function setLevelReview(state: boolean) { + return { + type: LEVEL_SET_REVIEW, + state, + }; +}; + +export const LEVEL_SET_LOOKEDAT = "LEVEL_SET_LOOKEDAT"; +export function setLevelLookedAt(ids: number[]) { + return { + type: LEVEL_SET_REVIEW, + lookedAt: ids, + }; +}; + +export const LEVEL_SET_CUR_VOCAB = "LEVEL_SET_CUR_VOCAB"; +export function setLevelCurrentVocab(vocab: IVocab) { + return { + type: LEVEL_SET_CUR_VOCAB, + vocab, + }; +}; + +export const LEVEL_SET_VOCAB = "LEVEL_SET_VOCAB"; +export function setLevelVocab(vocab: IVocab[]) { + return { + type: LEVEL_SET_VOCAB, + vocab, + }; +}; + +export const LEVEL_SET_LOADING = "LEVEL_SET_LOADING"; +export function setLevelLoading(state: boolean) { + return { + type: LEVEL_SET_LOADING, + state, + }; +}; diff --git a/src/components/app.tsx b/src/components/app.tsx index 123e00b..6caaa38 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -7,7 +7,7 @@ import AuthRoute from "../security/AuthRoute"; import Dashboard from "../pages/dashboard"; import LoginPage from "../containers/LoginPage"; import LevelListPage from "../pages/levelList"; -import LevelPage from "../pages/level"; +import LevelPage from "../containers/LevelPage"; import ReviewPage from "../pages/review"; import SummaryPage from "../pages/summary"; import WelcomePage from "../pages/intro"; @@ -146,35 +146,38 @@ export default class Application extends React.Component { }; } - getLevelVocab(id: number): IVocab[] { + getLevelVocab(id: number): Promise { console.log("STUB: Application::getLevelVocab"); // TODO: Actually implement this - // TODO: Don't fetch this when it was already fetched once. - return [{ - german: ["Wein"], - hint: "Worte auf '-um' sind meistens NeutrUM", - type: VocabType.NOMEN, - latin: { - grundform: "Vinum", - genitiv: "Vini", - genus: "Neutrum" - }, - id: 0 - }/* , { - * latin: "Vici", - * german: "", - * hint: "Wird \"Viki\" und nicht \"Vichi\" ausgesprochen", - * mnemonic: "Merk dir das Wort mit Caesars berühmten Worten: \"Veni Vidi Vici\"; Er kam, sah und siegte", - * type: VocabType.NOMEN, - * id: 2 - }, { - * latin: "fuga", - * german: "Flucht", - * hint: "Worte auf \"-a\" sind FeminA", - * type: VocabType.NOMEN, - * id: 3 - } */] as IVocab[]; + return new Promise((res, rej) => { + setTimeout(() => { + res([{ + german: ["Wein"], + hint: "Worte auf '-um' sind meistens NeutrUM", + type: VocabType.NOMEN, + latin: { + grundform: "Vinum", + genitiv: "Vini", + genus: "Neutrum" + }, + id: 0 + }/* , { + * latin: "Vici", + * german: "", + * hint: "Wird \"Viki\" und nicht \"Vichi\" ausgesprochen", + * mnemonic: "Merk dir das Wort mit Caesars berühmten Worten: \"Veni Vidi Vici\"; Er kam, sah und siegte", + * type: VocabType.NOMEN, + * id: 2 + }, { + * latin: "fuga", + * german: "Flucht", + * hint: "Worte auf \"-a\" sind FeminA", + * type: VocabType.NOMEN, + * id: 3 + } */]); + }, 2000); + }); } login = (username: string, password: string): Promise => { diff --git a/src/containers/LevelPage.ts b/src/containers/LevelPage.ts new file mode 100644 index 0000000..128a450 --- /dev/null +++ b/src/containers/LevelPage.ts @@ -0,0 +1,37 @@ +import { connect } from "react-redux"; + +import { + setDrawerButton, setLevelReview, setLevelLookedAt, + setLevelCurrentVocab, setLevelVocab, setLevelLoading +} from "../actions"; + +import { IVocab } from "../models/vocab"; + +import LevelPage from "../pages/level"; + +const mapStateToProps = state => { + const { currentVocab, lookedAt, toReview, vocab, loading } = state.level; + + return { + currentVocab, + lookedAt, + toReview, + vocab, + loading, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + drawerButtonState: (state: boolean) => dispatch(setDrawerButton(state)), + setReview: (state: boolean) => dispatch(setLevelReview(state)), + setLookedAt: (ids: number[]) => dispatch(setLevelLookedAt(ids)), + setCurrentVocab: (vocab: IVocab) => dispatch(setLevelCurrentVocab(vocab)), + setVocab: (vocab: IVocab[]) => dispatch(setLevelVocab(vocab)), + setLoading: (state: boolean) => dispatch(setLevelLoading(state)), + }; +}; + +const LevelPageContainer = connect(mapStateToProps, + mapDispatchToProps)(LevelPage); +export default LevelPageContainer; diff --git a/src/pages/level.tsx b/src/pages/level.tsx index 830e14f..7f800fc 100644 --- a/src/pages/level.tsx +++ b/src/pages/level.tsx @@ -5,8 +5,11 @@ import ListItem from "@material-ui/core/ListItem"; import ListItemText from "@material-ui/core/ListItemText"; import Grid from "@material-ui/core/Grid"; import Typography from "@material-ui/core/Typography"; +import Paper from "@material-ui/core/Paper"; +import Grid from "@material-ui/core/Grid"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; +import CircularProgress from "@material-ui/core/CircularProgress"; import { Redirect } from "react-router-dom"; @@ -14,35 +17,42 @@ import { IVocab } from "../models/vocab"; interface IProps { id: string; + levelVocab: (id: string) => Promise; - levelVocab: (id: string) => IVocab[]; - + loading: boolean; + setLoading: (state: boolean) => void; + vocab: IVocab[]; + setVocab: (vocab: IVocab[]) => void; + setLookedAt: (ids: number[]) => void; + setCurrentVocab: (vocab: IVocab) => void; + setReview: (state: boolean) => void; drawerButtonState: (state: boolean) => void; -}; - -interface IState { currentVocab: IVocab; lookedAt: number[]; toReview: boolean; }; -export default class LevelPage extends React.Component { +export default class LevelPage extends React.Component { private uid = 0; // To prevent React from redrawing the vocabulary list and prematurely // cancelling the animation private uids: { [key: string]: string } = {}; - constructor(props: any) { - super(props); - + componentDidMount() { // Hide the drawer this.props.drawerButtonState(false); - this.state = { - currentVocab: this.props.levelVocab(this.props.id)[0], - lookedAt: [0], - toReview: false, - }; + // Fetch the vocabulary + this.props.setLoading(true); + + // TODO: Error handling + this.props.levelVocab(this.props.id).then(vocab => { + this.props.setVocab(vocab); + this.props.setCurrentVocab(vocab[0]); + this.props.setLoading(false); + }); + + } genUID = (vocab: IVocab): string => { @@ -57,16 +67,16 @@ export default class LevelPage extends React.Component { renderVocabListItem = (vocab: IVocab): any => { // Check if the vocab was already looked at - const lookedAt = this.state.lookedAt.find((el) => el === vocab.id) || vocab.id === 0; + const lookedAt = this.props.lookedAt.find((el) => el === vocab.id) || vocab.id === 0; return { // Prevent the user from using too much memory by always clicking on the elements // Show the clicked at vocab word - this.setState({ - currentVocab: vocab, - lookedAt: lookedAt ? this.state.lookedAt : this.state.lookedAt.concat(vocab.id), - }); + this.props.setCurrentVocab(vocab); + this.props.setLookedAt(lookedAt ? ( + this.props.lookedAt + ) : this.props.lookedAt.concat(vocab.id)); }}> {`${vocab.latin.grundform} ${lookedAt ? "✔" : ""}`} @@ -75,23 +85,52 @@ export default class LevelPage extends React.Component { } toReview = () => { - const { levelVocab, id } = this.props; + const { vocab, lookedAt } = this.props; // Only go to the review if all vocabulary item have been looked at - if (levelVocab(id).length === this.state.lookedAt.length) { - this.setState({ - toReview: true, - }); + if (vocab.length === lookedAt.length) { + this.props.setReview(true); } } render() { - const { currentVocab } = this.state; + 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.toReview ? ( + + ) : undefined + } + + + + + + + + + +
; + } + + const { currentVocab } = this.props; return
- {this.props.levelVocab(this.props.id) + {this.props.vocab .map(this.renderVocabListItem)} {/* TODO*/} @@ -102,7 +141,7 @@ export default class LevelPage extends React.Component { { - this.state.toReview ? ( + this.props.toReview ? ( ) : undefined } diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 6586a99..5d334c2 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -2,6 +2,7 @@ import * as Actions from "../actions"; import { ILearner } from "../models/learner"; import { IUser } from "../models/user"; +import { IVocab } from "../models/vocab"; interface IState { drawer: boolean; @@ -17,6 +18,14 @@ interface IState { snackOpen: boolean; }; + level: { + currentVocab: IVocab; + lookedAt: number[]; + toReview: boolean; + vocab: IVocab[]; + loading: boolean; + }; + topTen: ILearner[]; }; @@ -27,7 +36,7 @@ const initialState: IState = { drawerButton: true, // Is the user authenticated? // TODO: Set this to false - authenticated: false, + authenticated: true, user: {}, @@ -37,6 +46,14 @@ const initialState: IState = { snackOpen: false, }, + level: { + currentVocab: {} as IVocab, + lookedAt: [0], + toReview: false, + vocab: [], + loading: true, + }, + // The top ten topTen: [], }; @@ -75,6 +92,39 @@ export function LateinicusApp(state: IState = initialState, action: any) { return Object.assign({}, state, { user: action.user, }); + case Actions.LEVEL_SET_REVIEW: + return Object.assign({}, state, { + level: Object.assign({}, state.level, { + toReview: action.state, + // Make sure that we are in a "loading mode", when the page gets + // called the next time + loading: true, + }), + }); + case Actions.LEVEL_SET_LOOKEDAT: + return Object.assign({}, state, { + level: Object.assign({}, state.level, { + lookedAt: action.lookedAt, + }), + }); + case Actions.LEVEL_SET_CUR_VOCAB: + return Object.assign({}, state, { + level: Object.assign({}, state.level, { + currentVocab: action.vocab, + }), + }); + case Actions.LEVEL_SET_VOCAB: + return Object.assign({}, state, { + level: Object.assign({}, state.level, { + vocab: action.vocab, + }), + }); + case Actions.LEVEL_SET_LOADING: + return Object.assign({}, state, { + level: Object.assign({}, state.level, { + loading: action.state, + }), + }); default: return state; }