feat: Implement getNextLevel and getTopTenLearners

This commit is contained in:
Alexander Polynomdivision 2018-09-23 16:14:14 +02:00
parent db4b46b5aa
commit 08cd51c2a3
11 changed files with 280 additions and 77 deletions

View File

@ -134,4 +134,44 @@ export function setLevelListLoading(state: boolean) {
type: LEVELLIST_SET_LOADING, type: LEVELLIST_SET_LOADING,
state, state,
}; };
} };
export const SET_SCORE_POPOVER = "SET_SCORE_POPOVER";
export function setScorePopover(state: boolean) {
return {
type: SET_SCORE_POPOVER,
state,
};
};
export const SET_NEXT_LEVEL = "SET_NEXT_LEVEL";
export function setNextLevel(level: ILevel) {
return {
type: SET_NEXT_LEVEL,
level,
};
};
export const DASHBOARD_SET_NL_LOADING = "DASHBOARD_SET_NL_LOADING";
export function setDashboardNLLoading(state: boolean) {
return {
type: DASHBOARD_SET_NL_LOADING,
state,
};
};
export const SET_TOP_TEN = "SET_TOP_TEN";
export function setTopTen(topTen: ILearner[]) {
return {
type: SET_TOP_TEN,
topTen,
};
};
export const DASHBOARD_SET_TT_LOADING = "DASHBOARD_SET_TT_LOADING";
export function setDashboardTTLoading(state: boolean) {
return {
type: DASHBOARD_SET_TT_LOADING,
state,
};
};

View File

@ -7,6 +7,7 @@ import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Popover from "@material-ui/core/Popover";
import SwipeableDrawer from "@material-ui/core/SwipeableDrawer"; import SwipeableDrawer from "@material-ui/core/SwipeableDrawer";
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";
@ -29,12 +30,17 @@ interface IProps {
user: IUser; user: IUser;
open: boolean; open: boolean;
scorePopoverOpen: boolean;
showButton: boolean; showButton: boolean;
authenticated: boolean; authenticated: boolean;
setDrawer: (state: boolean) => void; setDrawer: (state: boolean) => void;
setScorePopover: (state: boolean) => void;
}; };
export default class Drawer extends React.Component<IProps> { export default class Drawer extends React.Component<IProps> {
private scoreBtnRef: HTMLButtonElement = undefined;
openDrawer = () => { openDrawer = () => {
this.props.setDrawer(true); this.props.setDrawer(true);
} }
@ -42,6 +48,13 @@ export default class Drawer extends React.Component<IProps> {
this.props.setDrawer(false); this.props.setDrawer(false);
} }
toggleScorePopover = () => {
this.props.setScorePopover(!this.props.scorePopoverOpen);
}
closeScorePopover = () => {
this.props.setScorePopover(false);
}
render() { render() {
const level = userScoreToLevel(this.props.user.score); const level = userScoreToLevel(this.props.user.score);
@ -63,11 +76,33 @@ export default class Drawer extends React.Component<IProps> {
</Typography> </Typography>
{ {
this.props.authenticated ? ( this.props.authenticated ? (
<Button color="inherit"> <Button
color="inherit"
buttonRef={node => this.scoreBtnRef = node}
onClick={this.toggleScorePopover}>
{`${this.props.user.score} / ${level.levelCap}`} {`${this.props.user.score} / ${level.levelCap}`}
</Button> </Button>
) : undefined ) : undefined
} }
<Popover
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
anchorEl={this.scoreBtnRef}
open={this.props.scorePopoverOpen}
onClose={this.closeScorePopover}>
<div className="content">
<Typography variant="headline">Du bist: {level.name}</Typography>
<Typography variant="subheading">
Dir fehlen noch <b>{level.levelCap - this.props.user.score}</b> Erfahrungspunkte bis zum nächsten Level
</Typography>
</div>
</Popover>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<SwipeableDrawer <SwipeableDrawer

View File

@ -5,7 +5,7 @@ import { BrowserRouter, Route, Redirect } from "react-router-dom";
import AuthRoute from "../security/AuthRoute"; import AuthRoute from "../security/AuthRoute";
import { setSessionToken, removeSessionToken } from "../security/Token"; import { setSessionToken, removeSessionToken } from "../security/Token";
import Dashboard from "../pages/dashboard"; import Dashboard from "../containers/Dashboard";
import LoginPage from "../containers/LoginPage"; import LoginPage from "../containers/LoginPage";
import LevelListPage from "../containers/LevelList"; import LevelListPage from "../containers/LevelList";
import LevelPage from "../containers/LevelPage"; import LevelPage from "../containers/LevelPage";
@ -18,7 +18,7 @@ import Drawer from "../containers/Drawer";
import { BACKEND_URL } from "../config"; import { BACKEND_URL } from "../config";
import { ILevel } from "../models/level"; import { ILevel } from "../models/level";
import { ILearner } from "../models/learner"; import { ILearner, TopTen } from "../models/learner";
import { IVocab, VocabType } from "../models/vocab"; import { IVocab, VocabType } from "../models/vocab";
import { IReviewMetadata, ReviewType } from "../models/review"; import { IReviewMetadata, ReviewType } from "../models/review";
import { IUser } from "../models/user"; import { IUser } from "../models/user";
@ -132,35 +132,47 @@ export default class Application extends React.Component<IProps> {
}]; }];
} }
getTopTenLearners(): ILearner[] { getTopTenLearners = (): Promise<TopTen[]> => {
console.log("STUB: Application::getTopTenLearners"); const id = this.props.user.classId;
return new Promise((res, rej) => {
fetch(`${BACKEND_URL}/auth/class/${id}/topTen`, {
headers: new Headers({
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(),
err => rej(err))
.then(data => {
console.log(data);
// TODO: Implement if (data.error === "0") {
return [{ res(data.data.topTen);
username: "Polynomdivision", } else {
level: 5, rej(data);
score: 400, }
}, { });
username: "Polynomdivision2", });
level: 3,
score: 500,
}, {
username: "Der eine Typ",
level: 7,
score: 100,
}];
} }
getNextLevel(): ILevel { getNextLevel = (): Promise<ILevel> => {
console.log("STUB: Application::getNextLevel"); return new Promise((res, rej) => {
fetch(`${BACKEND_URL}/auth/user/nextLevel`, {
headers: new Headers({
"Content-Type": "application/json",
"Token": this.props.user.sessionToken,
}),
}).then(resp => resp.json(),
err => rej(err))
.then(data => {
console.log(data);
// TODO: Actually fetch data if (data.error === "0") {
return { res(data.data);
name: "???", } else {
desc: "Warum schreibe ich überhaupt was?dsd dddddddddddddddddddddd", rej(data);
level: 2, }
done: false, });
}; });
} }
getLevelVocab = (id: number): Promise<IVocab[]> => { getLevelVocab = (id: number): Promise<IVocab[]> => {
@ -171,14 +183,15 @@ export default class Application extends React.Component<IProps> {
"Content-Type": "application/json", "Content-Type": "application/json",
"Token": this.props.user.sessionToken, "Token": this.props.user.sessionToken,
}), }),
}).then(data => data.json()) }).then(data => data.json(), err => {
.then((resp: IResponse) => { rej(err);
if (resp.error === "0") { }).then((resp: IResponse) => {
res(resp.data.vocab); if (resp.error === "0") {
} else { res(resp.data.vocab);
rej(resp); } else {
} rej(resp);
}); }
});
}); });
} }
@ -195,19 +208,21 @@ export default class Application extends React.Component<IProps> {
username, username,
password, password,
}), }),
}).then(data => data.json()) }).then(data => data.json(), err => {
.then((resp: IResponse) => { // The fetch failed
if (resp.error === "0") { rej(err);
// Successful login }).then((resp: IResponse) => {
this.props.setUser(resp.data); if (resp.error === "0") {
setSessionToken(window, resp.data.sessionToken); // Successful login
this.props.setAuthenticated(true); this.props.setUser(resp.data);
setSessionToken(window, resp.data.sessionToken);
this.props.setAuthenticated(true);
res(resp.data); res(resp.data);
} else { } else {
rej(resp); rej(resp);
} }
}); });
}); });
} }
@ -241,7 +256,7 @@ export default class Application extends React.Component<IProps> {
path="/dashboard" path="/dashboard"
component={() => { component={() => {
return <Dashboard return <Dashboard
nextLevel={this.getNextLevel} getNextLevel={this.getNextLevel}
getLastReview={this.getLastReview} getLastReview={this.getLastReview}
getTopTen={this.getTopTenLearners} /> getTopTen={this.getTopTenLearners} />
}} /> }} />

View File

@ -7,15 +7,14 @@ import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { ILearner } from "../models/learner"; import { TopTen } from "../models/learner";
interface IProps { interface IProps {
topTen: ILearner[]; topTen: TopTen[];
} }
export default class Scoreboard extends React.Component<IProps> { export default class Scoreboard extends React.Component<IProps> {
private unique = 0; private unique = 0;
private nr = 1;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -28,11 +27,11 @@ export default class Scoreboard extends React.Component<IProps> {
return "SCOREBOARD" + this.unique++; return "SCOREBOARD" + this.unique++;
} }
tableRow(learner: ILearner) { tableRow(learner: TopTen) {
return <TableRow key={this.genId()}> return <TableRow key={this.genId()}>
<TableCell> <TableCell>
<Typography variant="title" component="b"> <Typography variant="title" component="b">
{this.nr++} {learner.nr}
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

@ -0,0 +1,28 @@
import { connect } from "react-redux";
import { setNextLevel, setDashboardNLLoading, setTopTen, setDashboardTTLoading } from "../actions";
import { ILevel } from "../models/level";
import DashboardPage from "../pages/dashboard";
const mapStateToProps = state => {
return {
nextLevel: state.nextLevel,
loadingNextLevel: state.dashboard.loadingNL,
loadingTopTen: state.dashboard.loadingTT,
topTen: state.topTen,
};
};
const mapDispatchToProps = dispatch => {
return {
setLoadingNL: (state: boolean) => dispatch(setDashboardNLLoading(state)),
setNextLevel: (level: ILevel) => dispatch(setNextLevel(level)),
setTopTen: (topTen: ILearner[]) => dispatch(setTopTen(topTen)),
setLoadingTT: (state: boolean) => dispatch(setDashboardTTLoading(state)),
}
};
const DashboardContainer = connect(mapStateToProps,
mapDispatchToProps)(DashboardPage);
export default DashboardContainer;

View File

@ -1,6 +1,6 @@
import { connect } from "react-redux"; import { connect } from "react-redux";
import { setDrawer } from "../actions"; import { setDrawer, setScorePopover } from "../actions";
import Drawer from "../components/Drawer"; import Drawer from "../components/Drawer";
@ -10,11 +10,13 @@ const mapStateToProps = state => {
open: state.drawer, open: state.drawer,
authenticated: state.authenticated, authenticated: state.authenticated,
showButton: state.drawerButton, showButton: state.drawerButton,
scorePopoverOpen: state.scorePopoverOpen,
}; };
}; };
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
setDrawer: (show: boolean) => dispatch(setDrawer(show)), setDrawer: (show: boolean) => dispatch(setDrawer(show)),
setScorePopover: (state: boolean) => dispatch(setScorePopover(state)),
}; };
}; };

View File

@ -3,3 +3,5 @@ export interface ILearner {
level: number; level: number;
score: number; score: number;
} }
export type TopTen = ILearner & { nr: number };

View File

@ -3,6 +3,7 @@ export interface IUser {
uid: string; uid: string;
showWelcome: boolean; showWelcome: boolean;
score: number; score: number;
classId: string;
sessionToken: string; sessionToken: string;
}; };

View File

@ -6,52 +6,93 @@ import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import CircularProgress from "@material-ui/core/CircularProgress";
import Scoreboard from "../components/scoreboard"; import Scoreboard from "../components/scoreboard";
import SummaryTable from "../components/SummaryTable"; import SummaryTable from "../components/SummaryTable";
import { ILevel } from "../models/level"; import { ILevel } from "../models/level";
import { ILearner } from "../models/learner"; import { ILearner, TopTen } from "../models/learner";
import { IReviewMetadata } from "../models/review"; import { IReviewMetadata } from "../models/review";
interface IProps { interface IProps {
nextLevel: () => ILevel; getNextLevel: () => Promise<ILevel>;
getLastReview: () => IReviewMetadata; getLastReview: () => IReviewMetadata;
getTopTen: () => ILearner[]; getTopTen: () => Promise<ILearner[]>;
nextLevel: ILevel;
loadingNextLevel: boolean;
setLoadingNL: (state: boolean) => void;
setNextLevel: (level: ILevel) => void;
topTen: TopTen[];
loadingTopTen: boolean;
setLoadingTT: (state: boolean) => void;
setTopTen: (topten: TopTen[]) => void;
} }
export default class Dashboard extends React.Component<IProps> { export default class Dashboard extends React.Component<IProps> {
componentDidMount() {
this.props.setLoadingNL(true);
this.props.getNextLevel().then(res => {
this.props.setLoadingNL(false);
this.props.setNextLevel(res);
}, err => {
console.log("Failed to fetch next level!", err);
});
this.props.getTopTen().then(res => {
this.props.setLoadingTT(false);
this.props.setTopTen(res);
}, err => {
console.log("Failed to fetch Top Ten");
});
}
render() { render() {
const small = window.matchMedia("(max-width: 700px)").matches; const small = window.matchMedia("(max-width: 700px)").matches;
const direction = small ? "column" : "row"; const direction = small ? "column" : "row";
const level = this.props.nextLevel(); const level = this.props.nextLevel;
return <div> return <div>
<Grid container direction={direction} spacing={16}> <Grid container direction={direction} spacing={16}>
<Grid item lg={4}> <Grid item lg={4}>
<Paper className="paper"> <Paper className="paper">
<Typography variant="title">{`Level ${level.level}`}</Typography> {this.props.loadingNextLevel ? (
<Typography variant="title" component="p">{level.name}</Typography> <CircularProgress />
<br /> ) : (
<Typography component="p"> <div>
{level.desc} <Typography variant="title">{`Level ${level.level}`}</Typography>
</Typography> <Typography variant="title" component="p">{level.name}</Typography>
<Button <br />
component={Link} <Typography component="p">
to={`/level/${level.level}`} {level.desc}
className="lesson-card-btn"> </Typography>
Zum Level <Button
</Button> component={Link}
to={`/level/${level.level}`}
className="lesson-card-btn">
Zum Level
</Button>
</div>
)}
</Paper> </Paper>
</Grid> </Grid>
<Grid item lg={4}> <Grid item lg={4}>
<Paper className="paper"> <Paper className="paper">
<Typography variant="title" component="p"> {this.props.loadingTopTen ? (
Rangliste: Top 10 <CircularProgress />
</Typography> ) : (
<div>
<Typography variant="title" component="p">
Rangliste: Top 10
</Typography>
<Scoreboard topTen={this.props.getTopTen()} /> <Scoreboard topTen={this.props.topTen} />
</div>
)}
</Paper> </Paper>
</Grid> </Grid>
<Grid item lg={4}> <Grid item lg={4}>

View File

@ -46,7 +46,7 @@ const LoginPageWithRouter = withRouter(
} else { } else {
this.props.history.push("/dashboard"); this.props.history.push("/dashboard");
} }
}, (err: IResponse) => { }).catch((err: IResponse) => {
this.props.setLoading(false); this.props.setLoading(false);
this.props.setSnackbar(true, "Failed to log in"); this.props.setSnackbar(true, "Failed to log in");
}); });

View File

@ -8,6 +8,7 @@ import { IReviewMetadata } from "../models/review";
interface IState { interface IState {
drawer: boolean; drawer: boolean;
scorePopoverOpen: boolean;
drawerButton: boolean; drawerButton: boolean;
authenticated: boolean; authenticated: boolean;
@ -34,6 +35,11 @@ interface IState {
loading: boolean; loading: boolean;
}; };
dashboard: {
loadingNL: boolean;
loadingTT: boolean;
};
review: { review: {
current: IReviewCard; current: IReviewCard;
@ -48,6 +54,7 @@ interface IState {
topTen: ILearner[]; topTen: ILearner[];
nextLevel: ILevel;
lastReview: any; lastReview: any;
}; };
@ -56,6 +63,8 @@ const initialState: IState = {
drawer: false, drawer: false,
// Should we show the button to open the drawer? // Should we show the button to open the drawer?
drawerButton: true, drawerButton: true,
scorePopoverOpen: false,
// Is the user authenticated? // Is the user authenticated?
// TODO: Set this to false // TODO: Set this to false
authenticated: false, authenticated: false,
@ -83,6 +92,11 @@ const initialState: IState = {
loading: true, loading: true,
}, },
dashboard: {
loadingNL: true,
loadingTT: true,
},
review: { review: {
current: {} as IReviewCard, current: {} as IReviewCard,
@ -95,6 +109,7 @@ const initialState: IState = {
popoverTextColor: "", popoverTextColor: "",
}, },
nextLevel: {} as ILevel,
lastReview: { lastReview: {
correct: 0, correct: 0,
wrong: 0, wrong: 0,
@ -198,6 +213,31 @@ export function LateinicusApp(state: IState = initialState, action: any) {
loading: action.state, loading: action.state,
}), }),
}); });
case Actions.SET_SCORE_POPOVER:
return Object.assign({}, state, {
scorePopoverOpen: action.state,
});
case Actions.SET_NEXT_LEVEL:
return Object.assign({}, state, {
nextLevel: action.level,
});
case Actions.DASHBOARD_SET_NL_LOADING:
return Object.assign({}, state, {
dashboard: Object.assign({}, state.dashboard, {
loadingNL: action.state,
}),
});
case Actions.SET_TOP_TEN:
console.log(action.topTen);
return Object.assign({}, state, {
topTen: action.topTen,
});
case Actions.DASHBOARD_SET_TT_LOADING:
return Object.assign({}, state, {
dashboard: Object.assign({}, state.dashboard, {
loadingTT: 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