Add tracking of previous mistakes
This commit is contained in:
@@ -226,5 +226,9 @@ service cloud.firestore {
|
||||
match /completed_progress/{setIds} {
|
||||
allow get: if isSignedIn();
|
||||
}
|
||||
|
||||
match /incorrect_answers/{incorrectAnswerId} {
|
||||
allow read: if isSignedIn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
<Route path="/history" exact>
|
||||
<History db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
||||
</Route>
|
||||
<Route path="/history/mistakes" exact>
|
||||
<MistakesHistory db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
||||
</Route>
|
||||
<Route path="/tos" exact>
|
||||
<TermsOfService logEvent={analytics.logEvent} page={this.page} />
|
||||
</Route>
|
||||
|
||||
@@ -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: <TimelineRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
|
||||
209
src/MistakesHistory.js
Normal file
209
src/MistakesHistory.js
Normal file
@@ -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: <HistoryRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
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 (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
<main>
|
||||
<h1>Mistakes</h1>
|
||||
|
||||
<div className="history-sections">
|
||||
<div className="historical-user-stats-container">
|
||||
<div className="stat-row stat-row--inline">
|
||||
<h1>{this.state.totalIncorrect}</h1>
|
||||
<p>mistakes</p>
|
||||
</div>
|
||||
{
|
||||
this.state.incorrectAnswers.length > 0 &&
|
||||
<div className="stat-row stat-row--inline">
|
||||
<h1>{this.state.incorrectAnswers[0].definition}</h1>
|
||||
<p>meaning</p>
|
||||
<h1>{this.state.incorrectAnswers[0].term}</h1>
|
||||
<p>is the most common</p>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.totalTests > 0 &&
|
||||
<div className="stat-row stat-row--inline">
|
||||
<h1>{(this.state.totalIncorrect / this.state.totalTests).toFixed(2)}</h1>
|
||||
<p>mistakes per test on average</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="mistakes-history-container">
|
||||
{
|
||||
this.state.incorrectAnswers.map((vocabItem) => (
|
||||
<>
|
||||
<div>
|
||||
<h2>{vocabItem.term}</h2>
|
||||
<p><b>{vocabItem.switchedCount} mistake{vocabItem.switchedCount !== 1 && "s"}{vocabItem.switchedCount > 0 && ":"}</b></p>
|
||||
{
|
||||
vocabItem.switchedCount > 0 &&
|
||||
<div>
|
||||
{
|
||||
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 && (
|
||||
<p>{answerItem.answer === "" ? <i>skipped</i> : answerItem.answer}</p>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<h2>{vocabItem.definition}</h2>
|
||||
<p><b>{vocabItem.count} mistake{vocabItem.count !== 1 && "s"}{vocabItem.count > 0 && ":"}</b></p>
|
||||
{
|
||||
vocabItem.count > 0 &&
|
||||
<div>
|
||||
{
|
||||
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 && (
|
||||
<p>{answerItem.answer === "" ? <i>skipped</i> : answerItem.answer}</p>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
17
src/css/MistakesHistory.css
Normal file
17
src/css/MistakesHistory.css
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user