diff --git a/firestore.rules b/firestore.rules index e73e464..4a6986b 100644 --- a/firestore.rules +++ b/firestore.rules @@ -226,5 +226,9 @@ service cloud.firestore { match /completed_progress/{setIds} { allow get: if isSignedIn(); } + + match /incorrect_answers/{incorrectAnswerId} { + allow read: if isSignedIn(); + } } } diff --git a/functions/index.js b/functions/index.js index 3e7183d..9dd960a 100644 --- a/functions/index.js +++ b/functions/index.js @@ -434,17 +434,12 @@ exports.processAnswer = functions.https.onCall((data, context) => { const currentIndex = progressDoc.data().progress; const currentVocab = progressDoc.data().questions[currentIndex]; - let answerDocId; - - if (progressDoc.data().switch_language) { - answerDocId = progressDocId + termDocId = progressDocId .collection("terms").doc(currentVocab); - } else { - answerDocId = progressDocId + definitionDocId = progressDocId .collection("definitions").doc(currentVocab); - } - return transaction.get(answerDocId).then((answerDoc) => { + return transaction.get(progressDoc.data().switch_language ? termDocId : definitionDocId).then((answerDoc) => { const docData = progressDoc.data(); const mode = docData.mode; const correctAnswers = answerDoc.data().item; @@ -507,6 +502,8 @@ exports.processAnswer = functions.https.onCall((data, context) => { docData.typo = false; + var userGroups, incorrectAnswerDoc, prompt; + if (isCorrectAnswer) { if (mode === "lives") { returnData.lives = docData.lives; @@ -542,8 +539,13 @@ exports.processAnswer = functions.https.onCall((data, context) => { docData.questions = doneQuestions.concat(shuffleArray(notDoneQuestions)); returnData.totalQuestions = docData.questions.length; returnData.totalIncorrect = docData.incorrect.length; + + userGroups = transaction.get(db.collection("users").doc(uid).collection("groups")).then((querySnapshot) => querySnapshot.docs.map((doc) => doc.id)); + incorrectAnswerDoc = db.collection("incorrect_answers").doc(); + prompt = transaction.get(progressDoc.data().switch_language ? definitionDocId : termDocId).then((doc) => doc.data().item); } + if (!returnData.moreAnswers) { if (docData.progress >= docData.questions.length || (mode === "lives" && docData.lives <= 0)) { const duration = Date.now() - docData.start_time; @@ -554,7 +556,16 @@ exports.processAnswer = functions.https.onCall((data, context) => { if (mode === "lives" && docData.lives <= 0) docData.questions.length = returnData.totalQuestions = docData.progress; const completedProgressDocId = db.collection("completed_progress").doc(progressDoc.data().setIds.sort().join("__")); - return transaction.get(completedProgressDocId).then((completedProgressDoc) => { + return transaction.get(completedProgressDocId).then(async (completedProgressDoc) => { + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + }); + const totalPercentage = completedProgressDoc.data().total_percentage + (docData.correct.length / docData.questions.length * 100); const attempts = completedProgressDoc.data().attempts + 1; transaction.set(completedProgressDocId, { @@ -564,7 +575,16 @@ exports.processAnswer = functions.https.onCall((data, context) => { returnData.averagePercentage = (totalPercentage / attempts).toFixed(2); transaction.set(progressDocId, docData); return returnData; - }).catch((error) => { + }).catch(async (error) => { + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + }); + const totalPercentage = docData.correct.length / docData.questions.length * 100; transaction.set(completedProgressDocId, { attempts: 1, @@ -583,7 +603,16 @@ exports.processAnswer = functions.https.onCall((data, context) => { .collection("definitions").doc(nextVocabId); const sound = null; - return transaction.get(promptDocId).then((promptDoc) => { + return transaction.get(promptDocId).then(async (promptDoc) => { + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + }); + returnData.nextPrompt = { item: promptDoc.data().item, sound: sound, @@ -596,7 +625,16 @@ exports.processAnswer = functions.https.onCall((data, context) => { const promptDocId = progressDocId .collection("terms").doc(nextVocabId); - return transaction.get(promptDocId).then((promptDoc) => { + return transaction.get(promptDocId).then(async (promptDoc) => { + if (!isCorrectAnswer) transaction.set(incorrectAnswerDoc, { + uid: uid, + groups: await userGroups, + term: progressDoc.data().switch_language ? correctAnswers : await prompt, + definition: progressDoc.data().switch_language ? await prompt : correctAnswers, + answer: inputAnswer.trim(), + switch_language: progressDoc.data().switch_language, + }); + const sound = promptDoc.data().sound; returnData.nextPrompt = { item: promptDoc.data().item, diff --git a/src/App.js b/src/App.js index 3a46891..bd66eae 100644 --- a/src/App.js +++ b/src/App.js @@ -14,6 +14,7 @@ import UserSets from "./UserSets"; import EditSet from "./EditSet"; import Error404 from "./Error404"; import History from "./History"; +import MistakesHistory from "./MistakesHistory"; import TermsOfService from "./TermsOfService"; import PrivacyPolicy from "./PrivacyPolicy"; import Button from "./Button"; @@ -320,6 +321,9 @@ class App extends React.Component { + + + diff --git a/src/History.js b/src/History.js index 62bcffd..1edcdb6 100644 --- a/src/History.js +++ b/src/History.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { HomeRounded as HomeRoundedIcon, QuestionAnswerRounded as QuestionAnswerRoundedIcon, PeopleRounded as PeopleRoundedIcon, SwapHorizRounded as SwapHorizRoundedIcon, DeleteRounded as DeleteRoundedIcon } from "@material-ui/icons"; +import { TimelineRounded as TimelineRoundedIcon, HomeRounded as HomeRoundedIcon, QuestionAnswerRounded as QuestionAnswerRoundedIcon, PeopleRounded as PeopleRoundedIcon, SwapHorizRounded as SwapHorizRoundedIcon, DeleteRounded as DeleteRoundedIcon } from "@material-ui/icons"; import NavBar from "./NavBar"; import Button from "./Button"; import Footer from "./Footer"; @@ -15,6 +15,13 @@ export default class History extends Component { user: props.user, db: props.db, navbarItems: [ + { + type: "link", + name: "Mistakes", + link: "/history/mistakes", + icon: , + hideTextMobile: true, + }, { type: "link", link: "/", diff --git a/src/MistakesHistory.js b/src/MistakesHistory.js new file mode 100644 index 0000000..cbb2bbb --- /dev/null +++ b/src/MistakesHistory.js @@ -0,0 +1,209 @@ +import React, { Component } from 'react'; +import { HistoryRounded as HistoryRoundedIcon, HomeRounded as HomeRoundedIcon, QuestionAnswerRounded as QuestionAnswerRoundedIcon, PeopleRounded as PeopleRoundedIcon, SwapHorizRounded as SwapHorizRoundedIcon, DeleteRounded as DeleteRoundedIcon } from "@material-ui/icons"; +import NavBar from "./NavBar"; +import Footer from "./Footer"; +import "./css/History.css"; +import "./css/MistakesHistory.css"; + +export default class IncorrectHistory extends Component { + constructor(props) { + super(props); + this.state = { + user: props.user, + db: props.db, + navbarItems: [ + { + type: "link", + name: "History", + link: "/history", + icon: , + hideTextMobile: true, + }, + { + type: "link", + link: "/", + icon: , + hideTextMobile: true, + } + ], + incorrectAnswers: [], + totalIncorrect: 0, + totalTests: 0, + }; + + 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() { + document.title = "Incorrect | History | Parandum"; + + let promises = []; + let newState = {}; + + promises.push( + this.state.db.collection("incorrect_answers") + .where("uid", "==", this.state.user.uid) + .orderBy("term", "asc") + .get() + .then((querySnapshot) => { + let incorrectAnswers = []; + querySnapshot.docs.map((doc, index, array) => { + if (index === 0 || doc.data().term !== array[index - 1].data().term || doc.data().definition !== array[index - 1].data().definition) { + incorrectAnswers.push({ + term: doc.data().term, + definition: doc.data().definition, + answers: [{ + answer: doc.data().answer, + switchLanguage: doc.data().switch_language, + }], + count: doc.data().switch_language ? 0 : 1, + switchedCount: doc.data().switch_language ? 1 : 0, + }); + } else { + incorrectAnswers[incorrectAnswers.length - 1].answers.push({ + answer: doc.data().answer, + switchLanguage: doc.data().switch_language, + }); + if (doc.data().switch_language) { + incorrectAnswers[incorrectAnswers.length - 1].switchedCount++; + } else { + incorrectAnswers[incorrectAnswers.length - 1].count++; + } + } + return true; + }); + newState.incorrectAnswers = incorrectAnswers.sort((a,b) => b.count + b.switchedCount - a.count - a.switchedCount); + newState.totalIncorrect = querySnapshot.docs.length; + }) + ); + + promises.push( + this.state.db.collection("progress") + .where("uid", "==", this.state.user.uid) + .get() + .then((querySnapshot) => newState.totalTests = querySnapshot.docs.length) + ); + + await Promise.all(promises); + + this.setState(newState); + + this.props.page.load(); + + this.props.logEvent("page_view"); + } + + componentWillUnmount() { + this.isMounted = false; + this.props.page.unload(); + } + + 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 ( +
+ +
+

Mistakes

+ +
+
+
+

{this.state.totalIncorrect}

+

mistakes

+
+ { + this.state.incorrectAnswers.length > 0 && +
+

{this.state.incorrectAnswers[0].definition}

+

meaning

+

{this.state.incorrectAnswers[0].term}

+

is the most common

+
+ } + { + this.state.totalTests > 0 && +
+

{(this.state.totalIncorrect / this.state.totalTests).toFixed(2)}

+

mistakes per test on average

+
+ } +
+
+ { + this.state.incorrectAnswers.map((vocabItem) => ( + <> +
+

{vocabItem.term}

+

{vocabItem.switchedCount} mistake{vocabItem.switchedCount !== 1 && "s"}{vocabItem.switchedCount > 0 && ":"}

+ { + vocabItem.switchedCount > 0 && +
+ { + vocabItem.answers.sort((a, b) => { + if (a.answer < b.answer) { + return -1; + } + if (a.answer > b.answer) { + return 1; + } + return 0; + }).map((answerItem) => answerItem.switchLanguage && ( +

{answerItem.answer === "" ? skipped : answerItem.answer}

+ )) + } +
+ } +
+
+

{vocabItem.definition}

+

{vocabItem.count} mistake{vocabItem.count !== 1 && "s"}{vocabItem.count > 0 && ":"}

+ { + vocabItem.count > 0 && +
+ { + vocabItem.answers.sort((a,b) => { + if (a.answer < b.answer) { + return -1; + } + if (a.answer > b.answer) { + return 1; + } + return 0; + }).map((answerItem) => !answerItem.switchLanguage && ( +

{answerItem.answer === "" ? skipped : answerItem.answer}

+ )) + } +
+ } +
+ + )) + } +
+
+
+
+ ) + } +} diff --git a/src/css/MistakesHistory.css b/src/css/MistakesHistory.css new file mode 100644 index 0000000..76353b9 --- /dev/null +++ b/src/css/MistakesHistory.css @@ -0,0 +1,17 @@ +.mistakes-history-container { + display: grid; + grid-column-gap: 12px; + grid-template-columns: repeat(2, minmax(110px,max-content)); + width: min-content; + word-wrap: break-word; + word-break: break-word; + width: 100%; +} + +.mistakes-history-container > div { + margin-bottom: 4px; +} + +.mistakes-history-container > div:last-child { + margin-bottom: 0; +} \ No newline at end of file