Add progress stats for group owners
This commit is contained in:
@@ -6,6 +6,7 @@ import LoggedInHome from "./LoggedInHome";
|
|||||||
import Login from "./Login";
|
import Login from "./Login";
|
||||||
import SetPage from "./SetPage";
|
import SetPage from "./SetPage";
|
||||||
import GroupPage from "./GroupPage";
|
import GroupPage from "./GroupPage";
|
||||||
|
import GroupStats from "./GroupStats";
|
||||||
import UserGroups from "./UserGroups";
|
import UserGroups from "./UserGroups";
|
||||||
import Settings from "./Settings";
|
import Settings from "./Settings";
|
||||||
import Progress from "./Progress";
|
import Progress from "./Progress";
|
||||||
@@ -299,6 +300,9 @@ class App extends React.Component {
|
|||||||
<Route path="/groups/:groupId" exact>
|
<Route path="/groups/:groupId" exact>
|
||||||
<GroupPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
<GroupPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/groups/:groupId/stats" exact>
|
||||||
|
<GroupStats db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
||||||
|
</Route>
|
||||||
<Route path="/settings">
|
<Route path="/settings">
|
||||||
<Settings db={db} user={this.state.user} sound={this.state.sound} coloredEdges={this.state.coloredEdges} handleColoredEdgesChange={this.handleColoredEdgesChange} handleSoundChange={this.handleSoundChange} theme={this.state.theme} handleThemeChange={this.handleThemeChange} themes={themes} logEvent={analytics.logEvent} page={this.page} />
|
<Settings db={db} user={this.state.user} sound={this.state.sound} coloredEdges={this.state.coloredEdges} handleColoredEdgesChange={this.handleColoredEdgesChange} handleSoundChange={this.handleSoundChange} theme={this.state.theme} handleThemeChange={this.handleThemeChange} themes={themes} logEvent={analytics.logEvent} page={this.page} />
|
||||||
</Route>
|
</Route>
|
||||||
@@ -371,7 +375,6 @@ class App extends React.Component {
|
|||||||
></Button>
|
></Button>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
{/* <div className="overlay"><img className="page-loader" src={Loader} alt="Loading..." /></div> */}
|
|
||||||
<Fade out={!this.state.pageLoading && this.state.userDataPresent} className="page-loader-container">
|
<Fade out={!this.state.pageLoading && this.state.userDataPresent} className="page-loader-container">
|
||||||
<img className="page-loader" src={Loader} alt="Loading..." />
|
<img className="page-loader" src={Loader} alt="Loading..." />
|
||||||
</Fade>
|
</Fade>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withRouter, Link } from "react-router-dom";
|
import { withRouter, Link } from "react-router-dom";
|
||||||
import { HomeRounded as HomeRoundedIcon, EditRounded as EditRoundedIcon, ArrowForwardRounded as ArrowForwardRoundedIcon, DeleteRounded as DeleteRoundedIcon } from "@material-ui/icons";
|
import { TimelineRounded as TimelineRoundedIcon, HomeRounded as HomeRoundedIcon, EditRounded as EditRoundedIcon, ArrowForwardRounded as ArrowForwardRoundedIcon, DeleteRounded as DeleteRoundedIcon } from "@material-ui/icons";
|
||||||
import NavBar from "./NavBar";
|
import NavBar from "./NavBar";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
import LinkButton from "./LinkButton";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
|
import Error404 from "./Error404";
|
||||||
|
|
||||||
import "./css/GroupPage.css";
|
import "./css/GroupPage.css";
|
||||||
import "./css/ConfirmationDialog.css";
|
import "./css/ConfirmationDialog.css";
|
||||||
@@ -55,30 +57,34 @@ export default withRouter(class GroupPage extends Component {
|
|||||||
if (this.isMounted) super.setState(state, callback);
|
if (this.isMounted) super.setState(state, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
|
let promises = [];
|
||||||
|
let newState = {
|
||||||
|
sets: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
promises.push(
|
||||||
this.state.db
|
this.state.db
|
||||||
.collection("users")
|
.collection("users")
|
||||||
.doc(this.state.user.uid)
|
.doc(this.state.user.uid)
|
||||||
.collection("groups")
|
.collection("groups")
|
||||||
.doc(this.props.match.params.groupId)
|
.doc(this.props.match.params.groupId)
|
||||||
.get()
|
.get()
|
||||||
.then((userGroupDoc) => {
|
.then((userGroupDoc) => userGroupDoc.data())
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(`Can't access user group: ${error}`);
|
||||||
|
return {
|
||||||
|
role: "none",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
promises.push(
|
||||||
this.state.db
|
this.state.db
|
||||||
.collection("groups")
|
.collection("groups")
|
||||||
.doc(this.props.match.params.groupId)
|
.doc(this.props.match.params.groupId)
|
||||||
.get()
|
.get()
|
||||||
.then(async (groupDoc) => {
|
.then(async (groupDoc) => {
|
||||||
document.title = `${groupDoc.data().display_name} | Parandum`;
|
|
||||||
|
|
||||||
let newState = {
|
|
||||||
role: userGroupDoc.data().role,
|
|
||||||
groupName: groupDoc.data().display_name,
|
|
||||||
originalGroupName: groupDoc.data().display_name,
|
|
||||||
sets: {},
|
|
||||||
memberCount: Object.keys(groupDoc.data().users).length + (Object.keys(groupDoc.data().users).includes(this.state.user.uid) ? 0 : 1),
|
|
||||||
joinCode: userGroupDoc.data().role === "owner" ? groupDoc.data().join_code : "",
|
|
||||||
};
|
|
||||||
|
|
||||||
await Promise.all(groupDoc.data().sets.map((setId) => {
|
await Promise.all(groupDoc.data().sets.map((setId) => {
|
||||||
return this.state.db.collection("sets")
|
return this.state.db.collection("sets")
|
||||||
.doc(setId)
|
.doc(setId)
|
||||||
@@ -91,12 +97,26 @@ export default withRouter(class GroupPage extends Component {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (newState.role === "owner") {
|
return groupDoc.data();
|
||||||
const getGroupMembers = () => {
|
}).catch((error) => {
|
||||||
return this.state.functions.getGroupMembers({ groupId: this.props.match.params.groupId })
|
console.log(`Can't access group: ${error}`);
|
||||||
.catch((error) => {
|
|
||||||
return {
|
return {
|
||||||
data: {
|
display_name: "",
|
||||||
|
users: {},
|
||||||
|
join_code: "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (newState.role === "owner") {
|
||||||
|
promises.push(
|
||||||
|
this.state.functions.getGroupMembers({ groupId: this.props.match.params.groupId })
|
||||||
|
.then((response) => {
|
||||||
|
newState.groupUsers = response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
newState.groupUsers = {
|
||||||
owners: [
|
owners: [
|
||||||
{
|
{
|
||||||
displayName: this.state.user.displayName,
|
displayName: this.state.user.displayName,
|
||||||
@@ -106,19 +126,22 @@ export default withRouter(class GroupPage extends Component {
|
|||||||
contributors: [],
|
contributors: [],
|
||||||
members: [],
|
members: [],
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupUsers = await getGroupMembers();
|
const completedPromises = await Promise.all(promises);
|
||||||
|
|
||||||
newState.groupUsers = groupUsers.data;
|
document.title = `${completedPromises[1].display_name} | Parandum`;
|
||||||
}
|
|
||||||
|
newState.role = completedPromises[0].role;
|
||||||
|
newState.groupName = completedPromises[1].display_name;
|
||||||
|
newState.originalGroupName = completedPromises[1].display_name;
|
||||||
|
newState.memberCount = Object.keys(completedPromises[1].users).length + (Object.keys(completedPromises[1].users).includes(this.state.user.uid) ? 0 : 1);
|
||||||
|
newState.joinCode = completedPromises[0].role === "owner" ? completedPromises[1].join_code : "";
|
||||||
|
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
this.props.page.load();
|
this.props.page.load();
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.logEvent("select_content", {
|
this.props.logEvent("select_content", {
|
||||||
content_type: "group",
|
content_type: "group",
|
||||||
@@ -336,6 +359,9 @@ export default withRouter(class GroupPage extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
(this.state.role === "none") ?
|
||||||
|
<Error404 />
|
||||||
|
:
|
||||||
<div>
|
<div>
|
||||||
<NavBar items={this.state.navbarItems} />
|
<NavBar items={this.state.navbarItems} />
|
||||||
<main>
|
<main>
|
||||||
@@ -375,12 +401,20 @@ export default withRouter(class GroupPage extends Component {
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.state.role === "owner" &&
|
this.state.role === "owner" &&
|
||||||
|
<div className="button-container">
|
||||||
|
<LinkButton
|
||||||
|
to={`/groups/${this.props.match.params.groupId}/stats`}
|
||||||
|
icon={<TimelineRoundedIcon />}
|
||||||
|
className="button--round"
|
||||||
|
title="Group progress"
|
||||||
|
></LinkButton>
|
||||||
<Button
|
<Button
|
||||||
onClick={this.showDeleteGroup}
|
onClick={this.showDeleteGroup}
|
||||||
icon={<DeleteRoundedIcon />}
|
icon={<DeleteRoundedIcon />}
|
||||||
className="button--round"
|
className="button--round"
|
||||||
title="Delete group"
|
title="Delete group"
|
||||||
></Button>
|
></Button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
|
|||||||
237
src/GroupStats.js
Normal file
237
src/GroupStats.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { GroupRounded as GroupRoundedIcon, HomeRounded as HomeRoundedIcon } from "@material-ui/icons";
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import NavBar from "./NavBar";
|
||||||
|
import Footer from "./Footer";
|
||||||
|
import Error404 from "./Error404";
|
||||||
|
import "./css/History.css";
|
||||||
|
import "./css/MistakesHistory.css";
|
||||||
|
|
||||||
|
export default withRouter(class GroupStats extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
user: props.user,
|
||||||
|
db: props.db,
|
||||||
|
navbarItems: [
|
||||||
|
{
|
||||||
|
type: "link",
|
||||||
|
link: `/groups/${this.props.match.params.groupId}`,
|
||||||
|
icon: <GroupRoundedIcon />,
|
||||||
|
hideTextMobile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "link",
|
||||||
|
link: "/",
|
||||||
|
icon: <HomeRoundedIcon />,
|
||||||
|
hideTextMobile: true,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
role: null,
|
||||||
|
groupName: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
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() {
|
||||||
|
let promises = [];
|
||||||
|
let newState = {};
|
||||||
|
|
||||||
|
await this.state.db
|
||||||
|
.collection("users")
|
||||||
|
.doc(this.state.user.uid)
|
||||||
|
.collection("groups")
|
||||||
|
.doc(this.props.match.params.groupId)
|
||||||
|
.get()
|
||||||
|
.then((userGroupDoc) => {
|
||||||
|
newState.role = userGroupDoc.data().role;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(`Can't access user group: ${error}`);
|
||||||
|
newState.role = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newState.role === "owner") {
|
||||||
|
promises.push(
|
||||||
|
this.state.db
|
||||||
|
.collection("groups")
|
||||||
|
.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;
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(`Can't access group: ${error}`);
|
||||||
|
newState.groupName = "";
|
||||||
|
document.title = "Stats | Parandum";
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
this.state.db.collection("incorrect_answers")
|
||||||
|
.where("groups", "array-contains", this.props.match.params.groupId)
|
||||||
|
.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;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
newState.incorrectAnswers = [];
|
||||||
|
newState.totalIncorrect = 0;
|
||||||
|
console.log(`Couldn't get group progress: ${error}`);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(newState);
|
||||||
|
this.props.page.load();
|
||||||
|
|
||||||
|
this.props.logEvent("select_content", {
|
||||||
|
content_type: "group_stats",
|
||||||
|
item_id: this.props.match.params.groupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.isMounted = false;
|
||||||
|
this.props.page.unload();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
this.state.role !== null ?
|
||||||
|
(this.state.role === "owner"
|
||||||
|
?
|
||||||
|
<div>
|
||||||
|
<NavBar items={this.state.navbarItems} />
|
||||||
|
<main>
|
||||||
|
<h1>Group Stats: {this.state.groupName}</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].term}</h1>
|
||||||
|
<p>meaning</p>
|
||||||
|
<h1>{this.state.incorrectAnswers[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>
|
||||||
|
<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, index) => answerItem.switchLanguage && (
|
||||||
|
<p key={index}>{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, index) => !answerItem.switchLanguage && (
|
||||||
|
<p key={index}>{answerItem.answer === "" ? <i>skipped</i> : answerItem.answer}</p>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<Error404 />)
|
||||||
|
:
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user