Add loader until page loads completely

This commit is contained in:
2021-10-03 15:59:24 +01:00
parent 90a31e8923
commit 80e7c24811
18 changed files with 280 additions and 147 deletions

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import './css/App.css'; import './css/App.css';
import './css/PopUp.css';
import { BrowserRouter as Router, Route, Switch, Redirect, Link } from 'react-router-dom'; import { BrowserRouter as Router, Route, Switch, Redirect, Link } from 'react-router-dom';
import Home from "./Home"; import Home from "./Home";
import LoggedInHome from "./LoggedInHome"; import LoggedInHome from "./LoggedInHome";
@@ -18,11 +19,14 @@ import TermsOfService from "./TermsOfService";
import PrivacyPolicy from "./PrivacyPolicy"; import PrivacyPolicy from "./PrivacyPolicy";
import Button from "./Button"; import Button from "./Button";
import { CheckRounded as CheckRoundedIcon } from "@material-ui/icons"; import { CheckRounded as CheckRoundedIcon } from "@material-ui/icons";
import Loader from "./puff-loader.svg";
import RouteChangeTracker from './RouteChangeTracker'; import RouteChangeTracker from './RouteChangeTracker';
import Cookies from 'universal-cookie'; import Cookies from 'universal-cookie';
import styled, { keyframes } from "styled-components";
import firebase from "firebase/app"; import firebase from "firebase/app";
import "firebase/auth"; import "firebase/auth";
import "firebase/functions"; import "firebase/functions";
@@ -48,10 +52,37 @@ appCheck.activate(
true true
); );
// firebase.functions().useEmulator("localhost", 5001); firebase.functions().useEmulator("localhost", 5001);
// firebase.auth().useEmulator("http://localhost:9099"); firebase.auth().useEmulator("http://localhost:9099");
// firebase.firestore().useEmulator("localhost", 8080); firebase.firestore().useEmulator("localhost", 8080);
const functions = firebase.app().functions("europe-west2");//firebase.functions(); const functions = firebase.functions();//firebase.app().functions("europe-west2");
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 0.65;
}
`;
const fadeOut = keyframes`
from {
opacity: 0.65;
}
to {
opacity: 0;
}
`;
const Fade = styled.div`
display: inline-block;
visibility: ${props => props.out ? 'hidden' : 'visible'};
animation: ${props => props.out ? fadeOut : fadeIn} 0.1s linear;
transition: visibility 0.1s linear;
`;
firebase.firestore().enablePersistence() firebase.firestore().enablePersistence()
.catch((err) => { .catch((err) => {
@@ -85,10 +116,27 @@ const analytics = firebase.analytics();
class App extends React.Component { class App extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
user: null, user: null,
userDataPresent: false,
sound: true, sound: true,
theme: "default", theme: "default",
pageLoading: true,
};
this.page = {
loaded: !this.state.pageLoading,
load: () => {
this.setState({
pageLoading: false,
});
},
unload: () => {
this.setState({
pageLoading: true,
});
},
}; };
} }
@@ -96,6 +144,7 @@ class App extends React.Component {
firebase.auth().onAuthStateChanged(async (userData) => { firebase.auth().onAuthStateChanged(async (userData) => {
let newState = { let newState = {
user: userData, user: userData,
userDataPresent: true,
}; };
if (userData) { if (userData) {
@@ -107,16 +156,16 @@ class App extends React.Component {
await firebase.firestore() await firebase.firestore()
.collection("users") .collection("users")
.doc(userData.uid) .doc(userData.uid)
.get() .get()
.then((userDoc) => { .then((userDoc) => {
newState.sound = userDoc.data().sound; newState.sound = userDoc.data().sound;
newState.theme = userDoc.data().theme; newState.theme = userDoc.data().theme;
}).catch((error) => { }).catch((error) => {
newState.sound = true; newState.sound = true;
newState.theme = "default"; newState.theme = "default";
}); });
} }
this.setState(newState); this.setState(newState);
@@ -217,73 +266,76 @@ class App extends React.Component {
<div className={this.state.theme}> <div className={this.state.theme}>
<Router> <Router>
<RouteChangeTracker /> <RouteChangeTracker />
{ {
this.state.user !== null this.state.userDataPresent &&
? (
<> this.state.user !== null
<Switch> ?
<Route path="/" exact> <>
<LoggedInHome db={db} firebase={firebase} functions={functions} user={this.state.user} logEvent={analytics.logEvent} /> <Switch>
</Route> <Route path="/" exact>
<Route path="/sets/:setId" exact> <LoggedInHome db={db} firebase={firebase} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<SetPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/sets/:setId" exact>
<Route path="/groups" exact> <SetPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<UserGroups db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/groups" exact>
<Route path="/groups/:groupId" exact> <UserGroups db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<GroupPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/groups/:groupId" exact>
<Route path="/settings"> <GroupPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<Settings db={db} user={this.state.user} sound={this.state.sound} handleSoundChange={this.handleSoundChange} theme={this.state.theme} handleThemeChange={this.handleThemeChange} themes={themes} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/settings">
<Route path="/progress/:progressId" exact> <Settings db={db} user={this.state.user} sound={this.state.sound} handleSoundChange={this.handleSoundChange} theme={this.state.theme} handleThemeChange={this.handleThemeChange} themes={themes} logEvent={analytics.logEvent} page={this.page} />
<Progress db={db} functions={functions} user={this.state.user} sound={this.state.sound} handleSoundChange={this.handleSoundChange} theme={this.state.theme} handleThemeChange={this.handleThemeChange} themes={themes} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/progress/:progressId" exact>
<Route path="/create-set" exact> <Progress db={db} functions={functions} user={this.state.user} sound={this.state.sound} handleSoundChange={this.handleSoundChange} theme={this.state.theme} handleThemeChange={this.handleThemeChange} themes={themes} logEvent={analytics.logEvent} page={this.page} />
<CreateSet db={db} user={this.state.user} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/create-set" exact>
<Route path="/my-sets" exact> <CreateSet db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<UserSets db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/my-sets" exact>
<Route path="/sets/:setId/edit" exact> <UserSets db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<EditSet db={db} user={this.state.user} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/sets/:setId/edit" exact>
<Route path="/history" exact> <EditSet db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<History db={db} user={this.state.user} logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/history" exact>
<Route path="/tos" exact> <History db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
<TermsOfService logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/tos" exact>
<Route path="/privacy" exact> <TermsOfService logEvent={analytics.logEvent} page={this.page} />
<PrivacyPolicy logEvent={analytics.logEvent} /> </Route>
</Route> <Route path="/privacy" exact>
<Redirect from="/login" to="/" /> <PrivacyPolicy logEvent={analytics.logEvent} page={this.page} />
<Route> </Route>
<Error404 /> <Redirect from="/login" to="/" />
</Route> <Route>
</Switch> <Error404 page={this.page} />
</> </Route>
: </Switch>
<> </>
<Switch> :
<Route path="/" exact> <>
<Home db={db} logEvent={analytics.logEvent} /> <Switch>
</Route> <Route path="/" exact>
<Route path="/login"> <Home logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/login">
<Login db={db} firebase={firebase} logEvent={analytics.logEvent} user={this.state.user} /> <Login db={db} firebase={firebase} logEvent={analytics.logEvent} user={this.state.user} />
</Route> </Route>
<Route path="/tos" exact> <Route path="/tos" exact>
<TermsOfService logEvent={analytics.logEvent} /> <TermsOfService logEvent={analytics.logEvent} page={this.page} />
</Route> </Route>
<Route path="/privacy" exact> <Route path="/privacy" exact>
<PrivacyPolicy logEvent={analytics.logEvent} /> <PrivacyPolicy logEvent={analytics.logEvent} page={this.page} />
</Route> </Route>
<Route> <Route>
<Error404 /> <Error404 page={this.page} />
</Route> </Route>
</Switch> </Switch>
</> </>
)
} }
<div className="cookie-notice" id="cookie-notice"> <div className="cookie-notice" id="cookie-notice">
<div> <div>
@@ -298,6 +350,10 @@ class App extends React.Component {
></Button> ></Button>
</div> </div>
</Router> </Router>
{/* <div className="overlay"><img className="page-loader" src={Loader} alt="Loading..." /></div> */}
<Fade out={!this.state.pageLoading && this.state.userDataPresent} className="overlay overlay--black">
<img className="page-loader" src={Loader} alt="Loading..." />
</Fade>
</div> </div>
); );
} }

View File

@@ -48,11 +48,14 @@ export default withRouter(class CreateSet extends React.Component {
document.title = "Create Set | Parandum"; document.title = "Create Set | Parandum";
this.setNameInput.focus(); this.setNameInput.focus();
this.props.page.load();
this.props.logEvent("page_view"); this.props.logEvent("page_view");
} }
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
stopLoading = () => { stopLoading = () => {

View File

@@ -103,11 +103,13 @@ export default withRouter(class EditSet extends Component {
} }
this.setState(newState); this.setState(newState);
this.props.page.load();
}); });
}).catch(() => { }).catch(() => {
this.setState({ this.setState({
setInaccessible: true, setInaccessible: true,
}); });
this.props.page.load();
}); });
this.props.logEvent("select_content", { this.props.logEvent("select_content", {
@@ -119,6 +121,7 @@ export default withRouter(class EditSet extends Component {
componentWillUnmount = () => { componentWillUnmount = () => {
window.removeEventListener('beforeunload', this.alertLeavingWithoutSaving); window.removeEventListener('beforeunload', this.alertLeavingWithoutSaving);
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
stopLoading = () => { stopLoading = () => {

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React, { useEffect } from 'react';
import NavBar from './NavBar'; import NavBar from './NavBar';
import Footer from "./Footer"; import Footer from "./Footer";
import { HomeRounded as HomeRoundedIcon } from "@material-ui/icons"; import { HomeRounded as HomeRoundedIcon } from "@material-ui/icons";
export default function PageNotFound() { export default function PageNotFound(props) {
const navbarItems = [ const navbarItems = [
{ {
type: "link", type: "link",
@@ -15,7 +15,18 @@ export default function PageNotFound() {
document.title = "Error 404 | Parandum"; document.title = "Error 404 | Parandum";
const page = props.page;
useEffect(() => {
if (page) {
page.load();
return () => page.unload();
}
}, [page]);
return ( return (
!page.loaded
?
<div> <div>
<NavBar items={navbarItems}/> <NavBar items={navbarItems}/>
<main> <main>
@@ -24,5 +35,7 @@ export default function PageNotFound() {
</main> </main>
<Footer /> <Footer />
</div> </div>
:
null
) )
} }

View File

@@ -9,8 +9,6 @@ import "./css/GroupPage.css";
import "./css/ConfirmationDialog.css"; import "./css/ConfirmationDialog.css";
import "./css/OptionsListOverlay.css"; import "./css/OptionsListOverlay.css";
import Loader from "./puff-loader.svg"
export default withRouter(class GroupPage extends Component { export default withRouter(class GroupPage extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -118,9 +116,10 @@ export default withRouter(class GroupPage extends Component {
} }
this.setState(newState); this.setState(newState);
this.props.page.load();
}); });
}); });
this.props.logEvent("select_content", { this.props.logEvent("select_content", {
content_type: "group", content_type: "group",
item_id: this.props.match.params.groupId, item_id: this.props.match.params.groupId,
@@ -129,6 +128,7 @@ export default withRouter(class GroupPage extends Component {
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
editGroupName = () => { editGroupName = () => {
@@ -340,10 +340,7 @@ export default withRouter(class GroupPage extends Component {
<NavBar items={this.state.navbarItems} /> <NavBar items={this.state.navbarItems} />
<main> <main>
{ {
(this.state.role === null) !(this.state.role === null) &&
?
<img className="page-loader" src={Loader} alt="Loading..." />
:
<> <>
<div className="page-header"> <div className="page-header">
{ {

View File

@@ -61,64 +61,67 @@ export default class History extends Component {
.then(async (querySnapshot) => { .then(async (querySnapshot) => {
let complete = []; let complete = [];
let incomplete = []; let incomplete = [];
let totalCorrect = 0; let totalCorrect = 0;
let totalIncorrect = 0; let totalIncorrect = 0;
let totalMarks = 0; let totalMarks = 0;
let totalTime = 0; let totalTime = 0;
let totalPercentage = 0; let totalPercentage = 0;
let userMarkHistory = []; let userMarkHistory = [];
querySnapshot.docs.map((doc) => {
const data = doc.data();
const pushData = {
id: doc.id,
setTitle: data.set_title,
switchLanguage: data.switch_language,
percentageProgress: (data.progress / data.questions.length * 100).toFixed(2),
grade: (data.progress > 0 ? data.correct.length / data.progress * 100 : 0).toFixed(2),
mode: data.mode,
correct: data.correct.length,
progress: data.progress,
};
querySnapshot.docs.map((doc) => { totalCorrect += data.correct.length;
const data = doc.data(); totalIncorrect += data.incorrect.length;
const pushData = { totalMarks += data.progress;
id: doc.id,
setTitle: data.set_title,
switchLanguage: data.switch_language,
percentageProgress: (data.progress / data.questions.length * 100).toFixed(2),
grade: (data.progress > 0 ? data.correct.length / data.progress * 100 : 0).toFixed(2),
mode: data.mode,
correct: data.correct.length,
progress: data.progress,
};
totalCorrect += data.correct.length;
totalIncorrect += data.incorrect.length;
totalMarks += data.progress;
if (data.duration !== null) {
totalPercentage += (data.correct.length / data.questions.length * 100);
totalTime += data.duration;
userMarkHistory.push({
x: new Date(data.start_time),
y: (data.correct.length / data.questions.length * 100),
});
return complete.push(pushData);
} else {
return incomplete.push(pushData);
}
});
this.setState({ if (data.duration !== null) {
progressHistoryComplete: complete, totalPercentage += (data.correct.length / data.questions.length * 100);
progressHistoryIncomplete: incomplete, totalTime += data.duration;
totalCorrect: totalCorrect, userMarkHistory.push({
totalIncorrect: totalIncorrect, x: new Date(data.start_time),
totalMarks: totalMarks, y: (data.correct.length / data.questions.length * 100),
totalTime: totalTime, });
totalPercentage: totalPercentage, return complete.push(pushData);
totalCompleteTests: complete.length, } else {
userMarkHistory: userMarkHistory, return incomplete.push(pushData);
personalSetsCount: (await userSets).docs.length, }
});
}).catch((error) => {
console.log(`Couldn't retrieve progress history: ${error}`);
}); });
this.props.logEvent("page_view"); this.setState({
progressHistoryComplete: complete,
progressHistoryIncomplete: incomplete,
totalCorrect: totalCorrect,
totalIncorrect: totalIncorrect,
totalMarks: totalMarks,
totalTime: totalTime,
totalPercentage: totalPercentage,
totalCompleteTests: complete.length,
userMarkHistory: userMarkHistory,
personalSetsCount: (await userSets).docs.length,
});
this.props.page.load();
}).catch((error) => {
console.log(`Couldn't retrieve progress history: ${error}`);
this.props.page.load();
});
this.props.logEvent("page_view");
} }
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
deleteProgress = (progressId) => { deleteProgress = (progressId) => {

View File

@@ -18,9 +18,16 @@ export default function Home(props) {
document.title = "Parandum"; document.title = "Parandum";
const page = props.page;
const logEvent = props.logEvent;
useEffect(() => { useEffect(() => {
if (props.logEvent) props.logEvent("page_view"); if (page) {
}); page.load();
return () => page.unload();
}
if (logEvent) logEvent("page_view");
}, [logEvent, page]);
return ( return (
<div> <div>

View File

@@ -121,9 +121,12 @@ export default withRouter(class LoggedInHome extends React.Component {
var userGroupSets = []; var userGroupSets = [];
return Promise.all(userGroupsQuerySnapshot.docs.map((group) => { return Promise.all(userGroupsQuerySnapshot.docs.map((group) => {
newState.user.groups.push(group.id); const groupData = groupRef.doc(group.id).get().catch((error) => {
console.log(`Couldn't get group data: ${error}`);
return true;
});
const groupData = groupRef.doc(group.id).get(); newState.user.groups.push(group.id);
return userGroupSetsRef return userGroupSetsRef
.where("public", "==", true) .where("public", "==", true)
@@ -175,6 +178,7 @@ export default withRouter(class LoggedInHome extends React.Component {
progressQuery progressQuery
]).then(() => { ]).then(() => {
this.setState(newState); this.setState(newState);
this.props.page.load();
}); });
this.props.logEvent("page_view"); this.props.logEvent("page_view");
@@ -182,6 +186,7 @@ export default withRouter(class LoggedInHome extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
stopLoading = () => { stopLoading = () => {

View File

@@ -25,9 +25,11 @@ export default function Login(props) {
document.title = "Login | Parandum"; document.title = "Login | Parandum";
const logEvent = props.logEvent;
useEffect(() => { useEffect(() => {
props.logEvent("page_view"); if (logEvent) logEvent("page_view");
}); }, [logEvent]);
return ( return (
<> <>

View File

@@ -13,9 +13,16 @@ export default function PrivacyPolicy(props) {
} }
]; ];
const page = props.page;
const logEvent = props.logEvent;
useEffect(() => { useEffect(() => {
props.logEvent("page_view"); if (page) {
}); page.load();
return () => page.unload();
}
if (logEvent) logEvent("page_view");
}, [logEvent, page]);
return ( return (
<div> <div>

View File

@@ -201,6 +201,8 @@ export default withRouter(class Progress extends React.Component {
if (!setDone) this.answerInput.focus(); if (!setDone) this.answerInput.focus();
}); });
this.props.page.load();
this.props.logEvent("select_content", { this.props.logEvent("select_content", {
content_type: "progress", content_type: "progress",
item_id: this.props.match.params.progressId, item_id: this.props.match.params.progressId,
@@ -209,6 +211,7 @@ export default withRouter(class Progress extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
showSettings = () => { showSettings = () => {

View File

@@ -98,11 +98,13 @@ export default withRouter(class SetPage extends React.Component {
}, },
currentSetGroups: setDoc.data().groups, currentSetGroups: setDoc.data().groups,
}); });
this.props.page.load();
}); });
}).catch((error) => { }).catch((error) => {
this.setState({ this.setState({
setInaccessible: true, setInaccessible: true,
}); });
this.props.page.load();
console.log(`Can't access set: ${error}`); console.log(`Can't access set: ${error}`);
}); });
@@ -114,6 +116,7 @@ export default withRouter(class SetPage extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
stopLoading = () => { stopLoading = () => {

View File

@@ -38,11 +38,14 @@ export default withRouter(class Settings extends Component {
componentDidMount() { componentDidMount() {
document.title = "Settings | Parandum"; document.title = "Settings | Parandum";
this.props.page.load();
this.props.logEvent("page_view"); this.props.logEvent("page_view");
} }
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
handleSoundInputChange = (event) => { handleSoundInputChange = (event) => {

View File

@@ -14,9 +14,16 @@ export default function TermsOfService(props) {
} }
]; ];
const page = props.page;
const logEvent = props.logEvent;
useEffect(() => { useEffect(() => {
props.logEvent("page_view"); if (page) {
}); page.load();
return () => page.unload();
}
if (logEvent) logEvent("page_view");
}, [logEvent, page]);
return ( return (
<div> <div>

View File

@@ -75,6 +75,7 @@ export default withRouter(class UserGroups extends Component {
})); }));
this.setState(newState); this.setState(newState);
this.props.page.load();
}); });
this.props.logEvent("page_view"); this.props.logEvent("page_view");
@@ -82,6 +83,7 @@ export default withRouter(class UserGroups extends Component {
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
showJoinGroup = () => { showJoinGroup = () => {

View File

@@ -49,7 +49,8 @@ export default withRouter(class UserSets extends Component {
userSetsRef.get().then((querySnapshot) => { userSetsRef.get().then((querySnapshot) => {
this.setState({ this.setState({
userSets: querySnapshot.docs, userSets: querySnapshot.docs,
}) });
this.props.page.load();
}); });
this.props.logEvent("page_view"); this.props.logEvent("page_view");
@@ -57,6 +58,7 @@ export default withRouter(class UserSets extends Component {
componentWillUnmount() { componentWillUnmount() {
this.isMounted = false; this.isMounted = false;
this.props.page.unload();
} }
render() { render() {

View File

@@ -292,6 +292,14 @@ label .MuiIconButton-label > input {
column-gap: 2px; column-gap: 2px;
} }
.page-loader-container {
width: 30%;
height: min-content;
line-height: 0;
border-radius: 150px;
background: var(--primary-color-dark);
}
.page-loader { .page-loader {
margin: auto; margin: auto;
width: 30%; width: 30%;
@@ -395,6 +403,10 @@ label .MuiIconButton-label > input {
visibility: hidden; visibility: hidden;
} }
.transparent {
opacity: 0 !important;
}
@media screen and (max-width: 420px) { @media screen and (max-width: 420px) {
.progress-history-container > div > *:nth-child(2), .progress-history-container--complete > div > *:nth-last-child(3), .progress-history-container--incomplete > div > *:nth-last-child(4) { .progress-history-container > div > *:nth-child(2), .progress-history-container--complete > div > *:nth-last-child(3), .progress-history-container--incomplete > div > *:nth-last-child(4) {
display: none; display: none;

View File

@@ -10,6 +10,11 @@
cursor: default; cursor: default;
} }
.overlay--black {
background-color: var(--background-color);
opacity: 0.8;
}
.popup-close-button { .popup-close-button {
position: absolute; position: absolute;
top: 24px; top: 24px;