feat: Rework the Level UI

This commit is contained in:
Alexander Polynomdivision 2018-10-10 13:44:45 +02:00
parent ede271f604
commit d9ec095d5c
7 changed files with 225 additions and 53 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "seminarfach", "name": "seminarfach",
"version": "1.1.0", "version": "1.1.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -85,6 +85,30 @@ export function setLevelLoading(state: boolean) {
}; };
}; };
export const LEVEL_SET_STEPPER = "LEVEL_SET_STEPPER";
export function setLevelStepper(index: number) {
return {
type: LEVEL_SET_STEPPER,
index,
};
};
export const LEVEL_SET_REVIEW_DIAG = "LEVEL_SET_REVIEW_DIAG";
export function setLevelReviewDiag(state: boolean) {
return {
type: LEVEL_SET_REVIEW_DIAG,
state,
};
};
export const LEVEL_SET_LEAVE_DIAG = "LEVEL_SET_LEAVE_DIAG";
export function setLevelLeaveDiag(state: boolean) {
return {
type: LEVEL_SET_LEAVE_DIAG,
state,
};
};
export const SET_LEVELS = "SET_LEVELS"; export const SET_LEVELS = "SET_LEVELS";
export function setLevels(levels: ILevel[]) { export function setLevels(levels: ILevel[]) {
return { return {

View File

@ -363,7 +363,7 @@ export default class Application extends React.Component<IProps> {
basename="/app/"> basename="/app/">
<div className="flex" > <div className="flex" >
<Drawer logout={this.logout} /> <Drawer logout={this.logout} />
<div className="content"> <div>
<Route exact path="/" component={() => <Redirect to="/login" />} /> <Route exact path="/" component={() => <Redirect to="/login" />} />
<Route exact path="/login" component={() => { <Route exact path="/login" component={() => {
return <LoginPage login={this.login} /> return <LoginPage login={this.login} />

View File

@ -1,8 +1,8 @@
import { connect } from "react-redux"; import { connect } from "react-redux";
import { import {
setDrawerButton, setLevelLookedAt, setDrawerButton, setLevelLookedAt, setLevelStepper, setLevelReviewDiag,
setLevelCurrentVocab, setLevelVocab, setLevelLoading setLevelCurrentVocab, setLevelVocab, setLevelLoading, setLevelLeaveDiag
} from "../actions"; } from "../actions";
import { IVocab } from "../models/vocab"; import { IVocab } from "../models/vocab";
@ -10,13 +10,19 @@ import { IVocab } from "../models/vocab";
import LevelPage from "../pages/level"; import LevelPage from "../pages/level";
const mapStateToProps = state => { const mapStateToProps = state => {
const { currentVocab, lookedAt, vocab, loading } = state.level; const {
currentVocab, lookedAt, vocab, loading, stepper,
reviewDialog, leaveDialog
} = state.level;
return { return {
currentVocab, currentVocab,
lookedAt, lookedAt,
vocab, vocab,
loading, loading,
stepperIndex: stepper,
reviewDialog,
leaveDialog,
}; };
}; };
@ -27,6 +33,9 @@ const mapDispatchToProps = dispatch => {
setCurrentVocab: (vocab: IVocab) => dispatch(setLevelCurrentVocab(vocab)), setCurrentVocab: (vocab: IVocab) => dispatch(setLevelCurrentVocab(vocab)),
setVocab: (vocab: IVocab[]) => dispatch(setLevelVocab(vocab)), setVocab: (vocab: IVocab[]) => dispatch(setLevelVocab(vocab)),
setLoading: (state: boolean) => dispatch(setLevelLoading(state)), setLoading: (state: boolean) => dispatch(setLevelLoading(state)),
setStepper: (index: number) => dispatch(setLevelStepper(index)),
setReviewDialog: (state: boolean) => dispatch(setLevelReviewDiag(state)),
setLeaveDialog: (state: boolean) => dispatch(setLevelLeaveDiag(state))
}; };
}; };

View File

@ -67,3 +67,24 @@ body {
bottom: 12px; bottom: 12px;
right: -12px; right: -12px;
} }
.level-card {
box-sizing: border-box;
width: 50vw;
}
@media only screen and (max-width: 700px) {
.level-card {
box-sizing: border-box !important;
width: 100vw;
}
}
.level-stepper {
position: absolute;
bottom: 0px;
width: 100vw;
/* We otherwise go larger than the page */
box-sizing: border-box;
}

View File

@ -1,14 +1,20 @@
import * as React from "react"; import * as React from "react";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
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";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import MobileStepper from '@material-ui/core/MobileStepper';
import Button from '@material-ui/core/Button';
import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import { withRouter } from "react-router-dom"; import { withRouter } from "react-router-dom";
@ -22,15 +28,21 @@ interface IProps {
history: any; history: any;
stepperIndex: number;
loading: boolean; loading: boolean;
setLoading: (state: boolean) => void;
vocab: IVocab[]; vocab: IVocab[];
lookedAt: number[];
currentVocab: IVocab;
leaveDialog: boolean;
reviewDialog: boolean;
setVocab: (vocab: IVocab[]) => void; setVocab: (vocab: IVocab[]) => void;
setLookedAt: (ids: number[]) => void; setLookedAt: (ids: number[]) => void;
setCurrentVocab: (vocab: IVocab) => void; setCurrentVocab: (vocab: IVocab) => void;
drawerButtonState: (state: boolean) => void; drawerButtonState: (state: boolean) => void;
currentVocab: IVocab; setLoading: (state: boolean) => void;
lookedAt: number[]; setStepper: (index: number) => void;
setReviewDialog: (state: boolean) => void;
setLeaveDialog: (state: boolean) => void;
}; };
const LevelPageWithRouter = withRouter( const LevelPageWithRouter = withRouter(
@ -52,10 +64,9 @@ const LevelPageWithRouter = withRouter(
this.props.setVocab(vocab); this.props.setVocab(vocab);
this.props.setCurrentVocab(vocab[0]); this.props.setCurrentVocab(vocab[0]);
this.props.setLookedAt([vocab[0].id]); this.props.setLookedAt([vocab[0].id]);
this.props.setStepper(0);
this.props.setLoading(false); this.props.setLoading(false);
}); });
} }
genUID = (vocab: IVocab): string => { genUID = (vocab: IVocab): string => {
@ -68,32 +79,61 @@ const LevelPageWithRouter = withRouter(
} }
} }
renderVocabListItem = (vocab: IVocab): any => { toReview = () => {
// Check if the vocab was already looked at this.props.setLoading(true);
const lookedAt = this.props.lookedAt.find((el) => el === vocab.id); this.props.setStepper(0);
this.props.history.push(`/review/level/${this.props.id}`);
return <ListItem button key={this.genUID(vocab)} onClick={() => {
// Prevent the user from using too much memory by always clicking on the elements
// Show the clicked at vocab word
this.props.setCurrentVocab(vocab);
this.props.setLookedAt(lookedAt ? (
this.props.lookedAt
) : this.props.lookedAt.concat(vocab.id));
}}>
<ListItemText>
{`${vocab.latin.grundform} ${lookedAt ? "✔" : ""}`}
</ListItemText>
</ListItem>;
} }
toReview = () => { openReview = () => {
const { vocab, lookedAt, id } = this.props; this.props.setReviewDialog(true);
}
closeReview = () => {
this.props.setReviewDialog(false);
}
// Only go to the review if all vocabulary item have been looked at openLeave = () => {
if (vocab.length === lookedAt.length) { this.props.setLeaveDialog(true);
this.props.setLoading(true); }
this.props.history.push(`/review/level/${id}`); closeLeave = () => {
this.props.setLeaveDialog(false);
}
cancelLevel = () => {
this.closeLeave();
this.props.history.push("/dashboard");
}
nextVocab = () => {
// Get the next vocab item
const { stepperIndex, vocab } = this.props;
// When we are about to go out of bounds, then we want to
// ask whether the user wants to review
if (stepperIndex + 1 >= vocab.length) {
this.openReview();
} else {
const newVocab = vocab[stepperIndex + 1];
// Set the stepperIndex and the currentVocab
this.props.setStepper(stepperIndex + 1);
this.props.setCurrentVocab(newVocab);
}
}
prevVocab = () => {
// Get the next vocab item
const { stepperIndex, vocab } = this.props;
// Prevent going out of bounds
if (stepperIndex - 1 < 0) {
this.openLeave();
} else {
const newVocab = vocab[stepperIndex - 1];
// Set the stepperIndex and the currentVocab
this.props.setStepper(stepperIndex - 1);
this.props.setCurrentVocab(newVocab);
} }
} }
@ -125,25 +165,15 @@ const LevelPageWithRouter = withRouter(
[VocabType.ADJEKTIV]: "Adjektiv", [VocabType.ADJEKTIV]: "Adjektiv",
[VocabType.ADVERB]: "Adverb", [VocabType.ADVERB]: "Adverb",
}; };
const { currentVocab, vocab, stepperIndex } = this.props;
const { currentVocab } = this.props;
return <div> return <div>
<Grid container direction="row"> <Grid container justify="center">
<Grid item xs={3}> <Grid item>
<List>
{this.props.vocab
.map(this.renderVocabListItem)}
<ListItem button onClick={this.toReview}>
<ListItemText>
Zur Übung
</ListItemText>
</ListItem>
</List>
</Grid>
<Grid item lg={7} xs={9}>
<Grid container direction="column"> <Grid container direction="column">
<Grid item style={{ margin: 12 }}> <Grid item className="level-card">
<Card> <Card
square={true}
style={{ margin: 12 }}>
<CardContent> <CardContent>
<Typography gutterBottom variant="headline" component="h2"> <Typography gutterBottom variant="headline" component="h2">
{`${currentVocab.latin.grundform} (${vocabTypeToStr[currentVocab.type]})`} {`${currentVocab.latin.grundform} (${vocabTypeToStr[currentVocab.type]})`}
@ -153,8 +183,72 @@ const LevelPageWithRouter = withRouter(
</Card> </Card>
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
<MobileStepper
steps={vocab.length + 1}
position="static"
activeStep={stepperIndex}
className="level-stepper"
nextButton={
<Button
onClick={this.nextVocab}
disabled={stepperIndex >= vocab.length + 1}>
<KeyboardArrowRight />
Nächste
</Button>
}
backButton={
<Button onClick={this.prevVocab}>
<KeyboardArrowLeft />
{
stepperIndex === 0 ? (
`Abbrechen`
) : (
`Zurück`
)
}
</Button>
} />
{/*The leave and the "to review" dialog*/}
<Dialog
open={this.props.reviewDialog}
onClose={this.closeReview}>
<DialogTitle>Willst du zur Übung?</DialogTitle>
<DialogActions>
<Button
onClick={this.toReview}>
Zur Übung
</Button>
<Button
onClick={this.closeReview}>
Noch nicht
</Button>
</DialogActions>
</Dialog>
<Dialog
open={this.props.leaveDialog}
onClose={this.closeLeave}>
<DialogTitle>Willst du das Level abbrechen?</DialogTitle>
<DialogContent>
<DialogContentText>
Wenn du jetzt abbricht, dann geht dein Fortschritt
in diesem Level verloren.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={this.closeLeave}>
Zurück zum Level
</Button>
<Button
onClick={this.cancelLevel}>
Level abbrechen
</Button>
</DialogActions>
</Dialog>
</div>; </div>;
} }
} }

View File

@ -30,6 +30,9 @@ interface IState {
lookedAt: number[]; lookedAt: number[];
vocab: IVocab[]; vocab: IVocab[];
loading: boolean; loading: boolean;
stepper: number;
leaveDialog: boolean;
reviewDialog: boolean;
}; };
levelList: { levelList: {
@ -95,6 +98,9 @@ const initialState: IState = {
lookedAt: [0], lookedAt: [0],
vocab: [], vocab: [],
loading: true, loading: true,
stepper: 0,
leaveDialog: false,
reviewDialog: false,
}, },
levelList: { levelList: {
@ -289,6 +295,24 @@ export function LateinicusApp(state: IState = initialState, action: any) {
loading: action.state, loading: action.state,
}), }),
}); });
case Actions.LEVEL_SET_STEPPER:
return Object.assign({}, state, {
level: Object.assign({}, state.level, {
stepper: action.index,
}),
});
case Actions.LEVEL_SET_REVIEW_DIAG:
return Object.assign({}, state, {
level: Object.assign({}, state.level, {
reviewDialog: action.state
}),
});
case Actions.LEVEL_SET_LEAVE_DIAG:
return Object.assign({}, state, {
level: Object.assign({}, state.level, {
leaveDialog: action.state,
}),
});
default: default:
// Ignore the initialization call to the reducer. By that we can // Ignore the initialization call to the reducer. By that we can
// catch all actions that are not implemented // catch all actions that are not implemented