Add pages

This commit is contained in:
2021-09-01 17:29:15 +01:00
parent 68a7dbabca
commit f07ab92cfc
12 changed files with 3423 additions and 0 deletions

269
src/CreateSet.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
)
}
})