refactor: MONOREPO

This commit is contained in:
Alexander Polynomdivision
2018-09-20 17:38:12 +02:00
parent 4c9e328ad0
commit 909149fdc7
50 changed files with 222 additions and 3 deletions

View File

@@ -0,0 +1,74 @@
import * as React from "react";
import { Link } from "react-router-dom";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import Paper from "@material-ui/core/Paper";
import Scoreboard from "../components/scoreboard";
import SummaryTable from "../components/SummaryTable";
import { ILevel } from "../models/level";
import { ILearner } from "../models/learner";
import { IReviewMetadata } from "../models/review";
interface IProps {
nextLevel: () => ILevel;
getLastReview: () => IReviewMetadata;
getTopTen: () => ILearner[];
}
export default class Dashboard extends React.Component<IProps> {
render() {
const small = window.matchMedia("(max-width: 700px)").matches;
const direction = small ? "column" : "row";
const level = this.props.nextLevel();
return <div>
<Grid container direction={direction} spacing={16}>
<Grid item lg={4}>
<Paper className="paper">
<Typography variant="title">{`Level ${level.level}`}</Typography>
<Typography variant="title" component="p">{level.name}</Typography>
<br />
<Typography component="p">
{level.desc}
</Typography>
<Button
component={Link}
to={`/level/${level.level}`}
className="lesson-card-btn">
Zum Level
</Button>
</Paper>
</Grid>
<Grid item lg={4}>
<Paper className="paper">
<Typography variant="title" component="p">
Rangliste: Top 10
</Typography>
<Scoreboard topTen={this.props.getTopTen()} />
</Paper>
</Grid>
<Grid item lg={4}>
<Paper className="paper">
<Typography variant="title">
Letzte Wiederholung
</Typography>
<SummaryTable reviewMeta={this.props.getLastReview} />
<Button
component={Link}
to="/review/queue">
Vokabeln üben
</Button>
</Paper>
</Grid>
</Grid>
</div>;
}
};

View File

@@ -0,0 +1,83 @@
import * as React from "react";
import { Link } from "react-router-dom";
import Typography from "@material-ui/core/Typography";
import Grid from "@material-ui/core/Grid";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import CardActions from "@material-ui/core/CardActions";
import Button from "@material-ui/core/Button";
export default class IntroPage extends React.Component<{}> {
render() {
const small = window.matchMedia("(max-width: 700px)").matches;
const cName = small ? "intro-card-xs" : "intro-card-lg";
return <div>
<Grid container justify="center">
<Card className={cName}>
<CardContent>
<Typography
component="p"
variant="title">
Wilkommen bei Lateinicus!
</Typography>
<div className="vert-spacer" />
<Typography
className="intro-subheading"
component="p"
variant="subheading">
Was ist Lateinicus?
</Typography>
<Typography
variant="body1"
component="p">
Lateinicus ist eine experimentelle Lernanwendung für Lateinvokabeln. Mit Hilfe dieser
Anwendung wollen wir die Frage beantworten, ob "Gamification" tatsächlich effektiver
ist als klassisches Lernen mit Vokabelheft.
</Typography>
<Typography
variant="body1"
component="p">
Um die Effektivität zu bewerten wird vor und nach Verwendung von Lateinicus
ein Vokabeltest geschrieben, welche von uns zum Zwecke der Datenerhebung benutzt
werden.
</Typography>
<Typography
variant="body1"
component="p">
Zudem erfassen wir Daten darüber, wann und wie lange gelernt wird, damit wir diese
Werte auch in unser Fazit mit einfließen lassen können.
</Typography>
<div className="vert-spacer" />
<Typography
className="intro-subheading"
component="p"
variant="subheading">
Was ist Gamification?
</Typography>
<Typography
variant="body1">
Gamification ist ein Konzept, bei welchem eine Tätigkeit, wie hier das Lernen von
Lateinvokabeln, in ein Spiel umgewandelt wird.
</Typography>
</CardContent>
<CardActions>
{/*TODO: Tell the server to not show this page again*/}
<Button
fullWidth={true}
component={Link}
to="/dashboard">
Lass uns loslegen
</Button>
</CardActions>
</Card>
</Grid>
</div>;
}
};

View File

@@ -0,0 +1,187 @@
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 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 { withRouter } from "react-router-dom";
import { IVocab } from "../models/vocab";
interface IProps {
id: string;
levelVocab: (id: string) => Promise<IVocab[]>;
history: any;
loading: boolean;
setLoading: (state: boolean) => void;
vocab: IVocab[];
setVocab: (vocab: IVocab[]) => void;
setLookedAt: (ids: number[]) => void;
setCurrentVocab: (vocab: IVocab) => void;
drawerButtonState: (state: boolean) => void;
currentVocab: IVocab;
lookedAt: number[];
};
const LevelPageWithRouter = withRouter(
class LevelPage extends React.Component<IProps> {
private uid = 0;
// To prevent React from redrawing the vocabulary list and prematurely
// cancelling the animation
private uids: { [key: string]: string } = {};
componentDidMount() {
// Hide the drawer
this.props.drawerButtonState(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 => {
const { grundform } = vocab.latin;
if (grundform in this.uids) {
return this.uids[grundform];
} else {
this.uids[grundform] = "LEVELPAGE" + this.uid++;
return this.uids[grundform];
}
}
renderVocabListItem = (vocab: IVocab): any => {
// Check if the vocab was already looked at
const lookedAt = this.props.lookedAt.find((el) => el === vocab.id) || vocab.id === 0;
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 = () => {
const { vocab, lookedAt, id } = this.props;
// Only go to the review if all vocabulary item have been looked at
if (vocab.length === lookedAt.length) {
this.props.setLoading(true);
this.props.history.push(`/review/level/${id}`);
}
}
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 { currentVocab } = this.props;
return <div>
<Grid container direction="row">
<Grid item xs={3}>
<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 item style={{ margin: 12 }}>
<Card>
<CardContent>
<Typography gutterBottom variant="headline" component="h2">
{currentVocab.latin.grundform}
</Typography>
<Typography gutterBottom variant="headline" component="h3">
{currentVocab.german.join(", ")}
</Typography>
{
currentVocab.hint ? (
<div style={{
border: "dashed",
borderColor: "red",
padding: 12,
}}>
<Typography variant="subheading" component="p">
<b>Tipp:</b>
</Typography>
<Typography variant="body2">
{currentVocab.hint}
</Typography>
</div>
) : undefined
}
{
currentVocab.mnemonic ? (
<div style={{
border: "dashed",
borderColor: "#f1c40f",
marginTop: 12,
padding: 12,
}}>
<Typography variant="subheading" component="p">
<b>Eselsbrücke:</b>
</Typography>
<Typography variant="body2">
{currentVocab.mnemonic}
</Typography>
</div>
) : undefined
}
</CardContent>
{/*TODO: Maybe "next" and "prev" buttons?*/}
</Card>
</Grid>
</Grid>
</Grid>
</Grid>
</div>;
}
}
);
export default LevelPageWithRouter;

View File

@@ -0,0 +1,91 @@
import * as React from "react";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
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 { ILevel } from "../models/level";
interface IProps {
getLevels: () => Promise<ILevel[]>;
setLevels: (levels: ILevel[]) => void;
setLoading: (state: boolean) => void;
loading: boolean;
levels: ILevel[];
}
export default class Dashboard extends React.Component<IProps> {
componentDidMount() {
this.props.setLoading(true);
// Fetch the levels
this.props.getLevels().then(res => {
this.props.setLevels(res);
this.props.setLoading(false);
});
}
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 cName = small ? "lesson-card-xs" : "lesson-card-lg";
let key = 0;
const levelToCard = (level: ILevel) => {
return <Grid item key={key++}>
<Card style={{
width: small ? window.width - 32 : "300px"
}}>
<CardContent className={cName}>
<Typography variant="title">{`Level ${level.level}`}</Typography>
<Typography variant="title" component="p">{level.name}</Typography>
<br />
<Typography component="p">
{level.desc}
</Typography>
</CardContent>
<CardActions>
<Button
component={Link}
to={`/level/${level.level}`}
className="lesson-card-btn">
Zum Level
</Button>
</CardActions>
</Card>
</Grid>;
};
return <Grid container spacing={16} direction="row">
{this.props.levels.map(levelToCard)}
</Grid>
}
};

View File

@@ -0,0 +1,109 @@
import * as React from "react";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import LinearProgress from "@material-ui/core/LinearProgress";
import Snackbar from "@material-ui/core/Snackbar";
import { withRouter } from "react-router-dom";
import { IUser } from "../models/user";
interface IProps {
login: (username: string, password: string) => Promise<IUser | {}>;
authenticated: boolean;
history: any;
setLoading: (state: boolean) => void;
setSnackbar: (state: boolean, msg: string) => void;
loading: boolean;
snackOpen: boolean;
snackMsg: string;
}
const LoginPageWithRouter = withRouter(
class LoginPage extends React.Component<IProps> {
private usernameRef: any = undefined;
private passwordRef: any = undefined;
performLogin = () => {
this.props.setLoading(true);
const username = this.usernameRef.value || "";
const password = this.passwordRef.value || "";
this.props.login(username, password).then((res: IUser) => {
if (res.showWelcome) {
// If the user logs in for the first time, a welcome
// screen should be shown
this.props.history.push("/welcome");
} else {
this.props.history.push("/dashboard");
}
}, (err) => {
this.props.setLoading(false);
this.props.setSnackbar(true, "Failed to log in");
});
}
componentDidMount() {
// If we're already authenticated, we can skip the login page
if (this.props.authenticated) {
this.props.history.push("/dashboard");
}
}
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

@@ -0,0 +1,243 @@
import * as React from "react";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
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 { withRouter } from "react-router-dom";
import { IVocab, IReviewCard, vocabToReviewCard, reviewQTypeToStr } from "../models/vocab";
import { ReviewType, IReviewMetadata } from "../models/review";
import { levW } from "../algorithms/levenshtein";
import { LEVENSHTEIN_MAX_DISTANCE } from "../config";
import { Queue } from "../utils/queue";
interface IProps {
levelId?: number;
vocabByLevel?: (level: number) => Promise<IVocab[]>;
vocabByQueue?: () => Promise<IVocab[]>;
reviewType: ReviewType;
history: any;
loading: boolean;
vocab: IVocab[];
current: IReviewCard;
popoverOpen: boolean;
popoverText: string;
popoverColor: string;
popoverTextColor: string;
setSummary: (state: boolean) => void;
setPopover: (state: boolean, text: string, color: string, textColor: string) => void;
drawerButtonState: (state: boolean) => void;
setLastReview: (meta: IReviewMetadata) => void;
setReview: (curent: IReviewCard, meta: IReviewMetadata) => void;
setLoading: (state: boolean) => void;
}
const ReviewPageWithRouter = withRouter(
class ReviewPage extends React.Component<IProps> {
private vocab: IVocab[] = [];
private reviewQueue: Queue<IReviewCard> = 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);
// 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;
// Just to make TSC shut up
const noopPromise = () => {
return new Promise<IVocab[]>((res, rej) => {
rej([]);
});
};
const vocabByLevelW = vocabByLevel || noopPromise;
const vocabByQueueW = vocabByQueue || noopPromise;
const getVocab = {
[ReviewType.LEVEL]: () => vocabByLevelW(levelId),
[ReviewType.QUEUE]: () => vocabByQueueW(),
}[reviewType];
getVocab().then((res: IVocab[]) => {
this.vocab = res;
vocToQueue();
});
}
increaseMeta = (correct: number, wrong: number): IReviewMetadata => {
const { metadata } = this;
return {
wrong: metadata.wrong + wrong,
correct: metadata.correct + correct,
};
}
vocabFromId = (id: number): IVocab | {} => {
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.inputRef.value || "";
// Map all possible answers to lowercase ( => ignore casing)
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("Review Queue size:", this.reviewQueue.size());
// Check if the user's answer was correct
if (minDist === 0) {
// TODO: Show it's correct?
// Show the next vocab word
if (this.reviewQueue.size() === 0) {
// Go to the summary screen
this.props.setLastReview(this.metadata);
this.props.setLoading(true);
this.props.history.push("/review/summary");
} else {
// Increase the vocab
this.props.setReview(this.reviewQueue.dequeue(), this.increaseMeta(1, 0));
this.inputRef.value = "";
}
} else if (minDist <= LEVENSHTEIN_MAX_DISTANCE) {
this.props.setPopover(true, "Das war fast richtig", "yellow", "black");
} else {
// Find the IVocab item
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);
} else {
console.log("[ReviewPage::checkInput] Could not find IVocab item for wrong IReviewCard");
}
this.props.setPopover(true, "Das war nicht richtig", "red", "white");
}
// TODO(?): Show a snackbar for showing the updated score
}
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 { question, qtype } = this.props.current;
const questionTitle = `${question} (${reviewQTypeToStr(qtype)})`;
// TODO:
const progress = 50;
return <div>
<Grid container justify="center">
<Grid item style={{ width: "100%" }}>
<Card>
<CardContent>
<Grid container direction="column">
<Typography variant="display2">
{questionTitle}
</Typography>
<TextField
margin="normal"
fullWidth={true}
inputRef={node => this.inputRef = node}
onKeyPress={(ev) => {
// Allow checking of the answer by pressing Enter
if (ev.key === "Enter")
this.checkInput();
}} />
<LinearProgress
variant="determinate"
value={progress} />
<Popover
open={this.props.popoverOpen}
anchorOrigin={{
vertical: "center",
horizontal: "center"
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center"
}}
anchorEl={this.buttonRef}
onClose={() => this.props.setPopover(false, "", "", "")}
PaperProps={{
style: {
backgroundColor: this.props.popoverColor,
padding: 10,
color: this.props.popoverTextColor,
}
}}>
<Typography
variant="button"
color="inherit">
{this.props.popoverText}
</Typography>
</Popover>
<Button
onClick={this.checkInput}
buttonRef={node => this.buttonRef = node}>
Prüfen
</Button>
</Grid>
</CardContent>
</Card>
</Grid>
</Grid>
</div>;
}
}
);
export default ReviewPageWithRouter;

View File

@@ -0,0 +1,56 @@
import * as React from "react";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import SummaryTable from "../components/SummaryTable";
import { withRouter } from "react-router-dom";
import { IReviewMetadata } from "../models/review";
interface IProps {
history: any;
reviewMeta: IReviewMetadata;
setDrawerButton: (state: boolean) => void;
}
// TODO: This stays at the default value
const SummaryPageWithRouter = withRouter(
class SummaryPage extends React.Component<IProps> {
toDashboard = () => {
// Show the drawer button
this.props.setDrawerButton(true);
// Go to the dashboard
this.props.history.push("/dashboard");
}
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">Zusammenfassung</Typography>
<Grid container direction="column">
<SummaryTable reviewMeta={() => this.props.reviewMeta} />
<Button onClick={this.toDashboard}>
Zum Dashboard
</Button>
</Grid>
</Paper>
</Grid>
</Grid>
</div>;
}
}
);
export default SummaryPageWithRouter;