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 } 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 "./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,
}
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.showSettings,
icon: ,
hideTextMobile: true,
},
{
type: "link",
link: "/",
icon: ,
hideTextMobile: true,
}
],
...this.changeableStateItems,
};
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,
};
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;
}).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,
});
}
}
proceed = () => {
if (this.state.canProceed) {
if (this.state.currentAnswerStatus === null) {
this.processAnswer();
} else {
this.nextQuestion();
}
}
}
cleanseVocabString = (item) => {
const chars = " .,()-_'\"";
let newString = item;
chars.split("").forEach((char) => {
newString = newString.replace(char, "");
});
return newString;
}
processAnswer = async () => {
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.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) 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;
newState.currentCorrect = [];
newState.currentPrompt = currentState.nextPrompt;
newState.currentSound = currentState.nextSound;
newState.currentSetOwner = currentState.nextSetOwner;
return newState;
}
nextQuestion = () => {
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.isMounted && !this.state.setComplete) && 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,
}).then((result) => {
const progressId = result.data;
this.stopLoading();
this.props.history.push("/progress/" + progressId);
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.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,
});
}
render() {
return (
{
this.props.page.loaded &&
(this.state.progressInaccessible
?
:
<>
{
this.state.currentAnswerStatus === null && !this.state.setComplete
?
{this.state.currentPrompt}
Are you sure?
{
this.state.currentCorrect && this.state.currentCorrect.length > 0
?
<>
{
this.state.moreAnswers
?
"Correct so far:"
:
"Answers:"
}
{this.state.currentCorrect.map((vocab, index) =>
{vocab}
)}
>
:
""
}
:
this.state.nextPrompt === null && !this.state.moreAnswers && this.state.setComplete
?
{/* DONE */}
{this.state.setTitle}
{
this.state.mode === "questions"
?
:
}
You got
{`${(this.state.correct / this.state.totalQuestions * 100).toFixed(2)}%`}
{`${this.state.correct} of ${this.state.totalQuestions}`}
marks
{
this.state.averagePercentage !== null &&
The average is
{`${this.state.averagePercentage}%`}
}
You took
{this.msToTime(this.state.duration)}
Attempt #
{this.state.attemptNumber}
{
this.state.startLives &&
with
{this.state.startLives}
lives
}
{
this.state.incorrect > 0 &&
}
{
this.state.incorrectAnswers && Object.keys(this.state.incorrectAnswers).length > 0 &&
<>
Incorrect answers:
Prompt
Answer
Mistakes
{
Object.keys(this.state.incorrectAnswers).map(key =>
[key, this.state.incorrectAnswers[key].count])
.sort((a,b) => b[1] - a[1]).map(item =>
{this.state.incorrectAnswers[item[0]].prompt ? this.state.incorrectAnswers[item[0]].prompt : ""}
{this.state.incorrectAnswers[item[0]].answer ? this.state.incorrectAnswers[item[0]].answer.join("/") : ""}
{this.state.incorrectAnswers[item[0]].count}
)
}
>
}
{Object.keys(this.state.attemptHistory).length > 1 &&
<>
History
>
}
:
{/* ANSWER PROCESSED */}
{this.state.currentPrompt}
{
this.state.currentAnswerStatus ? "Correct!" : "Incorrect"
}
{
this.state.currentCorrect
?
<>
{
this.state.moreAnswers
?
"Correct so far:"
:
"Answers:"
}
{this.state.currentCorrect.map((vocab, index) =>
{vocab}
)}
>
:
""
}
}
{
!this.state.setComplete &&
0 ? this.state.correct / (this.state.correct + this.state.incorrect) * 100 : 0}
maxQuestions={this.state.mode === "lives" ? this.state.originalTotalQuestions : null}
/>
}
{
this.state.showSettings &&
<>
}
className="button--no-background popup-close-button"
>
>
}
{
this.state.showTestRestart &&
}
{
this.state.showIncorrectTestStart &&
}
>)
}
)
}
})