diff --git a/frontend/public/index.html b/frontend/public/index.html index 0900d2e4ee..fd2f6b1339 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -25,7 +25,7 @@ diff --git a/frontend/src/App.css b/frontend/src/App.css index fbcf1dd9d8..63e26c16bd 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -23,3 +23,8 @@ border-left: 1px solid #5E6475; box-sizing: border-box; } + +#app .inner { + display: table-cell; + vertical-align: middle; +} diff --git a/frontend/src/App.js b/frontend/src/App.js index a1d1fe6785..8b63368ebc 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -5,7 +5,10 @@ import { connect } from 'react-redux'; import './App.css'; +import { NAME as L10N_NAME } from 'core/l10n'; +import { NAME as LOCALES_NAME } from 'core/locales'; import { Lightbox } from 'core/lightbox'; +import { WaveLoader } from 'core/loaders'; import * as locales from 'core/locales'; import { Navigation } from 'core/navigation'; import { UserAutoUpdater } from 'core/user'; @@ -13,6 +16,14 @@ import { EntitiesList } from 'modules/entitieslist'; import { EntityDetails } from 'modules/entitydetails'; import { SearchBox } from 'modules/search'; +import type { L10nState } from 'core/l10n'; +import type { LocalesState } from 'core/locales'; + + +type Props = {| + l10n: L10nState, + locales: LocalesState, +|}; type InternalProps = { dispatch: Function, @@ -28,6 +39,12 @@ class App extends React.Component { } render() { + const { l10n, locales } = this.props; + + if (l10n.fetching || locales.fetching) { + return ; + } + return
@@ -45,4 +62,11 @@ class App extends React.Component { } } -export default connect()(App); +const mapStateToProps = (state: Object): Props => { + return { + l10n: state[L10N_NAME], + locales: state[LOCALES_NAME], + }; +}; + +export default connect(mapStateToProps)(App); diff --git a/frontend/src/core/l10n/components/AppLocalizationProvider.js b/frontend/src/core/l10n/components/AppLocalizationProvider.js index 1e7380ea95..e6cf7074f3 100644 --- a/frontend/src/core/l10n/components/AppLocalizationProvider.js +++ b/frontend/src/core/l10n/components/AppLocalizationProvider.js @@ -47,11 +47,6 @@ export class AppLocalizationProviderBase extends React.Component render() { const { children, l10n } = this.props; - if (!l10n.bundles.length) { - // TODO: Show a loader. - return
LOADING
; - } - return { children } ; diff --git a/frontend/src/core/l10n/components/AppLocalizationProvider.test.js b/frontend/src/core/l10n/components/AppLocalizationProvider.test.js index 115c5bccd2..833e08195c 100644 --- a/frontend/src/core/l10n/components/AppLocalizationProvider.test.js +++ b/frontend/src/core/l10n/components/AppLocalizationProvider.test.js @@ -34,22 +34,6 @@ describe('', () => { expect(actions.get.callCount).toEqual(1); }); - it('shows a loader when the locales are not loaded yet', () => { - const store = createReduxStore(); - - store.dispatch(actions.request()); - - const wrapper = shallowUntilTarget( - -
- , - AppLocalizationProviderBase - ); - - expect(wrapper.text()).toEqual('LOADING'); - expect(wrapper.find('#content-test-AppLocalizationProvider')).toHaveLength(0); - }); - it('renders its children when locales are loaded', () => { const store = createReduxStore(); store.dispatch(actions.receive('fr', { messages: [ 'hello' ] })); diff --git a/frontend/src/modules/entitieslist/components/EntitiesLoader.css b/frontend/src/core/loaders/components/CircleLoader.css similarity index 99% rename from frontend/src/modules/entitieslist/components/EntitiesLoader.css rename to frontend/src/core/loaders/components/CircleLoader.css index 1ad9ecda81..751e90e04d 100644 --- a/frontend/src/modules/entitieslist/components/EntitiesLoader.css +++ b/frontend/src/core/loaders/components/CircleLoader.css @@ -4,4 +4,3 @@ padding: 40px 0 10px; text-align: center; } - diff --git a/frontend/src/modules/entitieslist/components/EntitiesLoader.js b/frontend/src/core/loaders/components/CircleLoader.js similarity index 59% rename from frontend/src/modules/entitieslist/components/EntitiesLoader.js rename to frontend/src/core/loaders/components/CircleLoader.js index b5991f1e24..ef6c155193 100644 --- a/frontend/src/modules/entitieslist/components/EntitiesLoader.js +++ b/frontend/src/core/loaders/components/CircleLoader.js @@ -1,14 +1,14 @@ /* @flow */ import React from 'react'; -import './EntitiesLoader.css'; +import './CircleLoader.css'; -const EntitiesLoader = () => ( +const CircleLoader = () => (

); -export default EntitiesLoader; +export default CircleLoader; diff --git a/frontend/src/core/loaders/components/WaveLoader.css b/frontend/src/core/loaders/components/WaveLoader.css new file mode 100644 index 0000000000..8f513df0b9 --- /dev/null +++ b/frontend/src/core/loaders/components/WaveLoader.css @@ -0,0 +1,101 @@ +/* +* SpinKit +* https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE +*/ + +.wave-loader { + position: fixed; + z-index: 14; + display: table; + overflow: hidden; + width: 100%; + height: 100%; + background: #3f4752; +} + +.wave-loader .animation { + margin: 20px auto; + width: 100px; + height: 60px; + text-align: center; + font-size: 20px; +} + +.wave-loader .animation > div { + display: inline-block; + width: 12px; + height: 100%; + background-color: #7BC876; + + -webkit-animation: stretchdelay 1.2s infinite ease-in-out; + animation: stretchdelay 1.2s infinite ease-in-out; +} + +.wave-loader .animation div:nth-child(2) { + -webkit-animation-delay: -1.1s; + animation-delay: -1.1s; +} + +.wave-loader .animation div:nth-child(3) { + -webkit-animation-delay: -1.0s; + animation-delay: -1.0s; +} + +.wave-loader .animation div:nth-child(4) { + -webkit-animation-delay: -0.9s; + animation-delay: -0.9s; +} + +.wave-loader .animation div:nth-child(5) { + -webkit-animation-delay: -0.8s; + animation-delay: -0.8s; +} + +@-webkit-keyframes stretchdelay { + 0%, 40%, 100% { + -webkit-transform: scaleY(0.4) + } + 20% { + -webkit-transform: scaleY(1.0) + } +} + +@keyframes stretchdelay { + 0%, 40%, 100% { + transform: scaleY(0.4); + -webkit-transform: scaleY(0.4); + } + 20% { + transform: scaleY(1.0); + -webkit-transform: scaleY(1.0); + } +} + +.wave-loader .inner { + display: table-cell; + vertical-align: middle; +} + +/* +* Loading text + */ +.wave-loader .text { + display: block; + text-align: center; + font-size: 1.5em; + opacity: 0; + animation: fadeInLoaderText 1s forwards; + animation-delay: 3s; +} + +@keyframes fadeInLoaderText { + 0% { + display: None; + opacity: 0; + } + + 100% { + display: block; + opacity: 1; + } +} diff --git a/frontend/src/core/loaders/components/WaveLoader.js b/frontend/src/core/loaders/components/WaveLoader.js new file mode 100644 index 0000000000..ccd570921f --- /dev/null +++ b/frontend/src/core/loaders/components/WaveLoader.js @@ -0,0 +1,26 @@ +/* @flow */ + +import React from 'react'; + +import './WaveLoader.css'; + +export const WaveLoader = (): React.Node => ( +
+
+
+
+   +
+   +
+   +
+   +
+
+
+
+); + + +export default WaveLoader; diff --git a/frontend/src/core/loaders/index.js b/frontend/src/core/loaders/index.js new file mode 100644 index 0000000000..c67c3a30f9 --- /dev/null +++ b/frontend/src/core/loaders/index.js @@ -0,0 +1,4 @@ +/* @flow */ + +export { default as WaveLoader } from './components/WaveLoader'; +export { default as CircleLoader} from './components/CircleLoader'; diff --git a/frontend/src/modules/entitieslist/components/EntitiesList.js b/frontend/src/modules/entitieslist/components/EntitiesList.js index 50f17d7c47..c5c5f6e09e 100644 --- a/frontend/src/modules/entitieslist/components/EntitiesList.js +++ b/frontend/src/modules/entitieslist/components/EntitiesList.js @@ -11,7 +11,7 @@ import * as navigation from 'core/navigation'; import { actions, NAME } from '..'; import Entity from './Entity'; -import EntitiesLoader from './EntitiesLoader' +import { CircleLoader } from 'core/loaders' import type { Locale } from 'core/locales'; import type { NavigationParams } from 'core/navigation'; @@ -119,7 +119,7 @@ export class EntitiesListBase extends React.Component { pageStart={ 1 } loadMore={ this.getMoreEntities } hasMore={ hasMore } - loader={ } + loader={ } useWindow={ false } threshold={ 600 } > diff --git a/frontend/src/modules/entitieslist/components/EntitiesList.test.js b/frontend/src/modules/entitieslist/components/EntitiesList.test.js index 788467ccf4..5a973eb2f3 100644 --- a/frontend/src/modules/entitieslist/components/EntitiesList.test.js +++ b/frontend/src/modules/entitieslist/components/EntitiesList.test.js @@ -37,7 +37,7 @@ describe('', () => { const wrapper = shallowUntilTarget(, EntitiesListBase); const scroll = wrapper.find('InfiniteScroll').shallow({ disableLifecycleMethods: true }); - expect(scroll.find('EntitiesLoader')).toHaveLength(1); + expect(scroll.find('CircleLoader')).toHaveLength(1); }); it("doesn't display a loading animation when there aren't entities to load", () => { @@ -48,7 +48,7 @@ describe('', () => { const wrapper = shallowUntilTarget(, EntitiesListBase); const scroll = wrapper.find('InfiniteScroll').shallow({ disableLifecycleMethods: true }); - expect(scroll.find('EntitiesLoader')).toHaveLength(0); + expect(scroll.find('CircleLoader')).toHaveLength(0); }); it("shows a loading animation when entities are being fetched from the server", () => { @@ -59,7 +59,7 @@ describe('', () => { const wrapper = shallowUntilTarget(, EntitiesListBase); const scroll = wrapper.find('InfiniteScroll').shallow({ disableLifecycleMethods: true }); - expect(scroll.find('EntitiesLoader')).toHaveLength(1); + expect(scroll.find('CircleLoader')).toHaveLength(1); }); it('shows the correct number of entities', () => {