refactor: Move the LevelList to Redux

This commit is contained in:
Alexander Polynomdivision 2018-09-19 18:00:38 +02:00
parent 2dc9aa3a53
commit 8a6ec0dbc7
8 changed files with 188 additions and 137 deletions

View File

@ -134,3 +134,11 @@ export function setReview(current: IReviewCard, meta: IReviewMetadata) {
meta, meta,
}; };
}; };
export const LEVELLIST_SET_LOADING = "LEVELLIST_SET_LOADING";
export function setLevelListLoading(state: boolean) {
return {
type: LEVELLIST_SET_LOADING,
state,
};
}

View File

@ -31,22 +31,28 @@ interface IProps {
// TODO: Cache API-Calls // TODO: Cache API-Calls
// TODO: When mounting without a login, check if the sessionToken is still valid // TODO: When mounting without a login, check if the sessionToken is still valid
export default class Application extends React.Component<IProps> { export default class Application extends React.Component<IProps> {
getLevels(): ILevel[] { getLevels(): Promise<ILevel[]> {
console.log("STUB: Application::getLevels"); console.log("STUB: Application::getLevels");
// TODO: Actually fetch them from somewhere
const levels = [{
name: "Der Bauer auf dem Feld",
desc: "So fängt alles an: Du bist ein einfacher Bauer und musst dich die Karriereleiter mit deinen freshen Latein-Skills hinaufarbeiten",
level: 1,
done: true,
}, {
name: "???",
desc: "Warum schreibe ich überhaupt was?dsd dddddddddddddddddddddd",
level: 2,
done: false,
}];
return levels; return new Promise((res, rej) => {
// TODO: Actually fetch them from somewhere
setTimeout(() => {
const levels = [{
name: "Der Bauer auf dem Feld",
desc: "So fängt alles an: Du bist ein einfacher Bauer und musst dich die Karriereleiter mit deinen freshen Latein-Skills hinaufarbeiten",
level: 1,
done: true,
}, {
name: "???",
desc: "Warum schreibe ich überhaupt was?dsd dddddddddddddddddddddd",
level: 2,
done: false,
}];
res(levels);
}, 2000);
});
} }
getLastReview = (): IReviewMetadata => { getLastReview = (): IReviewMetadata => {
@ -214,22 +220,6 @@ export default class Application extends React.Component<IProps> {
return true; return true;
} }
closeDrawer = () => {
this.setState({
drawerOpen: false,
});
}
drawerButtonState = (show: boolean) => {
// This is required as we would otherwise try to continously update
// the state, resulting in an infinte loop
if (show !== this.state.showDrawerButton) {
this.setState({
showDrawerButton: show,
});
}
}
render() { render() {
return <BrowserRouter return <BrowserRouter
basename="/app/"> basename="/app/">
@ -258,7 +248,8 @@ export default class Application extends React.Component<IProps> {
<AuthRoute <AuthRoute
isAuth={this.isAuthenticated} isAuth={this.isAuthenticated}
path="/levelList" path="/levelList"
component={() => <LevelListPage />} /> component={() => <LevelListPage
getLevels={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"

View File

@ -1,13 +1,23 @@
import { connect } from "react-redux"; import { connect } from "react-redux";
import { setLevelListLoading, setLevels } from "../actions";
import { ILevel } from "../models/level";
import LevelListPage from "../pages/levelList"; import LevelListPage from "../pages/levelList";
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
levels: state.levels, levels: state.levels,
loading: state.levelList.loading,
};
};
const mapDispatchToProps = dispatch => {
return {
setLoading: (state: boolean) => dispatch(setLevelListLoading(state)),
setLevels: (levels: ILevel[]) => dispatch(setLevels(levels)),
}; };
}; };
const mapDispatchToProps = dispatch => { return {} };
const LevelListContainer = connect(mapStateToProps, const LevelListContainer = connect(mapStateToProps,
mapDispatchToProps)(LevelListPage); mapDispatchToProps)(LevelListPage);

View File

@ -3,7 +3,6 @@ import * as React from "react";
import List from "@material-ui/core/List"; import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";

View File

@ -6,21 +6,55 @@ import Button from "@material-ui/core/Button";
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions'; import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent'; import CardContent from '@material-ui/core/CardContent';
import Paper from "@material-ui/core/Paper";
import CircularProgress from "@material-ui/core/CircularProgress";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { ILevel } from "../models/lesson"; import { ILevel } from "../models/level";
interface IProps { interface IProps {
getLevels: () => Promise<ILevel[]>;
setLevels: (levels: ILevel[]) => void;
setLoading: (state: boolean) => void;
loading: boolean;
levels: ILevel[]; levels: ILevel[];
} }
export default class Dashboard extends React.Component<{}> { export default class Dashboard extends React.Component<IProps> {
constructor(props: any) { componentDidMount() {
super(props); this.props.setLoading(true);
// Fetch the levels
this.props.getLevels().then(res => {
this.props.setLevels(res);
this.props.setLoading(false);
});
} }
render() { 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 small = window.matchMedia("(max-width: 700px)").matches; const small = window.matchMedia("(max-width: 700px)").matches;
const cName = small ? "lesson-card-xs" : "lesson-card-lg"; const cName = small ? "lesson-card-xs" : "lesson-card-lg";

View File

@ -8,13 +8,14 @@ import Button from "@material-ui/core/Button";
import LinearProgress from "@material-ui/core/LinearProgress"; import LinearProgress from "@material-ui/core/LinearProgress";
import Snackbar from "@material-ui/core/Snackbar"; import Snackbar from "@material-ui/core/Snackbar";
import { Redirect } from "react-router-dom"; import { withRouter } from "react-router-dom";
import { IUser } from "../models/user"; import { IUser } from "../models/user";
interface IProps { interface IProps {
login: (username: string, password: string) => Promise<IUser | {}>; login: (username: string, password: string) => Promise<IUser | {}>;
authenticated: boolean; authenticated: boolean;
history: any;
setLoading: (state: boolean) => void; setLoading: (state: boolean) => void;
setSnackbar: (state: boolean, msg: string) => void; setSnackbar: (state: boolean, msg: string) => void;
@ -22,84 +23,83 @@ interface IProps {
snackOpen: boolean; snackOpen: boolean;
snackMsg: string; snackMsg: string;
} }
/*
* interface IState {
* loading: boolean;
*
* snack: string; // The message
* open: boolean;
* } */
export default class LoginPage extends React.Component<IProps> { const LoginPageWithRouter = withRouter(
private usernameRef: any = undefined; class LoginPage extends React.Component<IProps> {
private passwordRef: any = undefined; private usernameRef: any = undefined;
private passwordRef: any = undefined;
performLogin = () => { performLogin = () => {
this.props.setLoading(true); this.props.setLoading(true);
const username = this.usernameRef.value || ""; const username = this.usernameRef.value || "";
const password = this.passwordRef.value || ""; const password = this.passwordRef.value || "";
this.props.login(username, password).then((res: IUser) => { this.props.login(username, password).then((res: IUser) => {
// Set the session key // Set the session key
window.sessionStorage.setItem("sessionToken", res.sessionToken); window.sessionStorage.setItem("sessionToken", res.sessionToken);
}, (err) => { this.props.history.push("/dashboard");
this.props.setLoading(false); }, (err) => {
this.props.setSnackbar(true, "Failed to log in"); this.props.setLoading(false);
}); this.props.setSnackbar(true, "Failed to log in");
} });
}
render() { componentDidMount() {
return <div> // If we're already authenticated, we can skip the login page
<Grid if (this.props.authenticated) {
container this.props.history.push("/dashboard");
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{ minHeight: '100vh' }}>
<Grid item xs={12}>
<Paper className="paper">
<Typography variant="title">Login</Typography>
<Grid container direction="column" spacing={8}>
<Grid item>
<TextField
label="Username"
inputRef={node => this.usernameRef = node} />
</Grid>
<Grid item>
<TextField
label="Passwort"
type="password"
inputRef={node => this.passwordRef = node} />
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
className="login-btn"
onClick={() => this.performLogin()}>
Login
</Button>
{
this.props.loading ? (
<LinearProgress />
) : undefined
}
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
<Snackbar
open={this.props.snackOpen}
onClose={() => this.props.setSnackbar(false, "")}
message={this.props.snackMsg}
autoHideDuration={6000} />
{
this.props.authenticated ? (
<Redirect to="/dashboard" />
) : undefined
} }
</div>; }
render() {
return <div>
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{ minHeight: '100vh' }}>
<Grid item xs={12}>
<Paper className="paper">
<Typography variant="title">Login</Typography>
<Grid container direction="column" spacing={8}>
<Grid item>
<TextField
label="Username"
inputRef={node => this.usernameRef = node} />
</Grid>
<Grid item>
<TextField
label="Passwort"
type="password"
inputRef={node => this.passwordRef = node} />
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
className="login-btn"
onClick={() => this.performLogin()}>
Login
</Button>
{
this.props.loading ? (
<LinearProgress />
) : undefined
}
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
<Snackbar
open={this.props.snackOpen}
onClose={() => this.props.setSnackbar(false, "")}
message={this.props.snackMsg}
autoHideDuration={6000} />
</div>;
}
} }
}; );
export default LoginPageWithRouter;

View File

@ -73,30 +73,23 @@ const ReviewPageWithRouter = withRouter(
// Get the correct vocabulary // Get the correct vocabulary
const { reviewType, vocabByLevel, levelId, vocabByQueue } = this.props; const { reviewType, vocabByLevel, levelId, vocabByQueue } = this.props;
switch (reviewType) {
case ReviewType.LEVEL:
if (!vocabByLevel || !levelId) {
alert("[ReviewPage::constructor] vocabByLevel or levelId undefined");
} else {
vocabByLevel(levelId).then(res => {
this.vocab = res;
vocToQueue();
});
}
break; // Just to make TSC shut up
case ReviewType.QUEUE: const noopPromise = () => {
if (!vocabByQueue) { return new Promise<IVocab[]>((res, rej) => {
alert("[ReviewPage::constructor] vocabByQueue undefined"); rej([]);
} else { });
vocabByQueue().then(res => { };
this.vocab = res; const vocabByLevelW = vocabByLevel || noopPromise;
vocToQueue(); const vocabByQueueW = vocabByQueue || noopPromise;
}); const getVocab = {
} [ReviewType.LEVEL]: () => vocabByLevelW(levelId),
[ReviewType.QUEUE]: () => vocabByQueueW(),
break; }[reviewType];
} getVocab().then((res: IVocab[]) => {
this.vocab = res;
vocToQueue();
});
} }
increaseMeta = (correct: number, wrong: number): IReviewMetadata => { increaseMeta = (correct: number, wrong: number): IReviewMetadata => {

View File

@ -31,6 +31,10 @@ interface IState {
loading: boolean; loading: boolean;
}; };
levelList: {
loading: boolean;
};
review: { review: {
current: IReviewCard; current: IReviewCard;
@ -75,6 +79,10 @@ const initialState: IState = {
loading: true, loading: true,
}, },
levelList: {
loading: true,
},
review: { review: {
current: {} as IReviewCard, current: {} as IReviewCard,
@ -191,8 +199,16 @@ export function LateinicusApp(state: IState = initialState, action: any) {
loading: action.state, loading: action.state,
}), }),
}); });
case Actions.LEVELLIST_SET_LOADING:
return Object.assign({}, state, {
levelList: Object.assign({}, state.levelList, {
loading: action.state,
}),
});
default: default:
if (action.type) { // Ignore the initialization call to the reducer. By that we can
// catch all actions that are not implemented
if (action.type && !action.type.startsWith("@@redux/INIT")) {
console.log("Reducer not implemented:", action.type); console.log("Reducer not implemented:", action.type);
} }