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, 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: , hideTextMobile: true, }, { type: "button", onClick: this.showSettings, icon: , hideTextMobile: true, }, { type: "link", link: "/", icon: , 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); } } } cleanseVocabString = (item) => { const chars = " .,()-_'\""; let newString = item; chars.split("").forEach((char) => { newString = newString.replace(char, ""); }); return newString; } 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 (
{ this.props.page.loaded && (this.state.progressInaccessible ? : <> { this.state.currentAnswerStatus === null && !this.state.setComplete ?

{this.state.currentPrompt}

e.preventDefault()} > (this.answerInput = inputEl)} autoComplete="off" autoCapitalize="none" autoCorrect="off" />

Are you sure?

{ this.state.currentCorrect && this.state.currentCorrect.length > 0 ? <>

{ this.state.moreAnswers ? this.state.numberOfAnswers !== null ? `${this.state.currentCorrect.length} of ${this.state.numberOfAnswers} correct so far:` : "Correct so far:" : "Answers:" }

{this.state.currentCorrect.map((vocab, index) =>

{vocab}

)} : this.state.numberOfAnswers !== null ?

{this.state.currentCorrect.length} of {this.state.numberOfAnswers} correct so far

: "" }
: 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}

e.preventDefault()} > (this.answerInput = inputEl)} readOnly autoComplete="off" autoCapitalize="none" autoCorrect="off" />

{ this.state.currentAnswerStatus ? "Correct!" : "Incorrect" }

{ this.state.currentCorrect ? <>

{ this.state.moreAnswers ? this.state.numberOfAnswers !== null ? `${this.state.currentCorrect.length} of ${this.state.numberOfAnswers} correct so far:` : "Correct so far:" : "Answers:" }

{this.state.currentCorrect.map((vocab, index) =>

{vocab}

)} : this.state.numberOfAnswers !== null ?

{this.state.currentCorrect.length} of {this.state.numberOfAnswers} correct so far

: "" }
} { !this.state.setComplete && 0 ? this.state.correct / (this.state.correct + this.state.incorrect) * 100 : 0} maxQuestions={this.state.mode === "lives" ? this.state.originalTotalQuestions : null} /> }
) } })