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