1069 lines
31 KiB
JavaScript
1069 lines
31 KiB
JavaScript
import React from 'react';
|
|
import { withRouter } from "react-router-dom";
|
|
import { HomeRounded as HomeRoundedIcon, ArrowForwardRounded as ArrowForwardRoundedIcon, SettingsRounded as SettingsRoundedIcon, CloseRounded as CloseRoundedIcon, PeopleRounded as PeopleRoundedIcon, QuestionAnswerRounded as QuestionAnswerRoundedIcon, KeyboardRounded as KeyboardRoundedIcon } from "@material-ui/icons";
|
|
import NavBar from "./NavBar";
|
|
import Button from "./Button";
|
|
import Error404 from "./Error404";
|
|
import SettingsContent from "./SettingsContent";
|
|
import Footer from "./Footer";
|
|
import LineChart from './LineChart';
|
|
import ProgressStats from './ProgressStats';
|
|
import ConfirmationDialog from "./ConfirmationDialog";
|
|
import CorrectSound from "./CorrectSound.mp3";
|
|
import IncorrectSound from "./IncorrectSound.mp3";
|
|
import Keyboard from 'react-simple-keyboard';
|
|
import 'react-simple-keyboard/build/css/index.css';
|
|
import KeyboardLayouts from "simple-keyboard-layouts";
|
|
|
|
import "./css/PopUp.css";
|
|
import "./css/Progress.css";
|
|
import "./css/Chart.css";
|
|
|
|
export default withRouter(class Progress extends React.Component {
|
|
changeableStateItems = {
|
|
loading: false,
|
|
canProceed: true,
|
|
canStartTest: true,
|
|
showTestRestart: false,
|
|
showIncorrectTestStart: false,
|
|
progressInaccessible: false,
|
|
correct: 0,
|
|
incorrect: 0,
|
|
totalQuestions: 0,
|
|
progress: 0,
|
|
setTitle: "",
|
|
switchLanguage: false,
|
|
answerInput: "",
|
|
currentPrompt: "",
|
|
currentSound: false,
|
|
currentSetOwner: "",
|
|
nextPrompt: "",
|
|
nextSound: false,
|
|
nextSetOwner: "",
|
|
currentAnswerStatus: null,
|
|
currentCorrect: [],
|
|
moreAnswers: true,
|
|
duration: 0,
|
|
incorrectAnswers: {},
|
|
showSettings: false,
|
|
soundInput: this.props.sound,
|
|
themeInput: this.props.theme,
|
|
coloredEdgesInput: this.props.coloredEdges,
|
|
setIds: [],
|
|
attemptNumber: 1,
|
|
attemptHistory: {},
|
|
questions: [],
|
|
originalTotalQuestions: 1,
|
|
lives: 1,
|
|
startLives: null,
|
|
setComplete: false,
|
|
averagePercentage: null,
|
|
pageLoaded: false,
|
|
startTime: null,
|
|
sound: false,
|
|
ignoreCaps: false,
|
|
ignoreAccents: false,
|
|
numberOfAnswers: null,
|
|
virtualKeyboardLanguage: "english",
|
|
virtualKeyboardLayoutName: "default",
|
|
showVirtualKeyboard: false,
|
|
showVirtualKeyboardOptions: false,
|
|
}
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
user: props.user,
|
|
db: props.db,
|
|
functions: {
|
|
processAnswer: props.functions.httpsCallable("processAnswer"),
|
|
createProgress: props.functions.httpsCallable("createProgress"),
|
|
createProgressWithIncorrect: props.functions.httpsCallable("createProgressWithIncorrect"),
|
|
},
|
|
navbarItems: [
|
|
{
|
|
type: "button",
|
|
onClick: this.showVirtualKeyboardOptions,
|
|
icon: <KeyboardRoundedIcon />,
|
|
hideTextMobile: true,
|
|
},
|
|
{
|
|
type: "button",
|
|
onClick: this.showSettings,
|
|
icon: <SettingsRoundedIcon />,
|
|
hideTextMobile: true,
|
|
},
|
|
{
|
|
type: "link",
|
|
link: "/",
|
|
icon: <HomeRoundedIcon />,
|
|
hideTextMobile: true,
|
|
}
|
|
],
|
|
...this.changeableStateItems,
|
|
};
|
|
|
|
this.keyboardLayouts = new KeyboardLayouts();
|
|
this.keyboardLayoutsObj = this.keyboardLayouts.get();
|
|
this.keyboardLayoutNames = Object.keys(this.keyboardLayoutsObj).map(key =>
|
|
[
|
|
key,
|
|
// title case from camel case
|
|
key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, " $1")
|
|
]
|
|
);
|
|
|
|
let isMounted = true;
|
|
Object.defineProperty(this, "isMounted", {
|
|
get: () => isMounted,
|
|
set: (value) => isMounted = value,
|
|
});
|
|
|
|
this.correctSound = new Audio(CorrectSound);
|
|
this.incorrectSound = new Audio(IncorrectSound);
|
|
}
|
|
|
|
setState = (state, callback=null) => {
|
|
if (this.isMounted) super.setState(state, callback);
|
|
}
|
|
|
|
async componentDidMount() {
|
|
this.unlisten = this.props.history.listen((location, action) => {
|
|
if (location.pathname.startsWith("/progress/")) this.setState(this.changeableStateItems, () => this.componentDidMount());
|
|
});
|
|
|
|
const progressId = this.props.match.params.progressId;
|
|
const progressRef = this.state.db.collection("progress").doc(progressId);
|
|
|
|
let [ newState, setDone, incorrectAnswers, duration, setIds ] = await progressRef.get().then((doc) => {
|
|
const data = doc.data();
|
|
|
|
document.title = `Study | ${data.set_title} | Parandum`;
|
|
|
|
let newState = {
|
|
correct: data.correct.length,
|
|
incorrect: data.incorrect.length,
|
|
totalQuestions: data.questions.length,
|
|
questions: data.questions,
|
|
progress: data.progress,
|
|
setTitle: data.set_title,
|
|
switchLanguage: data.switch_language,
|
|
currentSetOwner: data.set_owner,
|
|
currentCorrect: data.current_correct,
|
|
mode: data.mode,
|
|
nextPrompt: null,
|
|
setIds: data.setIds,
|
|
originalTotalQuestions: [...new Set(data.questions)].length,
|
|
setComplete: data.duration !== null,
|
|
pageLoaded: true,
|
|
numberOfAnswers: data.showNumberOfAnswers ? 0 : null,
|
|
ignoreCaps: data.ignoreCaps === true,
|
|
};
|
|
|
|
if (data.lives) {
|
|
newState.lives = data.lives;
|
|
newState.startLives = data.start_lives;
|
|
}
|
|
|
|
return [ newState, data.duration !== null, data.incorrect, data.duration, data.setIds ];
|
|
}).catch((error) => {
|
|
console.log(`Progress data inaccessible: ${error}`);
|
|
return [
|
|
{
|
|
progressInaccessible: true,
|
|
},
|
|
true
|
|
];
|
|
});
|
|
|
|
if (!newState.progressInaccessible) {
|
|
if (!setDone) {
|
|
let nextPromptRef;
|
|
if (!newState.switchLanguage) {
|
|
nextPromptRef = progressRef
|
|
.collection("terms")
|
|
.doc(newState.questions[newState.progress]);
|
|
} else {
|
|
nextPromptRef = progressRef
|
|
.collection("definitions")
|
|
.doc(newState.questions[newState.progress]);
|
|
}
|
|
|
|
await nextPromptRef.get().then((doc) => {
|
|
newState.currentPrompt = doc.data().item;
|
|
newState.currentSound = doc.data().sound === true;
|
|
if (newState.numberOfAnswers === 0) newState.numberOfAnswers = doc.data().numberOfAnswers;
|
|
}).catch((error) => {
|
|
newState.progressInaccessible = true;
|
|
console.log(`Progress data inaccessible: ${error}`);
|
|
});
|
|
} else {
|
|
newState.moreAnswers = false;
|
|
newState.currentAnswerStatus = true;
|
|
newState.duration = duration;
|
|
|
|
let promises = [];
|
|
promises.push(this.state.db.collection("progress")
|
|
.where("uid", "==", this.state.user.uid)
|
|
.where("setIds", "==", newState.setIds)
|
|
.orderBy("start_time")
|
|
.get()
|
|
.then((querySnapshot) => {
|
|
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
|
if (querySnapshot.docs.length > 1)
|
|
newState.attemptHistory = querySnapshot.docs.filter((doc) => doc.data().duration !== null)
|
|
.map((doc) => {
|
|
if (doc.id === this.props.match.params.progressId) newState.startTime = doc.data().start_time;
|
|
return {
|
|
x: new Date(doc.data().start_time),
|
|
y: (doc.data().correct.length / doc.data().questions.length * 100),
|
|
}
|
|
});
|
|
}));
|
|
|
|
promises.push(this.state.db.collection("completed_progress")
|
|
.doc(setIds.sort().join("__"))
|
|
.get()
|
|
.then((completedProgressDoc) => {
|
|
newState.averagePercentage = (completedProgressDoc.data().total_percentage / completedProgressDoc.data().attempts).toFixed(2);
|
|
}).catch((error) => {
|
|
console.log(`Couldn't get average percentage: ${error}`);
|
|
newState.averagePercentage = null;
|
|
}));
|
|
|
|
if (incorrectAnswers.length > 0) {
|
|
newState.incorrectAnswers = {};
|
|
|
|
promises.push(Promise.all(incorrectAnswers.map((vocabId) => {
|
|
if (newState.incorrectAnswers[vocabId]) {
|
|
return newState.incorrectAnswers[vocabId].count++;
|
|
} else {
|
|
newState.incorrectAnswers[vocabId] = {
|
|
count: 1,
|
|
};
|
|
|
|
return Promise.all([
|
|
progressRef.collection("terms")
|
|
.doc(vocabId)
|
|
.get().then((termDoc) => {
|
|
newState.switchLanguage ? newState.incorrectAnswers[vocabId].answer = termDoc.data().item.split("/") : newState.incorrectAnswers[vocabId].prompt = termDoc.data().item;
|
|
}),
|
|
progressRef.collection("definitions")
|
|
.doc(vocabId)
|
|
.get().then((definitionDoc) => {
|
|
newState.switchLanguage ? newState.incorrectAnswers[vocabId].prompt = definitionDoc.data().item : newState.incorrectAnswers[vocabId].answer = definitionDoc.data().item.split("/");
|
|
})
|
|
]);
|
|
}
|
|
})).catch((error) => {
|
|
console.log(`Couldn't retrieve incorrect answers: ${error}`);
|
|
}));
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
}
|
|
}
|
|
|
|
this.setState(newState, () => {
|
|
if (!newState.progressInaccessible && !setDone) this.answerInput.focus();
|
|
});
|
|
|
|
this.props.page.load();
|
|
|
|
this.props.logEvent("select_content", {
|
|
content_type: "progress",
|
|
item_id: this.props.match.params.progressId,
|
|
});
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.isMounted = false;
|
|
this.props.page.unload();
|
|
this.unlisten();
|
|
}
|
|
|
|
showSettings = () => {
|
|
this.setState({
|
|
showSettings: true,
|
|
});
|
|
}
|
|
|
|
handleSoundInputChange = (event) => {
|
|
this.setState({
|
|
soundInput: event.target.checked,
|
|
});
|
|
}
|
|
|
|
handleThemeInputChange = (newTheme) => {
|
|
if (this.state.themeInput !== newTheme) this.setState({
|
|
themeInput: newTheme,
|
|
});
|
|
}
|
|
|
|
handleColoredEdgesInputChange = (event) => {
|
|
this.setState({
|
|
coloredEdgesInput: event.target.checked,
|
|
});
|
|
}
|
|
|
|
saveSettings = (globalChange) => {
|
|
this.props.handleSoundChange(this.state.soundInput, globalChange);
|
|
this.props.handleThemeChange(this.state.themeInput, globalChange);
|
|
this.props.handleColoredEdgesChange(this.state.coloredEdgesInput, globalChange);
|
|
this.hideSettings();
|
|
}
|
|
|
|
hideSettings = () => {
|
|
this.setState({
|
|
showSettings: false,
|
|
});
|
|
}
|
|
|
|
handleAnswerInput = (event) => {
|
|
if (this.state.canProceed) {
|
|
this.setState({
|
|
answerInput: event.target.value,
|
|
});
|
|
this.virtualKeyboard.setInput(event.target.value);
|
|
}
|
|
}
|
|
|
|
onVirtualKeyboardChange = (answerInput) => {
|
|
this.setState({ answerInput });
|
|
};
|
|
|
|
onVirtualKeyboardKeyPress = (button) => {
|
|
if (button === "{shift}" || button === "{lock}") {
|
|
this.handleVirtualKeyboardShift();
|
|
} else if (button === "{enter}") {
|
|
this.proceed("virtual");
|
|
}
|
|
};
|
|
|
|
handleVirtualKeyboardShift = () => {
|
|
this.setState({
|
|
layoutName: this.state.virtualKeyboardLayoutName === "default" ? "shift" : "default",
|
|
});
|
|
}
|
|
|
|
proceed = (referrer="physical") => {
|
|
if (this.state.canProceed) {
|
|
if (this.state.currentAnswerStatus === null) {
|
|
this.processAnswer(referrer);
|
|
} else {
|
|
this.nextQuestion(referrer);
|
|
}
|
|
}
|
|
}
|
|
|
|
processAnswer = async (referrer="physical") => {
|
|
if (this.state.canProceed) {
|
|
this.startLoading();
|
|
|
|
this.state.functions.processAnswer({
|
|
answer: this.state.answerInput,
|
|
progressId: this.props.match.params.progressId,
|
|
}).then(async (result) => {
|
|
if (result.data.typo) {
|
|
this.setState({
|
|
typo: true,
|
|
loading: false,
|
|
canProceed: true,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const data = result.data;
|
|
let newState = {
|
|
currentAnswerStatus: data.correct ? null : false,
|
|
currentCorrect: data.correctAnswers,
|
|
moreAnswers: data.moreAnswers,
|
|
nextPrompt: data.nextPrompt ? data.nextPrompt.item : null,
|
|
nextSound: data.nextPrompt ? data.nextPrompt.sound : null,
|
|
nextSetOwner: data.nextPrompt ? data.nextPrompt.set_owner : null,
|
|
progress: data.progress,
|
|
totalQuestions: data.totalQuestions,
|
|
correct: data.totalCorrect,
|
|
incorrect: data.totalIncorrect,
|
|
currentVocabId: data.currentVocabId,
|
|
loading: false,
|
|
canProceed: true,
|
|
typo: false,
|
|
canStartTest: true,
|
|
};
|
|
if (this.state.numberOfAnswers !== null && data.numberOfAnswers !== 0) newState.numberOfAnswers = data.numberOfAnswers;
|
|
|
|
if (this.state.mode === "lives") newState.lives = data.lives;
|
|
|
|
if (data.correct) {
|
|
// correct answer given
|
|
newState.answerInput = "";
|
|
// play sound
|
|
if (this.props.sound) {
|
|
this.correctSound.currentTime = 0;
|
|
this.correctSound.play();
|
|
}
|
|
// show next question if there are no more answers
|
|
if (!data.moreAnswers) newState = this.showNextQuestion(newState, newState);
|
|
|
|
this.props.logEvent("correct_answer", {
|
|
progress_id: this.props.match.params.progressId,
|
|
});
|
|
} else {
|
|
// incorrect answer given
|
|
// store prompt and count=0
|
|
// store answer if in lives mode and no lives left
|
|
newState.incorrectAnswers = this.state.incorrectAnswers;
|
|
newState.incorrectAnswers[data.currentVocabId] = {
|
|
prompt: this.state.currentPrompt,
|
|
answer: data.lives === 0 ? data.correctAnswers : "",
|
|
count: 0,
|
|
};
|
|
// play sound
|
|
if (this.props.sound) {
|
|
this.incorrectSound.currentTime = 0;
|
|
this.incorrectSound.play();
|
|
}
|
|
|
|
this.props.logEvent("incorrect_answer", {
|
|
progress_id: this.props.match.params.progressId,
|
|
});
|
|
}
|
|
|
|
if ((data.correct || data.lives === 0) && !data.moreAnswers && this.state.incorrectAnswers[data.currentVocabId]) {
|
|
// all answers to question given correctly
|
|
// answer was previously wrong
|
|
// store correct answer
|
|
newState.incorrectAnswers = this.state.incorrectAnswers;
|
|
newState.incorrectAnswers[data.currentVocabId].answer = data.correctAnswers;
|
|
}
|
|
|
|
let promises = [];
|
|
|
|
if (data.duration) {
|
|
// test done
|
|
newState.duration = data.duration;
|
|
newState.averagePercentage = data.averagePercentage;
|
|
|
|
this.props.logEvent("test_complete", {
|
|
progress_id: this.props.match.params.progressId,
|
|
});
|
|
|
|
promises.push(this.state.db.collection("progress")
|
|
.where("uid", "==", this.state.user.uid)
|
|
.where("setIds", "==", this.state.setIds)
|
|
.orderBy("start_time")
|
|
.get()
|
|
.then((querySnapshot) => {
|
|
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
|
if (querySnapshot.docs.length > 1)
|
|
newState.attemptHistory = querySnapshot.docs.filter((doc) => doc.data().duration !== null)
|
|
.map((doc) => {
|
|
if (doc.id === this.props.match.params.progressId) newState.startTime = doc.data().start_time;
|
|
return {
|
|
x: new Date(doc.data().start_time),
|
|
y: (doc.data().correct.length / doc.data().questions.length * 100),
|
|
}
|
|
});
|
|
}));
|
|
}
|
|
|
|
if (data.incorrectAnswers) {
|
|
let unsavedAnswers = {};
|
|
|
|
if (!newState.incorrectAnswers) {
|
|
newState.incorrectAnswers = {};
|
|
}
|
|
|
|
data.incorrectAnswers.map((vocabId) => {
|
|
if (newState.incorrectAnswers[vocabId] && newState.incorrectAnswers[vocabId].answer !== "") {
|
|
// already been logged including prompt and correct answer
|
|
newState.incorrectAnswers[vocabId].count++;
|
|
} else {
|
|
// not been saved yet
|
|
// update count in unsaved answers
|
|
unsavedAnswers[vocabId] ? unsavedAnswers[vocabId]++ : unsavedAnswers[vocabId] = 1;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
promises.push(Promise.all(Object.keys(unsavedAnswers).map((vocabId) => {
|
|
// get and store vocab docs that haven't already been stored (due to refresh)
|
|
const progressDocRef = this.state.db
|
|
.collection("progress")
|
|
.doc(this.props.match.params.progressId);
|
|
|
|
newState.incorrectAnswers[vocabId] = {
|
|
count: unsavedAnswers[vocabId],
|
|
};
|
|
|
|
return Promise.all([
|
|
progressDocRef.collection("terms")
|
|
.doc(vocabId)
|
|
.get().then((termDoc) => {
|
|
this.state.switchLanguage ? newState.incorrectAnswers[vocabId].answer = termDoc.data().item.split("/") : newState.incorrectAnswers[vocabId].prompt = termDoc.data().item;
|
|
}),
|
|
progressDocRef.collection("definitions")
|
|
.doc(vocabId)
|
|
.get().then((definitionDoc) => {
|
|
this.state.switchLanguage ? newState.incorrectAnswers[vocabId].prompt = definitionDoc.data().item : newState.incorrectAnswers[vocabId].answer = definitionDoc.data().item.split("/");
|
|
})
|
|
]);
|
|
})));
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
this.setState(newState, () => {
|
|
if (!newState.duration && referrer === "physical") this.answerInput.focus();
|
|
});
|
|
}).catch((error) => {
|
|
console.log(`Couldn't process answer: ${error}`);
|
|
this.setState({
|
|
loading: false,
|
|
canProceed: true,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
showNextQuestion = (newState, currentState) => {
|
|
if (currentState.nextPrompt === null) {
|
|
newState.setComplete = true;
|
|
this.virtualKeyboard.clearInput();
|
|
newState.showVirtualKeyboard = false;
|
|
}
|
|
newState.currentCorrect = [];
|
|
newState.currentPrompt = currentState.nextPrompt;
|
|
newState.currentSound = currentState.nextSound;
|
|
newState.currentSetOwner = currentState.nextSetOwner;
|
|
return newState;
|
|
}
|
|
|
|
nextQuestion = (referrer="physical") => {
|
|
if (this.state.canProceed) {
|
|
this.startLoading();
|
|
|
|
let newState = {
|
|
currentAnswerStatus: null,
|
|
answerInput: "",
|
|
loading: false,
|
|
canProceed: true,
|
|
};
|
|
|
|
if (!this.state.moreAnswers) {
|
|
newState = this.showNextQuestion(newState, this.state);
|
|
}
|
|
|
|
this.setState(newState, () => {
|
|
this.virtualKeyboard.clearInput();
|
|
if (this.isMounted && !this.state.setComplete && referrer === "physical") {
|
|
this.answerInput.focus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
msToTime = (time) => {
|
|
const localeData = {
|
|
minimumIntegerDigits: 2,
|
|
useGrouping: false,
|
|
};
|
|
const seconds = Math.floor((time / 1000) % 60).toLocaleString("en-GB", localeData);
|
|
const minutes = Math.floor((time / 1000 / 60) % 60).toLocaleString("en-GB", localeData);
|
|
const hours = Math.floor(time / 1000 / 60 / 60).toLocaleString("en-GB", localeData);
|
|
|
|
return `${hours}:${minutes}:${seconds}`;
|
|
}
|
|
|
|
startLoading = () => {
|
|
this.setState({
|
|
canStartTest: false,
|
|
loading: true,
|
|
canProceed: false,
|
|
});
|
|
}
|
|
|
|
stopLoading = () => {
|
|
this.setState({
|
|
canStartTest: true,
|
|
loading: false,
|
|
});
|
|
}
|
|
|
|
recreateSameTest = () => {
|
|
if (!this.state.loading) {
|
|
this.state.functions.createProgress({
|
|
sets: this.state.setIds,
|
|
switch_language: this.state.switchLanguage,
|
|
mode: this.state.mode,
|
|
limit: this.state.mode === "questions" ? this.state.progress - this.state.incorrect
|
|
: this.state.mode === "lives" ? this.state.lives
|
|
: 1,
|
|
ignoreCaps: this.state.ignoreCaps,
|
|
showNumberOfAnswers: this.state.numberOfAnswers !== null,
|
|
}).then((result) => {
|
|
const progressId = result.data;
|
|
this.stopLoading();
|
|
this.props.history.push("/progress/" + progressId);
|
|
|
|
this.setState({
|
|
incorrectAnswers: {},
|
|
});
|
|
|
|
this.props.logEvent("restart_test", {
|
|
progress_id: progressId,
|
|
});
|
|
}).catch((error) => {
|
|
console.log(`Couldn't start test: ${error}`);
|
|
this.stopLoading();
|
|
});
|
|
|
|
this.startLoading();
|
|
}
|
|
}
|
|
|
|
createTestWithIncorrect = () => {
|
|
if (!this.state.loading) {
|
|
this.state.functions.createProgressWithIncorrect(this.props.match.params.progressId).then((result) => {
|
|
const progressId = result.data;
|
|
this.stopLoading();
|
|
this.props.history.push("/progress/" + progressId);
|
|
|
|
this.setState({
|
|
incorrectAnswers: {},
|
|
});
|
|
|
|
this.props.logEvent("start_test_with_incorrect", {
|
|
progress_id: progressId,
|
|
});
|
|
}).catch((error) => {
|
|
console.log(`Couldn't create test with incorrect answers: ${error}`);
|
|
this.stopLoading();
|
|
});
|
|
|
|
this.startLoading();
|
|
}
|
|
}
|
|
|
|
showTestRestart = () => {
|
|
if (this.state.canStartTest) {
|
|
this.setState({
|
|
showTestRestart: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
hideTestRestart = () => {
|
|
this.setState({
|
|
showTestRestart: false,
|
|
});
|
|
}
|
|
|
|
showIncorrectTestStart = () => {
|
|
if (this.state.canStartTest) {
|
|
this.setState({
|
|
showIncorrectTestStart: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
hideIncorrectTestStart = () => {
|
|
this.setState({
|
|
showIncorrectTestStart: false,
|
|
});
|
|
}
|
|
|
|
showVirtualKeyboardOptions = () => {
|
|
this.setState({
|
|
showVirtualKeyboardOptions: true,
|
|
})
|
|
}
|
|
|
|
hideVirtualKeyboardOptions = () => {
|
|
this.setState({
|
|
showVirtualKeyboardOptions: false,
|
|
})
|
|
}
|
|
|
|
showVirtualKeyboard = (language) => {
|
|
this.setState({
|
|
showVirtualKeyboard: true,
|
|
showVirtualKeyboardOptions: false,
|
|
virtualKeyboardLanguage: language,
|
|
});
|
|
}
|
|
|
|
hideVirtualKeyboard = () => {
|
|
this.setState({
|
|
showVirtualKeyboard: false,
|
|
showVirtualKeyboardOptions: false,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div>
|
|
{
|
|
this.props.page.loaded &&
|
|
(this.state.progressInaccessible
|
|
?
|
|
<Error404 />
|
|
:
|
|
<>
|
|
<NavBar items={this.state.navbarItems} />
|
|
{
|
|
this.state.currentAnswerStatus === null && !this.state.setComplete
|
|
?
|
|
<main className="progress-container">
|
|
<div>
|
|
<p className="current-prompt">{this.state.currentPrompt}</p>
|
|
<form className="answer-input-container" onSubmit={(e) => e.preventDefault()} >
|
|
<input type="submit" className="form-submit" onClick={this.proceed} />
|
|
<input
|
|
type="text"
|
|
name="answer_input"
|
|
className="answer-input"
|
|
onChange={this.handleAnswerInput}
|
|
value={this.state.answerInput}
|
|
ref={inputEl => (this.answerInput = inputEl)}
|
|
autoComplete="off"
|
|
autoCapitalize="none"
|
|
autoCorrect="off"
|
|
/>
|
|
<Button
|
|
onClick={() => this.proceed()}
|
|
icon={<ArrowForwardRoundedIcon />}
|
|
className="button--round"
|
|
disabled={!this.state.canProceed}
|
|
loading={this.state.loading}
|
|
></Button>
|
|
</form>
|
|
<p className={!this.state.typo ? "hide" : ""}>Are you sure?</p>
|
|
<div className="correct-answers">
|
|
{
|
|
this.state.currentCorrect && this.state.currentCorrect.length > 0
|
|
?
|
|
<>
|
|
<h2>
|
|
{
|
|
this.state.moreAnswers
|
|
?
|
|
this.state.numberOfAnswers !== null
|
|
?
|
|
`${this.state.currentCorrect.length} of ${this.state.numberOfAnswers} correct so far:`
|
|
:
|
|
"Correct so far:"
|
|
:
|
|
"Answers:"
|
|
}
|
|
</h2>
|
|
{this.state.currentCorrect.map((vocab, index) =>
|
|
<p key={index}>{vocab}</p>
|
|
)}
|
|
</>
|
|
:
|
|
this.state.numberOfAnswers !== null
|
|
?
|
|
<h2>
|
|
{this.state.currentCorrect.length} of {this.state.numberOfAnswers} correct so far
|
|
</h2>
|
|
:
|
|
""
|
|
}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
:
|
|
this.state.nextPrompt === null && !this.state.moreAnswers && this.state.setComplete
|
|
?
|
|
<main>
|
|
{/* DONE */}
|
|
<div className="page-header">
|
|
<h1>
|
|
{this.state.setTitle}
|
|
<span className="title-icon">
|
|
{
|
|
this.state.mode === "questions"
|
|
?
|
|
<QuestionAnswerRoundedIcon />
|
|
:
|
|
<PeopleRoundedIcon />
|
|
}
|
|
</span>
|
|
</h1>
|
|
</div>
|
|
<div className="progress-stat-row-container">
|
|
<div className="stat-row stat-row--inline">
|
|
<p>You got</p>
|
|
<h1>{`${(this.state.correct / this.state.totalQuestions * 100).toFixed(2)}%`}</h1>
|
|
</div>
|
|
<div className="stat-row stat-row--inline">
|
|
<h1>{`${this.state.correct} of ${this.state.totalQuestions}`}</h1>
|
|
<p>marks</p>
|
|
</div>
|
|
{
|
|
this.state.averagePercentage !== null &&
|
|
<div className="stat-row stat-row--inline">
|
|
<p>The average is</p>
|
|
<h1>{`${this.state.averagePercentage}%`}</h1>
|
|
</div>
|
|
}
|
|
<div className="stat-row stat-row--inline">
|
|
<p>You took</p>
|
|
<h1>{this.msToTime(this.state.duration)}</h1>
|
|
</div>
|
|
<div className="stat-row stat-row--inline stat-row--no-gap">
|
|
<p>Attempt #</p>
|
|
<h1>{this.state.attemptNumber}</h1>
|
|
</div>
|
|
{
|
|
this.state.startLives &&
|
|
<div className="stat-row stat-row--inline">
|
|
<p>with</p>
|
|
<h1>{this.state.startLives}</h1>
|
|
<p>lives</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div className="progress-end-button-container">
|
|
<Button
|
|
onClick={this.showTestRestart}
|
|
>
|
|
Restart test
|
|
</Button>
|
|
{
|
|
this.state.incorrect > 0 &&
|
|
<Button
|
|
onClick={this.showIncorrectTestStart}
|
|
>
|
|
Create test with incorrect
|
|
</Button>
|
|
}
|
|
</div>
|
|
|
|
{
|
|
this.state.incorrectAnswers && Object.keys(this.state.incorrectAnswers).length > 0 &&
|
|
<>
|
|
<h2>Incorrect answers:</h2>
|
|
<div className="progress-end-incorrect-answers">
|
|
<div>
|
|
<h3>Prompt</h3>
|
|
<h3>Answer</h3>
|
|
<h3>Mistakes</h3>
|
|
</div>
|
|
{
|
|
Object.keys(this.state.incorrectAnswers).map(key =>
|
|
[key, this.state.incorrectAnswers[key].count])
|
|
.sort((a,b) => b[1] - a[1]).map(item =>
|
|
<div key={item[0]}>
|
|
<p>{this.state.incorrectAnswers[item[0]].prompt ? this.state.incorrectAnswers[item[0]].prompt : ""}</p>
|
|
<p>{this.state.incorrectAnswers[item[0]].answer ? this.state.incorrectAnswers[item[0]].answer.join("/") : ""}</p>
|
|
<p>{this.state.incorrectAnswers[item[0]].count}</p>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
</>
|
|
}
|
|
|
|
{Object.keys(this.state.attemptHistory).length > 1 &&
|
|
<>
|
|
<h2 className="chart-title">History</h2>
|
|
<LineChart data={this.state.attemptHistory} currentPointX={this.state.startTime} />
|
|
</>
|
|
}
|
|
</main>
|
|
:
|
|
<main className="progress-container">
|
|
{/* ANSWER PROCESSED */}
|
|
<div>
|
|
<p className="current-prompt">{this.state.currentPrompt}</p>
|
|
<form className="answer-input-container answer-input-container--answer-entered" onSubmit={(e) => e.preventDefault()} >
|
|
<input type="submit" className="form-submit" onClick={this.proceed} />
|
|
<input
|
|
type="text"
|
|
name="answer_input"
|
|
className={`answer-input ${this.state.currentAnswerStatus ? "answer--correct" : "answer--incorrect"}`}
|
|
value={this.state.answerInput}
|
|
ref={inputEl => (this.answerInput = inputEl)}
|
|
readOnly
|
|
autoComplete="off"
|
|
autoCapitalize="none"
|
|
autoCorrect="off"
|
|
/>
|
|
<Button
|
|
onClick={() => this.proceed()}
|
|
icon={<ArrowForwardRoundedIcon />}
|
|
className="button--round"
|
|
disabled={!this.state.canProceed}
|
|
loading={this.state.loading}
|
|
></Button>
|
|
</form>
|
|
<p className={this.state.currentAnswerStatus ? "answer--correct" : "answer--incorrect"}>
|
|
{
|
|
this.state.currentAnswerStatus ? "Correct!" : "Incorrect"
|
|
}
|
|
</p>
|
|
<div className="correct-answers">
|
|
{
|
|
this.state.currentCorrect
|
|
?
|
|
<>
|
|
<h2>
|
|
{
|
|
this.state.moreAnswers
|
|
?
|
|
this.state.numberOfAnswers !== null
|
|
?
|
|
`${this.state.currentCorrect.length} of ${this.state.numberOfAnswers} correct so far:`
|
|
:
|
|
"Correct so far:"
|
|
:
|
|
"Answers:"
|
|
}
|
|
</h2>
|
|
{this.state.currentCorrect.map((vocab, index) =>
|
|
<p key={index}>{vocab}</p>
|
|
)}
|
|
</>
|
|
:
|
|
this.state.numberOfAnswers !== null
|
|
?
|
|
<h2>
|
|
{this.state.currentCorrect.length} of {this.state.numberOfAnswers} correct so far
|
|
</h2>
|
|
:
|
|
""
|
|
}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
}
|
|
{
|
|
!this.state.setComplete &&
|
|
<ProgressStats
|
|
correct={this.state.correct}
|
|
incorrect={this.state.incorrect}
|
|
progressNumerator={this.state.mode === "lives" ? this.state.lives : this.state.correct}
|
|
progressDenominator={this.state.mode === "lives" ? this.state.startLives : this.state.originalTotalQuestions}
|
|
progress={this.state.progress}
|
|
grade={(this.state.correct + this.state.incorrect) > 0 ? this.state.correct / (this.state.correct + this.state.incorrect) * 100 : 0}
|
|
maxQuestions={this.state.mode === "lives" ? this.state.originalTotalQuestions : null}
|
|
/>
|
|
}
|
|
<Footer />
|
|
|
|
{
|
|
this.state.showSettings &&
|
|
<>
|
|
<div className="overlay" onClick={this.hideSettings}></div>
|
|
<div className="overlay-content">
|
|
<SettingsContent
|
|
sound={this.props.sound}
|
|
theme={this.props.theme}
|
|
saveSettings={this.saveSettings}
|
|
handleSoundInputChange={this.handleSoundInputChange}
|
|
handleThemeInputChange={this.handleThemeInputChange}
|
|
handleColoredEdgesInputChange={this.handleColoredEdgesInputChange}
|
|
themes={this.props.themes}
|
|
soundInput={this.state.soundInput}
|
|
themeInput={this.state.themeInput}
|
|
coloredEdgesInput={this.state.coloredEdgesInput}
|
|
/>
|
|
|
|
|
|
<div className="settings-save-container">
|
|
<Button
|
|
onClick={() => this.saveSettings(true)}
|
|
>
|
|
Save
|
|
</Button>
|
|
<Button
|
|
onClick={() => this.saveSettings(false)}
|
|
>
|
|
Save for this session
|
|
</Button>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={this.hideSettings}
|
|
icon={<CloseRoundedIcon />}
|
|
className="button--no-background popup-close-button"
|
|
></Button>
|
|
</div>
|
|
</>
|
|
}
|
|
{
|
|
this.state.showTestRestart &&
|
|
<ConfirmationDialog
|
|
yesFunction={this.recreateSameTest}
|
|
noFunction={this.hideTestRestart}
|
|
message="Restart test?"
|
|
loading={this.state.loading}
|
|
/>
|
|
}
|
|
{
|
|
this.state.showIncorrectTestStart &&
|
|
<ConfirmationDialog
|
|
yesFunction={this.createTestWithIncorrect}
|
|
noFunction={this.hideIncorrectTestStart}
|
|
message="Create test with incorrect answers?"
|
|
loading={this.state.loading}
|
|
/>
|
|
}
|
|
{
|
|
this.state.showVirtualKeyboardOptions &&
|
|
<>
|
|
<div className="overlay" onClick={this.hideVirtualKeyboardOptions}></div>
|
|
<div className="overlay-content">
|
|
<h1>Virtual Keyboard</h1>
|
|
|
|
<Button
|
|
onClick={() => this.hideVirtualKeyboard()}
|
|
className="hide-virtual-keyboard-button"
|
|
icon={<KeyboardRoundedIcon/>}
|
|
>
|
|
Hide
|
|
</Button>
|
|
|
|
<h3>Or select a language:</h3>
|
|
<div className="inline-option-buttons-container">
|
|
{
|
|
this.keyboardLayoutNames.map(([layout, formattedName]) =>
|
|
<Button
|
|
onClick={() => this.showVirtualKeyboard(layout)}
|
|
className="button--no-background"
|
|
key={layout}
|
|
>
|
|
{formattedName}
|
|
</Button>
|
|
)
|
|
}
|
|
</div>
|
|
|
|
<Button
|
|
onClick={this.hideVirtualKeyboardOptions}
|
|
icon={<CloseRoundedIcon />}
|
|
className="button--no-background popup-close-button"
|
|
></Button>
|
|
</div>
|
|
</>
|
|
}
|
|
<div style={{display: this.state.showVirtualKeyboard ? "block" : "none" }}>
|
|
<Keyboard
|
|
keyboardRef={r => (this.virtualKeyboard = r)}
|
|
onChange={this.onVirtualKeyboardChange}
|
|
onKeyPress={this.onVirtualKeyboardKeyPress}
|
|
layout={this.keyboardLayoutsObj[this.state.virtualKeyboardLanguage].layout}
|
|
layoutName={this.state.layoutName}
|
|
/>
|
|
</div>
|
|
</>)
|
|
}
|
|
</div>
|
|
)
|
|
}
|
|
})
|