Spinner when wait for async actions

master
Tudor Stanciu 2020-04-12 01:18:08 +03:00
parent 1e70e1de6e
commit 8b95aec904
11 changed files with 108 additions and 14 deletions

View File

@ -0,0 +1,42 @@
/* via https://projects.lukehaas.me/css-loaders/ */
.loader,
.loader:after {
border-radius: 50%;
width: 10em;
height: 10em;
}
.loader {
margin: 60px auto;
font-size: 10px;
position: relative;
text-indent: -9999em;
border-top: 1.1em solid rgba(255, 151, 0, 0.2);
border-right: 1.1em solid rgba(255, 151, 0, 0.2);
border-bottom: 1.1em solid rgba(255, 151, 0, 0.2);
border-left: 1.1em solid #ff9700;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: load8 1.1s infinite linear;
animation: load8 1.1s infinite linear;
}
@-webkit-keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load8 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,8 @@
import React from "react";
import "./Spinner.css";
const Spinner = () => {
return <div className="loader">Loading...</div>;
};
export default Spinner;

View File

@ -6,6 +6,7 @@ import PropTypes from "prop-types";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import CourseList from "./CourseList"; import CourseList from "./CourseList";
import { Redirect } from "react-router-dom"; import { Redirect } from "react-router-dom";
import Spinner from "../common/Spinner";
class CoursesPage extends React.Component { class CoursesPage extends React.Component {
state = { state = {
@ -33,7 +34,10 @@ class CoursesPage extends React.Component {
<> <>
{this.state.redirectToAddCoursePage && <Redirect to="/course" />} {this.state.redirectToAddCoursePage && <Redirect to="/course" />}
<h2>Courses</h2> <h2>Courses</h2>
{this.props.loading ? (
<Spinner />
) : (
<>
<button <button
style={{ marginBottom: 20 }} style={{ marginBottom: 20 }}
className="btn btn-primary add-course" className="btn btn-primary add-course"
@ -44,6 +48,8 @@ class CoursesPage extends React.Component {
<CourseList courses={this.props.courses} /> <CourseList courses={this.props.courses} />
</> </>
)}
</>
); );
} }
} }
@ -51,7 +57,8 @@ class CoursesPage extends React.Component {
CoursesPage.propTypes = { CoursesPage.propTypes = {
courses: PropTypes.array.isRequired, courses: PropTypes.array.isRequired,
authors: PropTypes.array.isRequired, authors: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired actions: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
@ -66,7 +73,8 @@ function mapStateToProps(state) {
authorName: author.name authorName: author.name
}; };
}), }),
authors: state.authors authors: state.authors,
loading: state.apiCallsInProgress > 0
}; };
} }

View File

@ -6,6 +6,7 @@ import PropTypes from "prop-types";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import CourseForm from "./CourseForm"; import CourseForm from "./CourseForm";
import { newCourse } from "../../../tools/mockData"; import { newCourse } from "../../../tools/mockData";
import Spinner from "../common/Spinner";
function ManageCoursePage({ courses, authors, actions, history, ...props }) { function ManageCoursePage({ courses, authors, actions, history, ...props }) {
const [course, setCourse] = useState({ ...props.course }); const [course, setCourse] = useState({ ...props.course });
@ -42,7 +43,9 @@ function ManageCoursePage({ courses, authors, actions, history, ...props }) {
}); });
} }
return ( return authors.length === 0 || course.length === 0 ? (
<Spinner />
) : (
<CourseForm <CourseForm
course={course} course={course}
errors={errors} errors={errors}

View File

@ -3,3 +3,4 @@ export const LOAD_COURSES_SUCCESS = "LOAD_COURSES_SUCCESS";
export const LOAD_AUTHORS_SUCCESS = "LOAD_AUTHORS_SUCCESS"; export const LOAD_AUTHORS_SUCCESS = "LOAD_AUTHORS_SUCCESS";
export const CREATE_COURSE_SUCCESS = "CREATE_COURSE_SUCCESS"; export const CREATE_COURSE_SUCCESS = "CREATE_COURSE_SUCCESS";
export const UPDATE_COURSE_SUCCESS = "UPDATE_COURSE_SUCCESS"; export const UPDATE_COURSE_SUCCESS = "UPDATE_COURSE_SUCCESS";
export const BEGIN_API_CALL = "BEGIN_API_CALL";

View File

@ -0,0 +1,5 @@
import * as types from "./actionTypes";
export function beginApiCall() {
return { type: types.BEGIN_API_CALL };
}

View File

@ -1,5 +1,6 @@
import * as types from "./actionTypes"; import * as types from "./actionTypes";
import * as authorApi from "../../api/authorApi"; import * as authorApi from "../../api/authorApi";
import { beginApiCall } from "./apiStatusActions";
function loadAuthorsSuccess(authors) { function loadAuthorsSuccess(authors) {
return { type: types.LOAD_AUTHORS_SUCCESS, authors }; return { type: types.LOAD_AUTHORS_SUCCESS, authors };
@ -7,6 +8,7 @@ function loadAuthorsSuccess(authors) {
export function loadAuthors() { export function loadAuthors() {
return function (dispatch) { return function (dispatch) {
dispatch(beginApiCall());
return authorApi return authorApi
.getAuthors() .getAuthors()
.then((authors) => { .then((authors) => {

View File

@ -1,5 +1,6 @@
import * as types from "./actionTypes"; import * as types from "./actionTypes";
import * as courseApi from "../../api/courseApi"; import * as courseApi from "../../api/courseApi";
import { beginApiCall } from "./apiStatusActions";
function loadCoursesSuccess(courses) { function loadCoursesSuccess(courses) {
return { type: types.LOAD_COURSES_SUCCESS, courses }; return { type: types.LOAD_COURSES_SUCCESS, courses };
@ -15,6 +16,7 @@ export function updateCourseSuccess(course) {
export function loadCourses() { export function loadCourses() {
return function (dispatch) { return function (dispatch) {
dispatch(beginApiCall());
return courseApi return courseApi
.getCourses() .getCourses()
.then((courses) => { .then((courses) => {
@ -29,6 +31,7 @@ export function loadCourses() {
export function saveCourse(course) { export function saveCourse(course) {
//eslint-disable-next-line no-unused-vars //eslint-disable-next-line no-unused-vars
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(beginApiCall());
return courseApi return courseApi
.saveCourse(course) .saveCourse(course)
.then((savedCourse) => { .then((savedCourse) => {

View File

@ -0,0 +1,19 @@
import * as types from "../actions/actionTypes";
import initialState from "./initialState";
function actionTypeEndsInSuccess(type) {
return type.substring(type.length - 8) === "_SUCCESS";
}
export default function apiCallStatusReducer(
state = initialState.apiCallsInProgress,
action
) {
if (action.type == types.BEGIN_API_CALL) {
return state + 1;
} else if (actionTypeEndsInSuccess(action.type)) {
return state - 1;
}
return state;
}

View File

@ -1,10 +1,12 @@
import { combineReducers } from "redux"; import { combineReducers } from "redux";
import courseReducer from "./courseReducer"; import courseReducer from "./courseReducer";
import authorReducer from "./authorReducer"; import authorReducer from "./authorReducer";
import apiStatusReducer from "./apiStatusReducer";
const rootReducer = combineReducers({ const rootReducer = combineReducers({
courses: courseReducer, courses: courseReducer,
authors: authorReducer authors: authorReducer,
apiCallsInProgress: apiStatusReducer
}); });
export default rootReducer; export default rootReducer;

View File

@ -1,4 +1,5 @@
export default { export default {
courses: [], courses: [],
authors: [] authors: [],
apiCallsInProgress: 0
}; };