Add set-specific history tracking for group owners
This commit is contained in:
@@ -128,7 +128,7 @@ class App extends React.Component {
|
||||
};
|
||||
|
||||
this.page = {
|
||||
loaded: !this.state.pageLoading,
|
||||
loaded: () => !this.state.pageLoading,
|
||||
load: () => {
|
||||
this.setState({
|
||||
pageLoading: false,
|
||||
|
||||
@@ -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 {
|
||||
?
|
||||
<div className="group-links-container">
|
||||
{
|
||||
Object.keys(this.state.sets).map((setId) =>
|
||||
<div key={setId} className="group-set-link">
|
||||
<Link
|
||||
to={`/sets/${setId}`}
|
||||
>
|
||||
{this.state.sets[setId].displayName}
|
||||
</Link>
|
||||
{
|
||||
this.state.role === "owner" &&
|
||||
<Button
|
||||
className="button--no-background"
|
||||
onClick={() => this.removeSet(setId)}
|
||||
icon={<DeleteRoundedIcon />}
|
||||
loading={this.state.sets[setId].loading}
|
||||
disabled={this.state.sets[setId].loading}
|
||||
title="Remove set"
|
||||
></Button>
|
||||
Object.keys(this.state.sets)
|
||||
.sort((a, b) => {
|
||||
if (this.state.sets[a].displayName < this.state.sets[b].displayName) {
|
||||
return -1;
|
||||
}
|
||||
</div>
|
||||
)
|
||||
if (this.state.sets[a].displayName > this.state.sets[b].displayName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map((setId) =>
|
||||
<div key={setId} className="group-set-link">
|
||||
<Link
|
||||
to={`/sets/${setId}`}
|
||||
>
|
||||
{this.state.sets[setId].displayName}
|
||||
</Link>
|
||||
{
|
||||
this.state.role === "owner" &&
|
||||
<Button
|
||||
className="button--no-background"
|
||||
onClick={() => this.removeSet(setId)}
|
||||
icon={<DeleteRoundedIcon />}
|
||||
loading={this.state.sets[setId].loading}
|
||||
disabled={this.state.sets[setId].loading}
|
||||
title="Remove set"
|
||||
></Button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
:
|
||||
|
||||
@@ -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 {
|
||||
<main>
|
||||
<h1>Group Stats: {this.state.groupName}</h1>
|
||||
<div className="history-sections">
|
||||
<Select
|
||||
className="set-select-container"
|
||||
value={this.state.selectedSet}
|
||||
onChange={this.handleSetSelectionChange}
|
||||
defaultValue={"all_sets"}
|
||||
options={
|
||||
[
|
||||
{
|
||||
value: "all_sets",
|
||||
label: "All sets",
|
||||
}
|
||||
].concat(Object.keys(this.state.sets)
|
||||
.filter((setId) => this.state.setsWithHistory[setId] === true)
|
||||
.sort((a, b) => {
|
||||
if (this.state.sets[a].title < this.state.sets[b].title) {
|
||||
return -1;
|
||||
}
|
||||
if (this.state.sets[a].title > this.state.sets[b].title) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map((setId) => {
|
||||
return {
|
||||
value: setId,
|
||||
label: this.state.sets[setId].title
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
theme={theme => {
|
||||
const overlayColor = getComputedStyle(
|
||||
document.querySelector("#root > div")
|
||||
).getPropertyValue("--background-color-light")
|
||||
.trim();
|
||||
const textColor = getComputedStyle(
|
||||
document.querySelector("#root > div")
|
||||
).getPropertyValue("--text-color")
|
||||
.trim();
|
||||
const textColorTinted = getComputedStyle(
|
||||
document.querySelector("#root > div")
|
||||
).getPropertyValue("--text-color-tinted")
|
||||
.trim();
|
||||
const primaryColor = getComputedStyle(
|
||||
document.querySelector("#root > div")
|
||||
).getPropertyValue("--primary-color")
|
||||
.trim();
|
||||
const primaryColorDark = getComputedStyle(
|
||||
document.querySelector("#root > div")
|
||||
).getPropertyValue("--primary-color-dark")
|
||||
.trim();
|
||||
|
||||
return {
|
||||
...theme,
|
||||
borderRadius: 6,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary: primaryColor,
|
||||
primary25: primaryColorDark,
|
||||
neutral0: overlayColor,
|
||||
neutral5: overlayColor,
|
||||
neutral10: overlayColor,
|
||||
neutral20: textColorTinted,
|
||||
neutral30: textColor,
|
||||
neutral40: textColor,
|
||||
neutral50: textColor,
|
||||
neutral60: textColorTinted,
|
||||
neutral70: textColor,
|
||||
neutral80: textColor,
|
||||
neutral90: textColor,
|
||||
},
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label>
|
||||
<Checkbox
|
||||
checked={this.state.includeCompoundTests}
|
||||
onChange={this.handleIncludeCompoundTestsChange}
|
||||
inputProps={{ 'aria-label': 'checkbox' }}
|
||||
/>
|
||||
<span>Include compound tests</span>
|
||||
</label>
|
||||
|
||||
<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 &&
|
||||
this.state.filteredIncorrectAnswers.length > 0 &&
|
||||
<div className="stat-row stat-row--inline">
|
||||
<h1>{this.state.incorrectAnswers[0].term}</h1>
|
||||
{/* <h1>{this.state.incorrectAnswers[0].term}</h1> */}
|
||||
<h1>{this.state.filteredIncorrectAnswers[0].term}</h1>
|
||||
<p>meaning</p>
|
||||
<h1>{this.state.incorrectAnswers[0].definition}</h1>
|
||||
{/* <h1>{this.state.incorrectAnswers[0].definition}</h1> */}
|
||||
<h1>{this.state.filteredIncorrectAnswers[0].definition}</h1>
|
||||
<p>is the most common</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="mistakes-history-container">
|
||||
{
|
||||
this.state.incorrectAnswers.map((vocabItem, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div>
|
||||
<h2>{vocabItem.term}</h2>
|
||||
{
|
||||
vocabItem.switchedCount > 0
|
||||
?
|
||||
<Collapsible transitionTime={300} trigger={<><b>{vocabItem.switchedCount} mistake{vocabItem.switchedCount !== 1 && "s"}</b><ArrowDropDownRoundedIcon /></>}>
|
||||
this.state.filteredIncorrectAnswers
|
||||
.map((vocabItem, index) => {
|
||||
const sortedAnswers = vocabItem.answers
|
||||
.sort((a, b) => {
|
||||
if (a.answer < b.answer) {
|
||||
return -1;
|
||||
}
|
||||
if (a.answer > b.answer) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<div>
|
||||
<h2>{vocabItem.term}</h2>
|
||||
{
|
||||
vocabItem.switchedCount > 0 &&
|
||||
<div>
|
||||
vocabItem.switchedCount > 0
|
||||
?
|
||||
<Collapsible transitionTime={300} trigger={<><b>{vocabItem.switchedCount} mistake{vocabItem.switchedCount !== 1 && "s"}</b><ArrowDropDownRoundedIcon /></>}>
|
||||
{
|
||||
vocabItem.answers.sort((a, b) => {
|
||||
if (a.answer < b.answer) {
|
||||
return -1;
|
||||
vocabItem.switchedCount > 0 &&
|
||||
<div>
|
||||
{
|
||||
sortedAnswers
|
||||
.map((answerItem, index) => answerItem.switchLanguage && (
|
||||
<p key={index}>{answerItem.answer === "" ? <i>skipped</i> : answerItem.answer}</p>
|
||||
))
|
||||
}
|
||||
if (a.answer > b.answer) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).map((answerItem, index) => answerItem.switchLanguage && (
|
||||
<p key={index}>{answerItem.answer === "" ? <i>skipped</i> : answerItem.answer}</p>
|
||||
))
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Collapsible>
|
||||
:
|
||||
<b>0 mistakes</b>
|
||||
}
|
||||
</Collapsible>
|
||||
:
|
||||
<b>{vocabItem.switchedCount} mistake{vocabItem.switchedCount !== 1 && "s"}</b>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<h2>{vocabItem.definition}</h2>
|
||||
{
|
||||
vocabItem.count > 0
|
||||
?
|
||||
<Collapsible transitionTime={300} trigger={<><b>{vocabItem.count} mistake{vocabItem.count !== 1 && "s"}</b><ArrowDropDownRoundedIcon /></>}>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{vocabItem.definition}</h2>
|
||||
{
|
||||
vocabItem.count > 0 &&
|
||||
<div>
|
||||
vocabItem.count > 0
|
||||
?
|
||||
<Collapsible transitionTime={300} trigger={<><b>{vocabItem.count} mistake{vocabItem.count !== 1 && "s"}</b><ArrowDropDownRoundedIcon /></>}>
|
||||
{
|
||||
vocabItem.answers.sort((a, b) => {
|
||||
if (a.answer < b.answer) {
|
||||
return -1;
|
||||
vocabItem.count > 0 &&
|
||||
<div>
|
||||
{
|
||||
sortedAnswers
|
||||
.map((answerItem, index) => !answerItem.switchLanguage && (
|
||||
<p key={index}>{answerItem.answer === "" ? <i>skipped</i> : answerItem.answer}</p>
|
||||
))
|
||||
}
|
||||
if (a.answer > b.answer) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).map((answerItem, index) => !answerItem.switchLanguage && (
|
||||
<p key={index}>{answerItem.answer === "" ? <i>skipped</i> : answerItem.answer}</p>
|
||||
))
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Collapsible>
|
||||
:
|
||||
<b>0 mistakes</b>
|
||||
}
|
||||
</Collapsible>
|
||||
:
|
||||
<b>{vocabItem.switchedCount} mistake{vocabItem.switchedCount !== 1 && "s"}</b>
|
||||
}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -384,7 +384,7 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
Groups
|
||||
</LinkButton>
|
||||
{
|
||||
this.state.userSets && this.state.userSets.length > 0 &&
|
||||
(!this.props.page.loaded() || (this.state.userSets && this.state.userSets.length > 0)) &&
|
||||
<LinkButton
|
||||
to="/my-sets"
|
||||
>
|
||||
@@ -523,7 +523,8 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{this.state.userGroupSets && this.state.userGroupSets.length > 0 && this.state.userGroupSets.map(data =>
|
||||
{this.state.userGroupSets && this.state.userGroupSets.length > 0 && this.state.userGroupSets
|
||||
.map(data =>
|
||||
data.sets && data.sets.length > 0 &&
|
||||
<div key={data.group.id} className="checkbox-list-container">
|
||||
<Link to={`/groups/${data.group.id}`}>
|
||||
@@ -531,7 +532,49 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
</Link>
|
||||
|
||||
<div className="checkbox-list">
|
||||
{data.sets.map(set =>
|
||||
{data.sets
|
||||
.sort((a, b) => {
|
||||
if (a.data().title < b.data().title) {
|
||||
return -1;
|
||||
}
|
||||
if (a.data().title > b.data().title) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map(set =>
|
||||
<div key={set.id}>
|
||||
<label>
|
||||
<Checkbox
|
||||
name={set.id}
|
||||
checked={this.state.selections[set.id]}
|
||||
onChange={this.handleSetSelectionChange}
|
||||
inputProps={{ 'aria-label': 'checkbox' }}
|
||||
/>
|
||||
<Link to={`/sets/${set.id}`}>
|
||||
{set.data().title}
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.state.publicSets && this.state.publicSets.length > 0 &&
|
||||
<div className="checkbox-list-container">
|
||||
<h3><PublicRoundedIcon /> Public Sets</h3>
|
||||
<div className="checkbox-list">
|
||||
{this.state.publicSets
|
||||
.sort((a, b) => {
|
||||
if (a.data().title < b.data().title) {
|
||||
return -1;
|
||||
}
|
||||
if (a.data().title > b.data().title) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).map(set =>
|
||||
<div key={set.id}>
|
||||
<label>
|
||||
<Checkbox
|
||||
@@ -545,29 +588,8 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.state.publicSets && this.state.publicSets.length > 0 &&
|
||||
<div className="checkbox-list-container">
|
||||
<h3><PublicRoundedIcon /> Public Sets</h3>
|
||||
<div className="checkbox-list">
|
||||
{this.state.publicSets.map(set =>
|
||||
<div key={set.id}>
|
||||
<label>
|
||||
<Checkbox
|
||||
name={set.id}
|
||||
checked={this.state.selections[set.id]}
|
||||
onChange={this.handleSetSelectionChange}
|
||||
inputProps={{ 'aria-label': 'checkbox' }}
|
||||
/>
|
||||
<Link to={`/sets/${set.id}`}>
|
||||
{set.data().title}
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ html {
|
||||
--text-color-tinted: #cccccc;
|
||||
--background-color: #111111;
|
||||
--background-color-dark: #000000;
|
||||
--background-color-light: #222222;
|
||||
--overlay-color: #333333;
|
||||
|
||||
height: 100%;
|
||||
|
||||
@@ -24,4 +24,8 @@
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.set-select-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
Reference in New Issue
Block a user