diff --git a/frontend/components/LoadingIndicator/LoadingIndicatorBar.js b/frontend/components/LoadingIndicator/LoadingIndicatorBar.js index 33c414c1..dead7652 100644 --- a/frontend/components/LoadingIndicator/LoadingIndicatorBar.js +++ b/frontend/components/LoadingIndicator/LoadingIndicatorBar.js @@ -21,9 +21,10 @@ const Container = styled.div` z-index: 9999; background-color: #03A9F4; - width: 0; + width: 100%; animation: ${loadingFrame} 4s ease-in-out infinite; animation-delay: 250ms; + margin-left: -100%; `; const Loader = styled.div` diff --git a/frontend/components/LoadingListPlaceholder/LoadingListPlaceholder.js b/frontend/components/LoadingListPlaceholder/LoadingListPlaceholder.js new file mode 100644 index 00000000..26bf3dbc --- /dev/null +++ b/frontend/components/LoadingListPlaceholder/LoadingListPlaceholder.js @@ -0,0 +1,48 @@ +// @flow +import React from 'react'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import styled from 'styled-components'; +import { pulsate } from 'styles/animations'; +import { color } from 'styles/constants'; +import Flex from 'components/Flex'; + +import { randomInteger } from 'utils/random'; + +const randomValues = Array.from( + new Array(5), + () => `${randomInteger(85, 100)}%` +); + +export default (props: Object) => { + return ( + + + + + + + + + + + ); +}; + +const Item = styled(Flex)` + padding: 18px 0; +`; + +const Mask = styled(Flex)` + height: ${props => (props.header ? 28 : 18)}px; + margin-bottom: ${props => (props.header ? 18 : 0)}px; + background-color: ${color.smoke}; + animation: ${pulsate} 1.3s infinite; +`; diff --git a/frontend/components/LoadingListPlaceholder/index.js b/frontend/components/LoadingListPlaceholder/index.js new file mode 100644 index 00000000..17588c5a --- /dev/null +++ b/frontend/components/LoadingListPlaceholder/index.js @@ -0,0 +1,3 @@ +// @flow +import LoadingListPlaceholder from './LoadingListPlaceholder'; +export default LoadingListPlaceholder; diff --git a/frontend/components/LoadingPlaceholder/ListPlaceholder.js b/frontend/components/LoadingPlaceholder/ListPlaceholder.js new file mode 100644 index 00000000..bf26f0f5 --- /dev/null +++ b/frontend/components/LoadingPlaceholder/ListPlaceholder.js @@ -0,0 +1,33 @@ +// @flow +import React from 'react'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import styled from 'styled-components'; +import Mask from './components/Mask'; +import Flex from 'components/Flex'; + +export default (props: Object) => { + return ( + + + + + + + + + + + ); +}; + +const Item = styled(Flex)` + padding: 18px 0; +`; diff --git a/frontend/components/LoadingPlaceholder/LoadingPlaceholder.js b/frontend/components/LoadingPlaceholder/LoadingPlaceholder.js new file mode 100644 index 00000000..6f978e24 --- /dev/null +++ b/frontend/components/LoadingPlaceholder/LoadingPlaceholder.js @@ -0,0 +1,26 @@ +// @flow +import React from 'react'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import Mask from './components/Mask'; +import Flex from 'components/Flex'; + +export default (props: Object) => { + return ( + + + + + + + + + ); +}; diff --git a/frontend/components/LoadingPlaceholder/components/Mask.js b/frontend/components/LoadingPlaceholder/components/Mask.js new file mode 100644 index 00000000..a49810e1 --- /dev/null +++ b/frontend/components/LoadingPlaceholder/components/Mask.js @@ -0,0 +1,38 @@ +// @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { pulsate } from 'styles/animations'; +import { color } from 'styles/constants'; +import { randomInteger } from 'utils/random'; +import Flex from 'components/Flex'; + +class Mask extends Component { + width: number; + + shouldComponentUpdate() { + return false; + } + + constructor(props: Object) { + super(props); + this.width = randomInteger(75, 100); + } + + render() { + return ; + } +} + +const Redacted = styled(Flex)` + width: ${props => (props.header ? props.width / 2 : props.width)}%; + height: ${props => (props.header ? 28 : 18)}px; + margin-bottom: ${props => (props.header ? 18 : 12)}px; + background-color: ${color.smokeDark}; + animation: ${pulsate} 1.3s infinite; + + &:last-child { + margin-bottom: 0; + } +`; + +export default Mask; diff --git a/frontend/components/LoadingPlaceholder/index.js b/frontend/components/LoadingPlaceholder/index.js new file mode 100644 index 00000000..62e7311e --- /dev/null +++ b/frontend/components/LoadingPlaceholder/index.js @@ -0,0 +1,6 @@ +// @flow +import LoadingPlaceholder from './LoadingPlaceholder'; +import ListPlaceholder from './ListPlaceholder'; + +export default LoadingPlaceholder; +export { ListPlaceholder }; diff --git a/frontend/components/PreviewLoading/index.js b/frontend/components/PreviewLoading/index.js deleted file mode 100644 index e02fd34c..00000000 --- a/frontend/components/PreviewLoading/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// @flow -import PreviewLoading from './PreviewLoading'; -export default PreviewLoading; diff --git a/frontend/scenes/Collection/Collection.js b/frontend/scenes/Collection/Collection.js index cd00815c..7a47bbdb 100644 --- a/frontend/scenes/Collection/Collection.js +++ b/frontend/scenes/Collection/Collection.js @@ -8,7 +8,7 @@ import CollectionsStore from 'stores/CollectionsStore'; import CollectionStore from './CollectionStore'; import CenteredContent from 'components/CenteredContent'; -import PreviewLoading from 'components/PreviewLoading'; +import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; type Props = { collections: CollectionsStore, @@ -33,7 +33,7 @@ type Props = { return this.store.redirectUrl ? : - + ; } } diff --git a/frontend/scenes/Dashboard/Dashboard.js b/frontend/scenes/Dashboard/Dashboard.js index 077b4b4f..da95a0f2 100644 --- a/frontend/scenes/Dashboard/Dashboard.js +++ b/frontend/scenes/Dashboard/Dashboard.js @@ -7,6 +7,7 @@ import DocumentsStore from 'stores/DocumentsStore'; import DocumentList from 'components/DocumentList'; import PageTitle from 'components/PageTitle'; import CenteredContent from 'components/CenteredContent'; +import { ListPlaceholder } from 'components/LoadingPlaceholder'; const Subheading = styled.h3` font-size: 11px; @@ -31,16 +32,23 @@ type Props = { this.props.documents.fetchRecentlyViewed(); } + get showPlaceholder() { + const { isLoaded, isFetching } = this.props.documents; + return !isLoaded && isFetching; + } + render() { return (

Home

Recently viewed + {this.showPlaceholder && } Recently edited + {this.showPlaceholder && }
); } diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 7a2f0980..1b369a0b 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -12,12 +12,12 @@ import Document from 'models/Document'; import UiStore from 'stores/UiStore'; import DocumentsStore from 'stores/DocumentsStore'; import Menu from './components/Menu'; +import LoadingPlaceholder from 'components/LoadingPlaceholder'; import Editor from 'components/Editor'; import DropToImport from 'components/DropToImport'; import { HeaderAction, SaveAction } from 'components/Layout'; import LoadingIndicator from 'components/LoadingIndicator'; import PublishingInfo from 'components/PublishingInfo'; -import PreviewLoading from 'components/PreviewLoading'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; @@ -263,8 +263,8 @@ const Container = styled(Flex)` width: 100%; `; -const LoadingState = styled(PreviewLoading)` - margin: 80px 20px; +const LoadingState = styled(LoadingPlaceholder)` + margin: 90px 0; `; const StyledDropToImport = styled(DropToImport)` diff --git a/frontend/components/PreviewLoading/PreviewLoading.js b/frontend/scenes/Document/components/LoadingPlaceholder/LoadingPlaceholder.js similarity index 82% rename from frontend/components/PreviewLoading/PreviewLoading.js rename to frontend/scenes/Document/components/LoadingPlaceholder/LoadingPlaceholder.js index 150e1c8e..1052f403 100644 --- a/frontend/components/PreviewLoading/PreviewLoading.js +++ b/frontend/scenes/Document/components/LoadingPlaceholder/LoadingPlaceholder.js @@ -1,7 +1,9 @@ // @flow import React from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; -import styled, { keyframes } from 'styled-components'; +import styled from 'styled-components'; +import { pulsate } from 'styles/animations'; +import { color } from 'styles/constants'; import Flex from 'components/Flex'; import { randomInteger } from 'utils/random'; @@ -11,7 +13,7 @@ const randomValues = Array.from( () => `${randomInteger(85, 100)}%` ); -export default (props: {}) => { +export default (props: Object) => { return ( { ); }; -const pulsate = keyframes` - 0% { opacity: 1; } - 50% { opacity: 0.5; } - 100% { opacity: 1; } -`; - const Mask = styled(Flex)` height: ${props => (props.header ? 28 : 18)}px; margin-bottom: ${props => (props.header ? 32 : 14)}px; - background-color: #ddd; + background-color: ${color.smoke}; animation: ${pulsate} 1.3s infinite; `; diff --git a/frontend/scenes/Document/components/LoadingPlaceholder/index.js b/frontend/scenes/Document/components/LoadingPlaceholder/index.js new file mode 100644 index 00000000..fd22eb81 --- /dev/null +++ b/frontend/scenes/Document/components/LoadingPlaceholder/index.js @@ -0,0 +1,3 @@ +// @flow +import LoadingPlaceholder from './LoadingPlaceholder'; +export default LoadingPlaceholder; diff --git a/frontend/scenes/Starred/Starred.js b/frontend/scenes/Starred/Starred.js index a14b21e6..6704045c 100644 --- a/frontend/scenes/Starred/Starred.js +++ b/frontend/scenes/Starred/Starred.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { observer, inject } from 'mobx-react'; import CenteredContent from 'components/CenteredContent'; +import { ListPlaceholder } from 'components/LoadingPlaceholder'; import PageTitle from 'components/PageTitle'; import DocumentList from 'components/DocumentList'; import DocumentsStore from 'stores/DocumentsStore'; @@ -16,10 +17,13 @@ import DocumentsStore from 'stores/DocumentsStore'; } render() { + const { isLoaded, isFetching } = this.props.documents; + return (

Starred

+ {!isLoaded && isFetching && }
); diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js index 649f66ad..46a7b84c 100644 --- a/frontend/stores/DocumentsStore.js +++ b/frontend/stores/DocumentsStore.js @@ -12,6 +12,7 @@ class DocumentsStore { @observable recentlyViewedIds: Array = []; @observable data: Map = new ObservableMap([]); @observable isLoaded: boolean = false; + @observable isFetching: boolean = false; errors: ErrorsStore; /* Computed */ @@ -34,6 +35,8 @@ class DocumentsStore { /* Actions */ @action fetchAll = async (request: string = 'list'): Promise<*> => { + this.isFetching = true; + try { const res = await client.post(`/documents.${request}`); invariant(res && res.data, 'Document list not available'); @@ -47,6 +50,8 @@ class DocumentsStore { return data; } catch (e) { this.errors.add('Failed to load documents'); + } finally { + this.isFetching = false; } }; @@ -63,6 +68,8 @@ class DocumentsStore { }; @action fetch = async (id: string): Promise<*> => { + this.isFetching = true; + try { const res = await client.post('/documents.info', { id }); invariant(res && res.data, 'Document not available'); @@ -77,6 +84,8 @@ class DocumentsStore { return document; } catch (e) { this.errors.add('Failed to load documents'); + } finally { + this.isFetching = false; } }; diff --git a/frontend/styles/animations.js b/frontend/styles/animations.js index bdd611fa..257528b5 100644 --- a/frontend/styles/animations.js +++ b/frontend/styles/animations.js @@ -12,3 +12,9 @@ export const fadeAndScaleIn = keyframes` transform: scale(1); } `; + +export const pulsate = keyframes` + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +`; diff --git a/frontend/styles/constants.js b/frontend/styles/constants.js index 18c36877..ac4604c9 100644 --- a/frontend/styles/constants.js +++ b/frontend/styles/constants.js @@ -50,6 +50,7 @@ export const color = { /* Light Grays */ smoke: '#F4F7FA', smokeLight: '#F9FBFC', + smokeDark: '#E8EBED', /* Misc */ white: '#FFFFFF',