Add ability to search public sets! Also bump version
Public sets are now paginated and searchable, on a separate search page
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parandum",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
|
||||
@@ -19,6 +19,7 @@ import MistakesHistory from "./MistakesHistory";
|
||||
import TermsOfService from "./TermsOfService";
|
||||
import PrivacyPolicy from "./PrivacyPolicy";
|
||||
import Button from "./Button";
|
||||
import SearchSets from './SearchSets';
|
||||
import { CheckRounded as CheckRoundedIcon } from "@material-ui/icons";
|
||||
import Loader from "./puff-loader.svg";
|
||||
|
||||
@@ -294,6 +295,9 @@ class App extends React.Component {
|
||||
<Route path="/sets/:setId" exact>
|
||||
<SetPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
||||
</Route>
|
||||
<Route path="/search" exact>
|
||||
<SearchSets db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
||||
</Route>
|
||||
<Route path="/groups" exact>
|
||||
<UserGroups db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
|
||||
</Route>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import NavBar from "./NavBar";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimelineRounded as TimelineRoundedIcon, 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 { TimelineRounded as TimelineRoundedIcon, ExitToAppRounded as ExitToAppRoundedIcon, HistoryRounded as HistoryRoundedIcon, SettingsRounded as SettingsRoundedIcon, PersonRounded as PersonRoundedIcon, GroupRounded as GroupRoundedIcon, AddRounded as AddRoundedIcon, SwapHorizRounded as SwapHorizRoundedIcon, PeopleRounded as PeopleRoundedIcon, DeleteRounded as DeleteRoundedIcon, QuestionAnswerRounded as QuestionAnswerRoundedIcon, SearchRounded as SearchRoundedIcon } from "@material-ui/icons";
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
import Xarrow from 'react-xarrows';
|
||||
@@ -93,11 +93,6 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
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");
|
||||
@@ -115,12 +110,6 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
|
||||
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 = [];
|
||||
@@ -179,7 +168,6 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
|
||||
Promise.all([
|
||||
userSetsQuery,
|
||||
publicSetsQuery,
|
||||
userGroupsQuery,
|
||||
progressQuery
|
||||
]).then(() => {
|
||||
@@ -378,22 +366,23 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
>
|
||||
Test ({Object.values(this.state.selections).filter(x => x === true).length})
|
||||
</Button>
|
||||
<LinkButton
|
||||
to="/groups"
|
||||
>
|
||||
Groups
|
||||
</LinkButton>
|
||||
{
|
||||
(!this.props.page.loaded() || (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" title="Create set"></LinkButton>
|
||||
<LinkButton to="/create-set" icon={<AddRoundedIcon/>} className="button--round" title="Create set"></LinkButton>
|
||||
<LinkButton to="/search" icon={<SearchRoundedIcon/>} className="button--round" title="Search sets"></LinkButton>
|
||||
<LinkButton to="/groups" icon={<GroupRoundedIcon/>} className="button--round" title="Groups"></LinkButton>
|
||||
</div>
|
||||
<div className="button-container buttons--mobile">
|
||||
<LinkButton id="create-set-button-mobile" to="/create-set" icon={<AddRoundedIcon />} className="button--round buttons--mobile" title="Create set"></LinkButton>
|
||||
<LinkButton to="/search" icon={<SearchRoundedIcon/>} className="button--round" title="Search sets"></LinkButton>
|
||||
<LinkButton to="/groups" icon={<GroupRoundedIcon/>} className="button--round" title="Groups"></LinkButton>
|
||||
</div>
|
||||
<LinkButton id="create-set-button-mobile" to="/create-set" icon={<AddRoundedIcon />} className="button--round buttons--mobile" title="Create set"></LinkButton>
|
||||
</div>
|
||||
<div className="page-header page-header--left buttons--mobile">
|
||||
<Button
|
||||
@@ -402,11 +391,6 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
>
|
||||
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
|
||||
@@ -493,7 +477,11 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
<div className="form set-list">
|
||||
{this.state.userSets && this.state.userSets.length > 0 &&
|
||||
<div className="checkbox-list-container">
|
||||
<h3><PersonRoundedIcon /> Personal Sets</h3>
|
||||
<Link
|
||||
to="/my-sets"
|
||||
>
|
||||
<h3><PersonRoundedIcon /> My Sets</h3>
|
||||
</Link>
|
||||
<div className="checkbox-list">
|
||||
{this.state.userSets
|
||||
.sort((a, b) => {
|
||||
@@ -561,38 +549,6 @@ export default withRouter(class LoggedInHome extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.state.publicSets && this.state.publicSets.length > 0 &&
|
||||
<div className="checkbox-list-container">
|
||||
<h3><PublicRoundedIcon /> Public Sets</h3>
|
||||
<div className="checkbox-list">
|
||||
{this.state.publicSets
|
||||
.sort((a, b) => {
|
||||
if (a.data().title < b.data().title) {
|
||||
return -1;
|
||||
}
|
||||
if (a.data().title > b.data().title) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).map(set =>
|
||||
<div key={set.id}>
|
||||
<label>
|
||||
<Checkbox
|
||||
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 />
|
||||
|
||||
366
src/SearchSets.js
Normal file
366
src/SearchSets.js
Normal file
@@ -0,0 +1,366 @@
|
||||
import React, { Component } from "react";
|
||||
import { HomeRounded as HomeRoundedIcon, ArrowForwardRounded as ArrowForwardRoundedIcon } from "@material-ui/icons";
|
||||
import NavBar from "./NavBar";
|
||||
import Footer from "./Footer";
|
||||
import Button from "./Button";
|
||||
import { Link, withRouter } from "react-router-dom";
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import TestStart from "./TestStart";
|
||||
import ClassicTestStart from "./ClassicTestStart";
|
||||
import LivesTestStart from "./LivesTestStart";
|
||||
import CountdownTestStart from "./CountdownTestStart";
|
||||
|
||||
import "./css/SearchSets.css";
|
||||
|
||||
const paginationFrequency = 30;
|
||||
|
||||
export default withRouter(class SearchSets extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
user: props.user,
|
||||
db: props.db,
|
||||
loading: false,
|
||||
loadingSets: false,
|
||||
functions: {
|
||||
createProgress: props.functions.httpsCallable("createProgress"),
|
||||
},
|
||||
navbarItems: [
|
||||
{
|
||||
type: "link",
|
||||
link: "/",
|
||||
icon: <HomeRoundedIcon />,
|
||||
hideTextMobile: true,
|
||||
}
|
||||
],
|
||||
selections: {},
|
||||
sets: [],
|
||||
pageNumber: 0,
|
||||
loadedAllSets: false,
|
||||
showTestStart: false,
|
||||
showClassicTestStart: false,
|
||||
showLivesTestStart: false,
|
||||
searchInput: "",
|
||||
};
|
||||
|
||||
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.loadSets().then(() => {
|
||||
if (this.searchInputRef) this.searchInputRef.focus();
|
||||
this.props.page.load();
|
||||
this.props.logEvent("page_view");
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted = false;
|
||||
this.props.page.unload();
|
||||
}
|
||||
|
||||
loadSets = (reload = false) => {
|
||||
if (!this.state.loadingSets) {
|
||||
this.setState({
|
||||
loadingSets: true,
|
||||
});
|
||||
|
||||
const setsRef = this.state.db.collection("sets")
|
||||
.where("public", "==", true)
|
||||
.where('title', '>=', this.state.searchInput)
|
||||
.where('title', '<=', this.state.searchInput + '\uf8ff')
|
||||
.orderBy("title")
|
||||
.orderBy("owner");
|
||||
|
||||
let completeSetsRef;
|
||||
|
||||
if (this.state.pageNumber === 0 || reload) {
|
||||
completeSetsRef = setsRef.limit(paginationFrequency);
|
||||
} else {
|
||||
completeSetsRef = setsRef.startAfter(this.state.sets[this.state.sets.length - 1]).limit(paginationFrequency);
|
||||
}
|
||||
|
||||
return completeSetsRef.get().then((querySnapshot) => {
|
||||
let selections = this.state.selections;
|
||||
querySnapshot.docs.map((doc) => selections[doc.id] = false);
|
||||
|
||||
this.setState({
|
||||
sets: reload ? querySnapshot.docs : this.state.sets.concat(querySnapshot.docs),
|
||||
selections: selections,
|
||||
pageNumber: this.state.pageNumber + 1,
|
||||
loadedAllSets: querySnapshot.docs.length === 0,
|
||||
loadingSets: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stopLoading = () => {
|
||||
this.setState({
|
||||
canStartTest: true,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
showTestStart = () => {
|
||||
if (this.state.canStartTest) {
|
||||
this.setState({
|
||||
showTestStart: true,
|
||||
totalTestQuestions: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hideTestStart = () => {
|
||||
this.setState({
|
||||
showTestStart: false,
|
||||
});
|
||||
}
|
||||
|
||||
startTest = (mode) => {
|
||||
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: this.state.switchLanguage,
|
||||
mode: mode,
|
||||
limit: this.state.sliderValue,
|
||||
}).then((result) => {
|
||||
const progressId = result.data;
|
||||
this.stopLoading();
|
||||
this.props.history.push("/progress/" + progressId);
|
||||
|
||||
this.props.logEvent("start_test", {
|
||||
progress_id: 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 });
|
||||
}
|
||||
}
|
||||
|
||||
showIndividualTestPrompt = async (mode) => {
|
||||
if (!this.state.loading) {
|
||||
if (mode === "classic") {
|
||||
this.setState({
|
||||
loading: true,
|
||||
})
|
||||
const setIds = Object.keys(this.state.selections)
|
||||
.filter(x => this.state.selections[x]);
|
||||
|
||||
const totalTestQuestions = (await Promise.all(setIds.map((setId) =>
|
||||
this.state.db.collection("sets")
|
||||
.doc(setId)
|
||||
.collection("vocab")
|
||||
.get()
|
||||
.then(querySnapshot => querySnapshot.docs.length)
|
||||
))).reduce((a, b) => a + b);
|
||||
|
||||
this.setState({
|
||||
showTestStart: false,
|
||||
showClassicTestStart: true,
|
||||
sliderValue: totalTestQuestions,
|
||||
switchLanguage: false,
|
||||
totalTestQuestions: totalTestQuestions,
|
||||
loading: false,
|
||||
});
|
||||
} else if (mode === "lives") {
|
||||
this.setState({
|
||||
showTestStart: false,
|
||||
showLivesTestStart: true,
|
||||
switchLanguage: false,
|
||||
sliderValue: 5,
|
||||
});
|
||||
} else {
|
||||
// countdown
|
||||
// this.setState({
|
||||
// showTestStart: false,
|
||||
// showCountdownTestStart: true,
|
||||
// switchLanguage: false,
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideClassicTestStart = () => {
|
||||
this.setState({
|
||||
showClassicTestStart: false,
|
||||
});
|
||||
}
|
||||
|
||||
hideLivesTestStart = () => {
|
||||
this.setState({
|
||||
showLivesTestStart: false,
|
||||
});
|
||||
}
|
||||
|
||||
hideCountdownTestStart = () => {
|
||||
this.setState({
|
||||
showCountdownTestStart: false,
|
||||
});
|
||||
}
|
||||
|
||||
changeSliderValue = (value) => {
|
||||
if (value >= 1 && value <= 999) this.setState({
|
||||
sliderValue: value,
|
||||
});
|
||||
}
|
||||
|
||||
handleSwitchLanguageChange = (event) => {
|
||||
this.setState({
|
||||
switchLanguage: event.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleSearchInput = (event) => {
|
||||
if (!this.state.loadingSets) {
|
||||
this.setState({
|
||||
searchInput: event.target.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
search = () => {
|
||||
this.loadSets(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<NavBar items={this.state.navbarItems} />
|
||||
|
||||
<main>
|
||||
<div className="page-header">
|
||||
<h1>Search Sets</h1>
|
||||
<div className="button-container">
|
||||
<Button
|
||||
onClick={this.showTestStart}
|
||||
disabled={!this.state.canStartTest}
|
||||
>
|
||||
Test ({Object.values(this.state.selections).filter(x => x === true).length})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="search-box-container" onSubmit={(e) => e.preventDefault()} >
|
||||
<input type="submit" className="form-submit" onClick={this.search} />
|
||||
<input
|
||||
type="text"
|
||||
className="search-box"
|
||||
onChange={this.handleSearchInput}
|
||||
value={this.state.searchInput}
|
||||
ref={inputEl => (this.searchInputRef = inputEl)}
|
||||
autoComplete="off"
|
||||
placeholder="Search is case-sensitive"
|
||||
/>
|
||||
<Button
|
||||
onClick={this.search}
|
||||
icon={<ArrowForwardRoundedIcon />}
|
||||
className="button--round"
|
||||
disabled={this.state.loadingSets}
|
||||
loading={this.state.loadingSets}
|
||||
></Button>
|
||||
</form>
|
||||
|
||||
<div className="checkbox-list">
|
||||
{
|
||||
this.state.sets.slice(-50).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>
|
||||
{
|
||||
!this.state.loadedAllSets && this.state.sets.length === paginationFrequency &&
|
||||
<Button
|
||||
onClick={() => this.loadSets()}
|
||||
disabled={this.state.loadingSets}
|
||||
className="load-more-button"
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
}
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
{
|
||||
this.state.showTestStart &&
|
||||
<TestStart
|
||||
hideTestStart={this.hideTestStart}
|
||||
showIndividualTestPrompt={this.showIndividualTestPrompt}
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
}
|
||||
{
|
||||
this.state.showClassicTestStart &&
|
||||
<ClassicTestStart
|
||||
hide={this.hideClassicTestStart}
|
||||
startTest={this.startTest}
|
||||
max={this.state.totalTestQuestions}
|
||||
sliderValue={this.state.sliderValue}
|
||||
onSliderChange={this.changeSliderValue}
|
||||
switchLanguage={this.state.switchLanguage}
|
||||
handleSwitchLanguageChange={this.handleSwitchLanguageChange}
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
}
|
||||
{
|
||||
this.state.showLivesTestStart &&
|
||||
<LivesTestStart
|
||||
hide={this.hideLivesTestStart}
|
||||
startTest={this.startTest}
|
||||
max={20}
|
||||
sliderValue={this.state.sliderValue}
|
||||
onSliderChange={this.changeSliderValue}
|
||||
switchLanguage={this.state.switchLanguage}
|
||||
handleSwitchLanguageChange={this.handleSwitchLanguageChange}
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
}
|
||||
{
|
||||
this.state.showCountdownTestStart &&
|
||||
<CountdownTestStart
|
||||
hide={this.hideCountdownTestStart}
|
||||
startTest={this.startTest}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
20
src/css/SearchSets.css
Normal file
20
src/css/SearchSets.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.search-box-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
border: 1px solid var(--text-color-tinted);
|
||||
border-radius: 6px;
|
||||
flex-grow: 1;
|
||||
margin-right: 8px;
|
||||
font-size: 18px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.load-more-button {
|
||||
margin: auto;
|
||||
}
|
||||
Reference in New Issue
Block a user