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 './css/App.css';
import './css/PopUp.css';
import { BrowserRouter as Router, Route, Switch, Redirect, Link } from 'react-router-dom';
import Home from "./Home";
import LoggedInHome from "./LoggedInHome";
@@ -18,11 +19,14 @@ import TermsOfService from "./TermsOfService";
import PrivacyPolicy from "./PrivacyPolicy";
import Button from "./Button";
import { CheckRounded as CheckRoundedIcon } from "@material-ui/icons";
import Loader from "./puff-loader.svg";
import RouteChangeTracker from './RouteChangeTracker';
import Cookies from 'universal-cookie';
import styled, { keyframes } from "styled-components";
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/functions";
@@ -48,10 +52,37 @@ appCheck.activate(
true
);
// firebase.functions().useEmulator("localhost", 5001);
// firebase.auth().useEmulator("http://localhost:9099");
// firebase.firestore().useEmulator("localhost", 8080);
const functions = firebase.app().functions("europe-west2");//firebase.functions();
firebase.functions().useEmulator("localhost", 5001);
firebase.auth().useEmulator("http://localhost:9099");
firebase.firestore().useEmulator("localhost", 8080);
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()
.catch((err) => {
@@ -85,10 +116,27 @@ const analytics = firebase.analytics();
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
userDataPresent: false,
sound: true,
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) => {
let newState = {
user: userData,
userDataPresent: true,
};
if (userData) {
@@ -107,16 +156,16 @@ class App extends React.Component {
await firebase.firestore()
.collection("users")
.doc(userData.uid)
.get()
.then((userDoc) => {
newState.sound = userDoc.data().sound;
newState.theme = userDoc.data().theme;
}).catch((error) => {
newState.sound = true;
newState.theme = "default";
});
.collection("users")
.doc(userData.uid)
.get()
.then((userDoc) => {
newState.sound = userDoc.data().sound;
newState.theme = userDoc.data().theme;
}).catch((error) => {
newState.sound = true;
newState.theme = "default";
});
}
this.setState(newState);
@@ -217,73 +266,76 @@ class App extends React.Component {
<div className={this.state.theme}>
<Router>
<RouteChangeTracker />
{
this.state.user !== null
?
<>
<Switch>
<Route path="/" exact>
<LoggedInHome db={db} firebase={firebase} functions={functions} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/sets/:setId" exact>
<SetPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/groups" exact>
<UserGroups db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/groups/:groupId" exact>
<GroupPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/settings">
<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 path="/progress/:progressId" 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} />
</Route>
<Route path="/create-set" exact>
<CreateSet db={db} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/my-sets" exact>
<UserSets db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/sets/:setId/edit" exact>
<EditSet db={db} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/history" exact>
<History db={db} user={this.state.user} logEvent={analytics.logEvent} />
</Route>
<Route path="/tos" exact>
<TermsOfService logEvent={analytics.logEvent} />
</Route>
<Route path="/privacy" exact>
<PrivacyPolicy logEvent={analytics.logEvent} />
</Route>
<Redirect from="/login" to="/" />
<Route>
<Error404 />
</Route>
</Switch>
</>
:
<>
<Switch>
<Route path="/" exact>
<Home db={db} logEvent={analytics.logEvent} />
</Route>
<Route path="/login">
{
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} page={this.page} />
</Route>
<Route path="/sets/:setId" exact>
<SetPage 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>
<Route path="/groups/:groupId" exact>
<GroupPage db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/settings">
<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} />
</Route>
<Route path="/progress/:progressId" 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} />
</Route>
<Route path="/create-set" exact>
<CreateSet db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/my-sets" exact>
<UserSets db={db} functions={functions} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/sets/:setId/edit" exact>
<EditSet db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/history" exact>
<History db={db} user={this.state.user} logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/tos" exact>
<TermsOfService logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/privacy" exact>
<PrivacyPolicy logEvent={analytics.logEvent} page={this.page} />
</Route>
<Redirect from="/login" to="/" />
<Route>
<Error404 page={this.page} />
</Route>
</Switch>
</>
:
<>
<Switch>
<Route path="/" exact>
<Home logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/login">
<Login db={db} firebase={firebase} logEvent={analytics.logEvent} user={this.state.user} />
</Route>
<Route path="/tos" exact>
<TermsOfService logEvent={analytics.logEvent} />
<TermsOfService logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route path="/privacy" exact>
<PrivacyPolicy logEvent={analytics.logEvent} />
</Route>
<Route>
<Error404 />
</Route>
</Switch>
</>
<PrivacyPolicy logEvent={analytics.logEvent} page={this.page} />
</Route>
<Route>
<Error404 page={this.page} />
</Route>
</Switch>
</>
)
}
<div className="cookie-notice" id="cookie-notice">
<div>
@@ -298,6 +350,10 @@ class App extends React.Component {
></Button>
</div>
</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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,64 +61,67 @@ export default class History extends Component {
.then(async (querySnapshot) => {
let complete = [];
let incomplete = [];
let totalCorrect = 0;
let totalIncorrect = 0;
let totalMarks = 0;
let totalTime = 0;
let totalPercentage = 0;
let userMarkHistory = [];
let totalCorrect = 0;
let totalIncorrect = 0;
let totalMarks = 0;
let totalTime = 0;
let totalPercentage = 0;
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) => {
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,
};
totalCorrect += data.correct.length;
totalIncorrect += data.incorrect.length;
totalMarks += 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({
progressHistoryComplete: complete,
progressHistoryIncomplete: incomplete,
totalCorrect: totalCorrect,
totalIncorrect: totalIncorrect,
totalMarks: totalMarks,
totalTime: totalTime,
totalPercentage: totalPercentage,
totalCompleteTests: complete.length,
userMarkHistory: userMarkHistory,
personalSetsCount: (await userSets).docs.length,
});
}).catch((error) => {
console.log(`Couldn't retrieve progress history: ${error}`);
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.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() {
this.isMounted = false;
this.props.page.unload();
}
deleteProgress = (progressId) => {

View File

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

View File

@@ -121,9 +121,12 @@ export default withRouter(class LoggedInHome extends React.Component {
var userGroupSets = [];
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
.where("public", "==", true)
@@ -175,6 +178,7 @@ export default withRouter(class LoggedInHome extends React.Component {
progressQuery
]).then(() => {
this.setState(newState);
this.props.page.load();
});
this.props.logEvent("page_view");
@@ -182,6 +186,7 @@ export default withRouter(class LoggedInHome extends React.Component {
componentWillUnmount() {
this.isMounted = false;
this.props.page.unload();
}
stopLoading = () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -292,6 +292,14 @@ label .MuiIconButton-label > input {
column-gap: 2px;
}
.page-loader-container {
width: 30%;
height: min-content;
line-height: 0;
border-radius: 150px;
background: var(--primary-color-dark);
}
.page-loader {
margin: auto;
width: 30%;
@@ -395,6 +403,10 @@ label .MuiIconButton-label > input {
visibility: hidden;
}
.transparent {
opacity: 0 !important;
}
@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) {
display: none;

View File

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