From ac520b66e167cf17f074d5f9cb473754ee7bf60b Mon Sep 17 00:00:00 2001 From: Matthew Grove Date: Tue, 19 Oct 2021 23:00:25 +0100 Subject: [PATCH] Add set-specific history tracking for group owners --- package-lock.json | 162 +++++++++++++++---- package.json | 1 + src/App.js | 2 +- src/GroupPage.js | 52 +++--- src/GroupStats.js | 305 ++++++++++++++++++++++++++++-------- src/LoggedInHome.js | 74 ++++++--- src/css/App.css | 1 + src/css/MistakesHistory.css | 4 + 8 files changed, 454 insertions(+), 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98a564f..2318702 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1370,6 +1370,18 @@ "kuler": "^2.0.0" } }, + "@emotion/cache": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.5.0.tgz", + "integrity": "sha512-mAZ5QRpLriBtaj/k2qyrXwck6yeoz1V5lMt/jfj6igWU35yYlNKs2LziXVgvH81gnJZ+9QQNGelSsnuoAy6uIw==", + "requires": { + "@emotion/memoize": "^0.7.4", + "@emotion/sheet": "^1.0.3", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "stylis": "^4.0.10" + } + }, "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -1388,6 +1400,44 @@ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" }, + "@emotion/react": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.5.0.tgz", + "integrity": "sha512-MYq/bzp3rYbee4EMBORCn4duPQfgpiEB5XzrZEBnUZAL80Qdfr7CEv/T80jwaTl/dnZmt9SnTa8NkTrwFNpLlw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@emotion/cache": "^11.5.0", + "@emotion/serialize": "^1.0.2", + "@emotion/sheet": "^1.0.3", + "@emotion/utils": "^1.0.0", + "@emotion/weak-memoize": "^0.2.5", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "requires": { + "@emotion/hash": "^0.8.0", + "@emotion/memoize": "^0.7.4", + "@emotion/unitless": "^0.7.5", + "@emotion/utils": "^1.0.0", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz", + "integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==" + } + } + }, + "@emotion/sheet": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.0.3.tgz", + "integrity": "sha512-YoX5GyQ4db7LpbmXHMuc8kebtBGP6nZfRC5Z13OKJMixBEwdZrJ914D6yJv/P+ZH/YY3F5s89NYX2hlZAf3SRQ==" + }, "@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", @@ -1398,6 +1448,16 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "@emotion/utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz", + "integrity": "sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==" + }, + "@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -13928,6 +13988,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -14351,6 +14416,12 @@ "yargs-unparser": "2.0.0" }, "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -14465,6 +14536,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, "js-yaml": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", @@ -14522,6 +14599,25 @@ "picomatch": "^2.2.1" } }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -14546,6 +14642,15 @@ "isexe": "^2.0.0" } }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "yargs-parser": { "version": "20.2.4", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", @@ -17596,6 +17701,20 @@ } } }, + "react-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.1.0.tgz", + "integrity": "sha512-SkEBD1AYsSXrIdNj5HBt7+Ehe+jxdiB448J0atJqR6lE3l/GcFlRf4JYB3NlHe/02jrW4AnIQLo1t0IqWrxXOw==", + "requires": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.1.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0" + } + }, "react-shallow-renderer": { "version": "16.14.1", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", @@ -19560,6 +19679,11 @@ } } }, + "stylis": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz", + "integrity": "sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==" + }, "superstatic": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-7.1.0.tgz", @@ -22192,40 +22316,12 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "widest-line": { diff --git a/package.json b/package.json index fadf948..a0d8027 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-firebaseui": "^5.0.2", "react-router-dom": "^5.3.0", "react-scripts": "^4.0.3", + "react-select": "^5.1.0", "react-xarrows": "^2.0.2", "styled-components": "^5.3.1", "typescript": "^4.3.5", diff --git a/src/App.js b/src/App.js index 519a403..62ffbcb 100644 --- a/src/App.js +++ b/src/App.js @@ -128,7 +128,7 @@ class App extends React.Component { }; this.page = { - loaded: !this.state.pageLoading, + loaded: () => !this.state.pageLoading, load: () => { this.setState({ pageLoading: false, diff --git a/src/GroupPage.js b/src/GroupPage.js index 7503158..07b69df 100644 --- a/src/GroupPage.js +++ b/src/GroupPage.js @@ -232,6 +232,10 @@ export default withRouter(class GroupPage extends Component { }; delete newState.sets[setId]; this.setState(newState); + }).catch((error) => { + console.log(`Can't remove set from group: ${error}`); + newLoadingState.sets[setId].loading = false; + this.setState(newLoadingState); }); } @@ -443,26 +447,36 @@ export default withRouter(class GroupPage extends Component { ?
{ - Object.keys(this.state.sets).map((setId) => -
- - {this.state.sets[setId].displayName} - - { - this.state.role === "owner" && - + Object.keys(this.state.sets) + .sort((a, b) => { + if (this.state.sets[a].displayName < this.state.sets[b].displayName) { + return -1; } -
- ) + if (this.state.sets[a].displayName > this.state.sets[b].displayName) { + return 1; + } + return 0; + }) + .map((setId) => +
+ + {this.state.sets[setId].displayName} + + { + this.state.role === "owner" && + + } +
+ ) }
: diff --git a/src/GroupStats.js b/src/GroupStats.js index 2cf0738..8ccbac6 100644 --- a/src/GroupStats.js +++ b/src/GroupStats.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { ArrowDropDownRounded as ArrowDropDownRoundedIcon, GroupRounded as GroupRoundedIcon, HomeRounded as HomeRoundedIcon } from "@material-ui/icons"; import { withRouter } from 'react-router-dom'; +import Select from "react-select"; import NavBar from "./NavBar"; import Footer from "./Footer"; import Error404 from "./Error404"; @@ -8,6 +9,7 @@ import "./css/History.css"; import "./css/MistakesHistory.css"; import Collapsible from "react-collapsible"; +import Checkbox from '@material-ui/core/Checkbox'; export default withRouter(class GroupStats extends Component { constructor(props) { @@ -31,6 +33,15 @@ export default withRouter(class GroupStats extends Component { ], role: null, groupName: "", + sets: {}, + selectedSet: { + value: "all_sets", + label: "All sets", + }, + includeCompoundTests: true, + incorrectAnswers: [], + filteredIncorrectAnswers: [], + setsWithHistory: {}, }; let isMounted = true; @@ -46,7 +57,10 @@ export default withRouter(class GroupStats extends Component { async componentDidMount() { let promises = []; - let newState = {}; + let newState = { + sets: {}, + setsWithHistory: {}, + }; await this.state.db .collection("users") @@ -69,20 +83,19 @@ export default withRouter(class GroupStats extends Component { .doc(this.props.match.params.groupId) .get() .then(async (groupDoc) => { - // await Promise.all(groupDoc.data().sets.map((setId) => { - // return this.state.db.collection("sets") - // .doc(setId) - // .get() - // .then((doc) => { - // newState.sets[setId] = { - // displayName: doc.data().title, - // loading: false, - // }; - // }); - // })); - document.title = `Stats | ${groupDoc.data().display_name} | Parandum`; newState.groupName = groupDoc.data().display_name; + + return Promise.all(groupDoc.data().sets.map((setId) => { + return this.state.db.collection("sets") + .doc(setId) + .get() + .then((doc) => { + newState.sets[setId] = { + title: doc.data().title, + }; + }); + })); }).catch((error) => { console.log(`Can't access group: ${error}`); newState.groupName = ""; @@ -105,14 +118,22 @@ export default withRouter(class GroupStats extends Component { answers: [{ answer: doc.data().answer, switchLanguage: doc.data().switch_language, + setIds: doc.data().setIds, }], count: doc.data().switch_language ? 0 : 1, switchedCount: doc.data().switch_language ? 1 : 0, + setIds: [doc.data().setIds], }); } else { incorrectAnswers[incorrectAnswers.length - 1].answers.push({ answer: doc.data().answer, switchLanguage: doc.data().switch_language, + setIds: doc.data().setIds, + }); + doc.data().setIds.map((setId) => { + if (!incorrectAnswers[incorrectAnswers.length - 1].setIds.includes(setId)) + return incorrectAnswers[incorrectAnswers.length - 1].setIds.push(setId); + return true; }); if (doc.data().switch_language) { incorrectAnswers[incorrectAnswers.length - 1].switchedCount++; @@ -120,9 +141,13 @@ export default withRouter(class GroupStats extends Component { incorrectAnswers[incorrectAnswers.length - 1].count++; } } + + doc.data().setIds.map((setId) => newState.setsWithHistory[setId] = true); + return true; }); newState.incorrectAnswers = incorrectAnswers.sort((a, b) => b.count + b.switchedCount - a.count - a.switchedCount); + newState.filteredIncorrectAnswers = newState.incorrectAnswers; newState.totalIncorrect = querySnapshot.docs.length; }) .catch((error) => { @@ -149,6 +174,64 @@ export default withRouter(class GroupStats extends Component { this.props.page.unload(); } + arraysHaveSameMembers = (arr1, arr2) => { + const set1 = new Set(arr1); + const set2 = new Set(arr2); + return arr1.every(item => set2.has(item)) && + arr2.every(item => set1.has(item)); + } + + handleSetSelectionChange = (selectedSet = this.state.selectedSet) => { + let totalIncorrect = 0; + const filteredIncorrectAnswers = (selectedSet.value === "all_sets" ? + JSON.parse(JSON.stringify(this.state.incorrectAnswers)) + : + JSON.parse(JSON.stringify(this.state.incorrectAnswers)) + .filter((vocabItem) => + vocabItem.setIds.includes(selectedSet.value) + ) + ) + .map((vocabItem) => { + let newVocabItem = vocabItem; + if (selectedSet.value === "all_sets") { + if (this.state.includeCompoundTests) { + newVocabItem.answers = vocabItem.answers; + } else { + newVocabItem.answers = vocabItem.answers + .filter((answer) => + answer.setIds.length === 1 + ) + } + } else { + newVocabItem.answers = vocabItem.answers + .filter((answer) => + this.arraysHaveSameMembers(answer.setIds, [selectedSet.value]) || + ( + this.state.includeCompoundTests && + answer.setIds.includes(this.state.selectedSet.value) + ) + ) + } + newVocabItem.switchedCount = newVocabItem.answers.filter((answer) => answer.switchLanguage).length; + newVocabItem.count = newVocabItem.answers.length - newVocabItem.switchedCount; + + totalIncorrect += newVocabItem.answers.length; + + return newVocabItem; + }); + this.setState({ + filteredIncorrectAnswers: filteredIncorrectAnswers, + selectedSet: selectedSet, + totalIncorrect: totalIncorrect, + }); + } + + handleIncludeCompoundTestsChange = (event) => { + this.setState({ + includeCompoundTests: event.target.checked, + }, () => this.handleSetSelectionChange()); + } + render() { return ( this.state.role !== null ? @@ -159,85 +242,171 @@ export default withRouter(class GroupStats extends Component {

Group Stats: {this.state.groupName}

+