Add pages
This commit is contained in:
269
src/CreateSet.js
Normal file
269
src/CreateSet.js
Normal file
@@ -0,0 +1,269 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import Footer from "./Footer";
|
||||
import { HomeRounded as HomeRoundedIcon, CheckCircleOutlineRounded as CheckCircleOutlineRoundedIcon } from "@material-ui/icons";
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
// import { doc, collection, setDoc, writeBatch } from "firebase/firestore";
|
||||
|
||||
import "./css/Form.css";
|
||||
|
||||
export default withRouter(class CreateSet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
loading: false,
|
||||
canCreateSet: false,
|
||||
inputContents: [],
|
||||
inputs: {
|
||||
title: "",
|
||||
public: false,
|
||||
},
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = "Create Set | Parandum";
|
||||
this.setNameInput.focus();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
stopLoading = () => {
|
||||
this.setState({
|
||||
canCreateSet: true,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
cleanseVocabString = (item) => {
|
||||
const chars = " .,()-_'\"";
|
||||
|
||||
let newString = item;
|
||||
|
||||
chars.split("").forEach((char) => {
|
||||
newString = newString.replace(char, "");
|
||||
});
|
||||
|
||||
return newString;
|
||||
}
|
||||
|
||||
handleSetDataChange = () => {
|
||||
const numberOfVocabPairs = this.state.inputContents.map(contents =>
|
||||
this.cleanseVocabString(contents.term) !== "" &&
|
||||
this.cleanseVocabString(contents.definition) !== "")
|
||||
.filter(x => x === true)
|
||||
.length;
|
||||
|
||||
if (this.state.inputs.title !== "" && numberOfVocabPairs > 0 && numberOfVocabPairs === this.state.inputContents.length) {
|
||||
this.setState({
|
||||
canCreateSet: true,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
canCreateSet: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onTermInputChange = (event) => {
|
||||
const index = Number(event.target.name.replace("term_", ""));
|
||||
const input = event.target.value;
|
||||
|
||||
let inputContents = this.state.inputContents;
|
||||
|
||||
if (index >= this.state.inputContents.length && input !== "") {
|
||||
inputContents.push({
|
||||
term: input,
|
||||
definition: "",
|
||||
});
|
||||
} else {
|
||||
if (index === this.state.inputContents.length - 1 && input === "" && this.state.inputContents[index].definition === "") {
|
||||
inputContents.splice(-1);
|
||||
} else {
|
||||
inputContents[index].term = input;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inputContents: inputContents,
|
||||
}, this.handleSetDataChange());
|
||||
}
|
||||
|
||||
onDefinitionInputChange = (event) => {
|
||||
const index = Number(event.target.name.replace("definition_", ""));
|
||||
const input = event.target.value;
|
||||
|
||||
let inputContents = this.state.inputContents;
|
||||
|
||||
if (index >= this.state.inputContents.length && input !== "") {
|
||||
inputContents.push({
|
||||
term: "",
|
||||
definition: input,
|
||||
});
|
||||
} else {
|
||||
if (index === this.state.inputContents.length - 1 && input === "" && this.state.inputContents[index].term === "") {
|
||||
inputContents.splice(-1);
|
||||
} else {
|
||||
inputContents[index].definition = input;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inputContents: inputContents,
|
||||
}, this.handleSetDataChange());
|
||||
}
|
||||
|
||||
onSetTitleInputChange = (event) => {
|
||||
this.setState({
|
||||
inputs: {
|
||||
...this.state.inputs,
|
||||
title: event.target.value,
|
||||
}
|
||||
}, this.handleSetDataChange());
|
||||
}
|
||||
|
||||
onPublicSetInputChange = (event) => {
|
||||
this.setState({
|
||||
inputs: {
|
||||
...this.state.inputs,
|
||||
public: event.target.checked,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createSet = () => {
|
||||
if (this.state.canCreateSet) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
canCreateSet: false,
|
||||
});
|
||||
|
||||
const db = this.state.db;
|
||||
const setDocRef = db.collection("sets").doc();
|
||||
// const setDocRef = doc(collection(this.state.db, "sets"));
|
||||
|
||||
setDocRef.set({
|
||||
title: this.state.inputs.title,
|
||||
public: this.state.inputs.public,
|
||||
owner: this.state.user.uid,
|
||||
groups: [],
|
||||
})
|
||||
// setDoc(setDocRef, {
|
||||
// title: this.state.inputs.title,
|
||||
// public: this.state.inputs.public,
|
||||
// owner: this.state.user.uid,
|
||||
// groups: [],
|
||||
// })
|
||||
.then(() => {
|
||||
const batch = db.batch();
|
||||
// const batch = writeBatch(this.state.db);
|
||||
|
||||
this.state.inputContents.map((contents) => {
|
||||
const vocabDocRef = setDocRef.collection("vocab").doc();
|
||||
// const vocabDocRef = doc(collection(setDocRef, "vocab"));
|
||||
return batch.set(vocabDocRef, {
|
||||
term: contents.term,
|
||||
definition: contents.definition,
|
||||
sound: false,
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: sound files
|
||||
|
||||
batch.commit().then(() => {
|
||||
this.stopLoading();
|
||||
this.props.history.push("/sets/" + setDocRef.id);
|
||||
}).catch((e) => {
|
||||
console.log("Couldn't create set. Batch commit error: " + e);
|
||||
this.stopLoading();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
|
||||
<main>
|
||||
<h2 className="page-header">
|
||||
<input
|
||||
type="text"
|
||||
name="set_title"
|
||||
onChange={this.onSetTitleInputChange}
|
||||
placeholder="Set Title"
|
||||
className="set-title-input"
|
||||
ref={inputEl => (this.setNameInput = inputEl)}
|
||||
/>
|
||||
</h2>
|
||||
|
||||
<div className="form create-set-vocab-list">
|
||||
<label>
|
||||
<Checkbox
|
||||
checked={this.state.inputs.public}
|
||||
onChange={this.onPublicSetInputChange}
|
||||
inputProps={{ 'aria-label': 'checkbox' }}
|
||||
/>
|
||||
<span>Public</span>
|
||||
</label>
|
||||
|
||||
<div className="create-set-header">
|
||||
<h3>Terms</h3>
|
||||
<h3>Definitions</h3>
|
||||
</div>
|
||||
|
||||
{this.state.inputContents.concat({term: "", definition: ""}).map((contents, index) =>
|
||||
<div className="create-set-input-row" key={index}>
|
||||
<input
|
||||
type="text"
|
||||
name={`term_${index}`}
|
||||
onChange={this.onTermInputChange}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name={`definition_${index}`}
|
||||
onChange={this.onDefinitionInputChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={this.createSet}
|
||||
icon={<CheckCircleOutlineRoundedIcon />}
|
||||
loading={this.state.loading}
|
||||
disabled={!this.state.canCreateSet}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
335
src/EditSet.js
Normal file
335
src/EditSet.js
Normal file
@@ -0,0 +1,335 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter, Prompt } from "react-router-dom";
|
||||
import { HomeRounded as HomeRoundedIcon } from "@material-ui/icons";
|
||||
import NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import Error404 from "./Error404";
|
||||
import Footer from "./Footer";
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
export default withRouter(class EditSet extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
loading: false,
|
||||
canSaveSet: true,
|
||||
inputs: {
|
||||
title: "",
|
||||
public: false,
|
||||
},
|
||||
inputContents: [],
|
||||
setInaccessible: false,
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
canMakeSetNonPublic: true,
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
alertLeavingWithoutSaving = (e = null) => {
|
||||
if (this.state.canSaveSet) {
|
||||
var confirmationMessage = "Are you sure you want to leave? You will lose any unsaved changes.";
|
||||
|
||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("beforeunload", this.alertLeavingWithoutSaving);
|
||||
|
||||
const setId = this.props.match.params.setId;
|
||||
const setRef = this.state.db.collection("sets")
|
||||
.doc(setId);
|
||||
const setVocabRef = setRef.collection("vocab")
|
||||
.orderBy("term");
|
||||
|
||||
setRef.get().then((setDoc) => {
|
||||
document.title = `Edit | ${setDoc.data().title} | Parandum`;
|
||||
|
||||
setVocabRef.get().then((querySnapshot) => {
|
||||
let vocab = [];
|
||||
let vocabPairsCount = 0;
|
||||
|
||||
querySnapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
|
||||
if (this.cleanseVocabString(data.term) !== "" &&
|
||||
this.cleanseVocabString(data.definition) !== "") {
|
||||
vocabPairsCount++;
|
||||
}
|
||||
|
||||
return vocab.push({
|
||||
vocabId: doc.id,
|
||||
term: data.term,
|
||||
definition: data.definition,
|
||||
sound: data.sound,
|
||||
});
|
||||
});
|
||||
|
||||
let newState = {
|
||||
inputs: {
|
||||
title: setDoc.data().title,
|
||||
public: setDoc.data().public,
|
||||
},
|
||||
inputContents: vocab,
|
||||
canMakeSetNonPublic: !(setDoc.data().groups && setDoc.data().groups.length > 0),
|
||||
};
|
||||
|
||||
if (!(newState.inputs.title !== "" && vocabPairsCount > 0 && vocabPairsCount === this.state.inputContents.length)) {
|
||||
newState.canSaveSet = false;
|
||||
}
|
||||
|
||||
if (setDoc.data().owner !== this.state.user.uid) {
|
||||
newState.setInaccessible = true;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({
|
||||
setInaccessible: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
window.removeEventListener('beforeunload', this.alertLeavingWithoutSaving);
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
stopLoading = () => {
|
||||
this.setState({
|
||||
canStartTest: true,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
cleanseVocabString = (item) => {
|
||||
const chars = " .,()-_'\"";
|
||||
|
||||
let newString = item;
|
||||
|
||||
chars.split("").forEach((char) => {
|
||||
newString = newString.replace(char, "");
|
||||
});
|
||||
|
||||
return newString;
|
||||
}
|
||||
|
||||
handleSetDataChange = () => {
|
||||
const numberOfVocabPairs = this.state.inputContents.map(contents =>
|
||||
this.cleanseVocabString(contents.term) !== "" &&
|
||||
this.cleanseVocabString(contents.definition) !== "")
|
||||
.filter(x => x === true)
|
||||
.length;
|
||||
|
||||
if (this.state.inputs.title !== "" && numberOfVocabPairs > 0 && numberOfVocabPairs === this.state.inputContents.length) {
|
||||
this.setState({
|
||||
canSaveSet: true,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
canSaveSet: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onTermInputChange = (event) => {
|
||||
const index = Number(event.target.name.replace("term_", ""));
|
||||
const input = event.target.value;
|
||||
|
||||
let inputContents = this.state.inputContents;
|
||||
|
||||
if (index >= this.state.inputContents.length && input !== "") {
|
||||
inputContents.push({
|
||||
term: input,
|
||||
definition: "",
|
||||
sound: false,
|
||||
});
|
||||
} else {
|
||||
if (index === this.state.inputContents.length - 1 && input === "" && this.state.inputContents[index].definition === "") {
|
||||
inputContents.splice(-1);
|
||||
} else {
|
||||
inputContents[index].term = input;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inputContents: inputContents,
|
||||
}, this.handleSetDataChange());
|
||||
}
|
||||
|
||||
onDefinitionInputChange = (event) => {
|
||||
const index = Number(event.target.name.replace("definition_", ""));
|
||||
const input = event.target.value;
|
||||
|
||||
let inputContents = this.state.inputContents;
|
||||
|
||||
if (index >= this.state.inputContents.length && input !== "") {
|
||||
inputContents.push({
|
||||
term: "",
|
||||
definition: input,
|
||||
sound: false,
|
||||
});
|
||||
} else {
|
||||
if (index === this.state.inputContents.length - 1 && input === "" && this.state.inputContents[index].term === "") {
|
||||
inputContents.splice(-1);
|
||||
} else {
|
||||
inputContents[index].definition = input;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inputContents: inputContents,
|
||||
}, this.handleSetDataChange());
|
||||
}
|
||||
|
||||
onSetTitleInputChange = (event) => {
|
||||
this.setState({
|
||||
inputs: {
|
||||
...this.state.inputs,
|
||||
title: event.target.value,
|
||||
}
|
||||
}, this.handleSetDataChange());
|
||||
}
|
||||
|
||||
onPublicSetInputChange = (event) => {
|
||||
if (this.state.canMakeSetNonPublic) this.setState({
|
||||
inputs: {
|
||||
...this.state.inputs,
|
||||
public: event.target.checked,
|
||||
}
|
||||
}, this.handleSetDataChange());
|
||||
}
|
||||
|
||||
saveSet = () => {
|
||||
if (this.state.canSaveSet) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
canSaveSet: false,
|
||||
});
|
||||
|
||||
const db = this.state.db;
|
||||
const setId = this.props.match.params.setId;
|
||||
const setDocRef = db.collection("sets").doc(setId);
|
||||
|
||||
setDocRef.update({
|
||||
title: this.state.inputs.title,
|
||||
public: this.state.inputs.public,
|
||||
}).then(() => {
|
||||
const batch = db.batch();
|
||||
|
||||
this.state.inputContents.map((contents) => {
|
||||
const vocabDocRef = setDocRef.collection("vocab").doc(contents.vocabId);
|
||||
return batch.set(vocabDocRef, {
|
||||
term: contents.term,
|
||||
definition: contents.definition,
|
||||
sound: contents.sound,
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: sound files
|
||||
|
||||
batch.commit().then(() => {
|
||||
this.stopLoading();
|
||||
this.props.history.push("/sets/" + setDocRef.id);
|
||||
}).catch((e) => {
|
||||
console.log("Couldn't update set. Batch commit error: " + e);
|
||||
this.stopLoading();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
this.state.setInaccessible
|
||||
?
|
||||
<Error404 />
|
||||
:
|
||||
<div>
|
||||
<Prompt
|
||||
when={this.state.canSaveSet}
|
||||
message="Are you sure you want to leave? You will lose any unsaved changes."
|
||||
/>
|
||||
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
|
||||
<main>
|
||||
<div className="page-header">
|
||||
<h2>
|
||||
<input
|
||||
type="text"
|
||||
name="set_title"
|
||||
onChange={this.onSetTitleInputChange}
|
||||
placeholder="Set Title"
|
||||
value={this.state.inputs.title}
|
||||
className="set-title-input"
|
||||
/>
|
||||
</h2>
|
||||
<Button
|
||||
onClick={this.saveSet}
|
||||
loading={this.state.loading}
|
||||
disabled={!this.state.canSaveSet}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="form create-set-vocab-list">
|
||||
<label>
|
||||
<Checkbox
|
||||
checked={this.state.inputs.public}
|
||||
onChange={this.onPublicSetInputChange}
|
||||
inputProps={{ 'aria-label': 'checkbox' }}
|
||||
/>
|
||||
<span>Public</span>
|
||||
</label>
|
||||
|
||||
<div className="create-set-header">
|
||||
<h3>Terms</h3>
|
||||
<h3>Definitions</h3>
|
||||
</div>
|
||||
|
||||
{this.state.inputContents.concat({ term: "", definition: "" }).map((contents, index) =>
|
||||
<div className="create-set-input-row" key={index}>
|
||||
<input
|
||||
type="text"
|
||||
name={`term_${index}`}
|
||||
onChange={this.onTermInputChange}
|
||||
value={this.state.inputContents[index] ? this.state.inputContents[index].term : ""}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name={`definition_${index}`}
|
||||
onChange={this.onDefinitionInputChange}
|
||||
value={this.state.inputContents[index] ? this.state.inputContents[index].definition : ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
577
src/GroupPage.js
Normal file
577
src/GroupPage.js
Normal file
@@ -0,0 +1,577 @@
|
||||
import React, { Component } from 'react';
|
||||
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 NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import Footer from "./Footer";
|
||||
|
||||
import "./css/GroupPage.css";
|
||||
import "./css/ConfirmationDialog.css";
|
||||
|
||||
import Loader from "./puff-loader.svg"
|
||||
|
||||
export default withRouter(class GroupPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
functions: {
|
||||
removeSetFromGroup: props.functions.httpsCallable("removeSetFromGroup"),
|
||||
getGroupMembers: props.functions.httpsCallable("getGroupMembers"),
|
||||
},
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
role: null,
|
||||
groupName: "",
|
||||
sets: {},
|
||||
memberCount: null,
|
||||
joinCode: "",
|
||||
editGroupName: false,
|
||||
loading: false,
|
||||
groupUsers: {
|
||||
owners: [],
|
||||
contributors: [],
|
||||
members: [],
|
||||
},
|
||||
editingUser: null,
|
||||
showDeleteGroup: false,
|
||||
deleteGroupLoading: false,
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.state.db
|
||||
.collection("users")
|
||||
.doc(this.state.user.uid)
|
||||
.collection("groups")
|
||||
.doc(this.props.match.params.groupId)
|
||||
.get()
|
||||
.then((userGroupDoc) => {
|
||||
this.state.db
|
||||
.collection("groups")
|
||||
.doc(this.props.match.params.groupId)
|
||||
.get()
|
||||
.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) => {
|
||||
return this.state.db.collection("sets")
|
||||
.doc(setId)
|
||||
.get()
|
||||
.then((doc) => {
|
||||
newState.sets[setId] = {
|
||||
displayName: doc.data().title,
|
||||
loading: false,
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
if (newState.role === "owner") {
|
||||
var counter = 0;
|
||||
const getGroupMembers = () => {
|
||||
return this.state.functions.getGroupMembers({ groupId: this.props.match.params.groupId })
|
||||
.catch((error) => {
|
||||
return {
|
||||
data: {
|
||||
owners: [
|
||||
{
|
||||
displayName: this.state.user.displayName,
|
||||
uid: this.state.user.uid,
|
||||
}
|
||||
],
|
||||
contributors: [],
|
||||
members: [],
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const groupUsers = await getGroupMembers();
|
||||
|
||||
newState.groupUsers = groupUsers.data;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
editGroupName = () => {
|
||||
this.setState({
|
||||
editGroupName: true,
|
||||
}, () => this.groupNameInput.focus());
|
||||
}
|
||||
|
||||
handleGroupNameChange = (event) => {
|
||||
this.setState({
|
||||
groupName: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleInputKeypress = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
this.renameGroup();
|
||||
} else if (event.key === "Escape") {
|
||||
this.cancelGroupRename();
|
||||
}
|
||||
}
|
||||
|
||||
stopLoading = () => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
editGroupName: false,
|
||||
})
|
||||
}
|
||||
|
||||
cancelGroupRename = () => {
|
||||
this.setState({
|
||||
editGroupName: false,
|
||||
groupName: this.state.originalGroupName,
|
||||
})
|
||||
}
|
||||
|
||||
renameGroup = () => {
|
||||
if (!this.state.loading && this.state.groupName.replace(" ", "") !== "") {
|
||||
if (this.state.groupName.trim() === this.state.originalGroupName) {
|
||||
this.cancelGroupRename();
|
||||
} else {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
|
||||
this.state.db.collection("groups")
|
||||
.doc(this.props.match.params.groupId)
|
||||
.update({
|
||||
display_name: this.state.groupName.trim(),
|
||||
}).then(() => {
|
||||
this.stopLoading();
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't update group name: ${error}`);
|
||||
this.setState({
|
||||
loading: false,
|
||||
groupName: this.state.originalGroupName,
|
||||
editGroupName: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeSet = (setId) => {
|
||||
let newLoadingState = {
|
||||
sets: this.state.sets,
|
||||
};
|
||||
newLoadingState.sets[setId].loading = true;
|
||||
this.setState(newLoadingState);
|
||||
|
||||
this.state.functions.removeSetFromGroup({
|
||||
groupId: this.props.match.params.groupId,
|
||||
setId: setId,
|
||||
}).then(() => {
|
||||
let newState = {
|
||||
sets: this.state.sets,
|
||||
};
|
||||
delete newState.sets[setId];
|
||||
this.setState(newState);
|
||||
});
|
||||
}
|
||||
|
||||
showEditUserRole = (role, index) => {
|
||||
let user;
|
||||
if (role === "owner") {
|
||||
user = this.state.groupUsers.owners[index];
|
||||
} else if (role === "contributor") {
|
||||
user = this.state.groupUsers.contributors[index];
|
||||
} else {
|
||||
user = this.state.groupUsers.members[index];
|
||||
}
|
||||
this.setState({
|
||||
editingUser: {
|
||||
uid: user.uid,
|
||||
role: role,
|
||||
index: index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
hideEditUserRole = () => {
|
||||
this.setState({
|
||||
editingUser: null,
|
||||
});
|
||||
}
|
||||
|
||||
editUserRole = (role) => {
|
||||
if (role === this.state.editingUser.role) {
|
||||
this.setState({
|
||||
editingUser: null,
|
||||
});
|
||||
} else {
|
||||
if (role === "remove") {
|
||||
this.state.db.collection("users")
|
||||
.doc(this.state.editingUser.uid)
|
||||
.collection("groups")
|
||||
.doc(this.props.match.params.groupId)
|
||||
.delete()
|
||||
.then(() => {
|
||||
let groupUsers = this.state.groupUsers;
|
||||
if (this.state.editingUser.role === "owner") {
|
||||
groupUsers.owners.splice(this.state.editingUser.index, 1);
|
||||
} else if (this.state.editingUser.role === "contributor") {
|
||||
groupUsers.contributors.splice(this.state.editingUser.index, 1);
|
||||
} else {
|
||||
groupUsers.members.splice(this.state.editingUser.index, 1);
|
||||
}
|
||||
this.setState({
|
||||
editingUser: null,
|
||||
groupUsers: groupUsers,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
editingUser: null,
|
||||
});
|
||||
console.log(`Couldn't change user role: ${error}`)
|
||||
});
|
||||
} else {
|
||||
this.state.db.collection("users")
|
||||
.doc(this.state.editingUser.uid)
|
||||
.collection("groups")
|
||||
.doc(this.props.match.params.groupId)
|
||||
.update({
|
||||
role: role,
|
||||
}).then(() => {
|
||||
let groupUsers = this.state.groupUsers;
|
||||
let userData;
|
||||
if (this.state.editingUser.role === "owner") {
|
||||
userData = groupUsers.owners.splice(this.state.editingUser.index, 1)[0];
|
||||
} else if (this.state.editingUser.role === "contributor") {
|
||||
userData = groupUsers.contributors.splice(this.state.editingUser.index, 1)[0];
|
||||
} else {
|
||||
userData = groupUsers.members.splice(this.state.editingUser.index, 1)[0];
|
||||
}
|
||||
if (role === "owner") {
|
||||
groupUsers.owners.push(userData);
|
||||
} else if (role === "contributor") {
|
||||
groupUsers.contributors.push(userData);
|
||||
} else {
|
||||
groupUsers.members.push(userData);
|
||||
}
|
||||
this.setState({
|
||||
editingUser: null,
|
||||
groupUsers: groupUsers,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
editingUser: null,
|
||||
});
|
||||
console.log(`Couldn't change user role: ${error}`)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showDeleteGroup = () => {
|
||||
this.setState({
|
||||
showDeleteGroup: true,
|
||||
});
|
||||
}
|
||||
|
||||
hideDeleteGroup = () => {
|
||||
this.setState({
|
||||
showDeleteGroup: false,
|
||||
});
|
||||
}
|
||||
|
||||
deleteGroup = () => {
|
||||
this.setState({
|
||||
deleteGroupLoading: true,
|
||||
});
|
||||
|
||||
this.state.db.collection("groups")
|
||||
.doc(this.props.match.params.groupId)
|
||||
.delete()
|
||||
.then(() => {
|
||||
this.props.history.push("/groups");
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't delete group: ${error}`);
|
||||
this.setState({
|
||||
deleteGroupLoading: false,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
<main>
|
||||
{
|
||||
(this.state.role === null)
|
||||
?
|
||||
<img className="page-loader" src={Loader} alt="Loading..." />
|
||||
:
|
||||
<>
|
||||
<div className="page-header">
|
||||
{
|
||||
this.state.editGroupName && this.state.role === "owner"
|
||||
?
|
||||
<h1 className="group-name-header-input-container">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.handleGroupNameChange}
|
||||
value={this.state.groupName}
|
||||
onKeyDown={this.handleInputKeypress}
|
||||
ref={inputEl => (this.groupNameInput = inputEl)}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.renameGroup}
|
||||
icon={<ArrowForwardRoundedIcon />}
|
||||
className="button--round"
|
||||
disabled={this.state.loading || this.state.groupName.replace(" ", "") === ""}
|
||||
loading={this.state.loading}
|
||||
></Button>
|
||||
</h1>
|
||||
:
|
||||
<h1 onClick={this.state.role === "owner" ? this.editGroupName : () => {}}>
|
||||
{this.state.groupName}
|
||||
{
|
||||
this.state.role === "owner" &&
|
||||
<span className="group-edit-icon">
|
||||
<EditRoundedIcon />
|
||||
</span>
|
||||
}
|
||||
</h1>
|
||||
}
|
||||
{
|
||||
this.state.role === "owner" &&
|
||||
<Button
|
||||
onClick={this.showDeleteGroup}
|
||||
icon={<DeleteRoundedIcon />}
|
||||
className="button--round"
|
||||
></Button>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
this.state.joinCode &&
|
||||
<div className="stat-row stat-row--inline">
|
||||
<p>Join code</p>
|
||||
<h2>{this.state.joinCode}</h2>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.memberCount &&
|
||||
<div className="stat-row stat-row--inline">
|
||||
<h2>{this.state.memberCount}</h2>
|
||||
<p>
|
||||
member
|
||||
{ this.state.memberCount !== 1 && "s" }
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<h2>Sets</h2>
|
||||
{
|
||||
Object.keys(this.state.sets).length > 0
|
||||
?
|
||||
<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}
|
||||
></Button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<p>
|
||||
This group doesn't have any sets yet!
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
this.state.role === "owner" &&
|
||||
<>
|
||||
<div>
|
||||
<h2>Members</h2>
|
||||
{
|
||||
this.state.groupUsers && this.state.groupUsers.owners.length > 0 &&
|
||||
<>
|
||||
<h3 className="group-role-header">Owners</h3>
|
||||
<div className="group-links-container">
|
||||
{
|
||||
this.state.groupUsers.owners.map((user, index) =>
|
||||
<p
|
||||
key={user.uid}
|
||||
className={`group-set-link ${user.uid !== this.state.user.uid ? "group-set-link--enabled" : ""}`}
|
||||
onClick={user.uid === this.state.user.uid ? () => { } : () => this.showEditUserRole("owner", index)}
|
||||
>
|
||||
{
|
||||
user.uid === this.state.user.uid
|
||||
?
|
||||
"You"
|
||||
:
|
||||
<>
|
||||
{user.displayName}
|
||||
<EditRoundedIcon />
|
||||
</>
|
||||
}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{
|
||||
this.state.groupUsers && this.state.groupUsers.contributors.length > 0 &&
|
||||
<>
|
||||
<h3 className="group-role-header">Contributors</h3>
|
||||
<div className="group-links-container">
|
||||
{
|
||||
this.state.groupUsers.contributors.map((user, index) =>
|
||||
<p
|
||||
key={user.uid}
|
||||
className={`group-set-link ${user.uid !== this.state.user.uid ? "group-set-link--enabled" : ""}`}
|
||||
onClick={user.uid === this.state.user.uid ? () => { } : () => this.showEditUserRole("contributor", index)}
|
||||
>
|
||||
{
|
||||
user.uid === this.state.user.uid
|
||||
?
|
||||
"You"
|
||||
:
|
||||
<>
|
||||
{user.displayName}
|
||||
<EditRoundedIcon />
|
||||
</>
|
||||
}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{
|
||||
this.state.groupUsers && this.state.groupUsers.members.length > 0 &&
|
||||
<>
|
||||
<h3 className="group-role-header">Members</h3>
|
||||
<div className="group-links-container">
|
||||
{
|
||||
this.state.groupUsers.members.map((user, index) =>
|
||||
<p
|
||||
key={user.uid}
|
||||
className={`group-set-link ${user.uid !== this.state.user.uid ? "group-set-link--enabled" : ""}`}
|
||||
onClick={user.uid === this.state.user.uid ? () => { } : () => this.showEditUserRole("member", index)}
|
||||
>
|
||||
{
|
||||
user.uid === this.state.user.uid
|
||||
?
|
||||
"You"
|
||||
:
|
||||
<>
|
||||
{user.displayName}
|
||||
<EditRoundedIcon />
|
||||
</>
|
||||
}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
this.state.editingUser &&
|
||||
<>
|
||||
<div className="overlay" onClick={this.hideEditUserRole}></div>
|
||||
<div className="overlay-content group-page-overlay-content">
|
||||
{
|
||||
["Owner", "Contributor", "Member", "Remove"].map((role) =>
|
||||
<h3
|
||||
key={role}
|
||||
onClick={() => this.editUserRole(role.toLowerCase())}
|
||||
>
|
||||
{role}
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
||||
<div onClick={this.hideEditUserRole}>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{
|
||||
this.state.showDeleteGroup &&
|
||||
<>
|
||||
<div className="overlay" onClick={this.hideDeleteGroup}></div>
|
||||
<div className="overlay-content confirmation-dialog">
|
||||
<h3>Are you sure you want to delete this group?</h3>
|
||||
<div className="button-container">
|
||||
<Button
|
||||
onClick={this.hideDeleteGroup}
|
||||
>
|
||||
No
|
||||
</Button>
|
||||
<Button
|
||||
onClick={this.deleteGroup}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
199
src/History.js
Normal file
199
src/History.js
Normal file
@@ -0,0 +1,199 @@
|
||||
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 NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import Footer from "./Footer";
|
||||
import { Link } from 'react-router-dom';
|
||||
import "./css/History.css";
|
||||
|
||||
export default class History extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
progressHistoryComplete: [],
|
||||
progressHistoryIncomplete: [],
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = "History | Parandum";
|
||||
|
||||
this.state.db.collection("progress")
|
||||
.where("uid", "==", this.state.user.uid)
|
||||
.orderBy("start_time", "desc")
|
||||
.get()
|
||||
.then((querySnapshot) => {
|
||||
let complete = [];
|
||||
let incomplete = [];
|
||||
|
||||
querySnapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
|
||||
if (data.duration !== null) {
|
||||
return complete.push({
|
||||
id: doc.id,
|
||||
setTitle: data.set_title,
|
||||
switchLanguage: data.switch_language,
|
||||
progress: (data.progress / data.questions.length * 100).toFixed(2),
|
||||
mark: (data.progress > 0 ? data.correct.length / data.progress * 100 : 0).toFixed(2),
|
||||
mode: data.mode,
|
||||
});
|
||||
} else {
|
||||
return incomplete.push({
|
||||
id: doc.id,
|
||||
setTitle: data.set_title,
|
||||
switchLanguage: data.switch_language,
|
||||
progress: (data.progress / data.questions.length * 100).toFixed(2),
|
||||
mark: (data.progress > 0 ? data.correct.length / data.progress * 100 : 0).toFixed(2),
|
||||
mode: data.mode,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
progressHistoryComplete: complete,
|
||||
progressHistoryIncomplete: incomplete,
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't retrieve progress history: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
deleteProgress = (progressId) => {
|
||||
this.state.db.collection("progress")
|
||||
.doc(progressId)
|
||||
.delete()
|
||||
.then(() => {
|
||||
const progressIndex = this.state.progressHistoryIncomplete.map((obj) => obj.id).indexOf(progressId);
|
||||
let newState = {
|
||||
progressHistoryIncomplete: this.state.progressHistoryIncomplete,
|
||||
};
|
||||
delete newState.progressHistoryIncomplete[progressIndex];
|
||||
this.setState(newState);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
<main>
|
||||
<h1>History</h1>
|
||||
|
||||
{
|
||||
this.state.progressHistoryComplete.length > 0 || this.state.progressHistoryIncomplete.length > 0
|
||||
?
|
||||
<>
|
||||
{
|
||||
this.state.progressHistoryIncomplete.length > 0 &&
|
||||
<div className="progress-history-container">
|
||||
<h2>Incomplete</h2>
|
||||
<div>
|
||||
<h3>Set</h3>
|
||||
<h3>Progress</h3>
|
||||
<h3>Mark</h3>
|
||||
<h3>Mode</h3>
|
||||
</div>
|
||||
{
|
||||
this.state.progressHistoryIncomplete.map((progressItem) =>
|
||||
<div key={progressItem.id}>
|
||||
<Link
|
||||
to={`/progress/${progressItem.id}`}
|
||||
>
|
||||
{progressItem.setTitle}
|
||||
{
|
||||
progressItem.switchLanguage &&
|
||||
<SwapHorizRoundedIcon />
|
||||
}
|
||||
</Link>
|
||||
<p>{progressItem.progress}%</p>
|
||||
<p>{progressItem.mark}%</p>
|
||||
<p>
|
||||
{
|
||||
progressItem.mode === "questions"
|
||||
?
|
||||
<QuestionAnswerRoundedIcon />
|
||||
:
|
||||
<PeopleRoundedIcon />
|
||||
}
|
||||
</p>
|
||||
<Button
|
||||
className="button--no-background"
|
||||
onClick={() => this.deleteProgress(progressItem.id)}
|
||||
icon={<DeleteRoundedIcon />}
|
||||
></Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.progressHistoryComplete.length > 0 &&
|
||||
<div className="progress-history-container">
|
||||
<h2>Completed</h2>
|
||||
<div>
|
||||
<h3>Set</h3>
|
||||
<h3>Progress</h3>
|
||||
<h3>Mark</h3>
|
||||
<h3>Mode</h3>
|
||||
</div>
|
||||
{
|
||||
this.state.progressHistoryComplete.map((progressItem) =>
|
||||
<div key={progressItem.id}>
|
||||
<Link
|
||||
to={`/progress/${progressItem.id}`}
|
||||
>
|
||||
{progressItem.setTitle}
|
||||
{
|
||||
progressItem.switchLanguage &&
|
||||
<SwapHorizRoundedIcon />
|
||||
}
|
||||
</Link>
|
||||
<p>{progressItem.progress}%</p>
|
||||
<p>{progressItem.mark}%</p>
|
||||
{
|
||||
progressItem.mode === "questions"
|
||||
?
|
||||
<QuestionAnswerRoundedIcon />
|
||||
:
|
||||
<PeopleRoundedIcon />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
:
|
||||
<p>You haven't done any tests yet.</p>
|
||||
}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
34
src/Home.js
Normal file
34
src/Home.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import "@material-ui/core";
|
||||
import { AccountCircleRounded as AccountCircleIcon } from "@material-ui/icons";
|
||||
import "./css/Home.css";
|
||||
import NavBar from './NavBar';
|
||||
import Footer from "./Footer";
|
||||
|
||||
export default function Home() {
|
||||
const navbarItems = [
|
||||
{
|
||||
type: "link",
|
||||
name: "Login",
|
||||
link: "/login",
|
||||
icon: <AccountCircleIcon />,
|
||||
hideTextMobile: false,
|
||||
}
|
||||
];
|
||||
|
||||
document.title = "Parandum";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={navbarItems} />
|
||||
|
||||
<main>
|
||||
<div className="description-section">
|
||||
<h1>Parandum</h1>
|
||||
<p>The next-generation ad-free language-learning platform</p>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
434
src/LoggedInHome.js
Normal file
434
src/LoggedInHome.js
Normal file
@@ -0,0 +1,434 @@
|
||||
import React from 'react';
|
||||
import NavBar from "./NavBar";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ExitToAppRounded as ExitToAppRoundedIcon, HistoryRounded as HistoryRoundedIcon, SettingsRounded as SettingsRoundedIcon, PersonRounded as PersonRoundedIcon, PublicRounded as PublicRoundedIcon, GroupRounded as GroupRoundedIcon, AddRounded as AddRoundedIcon, SwapHorizRounded as SwapHorizRoundedIcon, PeopleRounded as PeopleRoundedIcon, DeleteRounded as DeleteRoundedIcon, QuestionAnswerRounded as QuestionAnswerRoundedIcon } from "@material-ui/icons";
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
import Xarrow from 'react-xarrows';
|
||||
|
||||
import "firebase/auth";
|
||||
import "firebase/firestore";
|
||||
import "firebase/functions";
|
||||
import Button from './Button';
|
||||
import LinkButton from './LinkButton';
|
||||
import Footer from "./Footer";
|
||||
import "./css/Form.css";
|
||||
import "./css/History.css";
|
||||
import "./css/LoggedInHome.css";
|
||||
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
export default withRouter(class LoggedInHome extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
functions: {
|
||||
createProgress: props.functions.httpsCallable("createProgress"),
|
||||
},
|
||||
canStartTest: false,
|
||||
loading: false,
|
||||
selections: {},
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
name: "History",
|
||||
link: "/history",
|
||||
icon: <HistoryRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
name: "Settings",
|
||||
link: "/settings",
|
||||
icon: <SettingsRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
name: "Logout",
|
||||
onClick: () => this.state.firebase.auth().signOut(),
|
||||
icon: <ExitToAppRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
progressHistoryIncomplete: [],
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = "Parandum";
|
||||
|
||||
const userSetsRef = this.state.db.collection("sets")
|
||||
.where("owner", "==", this.state.user.uid)
|
||||
.orderBy("title");
|
||||
const publicSetsRef = this.state.db.collection("sets")
|
||||
.where("public", "==", true)
|
||||
.where("owner", "!=", this.state.user.uid)
|
||||
.orderBy("owner")
|
||||
.orderBy("title");
|
||||
const userGroupsRef = this.state.db.collection("users")
|
||||
.doc(this.state.user.uid)
|
||||
.collection("groups");
|
||||
const userGroupSetsRef = this.state.db.collection("sets");
|
||||
const groupRef = this.state.db.collection("groups");
|
||||
const progressRef = this.state.db.collection("progress")
|
||||
.where("uid", "==", this.state.user.uid)
|
||||
.where("duration", "==", null)
|
||||
.orderBy("start_time", "desc")
|
||||
|
||||
let newState = this.state;
|
||||
|
||||
const userSetsQuery = userSetsRef.get().then((userSetsQuerySnapshot) => {
|
||||
newState.userSets = userSetsQuerySnapshot.docs;
|
||||
|
||||
userSetsQuerySnapshot.docs.map((doc) => newState.selections[doc.id] = false);
|
||||
|
||||
});
|
||||
const publicSetsQuery = publicSetsRef.get().then((publicSetsQuerySnapshot) => {
|
||||
newState.publicSets = publicSetsQuerySnapshot.docs;
|
||||
|
||||
publicSetsQuerySnapshot.docs.map((doc) => newState.selections[doc.id] = false);
|
||||
|
||||
});
|
||||
const userGroupsQuery = userGroupsRef.get().then(async (userGroupsQuerySnapshot) => {
|
||||
newState.user.groups = [];
|
||||
var userGroupSets = [];
|
||||
|
||||
return Promise.all(userGroupsQuerySnapshot.docs.map((group) => {
|
||||
newState.user.groups.push(group.id);
|
||||
|
||||
const groupData = groupRef.doc(group.id).get();
|
||||
|
||||
return userGroupSetsRef
|
||||
.where("public", "==", true)
|
||||
.where("groups", "array-contains", group.id)
|
||||
.get().then(async (userGroupSetsQuerySnapshot) => {
|
||||
groupData.then((result) => {
|
||||
if (typeof result !== "undefined" && typeof result.data === "function" && userGroupSetsQuerySnapshot.docs.length > 0) {
|
||||
userGroupSets.push({
|
||||
group: result,
|
||||
sets: userGroupSetsQuerySnapshot.docs,
|
||||
});
|
||||
|
||||
userGroupSetsQuerySnapshot.docs.map((doc) => newState.selections[doc.id] = false);
|
||||
}
|
||||
});
|
||||
});
|
||||
})).then(() => {
|
||||
newState.userGroupSets = userGroupSets.sort((a,b) => {
|
||||
if (a.group.data().display_name < b.group.data().display_name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.group.data().display_name > b.group.data().display_name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
});
|
||||
});
|
||||
const progressQuery = progressRef.get().then((progressQuerySnapshot) => {
|
||||
progressQuerySnapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
return newState.progressHistoryIncomplete.push({
|
||||
id: doc.id,
|
||||
setTitle: data.set_title,
|
||||
switchLanguage: data.switch_language,
|
||||
progress: (data.progress / data.questions.length * 100).toFixed(2),
|
||||
mark: (data.progress > 0 ? data.correct.length / data.progress * 100 : 0).toFixed(2),
|
||||
mode: data.mode,
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
Promise.all([
|
||||
userSetsQuery,
|
||||
publicSetsQuery,
|
||||
userGroupsQuery,
|
||||
progressQuery
|
||||
]).then(() => {
|
||||
this.setState(newState);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
stopLoading = () => {
|
||||
this.setState({
|
||||
canStartTest: true,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
startTest = () => {
|
||||
if (this.state.canStartTest) {
|
||||
const selections = Object.keys(this.state.selections)
|
||||
.filter(x => this.state.selections[x]);
|
||||
this.state.functions.createProgress({
|
||||
sets: selections,
|
||||
switch_language: false,
|
||||
mode: "questions",
|
||||
limit: 10,
|
||||
}).then((result) => {
|
||||
const progressId = result.data;
|
||||
this.stopLoading();
|
||||
this.props.history.push("/progress/" + progressId);
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't start test: ${error}`);
|
||||
this.stopLoading();
|
||||
});
|
||||
|
||||
this.setState({
|
||||
canStartTest: false,
|
||||
loading: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSetSelectionChange = (event) => {
|
||||
let newState = { ...this.state };
|
||||
newState.selections[event.target.name] = event.target.checked;
|
||||
this.setState(newState);
|
||||
if (Object.values(this.state.selections).indexOf(true) > -1) {
|
||||
this.setState({ canStartTest: true });
|
||||
} else {
|
||||
this.setState({ canStartTest: false });
|
||||
}
|
||||
}
|
||||
|
||||
deleteProgress = (progressId) => {
|
||||
this.state.db.collection("progress")
|
||||
.doc(progressId)
|
||||
.delete()
|
||||
.then(() => {
|
||||
const progressIndex = this.state.progressHistoryIncomplete.map((obj) => obj.id).indexOf(progressId);
|
||||
let newState = {
|
||||
progressHistoryIncomplete: this.state.progressHistoryIncomplete,
|
||||
};
|
||||
newState.progressHistoryIncomplete.splice(progressIndex);
|
||||
this.setState(newState);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
|
||||
<main>
|
||||
<div className="page-header page-header--home">
|
||||
<h1>Study</h1>
|
||||
<div className="button-container buttons--desktop">
|
||||
<Button
|
||||
onClick={this.startTest}
|
||||
loading={this.state.loading}
|
||||
disabled={!this.state.canStartTest}
|
||||
>
|
||||
Test ({Object.values(this.state.selections).filter(x => x === true).length})
|
||||
</Button>
|
||||
<LinkButton
|
||||
to="/groups"
|
||||
>
|
||||
Groups
|
||||
</LinkButton>
|
||||
{
|
||||
this.state.userSets && this.state.userSets.length > 0 &&
|
||||
<LinkButton
|
||||
to="/my-sets"
|
||||
>
|
||||
My Sets
|
||||
</LinkButton>
|
||||
}
|
||||
<LinkButton id="create-set-button-desktop" to="/create-set" icon={<AddRoundedIcon/>} className="button--round"></LinkButton>
|
||||
</div>
|
||||
<LinkButton id="create-set-button-mobile" to="/create-set" icon={<AddRoundedIcon />} className="button--round buttons--mobile"></LinkButton>
|
||||
</div>
|
||||
{
|
||||
this.state.progressHistoryIncomplete.length > 0 &&
|
||||
<>
|
||||
<h2>Incomplete Tests</h2>
|
||||
<div className="progress-history-container">
|
||||
<div>
|
||||
<h3>Set</h3>
|
||||
<h3>Progress</h3>
|
||||
<h3>Mark</h3>
|
||||
<h3>Mode</h3>
|
||||
</div>
|
||||
{
|
||||
this.state.progressHistoryIncomplete.map((progressItem) =>
|
||||
<div key={progressItem.id}>
|
||||
<Link
|
||||
to={`/progress/${progressItem.id}`}
|
||||
>
|
||||
{progressItem.setTitle}
|
||||
{
|
||||
progressItem.switchLanguage &&
|
||||
<SwapHorizRoundedIcon />
|
||||
}
|
||||
</Link>
|
||||
<p>{progressItem.progress}%</p>
|
||||
<p>{progressItem.mark}%</p>
|
||||
<p>
|
||||
{
|
||||
progressItem.mode === "questions"
|
||||
?
|
||||
<QuestionAnswerRoundedIcon />
|
||||
:
|
||||
<PeopleRoundedIcon />
|
||||
}
|
||||
</p>
|
||||
<Button
|
||||
className="button--no-background"
|
||||
onClick={() => this.deleteProgress(progressItem.id)}
|
||||
icon={<DeleteRoundedIcon />}
|
||||
></Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<div className="page-header page-header--left buttons--mobile">
|
||||
<Button
|
||||
onClick={this.startTest}
|
||||
loading={this.state.loading}
|
||||
disabled={!this.state.canStartTest}
|
||||
>
|
||||
Test ({Object.values(this.state.selections).filter(x => x === true).length})
|
||||
</Button>
|
||||
<LinkButton
|
||||
to="/groups"
|
||||
>
|
||||
Groups
|
||||
</LinkButton>
|
||||
{
|
||||
this.state.userSets && this.state.userSets.length > 0 &&
|
||||
<LinkButton
|
||||
to="/my-sets"
|
||||
>
|
||||
My Sets
|
||||
</LinkButton>
|
||||
}
|
||||
</div>
|
||||
<p id="page-intro" className="page-intro">
|
||||
{
|
||||
this.state.userSets && this.state.userSets.length > 0
|
||||
?
|
||||
"Choose sets to study"
|
||||
:
|
||||
"Create a set to start studying!"
|
||||
}
|
||||
</p>
|
||||
{
|
||||
(this.state.userSets && this.state.userSets.length === 0) && (this.state.userGroupSets && this.state.userGroupSets.length === 0) && this.state.progressHistoryIncomplete.length === 0 &&
|
||||
<>
|
||||
<Xarrow
|
||||
start="page-intro"
|
||||
end="create-set-button-mobile"
|
||||
curveness={0.5}
|
||||
color="white"
|
||||
divContainerProps={{ className: "buttons--mobile" }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
<div className="form set-list">
|
||||
{this.state.userSets && this.state.userSets.length > 0 &&
|
||||
<div className="checkbox-list-container">
|
||||
<h3><PersonRoundedIcon /> Personal Sets</h3>
|
||||
<div className="checkbox-list">
|
||||
{this.state.userSets
|
||||
.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.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}`}>
|
||||
<h3><GroupRoundedIcon /> {data.group.data().display_name}</h3>
|
||||
</Link>
|
||||
|
||||
<div className="checkbox-list">
|
||||
{data.sets.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.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>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
44
src/Login.js
Normal file
44
src/Login.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import Home from './Home';
|
||||
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';
|
||||
import "./css/Login.css";
|
||||
import "./css/PopUp.css";
|
||||
import { Link } from 'react-router-dom';
|
||||
import "@material-ui/core";
|
||||
import { CloseRounded as CloseRoundedIcon } from "@material-ui/icons";
|
||||
|
||||
import "firebase/auth";
|
||||
|
||||
export default function Login(props) {
|
||||
const auth = props.auth;
|
||||
const uiConfig = {
|
||||
signInFlow: 'redirect',
|
||||
signInSuccessUrl: '/',
|
||||
signInOptions: [
|
||||
props.firebase.auth.GoogleAuthProvider.PROVIDER_ID,
|
||||
"microsoft.com",
|
||||
props.firebase.auth.EmailAuthProvider.PROVIDER_ID
|
||||
],
|
||||
credentialHelper: props.firebase.auth.CredentialHelper.GOOGLE_YOLO,
|
||||
callbacks: {
|
||||
signInSuccessWithAuthResult: () => false,
|
||||
},
|
||||
};
|
||||
|
||||
document.body.style.overflow = "hidden";
|
||||
document.title = "Login | Parandum";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Home />
|
||||
<Link to="/" className="overlay"></Link>
|
||||
<div className="overlay-content login-overlay-content">
|
||||
<h1>Login</h1>
|
||||
<StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={auth} />
|
||||
<Link className="popup-close-button" to="/">
|
||||
<CloseRoundedIcon />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
622
src/Progress.js
Normal file
622
src/Progress.js
Normal file
@@ -0,0 +1,622 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { HomeRounded as HomeRoundedIcon, ArrowForwardRounded as ArrowForwardRoundedIcon, SettingsRounded as SettingsRoundedIcon, CloseRounded as CloseRoundedIcon } from "@material-ui/icons";
|
||||
import NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import LinkButton from "./LinkButton";
|
||||
import Error404 from "./Error404";
|
||||
import SettingsContent from "./SettingsContent";
|
||||
import Footer from "./Footer";
|
||||
|
||||
import "./css/PopUp.css";
|
||||
import "./css/Progress.css";
|
||||
|
||||
export default withRouter(class Progress extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
functions: {
|
||||
processAnswer: props.functions.httpsCallable("processAnswer"),
|
||||
},
|
||||
loading: false,
|
||||
canProceed: true,
|
||||
navbarItems: [
|
||||
{
|
||||
type: "button",
|
||||
onClick: this.showSettings,
|
||||
icon: <SettingsRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
progressInaccessible: false,
|
||||
correct: 0,
|
||||
incorrect: 0,
|
||||
totalQuestions: 0,
|
||||
progress: 0,
|
||||
setTitle: "",
|
||||
switchLanguage: false,
|
||||
answerInput: "",
|
||||
currentPrompt: "",
|
||||
currentSound: false,
|
||||
currentSetOwner: "",
|
||||
nextPrompt: "",
|
||||
nextSound: false,
|
||||
nextSetOwner: "",
|
||||
currentAnswerStatus: null,
|
||||
currentCorrect: [],
|
||||
moreAnswers: true,
|
||||
duration: 0,
|
||||
incorrectAnswers: {},
|
||||
showSettings: false,
|
||||
soundInput: this.props.sound,
|
||||
themeInput: this.props.theme,
|
||||
setIds: [],
|
||||
attemptNumber: 1,
|
||||
};
|
||||
|
||||
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() {
|
||||
const progressId = this.props.match.params.progressId;
|
||||
const progressRef = this.state.db.collection("progress").doc(progressId);
|
||||
|
||||
let [ newState, setDone, incorrectAnswers, duration ] = await progressRef.get().then((doc) => {
|
||||
const data = doc.data();
|
||||
|
||||
document.title = `Study | ${data.set_title} | Parandum`;
|
||||
|
||||
let newState = {
|
||||
correct: data.correct.length,
|
||||
incorrect: data.incorrect.length,
|
||||
totalQuestions: data.questions.length,
|
||||
questions: data.questions,
|
||||
progress: data.progress,
|
||||
setTitle: data.set_title,
|
||||
switchLanguage: data.switch_language,
|
||||
currentSetOwner: data.set_owner,
|
||||
currentCorrect: data.current_correct,
|
||||
mode: data.mode,
|
||||
nextPrompt: null,
|
||||
setIds: data.setIds,
|
||||
};
|
||||
|
||||
if (data.lives) {
|
||||
newState.lives = data.lives;
|
||||
}
|
||||
|
||||
return [ newState, data.duration !== null, data.incorrect, data.duration ];
|
||||
}).catch((error) => {
|
||||
console.log(`Progress data inaccessible: ${error}`);
|
||||
return [
|
||||
{
|
||||
progressInaccessible: true,
|
||||
},
|
||||
true
|
||||
];
|
||||
});
|
||||
|
||||
if (!newState.progressInaccessible && !setDone) {
|
||||
let nextPromptRef;
|
||||
if (!newState.switchLanguage) {
|
||||
nextPromptRef = progressRef
|
||||
.collection("terms")
|
||||
.doc(newState.questions[newState.progress]);
|
||||
} else {
|
||||
nextPromptRef = progressRef
|
||||
.collection("definitions")
|
||||
.doc(newState.questions[newState.progress]);
|
||||
}
|
||||
|
||||
await nextPromptRef.get().then((doc) => {
|
||||
newState.currentPrompt = doc.data().item;
|
||||
newState.currentSound = doc.data().sound === true;
|
||||
}).catch((error) => {
|
||||
newState.progressInaccessible = true;
|
||||
console.log(`Progress data inaccessible: ${error}`);
|
||||
});
|
||||
} else if (setDone) {
|
||||
newState.moreAnswers = false;
|
||||
newState.currentAnswerStatus = true;
|
||||
newState.duration = duration;
|
||||
|
||||
let promises = [];
|
||||
|
||||
promises.push(this.state.db.collection("progress")
|
||||
.where("uid", "==", this.state.user.uid)
|
||||
.where("setIds", "==", newState.setIds)
|
||||
.orderBy("start_time")
|
||||
.get()
|
||||
.then((querySnapshot) => {
|
||||
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
||||
}));
|
||||
|
||||
if (incorrectAnswers.length > 0) {
|
||||
newState.incorrectAnswers = {};
|
||||
|
||||
promises.push(Promise.all(incorrectAnswers.map((vocabId) => {
|
||||
if (newState.incorrectAnswers[vocabId]) {
|
||||
return newState.incorrectAnswers[vocabId].count++;
|
||||
} else {
|
||||
newState.incorrectAnswers[vocabId] = {
|
||||
count: 1,
|
||||
};
|
||||
|
||||
return Promise.all([
|
||||
progressRef.collection("terms")
|
||||
.doc(vocabId)
|
||||
.get().then((termDoc) => {
|
||||
newState.switchLanguage ? newState.incorrectAnswers[vocabId].answer = termDoc.data().item.split("/") : newState.incorrectAnswers[vocabId].prompt = termDoc.data().item;
|
||||
}),
|
||||
progressRef.collection("definitions")
|
||||
.doc(vocabId)
|
||||
.get().then((definitionDoc) => {
|
||||
newState.switchLanguage ? newState.incorrectAnswers[vocabId].prompt = definitionDoc.data().item : newState.incorrectAnswers[vocabId].answer = definitionDoc.data().item.split("/");
|
||||
})
|
||||
]);
|
||||
}
|
||||
})).catch((error) => {
|
||||
console.log(`Couldn't retrieve incorrect answers: ${error}`);
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
this.setState(newState, () => {
|
||||
if (!setDone) this.answerInput.focus()
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
showSettings = () => {
|
||||
this.setState({
|
||||
showSettings: true,
|
||||
});
|
||||
}
|
||||
|
||||
handleSoundInputChange = (event) => {
|
||||
this.setState({
|
||||
soundInput: event.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleThemeInputChange = (newTheme) => {
|
||||
if (this.state.themeInput !== newTheme) this.setState({
|
||||
themeInput: newTheme,
|
||||
});
|
||||
}
|
||||
|
||||
saveSettings = (globalChange) => {
|
||||
this.props.handleSoundChange(this.state.soundInput, globalChange);
|
||||
this.props.handleThemeChange(this.state.themeInput, globalChange);
|
||||
this.hideSettings();
|
||||
}
|
||||
|
||||
hideSettings = () => {
|
||||
this.setState({
|
||||
showSettings: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleAnswerInput = (event) => {
|
||||
this.setState({
|
||||
answerInput: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
showNextItem = () => {
|
||||
if (this.state.canProceed) {
|
||||
if (this.state.currentAnswerStatus === null) {
|
||||
this.processAnswer();
|
||||
} else {
|
||||
this.nextQuestion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startLoading = () => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
canProceed: false,
|
||||
});
|
||||
}
|
||||
|
||||
cleanseVocabString = (item) => {
|
||||
const chars = " .,()-_'\"";
|
||||
|
||||
let newString = item;
|
||||
|
||||
chars.split("").forEach((char) => {
|
||||
newString = newString.replace(char, "");
|
||||
});
|
||||
|
||||
return newString;
|
||||
}
|
||||
|
||||
processAnswer = async () => {
|
||||
if (this.state.canProceed) {
|
||||
const cleansedCurrentCorrect = this.state.currentCorrect.map(item => this.cleanseVocabString(item));
|
||||
|
||||
this.startLoading();
|
||||
|
||||
if (!cleansedCurrentCorrect.includes(this.cleanseVocabString(this.state.answerInput))) {
|
||||
this.state.functions.processAnswer({
|
||||
answer: this.state.answerInput,
|
||||
progressId: this.props.match.params.progressId,
|
||||
}).then(async (result) => {
|
||||
const data = result.data;
|
||||
let newState = {
|
||||
currentAnswerStatus: data.correct,
|
||||
currentCorrect: data.correctAnswers,
|
||||
moreAnswers: data.moreAnswers,
|
||||
nextPrompt: data.nextPrompt ? data.nextPrompt.item : null,
|
||||
nextSound: data.nextPrompt ? data.nextPrompt.sound : null,
|
||||
nextSetOwner: data.nextPrompt ? data.nextPrompt.set_owner : null,
|
||||
progress: data.progress,
|
||||
totalQuestions: data.totalQuestions,
|
||||
correct: data.totalCorrect,
|
||||
incorrect: data.totalIncorrect,
|
||||
currentVocabId: data.currentVocabId,
|
||||
loading: false,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
if (data.correct && !data.moreAnswers && this.state.incorrectAnswers[data.currentVocabId]) {
|
||||
// all answers to question given correctly
|
||||
// answer was previously wrong
|
||||
// store correct answer
|
||||
newState.incorrectAnswers = this.state.incorrectAnswers;
|
||||
newState.incorrectAnswers[data.currentVocabId].answer = data.correctAnswers;
|
||||
} else if (!data.correct) {
|
||||
// incorrect answer given
|
||||
// store prompt and count=0
|
||||
newState.incorrectAnswers = this.state.incorrectAnswers;
|
||||
newState.incorrectAnswers[data.currentVocabId] = {
|
||||
prompt: this.state.currentPrompt,
|
||||
answer: "",
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
|
||||
if (data.duration) {
|
||||
// test done
|
||||
newState.duration = data.duration;
|
||||
|
||||
promises.push(this.state.db.collection("progress")
|
||||
.where("uid", "==", this.state.user.uid)
|
||||
.where("setIds", "==", this.state.setIds)
|
||||
.orderBy("start_time")
|
||||
.get()
|
||||
.then((querySnapshot) => {
|
||||
console.log(querySnapshot);
|
||||
newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1;
|
||||
}));
|
||||
}
|
||||
|
||||
if (data.incorrectAnswers) {
|
||||
let unsavedAnswers = {};
|
||||
|
||||
if (!newState.incorrectAnswers) {
|
||||
newState.incorrectAnswers = {};
|
||||
}
|
||||
|
||||
data.incorrectAnswers.map((vocabId) => {
|
||||
if (newState.incorrectAnswers[vocabId]) {
|
||||
// already been logged including prompt and correct answer
|
||||
newState.incorrectAnswers[vocabId].count++;
|
||||
} else {
|
||||
// not been saved yet
|
||||
// update count in unsaved answers
|
||||
unsavedAnswers[vocabId] ? unsavedAnswers[vocabId]++ : unsavedAnswers[vocabId] = 1;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
promises.push(Promise.all(Object.keys(unsavedAnswers).map((vocabId) => {
|
||||
// get and store vocab docs that haven't already been stored (due to refresh)
|
||||
const progressDocRef = this.state.db
|
||||
.collection("progress")
|
||||
.doc(this.props.match.params.progressId);
|
||||
|
||||
newState.incorrectAnswers[vocabId] = {
|
||||
count: unsavedAnswers[vocabId],
|
||||
};
|
||||
|
||||
return Promise.all([
|
||||
progressDocRef.collection("terms")
|
||||
.doc(vocabId)
|
||||
.get().then((termDoc) => {
|
||||
this.state.switchLanguage ? newState.incorrectAnswers[vocabId].answer = termDoc.data().item.split("/") : newState.incorrectAnswers[vocabId].prompt = termDoc.data().item;
|
||||
}),
|
||||
progressDocRef.collection("definitions")
|
||||
.doc(vocabId)
|
||||
.get().then((definitionDoc) => {
|
||||
this.state.switchLanguage ? newState.incorrectAnswers[vocabId].prompt = definitionDoc.data().item : newState.incorrectAnswers[vocabId].answer = definitionDoc.data().item.split("/");
|
||||
})
|
||||
]);
|
||||
})));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.setState(newState);
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't process answer: ${error}`);
|
||||
this.setState({
|
||||
loading: false,
|
||||
canProceed: true,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
currentAnswerStatus: null,
|
||||
answerInput: "",
|
||||
loading: false,
|
||||
canProceed: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextQuestion = () => {
|
||||
if (this.state.canProceed) {
|
||||
this.startLoading();
|
||||
|
||||
let newState = {
|
||||
currentAnswerStatus: null,
|
||||
answerInput: "",
|
||||
loading: false,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
if (!this.state.moreAnswers) {
|
||||
newState.currentCorrect = [];
|
||||
newState.currentPrompt = this.state.nextPrompt;
|
||||
newState.currentSound = this.state.nextSound;
|
||||
newState.currentSetOwner = this.state.nextSetOwner;
|
||||
}
|
||||
|
||||
this.setState(newState, () => (this.isMounted) && this.answerInput.focus());
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
this.state.progressInaccessible
|
||||
?
|
||||
<Error404 />
|
||||
:
|
||||
<>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
<main>
|
||||
{
|
||||
this.state.currentAnswerStatus === null
|
||||
?
|
||||
<>
|
||||
<p className="current-prompt">{this.state.currentPrompt}</p>
|
||||
<form className="answer-input-container" onSubmit={(e) => e.preventDefault()} >
|
||||
<input type="submit" className="form-submit" onClick={this.showNextItem} />
|
||||
<input
|
||||
type="text"
|
||||
name="answer_input"
|
||||
className="answer-input"
|
||||
onChange={this.handleAnswerInput}
|
||||
value={this.state.answerInput}
|
||||
ref={inputEl => (this.answerInput = inputEl)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => this.processAnswer()}
|
||||
icon={<ArrowForwardRoundedIcon />}
|
||||
className="button--round"
|
||||
disabled={!this.state.canProceed}
|
||||
loading={this.state.loading}
|
||||
></Button>
|
||||
</form>
|
||||
<div className="correct-answers">
|
||||
{
|
||||
this.state.currentCorrect && this.state.currentCorrect.length > 0
|
||||
?
|
||||
<>
|
||||
<h2>
|
||||
{
|
||||
this.state.moreAnswers
|
||||
?
|
||||
"Correct so far:"
|
||||
:
|
||||
"Answers:"
|
||||
}
|
||||
</h2>
|
||||
{this.state.currentCorrect.map((vocab, index) =>
|
||||
<p key={index}>{vocab}</p>
|
||||
)}
|
||||
</>
|
||||
:
|
||||
""
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
this.state.nextPrompt === null && !this.state.moreAnswers
|
||||
?
|
||||
<>
|
||||
{/* DONE */}
|
||||
<h1>{this.state.setTitle}</h1>
|
||||
<div className="stat-row stat-row--inline">
|
||||
<p>You got</p>
|
||||
<h1>{`${(this.state.correct / this.state.totalQuestions * 100).toFixed(2)}%`}</h1>
|
||||
</div>
|
||||
<div className="stat-row stat-row--inline">
|
||||
<h1>{`${this.state.correct} of ${this.state.totalQuestions}`}</h1>
|
||||
<p>marks</p>
|
||||
</div>
|
||||
<div className="stat-row stat-row--inline">
|
||||
<p>You took</p>
|
||||
<h1>{this.msToTime(this.state.duration)}</h1>
|
||||
</div>
|
||||
<div className="stat-row stat-row--inline stat-row--no-gap">
|
||||
<p>Attempt #</p>
|
||||
<h1>{this.state.attemptNumber}</h1>
|
||||
</div>
|
||||
{
|
||||
this.state.incorrectAnswers && Object.keys(this.state.incorrectAnswers).length > 0 &&
|
||||
<>
|
||||
<h2>Incorrect answers:</h2>
|
||||
<div className="progress-end-incorrect-answers">
|
||||
<div>
|
||||
<h3>Prompt</h3>
|
||||
<h3>Answer</h3>
|
||||
<h3>Mistakes</h3>
|
||||
</div>
|
||||
{
|
||||
Object.keys(this.state.incorrectAnswers).map(key =>
|
||||
[key, this.state.incorrectAnswers[key].count])
|
||||
.sort((a,b) => b[1] - a[1]).map(item =>
|
||||
<div key={item[0]}>
|
||||
<p>{this.state.incorrectAnswers[item[0]].prompt ? this.state.incorrectAnswers[item[0]].prompt : ""}</p>
|
||||
<p>{this.state.incorrectAnswers[item[0]].answer ? this.state.incorrectAnswers[item[0]].answer.join("/") : ""}</p>
|
||||
<p>{this.state.incorrectAnswers[item[0]].count}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{/* TODO: provide the attempt number -- .get() where array-contains-all array of setIds from original sets? would mean a new field in db and adjusting cloud fns again */}
|
||||
|
||||
<div className="progress-end-button-container">
|
||||
<LinkButton
|
||||
to="/"
|
||||
className="progress-end-button"
|
||||
>
|
||||
Done
|
||||
</LinkButton>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
{/* ANSWER PROCESSED */}
|
||||
<p className="current-prompt">{this.state.currentPrompt}</p>
|
||||
<form className="answer-input-container answer-input-container--answer-entered" onSubmit={(e) => e.preventDefault()} >
|
||||
<input type="submit" className="form-submit" onClick={this.showNextItem} />
|
||||
<input
|
||||
type="text"
|
||||
name="answer_input"
|
||||
className={`answer-input ${this.state.currentAnswerStatus ? "answer-input--correct" : "answer-input--incorrect"}`}
|
||||
value={this.state.answerInput}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
onClick={() => this.nextQuestion()}
|
||||
icon={<ArrowForwardRoundedIcon />}
|
||||
className="button--round"
|
||||
disabled={!this.state.canProceed}
|
||||
loading={this.state.loading}
|
||||
></Button>
|
||||
</form>
|
||||
<div className={`correct-answers ${this.state.currentAnswerStatus ? "correct-answers--correct" : "correct-answers--incorrect"}`}>
|
||||
{
|
||||
this.state.currentCorrect
|
||||
?
|
||||
<>
|
||||
<h2>
|
||||
{
|
||||
this.state.moreAnswers
|
||||
?
|
||||
"Correct so far:"
|
||||
:
|
||||
"Answers:"
|
||||
}
|
||||
</h2>
|
||||
{this.state.currentCorrect.map((vocab, index) =>
|
||||
<p key={index}>{vocab}</p>
|
||||
)}
|
||||
</>
|
||||
:
|
||||
""
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
{
|
||||
this.state.showSettings &&
|
||||
<>
|
||||
<div className="overlay" onClick={this.hideSettings}></div>
|
||||
<div className="overlay-content progress-settings-overlay-content">
|
||||
<SettingsContent
|
||||
sound={this.props.sound}
|
||||
theme={this.props.theme}
|
||||
saveSettings={this.saveSettings}
|
||||
handleSoundInputChange={this.handleSoundInputChange}
|
||||
handleThemeInputChange={this.handleThemeInputChange}
|
||||
themes={this.props.themes}
|
||||
soundInput={this.state.soundInput}
|
||||
themeInput={this.state.themeInput}
|
||||
/>
|
||||
|
||||
|
||||
<div className="settings-save-container">
|
||||
<Button
|
||||
onClick={() => this.saveSettings(true)}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => this.saveSettings(false)}
|
||||
>
|
||||
Save for this session
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={this.hideSettings}
|
||||
icon={<CloseRoundedIcon />}
|
||||
className="button--no-background popup-close-button"
|
||||
></Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
370
src/SetPage.js
Normal file
370
src/SetPage.js
Normal file
@@ -0,0 +1,370 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { HomeRounded as HomeRoundedIcon, PlayArrowRounded as PlayArrowRoundedIcon, EditRounded as EditRoundedIcon, CloudQueueRounded as CloudQueueRoundedIcon, GroupAddRounded as GroupAddRoundedIcon, CloseRounded as CloseRoundedIcon, DeleteRounded as DeleteRoundedIcon } from "@material-ui/icons";
|
||||
import NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import LinkButton from "./LinkButton";
|
||||
import Error404 from "./Error404";
|
||||
import Footer from "./Footer";
|
||||
|
||||
import "./css/PopUp.css";
|
||||
import "./css/SetPage.css";
|
||||
import "./css/ConfirmationDialog.css";
|
||||
|
||||
export default withRouter(class SetPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
functions: {
|
||||
createProgress: props.functions.httpsCallable("createProgress"),
|
||||
addSetToGroup: props.functions.httpsCallable("addSetToGroup"),
|
||||
},
|
||||
loading: false,
|
||||
canStartTest: true,
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
set: {
|
||||
title: "",
|
||||
public: false,
|
||||
vocab: [],
|
||||
},
|
||||
setInaccessible: false,
|
||||
showAddSetToGroup: false,
|
||||
canAddSetToGroup: true,
|
||||
addSetToGroupLoading: {},
|
||||
groups: {},
|
||||
currentSetGroups: [],
|
||||
showDeleteConfirmation: false,
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const setId = this.props.match.params.setId;
|
||||
const setRef = this.state.db
|
||||
.collection("sets")
|
||||
.doc(setId);
|
||||
const setVocabRef = setRef
|
||||
.collection("vocab")
|
||||
.orderBy("term");
|
||||
|
||||
setRef.get().then((setDoc) => {
|
||||
document.title = `${setDoc.data().title} | Parandum`;
|
||||
|
||||
setVocabRef.get().then((querySnapshot) => {
|
||||
let vocab = [];
|
||||
querySnapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
return vocab.push({
|
||||
term: data.term,
|
||||
definition: data.definition,
|
||||
sound: data.sound,
|
||||
});
|
||||
});
|
||||
this.setState({
|
||||
set: {
|
||||
...this.state.set,
|
||||
title: setDoc.data().title,
|
||||
public: setDoc.data().public,
|
||||
vocab: vocab,
|
||||
owner: setDoc.data().owner,
|
||||
},
|
||||
currentSetGroups: setDoc.data().groups,
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
setInaccessible: true,
|
||||
});
|
||||
console.log(`Can't access set: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
stopLoading = () => {
|
||||
this.setState({
|
||||
canStartTest: true,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
startTest = () => {
|
||||
if (this.state.canStartTest) {
|
||||
this.state.functions.createProgress({
|
||||
sets: [this.props.match.params.setId],
|
||||
switch_language: false,
|
||||
mode: "questions",
|
||||
limit: 10,
|
||||
}).then((result) => {
|
||||
const progressId = result.data;
|
||||
this.stopLoading();
|
||||
this.props.history.push("/progress/" + progressId);
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't start test: ${error}`);
|
||||
this.stopLoading();
|
||||
});
|
||||
|
||||
this.setState({
|
||||
canStartTest: false,
|
||||
loading: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
showAddSetToGroup = async () => {
|
||||
let newState = {
|
||||
showAddSetToGroup: true,
|
||||
groups: {},
|
||||
addSetToGroupLoading: {},
|
||||
};
|
||||
await this.state.db.collection("users")
|
||||
.doc(this.state.user.uid)
|
||||
.collection("groups")
|
||||
.where("role", "!=", "member")
|
||||
.get()
|
||||
.then((querySnapshot) => {
|
||||
return Promise.all(querySnapshot.docs.map((userGroupDoc) => {
|
||||
if (!this.state.currentSetGroups.includes(userGroupDoc.id))
|
||||
return this.state.db.collection("groups")
|
||||
.doc(userGroupDoc.id)
|
||||
.get()
|
||||
.then((groupDoc) => {
|
||||
newState.groups[userGroupDoc.id] = groupDoc.data().display_name;
|
||||
newState.addSetToGroupLoading[userGroupDoc.id] = false;
|
||||
});
|
||||
return true;
|
||||
}));
|
||||
})
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
hideAddSetToGroup = () => {
|
||||
this.setState({
|
||||
showAddSetToGroup: false,
|
||||
});
|
||||
}
|
||||
|
||||
stopAddSetToGroupLoading = (groupId, addedSetToGroup = false) => {
|
||||
let newState = {
|
||||
addSetToGroupLoading: {
|
||||
...this.state.addSetToGroupLoading,
|
||||
[groupId]: false,
|
||||
},
|
||||
canAddSetToGroup: true,
|
||||
showAddSetToGroup: false,
|
||||
};
|
||||
if (addedSetToGroup) newState.currentSetGroups = this.state.currentSetGroups.concat(groupId);
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
addSetToGroup = (groupId) => {
|
||||
if (this.state.canAddSetToGroup) {
|
||||
this.setState({
|
||||
addSetToGroupLoading: {
|
||||
...this.state.addSetToGroupLoading,
|
||||
[groupId]: true,
|
||||
},
|
||||
canAddSetToGroup: false,
|
||||
});
|
||||
|
||||
this.state.functions.addSetToGroup({
|
||||
groupId: groupId,
|
||||
setId: this.props.match.params.setId,
|
||||
}).then((result) => {
|
||||
this.stopAddSetToGroupLoading(groupId, true);
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't add set to group: ${error}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showDeleteSet = () => {
|
||||
this.setState({
|
||||
showDeleteConfirmation: true,
|
||||
});
|
||||
}
|
||||
|
||||
hideDeleteConfirmation = () => {
|
||||
this.setState({
|
||||
showDeleteConfirmation: false,
|
||||
});
|
||||
}
|
||||
|
||||
deleteSet = () => {
|
||||
this.state.db.collection("sets")
|
||||
.doc(this.props.match.params.setId)
|
||||
.delete()
|
||||
.then(() => {
|
||||
this.props.history.push("/");
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't delete set: ${error}`);
|
||||
this.setState({
|
||||
showDeleteConfirmation: false,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.setInaccessible
|
||||
?
|
||||
<Error404 />
|
||||
:
|
||||
<>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
|
||||
<main>
|
||||
<div className="page-header">
|
||||
<h1>
|
||||
{this.state.set.title}
|
||||
{
|
||||
this.state.set.public
|
||||
?
|
||||
<span className="set-cloud-icon"><CloudQueueRoundedIcon /></span>
|
||||
:
|
||||
""
|
||||
}
|
||||
</h1>
|
||||
<div className="button-container">
|
||||
<Button
|
||||
loading={this.state.loading}
|
||||
onClick={() => this.startTest()}
|
||||
icon={<PlayArrowRoundedIcon />}
|
||||
disabled={!this.state.canStartTest}
|
||||
className="button--round"
|
||||
></Button>
|
||||
<Button
|
||||
onClick={() => this.showAddSetToGroup()}
|
||||
icon={<GroupAddRoundedIcon />}
|
||||
className="button--round"
|
||||
></Button>
|
||||
{
|
||||
this.state.set.owner === this.state.user.uid &&
|
||||
<LinkButton
|
||||
to={`/sets/${this.props.match.params.setId}/edit`}
|
||||
icon={<EditRoundedIcon />}
|
||||
className="button--round"
|
||||
></LinkButton>
|
||||
}
|
||||
{
|
||||
this.state.set.owner === this.state.user.uid && this.state.currentSetGroups.length === 0 &&
|
||||
<Button
|
||||
onClick={() => this.showDeleteSet()}
|
||||
icon={<DeleteRoundedIcon />}
|
||||
className="button--round"
|
||||
></Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="vocab-list">
|
||||
<div className="vocab-list-header">
|
||||
<h3>Terms</h3>
|
||||
<h3>Definitions</h3>
|
||||
</div>
|
||||
|
||||
{this.state.set.vocab.map((contents, index) =>
|
||||
<div className="vocab-row" key={index}>
|
||||
<span>{contents.term}</span>
|
||||
<span>{contents.definition}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
{
|
||||
this.state.showAddSetToGroup
|
||||
?
|
||||
<>
|
||||
<div className="overlay" onClick={this.hideAddSetToGroup}></div>
|
||||
<div className="overlay-content set-page-group-overlay-content">
|
||||
{
|
||||
Object.keys(this.state.groups).length < 1
|
||||
?
|
||||
<>
|
||||
<h1>No Groups Found</h1>
|
||||
<span>This could be because:</span>
|
||||
<ul className="no-groups-message-list">
|
||||
<li>you're not a member of any groups</li>
|
||||
<li>this set is already a part of your groups</li>
|
||||
<li>you don't have the required permissions</li>
|
||||
</ul>
|
||||
<p>To add sets to a group, you must be an owner or collaborator.</p>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<h1>Select a Group</h1>
|
||||
|
||||
<div className="set-page-overlay-group-container">
|
||||
{
|
||||
Object.keys(this.state.groups).map((groupId) =>
|
||||
<Button
|
||||
onClick={() => this.addSetToGroup(groupId)}
|
||||
className="button--no-background"
|
||||
loading={this.state.addSetToGroupLoading[groupId]}
|
||||
disabled={!this.state.canAddSetToGroup}
|
||||
key={groupId}
|
||||
>{this.state.groups[groupId]}</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
<Button
|
||||
onClick={this.hideAddSetToGroup}
|
||||
icon={<CloseRoundedIcon />}
|
||||
className="button--no-background popup-close-button"
|
||||
></Button>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
this.state.showDeleteConfirmation &&
|
||||
<>
|
||||
<div className="overlay" onClick={this.hideDeleteConfirmation}></div>
|
||||
<div className="overlay-content confirmation-dialog">
|
||||
<h3>Are you sure you want to delete this set?</h3>
|
||||
<div className="button-container">
|
||||
<Button
|
||||
onClick={this.hideDeleteConfirmation}
|
||||
>
|
||||
No
|
||||
</Button>
|
||||
<Button
|
||||
onClick={this.deleteSet}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
97
src/Settings.js
Normal file
97
src/Settings.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { Component } from 'react';
|
||||
import { HomeRounded as HomeRoundedIcon } from "@material-ui/icons";
|
||||
import NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import SettingsContent from "./SettingsContent";
|
||||
import Footer from "./Footer";
|
||||
|
||||
export default withRouter(class Settings extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
soundInput: this.props.sound,
|
||||
themeInput: this.props.theme,
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = "Settings | Parandum";
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
handleSoundInputChange = (event) => {
|
||||
this.setState({
|
||||
soundInput: event.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleThemeInputChange = (newTheme) => {
|
||||
if (this.state.themeInput !== newTheme) this.setState({
|
||||
themeInput: newTheme,
|
||||
});
|
||||
}
|
||||
|
||||
saveSettings = (globalChange) => {
|
||||
this.props.handleSoundChange(this.state.soundInput, globalChange);
|
||||
this.props.handleThemeChange(this.state.themeInput, globalChange);
|
||||
this.props.history.push("/");
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
<main>
|
||||
<SettingsContent
|
||||
sound={this.props.sound}
|
||||
theme={this.props.theme}
|
||||
saveSettings={this.saveSettings}
|
||||
handleSoundInputChange={this.handleSoundInputChange}
|
||||
handleThemeInputChange={this.handleThemeInputChange}
|
||||
themes={this.props.themes}
|
||||
soundInput={this.state.soundInput}
|
||||
themeInput={this.state.themeInput}
|
||||
/>
|
||||
|
||||
<div className="settings-save-container">
|
||||
<Button
|
||||
onClick={() => this.saveSettings(true)}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => this.saveSettings(false)}
|
||||
>
|
||||
Save for this session
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
347
src/UserGroups.js
Normal file
347
src/UserGroups.js
Normal file
@@ -0,0 +1,347 @@
|
||||
import React, { Component } from 'react';
|
||||
import { HomeRounded as HomeRoundedIcon, GroupAddRounded as GroupAddRoundedIcon, GroupRounded as GroupRoundedIcon, CloseRounded as CloseRoundedIcon, ArrowForwardRounded as ArrowForwardRoundedIcon, PersonRounded as PersonRoundedIcon, EditRounded as EditRoundedIcon, VisibilityRounded as VisibilityRoundedIcon } from "@material-ui/icons";
|
||||
import NavBar from "./NavBar";
|
||||
import Button from "./Button";
|
||||
import Footer from "./Footer";
|
||||
import { withRouter, Link } from "react-router-dom";
|
||||
|
||||
import "./css/PopUp.css";
|
||||
import "./css/UserGroups.css";
|
||||
|
||||
export default withRouter(class UserGroups extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
functions: {
|
||||
createGroup: props.functions.httpsCallable("createGroup"),
|
||||
},
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
createGroup: false,
|
||||
joinGroup: false,
|
||||
groupName: "",
|
||||
joinCode: "",
|
||||
canCreateGroup: false,
|
||||
canJoinGroup: false,
|
||||
canFindGroup: true,
|
||||
userGroups: null,
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = "Groups | Parandum";
|
||||
|
||||
const userGroupsRef = this.state.db.collection("users")
|
||||
.doc(this.state.user.uid)
|
||||
.collection("groups")
|
||||
.orderBy("role");
|
||||
|
||||
userGroupsRef.get().then(async (querySnapshot) => {
|
||||
let newState = {
|
||||
userGroups: {},
|
||||
};
|
||||
|
||||
await Promise.all(querySnapshot.docs.map((userGroupDoc) => {
|
||||
return this.state.db
|
||||
.collection("groups")
|
||||
.doc(userGroupDoc.id)
|
||||
.get()
|
||||
.then((groupDoc) => {
|
||||
if (userGroupDoc.data() && groupDoc.data()) newState.userGroups[userGroupDoc.id] = {
|
||||
role: userGroupDoc.data().role,
|
||||
displayName: groupDoc.data().display_name,
|
||||
setCount: groupDoc.data().sets.length,
|
||||
memberCount: Object.keys(groupDoc.data().users).length,
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
this.setState(newState);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
showJoinGroup = () => {
|
||||
this.setState({
|
||||
joinGroup: true,
|
||||
}, () => this.joinCodeInput.focus());
|
||||
}
|
||||
|
||||
hideJoinGroup = () => {
|
||||
this.setState({
|
||||
joinGroup: false,
|
||||
});
|
||||
}
|
||||
|
||||
showCreateGroup = () => {
|
||||
this.setState({
|
||||
createGroup: true,
|
||||
}, () => this.groupNameInput.focus());
|
||||
}
|
||||
|
||||
hideCreateGroup = () => {
|
||||
this.setState({
|
||||
createGroup: false,
|
||||
});
|
||||
}
|
||||
|
||||
createGroup = () => {
|
||||
if (this.state.canCreateGroup) {
|
||||
this.startCreateGroupLoading();
|
||||
|
||||
this.state.functions.createGroup(this.state.groupName)
|
||||
.then((result) => {
|
||||
this.props.history.push(`/groups/${result.data}`);
|
||||
this.stopCreateGroupLoading();
|
||||
}).catch((error) => {
|
||||
console.log(`Couldn't create group: ${error}`);
|
||||
this.stopCreateGroupLoading();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
joinGroup = () => {
|
||||
if (this.state.canJoinGroup) {
|
||||
this.startJoinGroupLoading();
|
||||
|
||||
this.state.db.collection("join_codes")
|
||||
.doc(this.state.joinCode)
|
||||
.get()
|
||||
.then((joinCodeDoc) => {
|
||||
this.state.db.collection("users")
|
||||
.doc(this.state.user.uid)
|
||||
.collection("groups")
|
||||
.doc(joinCodeDoc.data().group)
|
||||
.set({
|
||||
role: "member",
|
||||
}).then(() => {
|
||||
this.props.history.push(`/groups/${joinCodeDoc.data().group}`);
|
||||
this.stopJoinGroupLoading();
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.stopJoinGroupLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleGroupNameChange = (event) => {
|
||||
this.setState({
|
||||
groupName: event.target.value,
|
||||
canCreateGroup: event.target.value.replace(" ", "") !== "",
|
||||
});
|
||||
}
|
||||
|
||||
handleJoinCodeChange = (event) => {
|
||||
this.setState({
|
||||
joinCode: event.target.value,
|
||||
canJoinGroup: event.target.value.replace(" ", "") !== "",
|
||||
canFindGroup: true,
|
||||
});
|
||||
}
|
||||
|
||||
startJoinGroupLoading = () => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
canJoinGroup: false,
|
||||
});
|
||||
}
|
||||
|
||||
stopJoinGroupLoading = (canFindGroup = true) => {
|
||||
let newState = {
|
||||
loading: false,
|
||||
canJoinGroup: true,
|
||||
};
|
||||
if (!canFindGroup) newState.canFindGroup = false;
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
startCreateGroupLoading = () => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
canCreateGroup: false,
|
||||
});
|
||||
}
|
||||
|
||||
stopCreateGroupLoading = () => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
canCreateGroup: true,
|
||||
});
|
||||
}
|
||||
|
||||
handleGroupNameInputKeypress = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
this.createGroup();
|
||||
}
|
||||
}
|
||||
|
||||
handleJoinCodeInputKeypress = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
this.joinGroup();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
<main>
|
||||
<div className="page-header">
|
||||
<h1>Groups</h1>
|
||||
<div className="button-container">
|
||||
<Button
|
||||
onClick={this.showJoinGroup}
|
||||
icon={<GroupRoundedIcon />}
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
<Button
|
||||
onClick={this.showCreateGroup}
|
||||
icon={<GroupAddRoundedIcon />}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
this.state.userGroups && Object.keys(this.state.userGroups).length > 0
|
||||
?
|
||||
<>
|
||||
<div className="user-groups-list">
|
||||
{Object.keys(this.state.userGroups)
|
||||
.sort((a, b) => {
|
||||
if (this.state.userGroups[a].displayName < this.state.userGroups[b].displayName) {
|
||||
return -1;
|
||||
}
|
||||
if (this.state.userGroups[a].displayName > this.state.userGroups[b].displayName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map((groupId, index) =>
|
||||
<Link
|
||||
to={`/groups/${groupId}`}
|
||||
key={index}
|
||||
>
|
||||
{this.state.userGroups[groupId].displayName}
|
||||
<span className="user-group-role-icon">
|
||||
{
|
||||
this.state.userGroups[groupId].role === "owner"
|
||||
?
|
||||
<PersonRoundedIcon />
|
||||
:
|
||||
this.state.userGroups[groupId].role === "contributor"
|
||||
?
|
||||
<EditRoundedIcon />
|
||||
:
|
||||
<VisibilityRoundedIcon />
|
||||
}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<h4>You aren't a member of any groups</h4>
|
||||
}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
{
|
||||
this.state.createGroup
|
||||
?
|
||||
<>
|
||||
<div className="overlay" onClick={this.hideCreateGroup}></div>
|
||||
<div className="overlay-content user-groups-overlay-content">
|
||||
<h1>Create Group</h1>
|
||||
|
||||
<div className="user-groups-overlay-input-container">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.handleGroupNameChange}
|
||||
value={this.state.groupName}
|
||||
placeholder="Group Name"
|
||||
onKeyDown={this.handleGroupNameInputKeypress}
|
||||
ref={inputEl => (this.groupNameInput = inputEl)}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.createGroup}
|
||||
icon={<ArrowForwardRoundedIcon />}
|
||||
className="button--round"
|
||||
loading={this.state.loading}
|
||||
disabled={!this.state.canCreateGroup}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={this.hideCreateGroup}
|
||||
icon={<CloseRoundedIcon />}
|
||||
className="button--no-background popup-close-button"
|
||||
></Button>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
this.state.joinGroup &&
|
||||
<>
|
||||
<div className="overlay" onClick={this.hideJoinGroup}></div>
|
||||
<div className="overlay-content user-groups-overlay-content">
|
||||
<h1>Join Group</h1>
|
||||
|
||||
<div className="user-groups-overlay-input-container">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.handleJoinCodeChange}
|
||||
value={this.state.joinCode}
|
||||
placeholder="Join Code"
|
||||
onKeyDown={this.handleJoinCodeInputKeypress}
|
||||
ref={inputEl => (this.joinCodeInput = inputEl)}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.joinGroup}
|
||||
icon={<ArrowForwardRoundedIcon />}
|
||||
className="button--round"
|
||||
loading={this.state.loading}
|
||||
disabled={!this.state.canJoinGroup}
|
||||
></Button>
|
||||
</div>
|
||||
|
||||
{
|
||||
!this.state.canFindGroup &&
|
||||
<p>Can't find that group!</p>
|
||||
}
|
||||
|
||||
<Button
|
||||
onClick={this.hideJoinGroup}
|
||||
icon={<CloseRoundedIcon />}
|
||||
className="button--no-background popup-close-button"
|
||||
></Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
95
src/UserSets.js
Normal file
95
src/UserSets.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter, Link } from 'react-router-dom';
|
||||
import { HomeRounded as HomeRoundedIcon, EditRounded as EditRoundedIcon } from "@material-ui/icons";
|
||||
import NavBar from "./NavBar";
|
||||
import Footer from "./Footer";
|
||||
|
||||
import "./css/UserSets.css";
|
||||
|
||||
export default withRouter(class UserSets extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
functions: {
|
||||
createProgress: props.functions.httpsCallable("createProgress"),
|
||||
},
|
||||
canStartTest: false,
|
||||
loading: false,
|
||||
selections: {},
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
let isMounted = true;
|
||||
Object.defineProperty(this, "isMounted", {
|
||||
get: () => isMounted,
|
||||
set: (value) => isMounted = value,
|
||||
});
|
||||
}
|
||||
|
||||
setState = (state, callback = null) => {
|
||||
if (this.isMounted) super.setState(state, callback);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = "Sets | Parandum";
|
||||
|
||||
const userSetsRef = this.state.db.collection("sets")
|
||||
.where("owner", "==", this.state.user.uid)
|
||||
.orderBy("title");
|
||||
|
||||
userSetsRef.get().then((querySnapshot) => {
|
||||
this.setState({
|
||||
userSets: querySnapshot.docs,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
|
||||
<main>
|
||||
<h1>My Sets</h1>
|
||||
<div className="user-sets-list">
|
||||
{
|
||||
this.state.userSets && this.state.userSets.length > 0
|
||||
?
|
||||
this.state.userSets
|
||||
.map((set, index) =>
|
||||
<div className="user-sets-row" key={index}>
|
||||
<Link
|
||||
to={`/sets/${set.id}`}
|
||||
>
|
||||
{set.data().title}
|
||||
</Link>
|
||||
<Link
|
||||
to={`/sets/${set.id}/edit`}
|
||||
>
|
||||
<EditRoundedIcon />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
:
|
||||
<p>You haven't made any sets yet!</p>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user