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