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 LinkButton from "./LinkButton";
import Error404 from "./Error404";
import SettingsContent from "./SettingsContent";
import Footer from "./Footer";
import LineChart from './LineChart';
import ProgressStats from './ProgressStats';
import "./css/PopUp.css";
import "./css/Progress.css";
import "./css/Chart.css";
export default withRouter(class Progress extends React.Component {
constructor(props) {
super(props);
this.state = {
user: props.user,
db: props.db,
functions: {
processAnswer: props.functions.httpsCallable("processAnswer"),
},
loading: false,
canProceed: true,
navbarItems: [
{
type: "button",
onClick: this.showSettings,
icon: ,
hideTextMobile: true,
},
{
type: "link",
link: "/",
icon: ,
hideTextMobile: true,
}
],
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,
setIds: [],
attemptNumber: 1,
attemptHistory: {},
questions: [],
originalTotalQuestions: 1,
lives: 1,
startLives: null,
setComplete: false,
};
let isMounted = true;
Object.defineProperty(this, "isMounted", {
get: () => isMounted,
set: (value) => isMounted = value,
});
}
setState = (state, callback=null) => {
if (this.isMounted) super.setState(state, callback);
}
async componentDidMount() {
const progressId = this.props.match.params.progressId;
const progressRef = this.state.db.collection("progress").doc(progressId);
let [ newState, setDone, incorrectAnswers, duration ] = 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,
};
if (data.lives) {
newState.lives = data.lives;
newState.startLives = data.start_lives;
}
return [ newState, data.duration !== null, data.incorrect, data.duration ];
}).catch((error) => {
console.log(`Progress data inaccessible: ${error}`);
return [
{
progressInaccessible: true,
},
true
];
});
if (!newState.progressInaccessible && !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 if (setDone) {
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) => ({
x: new Date(doc.data().start_time),
y: (doc.data().correct.length / doc.data().questions.length * 100),
}));
}));
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 (!setDone) this.answerInput.focus();
});
this.props.logEvent("select_content", {
content_type: "progress",
item_id: this.props.match.params.progressId,
});
}
componentWillUnmount() {
this.isMounted = false;
}
showSettings = () => {
this.setState({
showSettings: true,
});
}
handleSoundInputChange = (event) => {
this.setState({
soundInput: event.target.checked,
});
}
handleThemeInputChange = (newTheme) => {
if (this.state.themeInput !== newTheme) this.setState({
themeInput: newTheme,
});
}
saveSettings = (globalChange) => {
this.props.handleSoundChange(this.state.soundInput, globalChange);
this.props.handleThemeChange(this.state.themeInput, globalChange);
this.hideSettings();
}
hideSettings = () => {
this.setState({
showSettings: false,
});
}
handleAnswerInput = (event) => {
this.setState({
answerInput: event.target.value,
});
}
showNextItem = () => {
if (this.state.canProceed) {
if (this.state.currentAnswerStatus === null) {
this.processAnswer();
} else {
this.nextQuestion();
}
}
}
startLoading = () => {
this.setState({
loading: true,
canProceed: false,
});
}
cleanseVocabString = (item) => {
const chars = " .,()-_'\"";
let newString = item;
chars.split("").forEach((char) => {
newString = newString.replace(char, "");
});
return newString;
}
processAnswer = async () => {
if (this.state.canProceed) {
const cleansedCurrentCorrect = this.state.currentCorrect.map(item => this.cleanseVocabString(item));
this.startLoading();
if (!cleansedCurrentCorrect.includes(this.cleanseVocabString(this.state.answerInput))) {
this.state.functions.processAnswer({
answer: this.state.answerInput,
progressId: this.props.match.params.progressId,
}).then(async (result) => {
const data = result.data;
let newState = {
currentAnswerStatus: data.correct,
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,
};
if (data.correct) {
this.props.logEvent("correct_answer", {
progress_id: this.props.match.params.progressId,
});
} else {
this.props.logEvent("incorrect_answer", {
progress_id: this.props.match.params.progressId,
});
}
if (this.state.mode === "lives") newState.lives = data.lives;
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;
} else if (!data.correct) {
// 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,
};
}
let promises = [];
if (data.duration) {
// test done
newState.duration = data.duration;
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) => ({
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,
});
});
} else {
this.setState({
currentAnswerStatus: null,
answerInput: "",
loading: false,
canProceed: true,
});
}
}
}
nextQuestion = () => {
if (this.state.canProceed) {
this.startLoading();
let newState = {
currentAnswerStatus: null,
answerInput: "",
loading: false,
canProceed: true,
};
if (!this.state.moreAnswers) {
if (this.state.nextPrompt === null) newState.setComplete = true;
newState.currentCorrect = [];
newState.currentPrompt = this.state.nextPrompt;
newState.currentSound = this.state.nextSound;
newState.currentSetOwner = this.state.nextSetOwner;
}
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}`;
}
render() {
return (
{
this.state.progressInaccessible
?
:
<>
{
this.state.currentAnswerStatus === null && !this.state.setComplete
?
{this.state.currentPrompt}
{
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
You took
{this.msToTime(this.state.duration)}
Attempt #
{this.state.attemptNumber}
{
this.state.startLives &&
with
{this.state.startLives}
lives
}
{
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
>
}
Done
:
{/* ANSWER PROCESSED */}
{this.state.currentPrompt}
{
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"
>
>
}
>
}
)
}
})