(
-
+
Change password, delete account
-
+
)
diff --git a/src/containers/reducers.js b/src/containers/reducers.js
new file mode 100644
index 00000000..9c6fcc69
--- /dev/null
+++ b/src/containers/reducers.js
@@ -0,0 +1,10 @@
+/**
+ * Export reducers from ducks modules of different containers (i.e. default export)
+ * We are following Ducks module proposition:
+ * https://github.com/erikras/ducks-modular-redux
+ */
+
+/* eslint-disable import/prefer-default-export */
+import SearchPage from './SearchPage/SearchPage.ducks';
+
+export { SearchPage };
diff --git a/src/ducks/FlashNotification.ducks.js b/src/ducks/FlashNotification.ducks.js
new file mode 100644
index 00000000..27eb27fe
--- /dev/null
+++ b/src/ducks/FlashNotification.ducks.js
@@ -0,0 +1,51 @@
+/**
+ * This file contains Action constants, Action creators, and reducer of global
+ * FlashMessages. Global actions can be used in multiple pages.
+ * We are following Ducks module proposition:
+ * https://github.com/erikras/ducks-modular-redux
+ */
+
+import { find, findIndex } from 'lodash';
+
+// Actions: system notifications
+export const ADD_FLASH_NOTIFICATION = 'app/FlashNotification/ADD_NOTIFICATION';
+export const REMOVE_FLASH_NOTIFICATION = 'app/FlashNotification/REMOVE_NOTIFICATION';
+
+const initialState = [];
+
+// Reducer
+export default (state = initialState, action) => {
+ const { type, payload } = action;
+ switch (type) {
+ case ADD_FLASH_NOTIFICATION:
+ if (!find(state, n => n.content === payload.content && !n.isRead)) {
+ return state.concat([
+ { id: payload.id, type: payload.type, content: payload.content, isRead: false },
+ ]);
+ }
+ return state;
+
+ case REMOVE_FLASH_NOTIFICATION:
+ return state.map(
+ findIndex(state, msg => msg.id === payload.id),
+ msg => msg.set('isRead', true),
+ );
+
+ default:
+ return state;
+ }
+}
+
+// Action Creators
+let nextMessageId = 1;
+
+export const addFlashNotification = (type, content) => {
+ const id = nextMessageId;
+ nextMessageId += 1;
+ return {
+ type: ADD_FLASH_NOTIFICATION,
+ payload: { id: `note_${id}`, type, content, isRead: false },
+ };
+};
+
+export const removeFlashNotification = id => ({ type: REMOVE_FLASH_NOTIFICATION, payload: { id } });
diff --git a/src/ducks/FlashNotification.test.js b/src/ducks/FlashNotification.test.js
new file mode 100644
index 00000000..3a2064c8
--- /dev/null
+++ b/src/ducks/FlashNotification.test.js
@@ -0,0 +1,49 @@
+import reducer, {
+ ADD_FLASH_NOTIFICATION,
+ REMOVE_FLASH_NOTIFICATION,
+ addFlashNotification,
+ removeFlashNotification,
+} from './FlashNotification.ducks';
+
+describe('FlashNotification', () => {
+ describe('actions', () => {
+ it('should create an action to add a filter', () => {
+ const content = 'Error message';
+ const type = 'error';
+ const expectedAction = {
+ type: ADD_FLASH_NOTIFICATION,
+ payload: { id: 'note_1', type, content, isRead: false },
+ };
+ const received = addFlashNotification(type, content);
+ expect(received).toEqual(expectedAction);
+ });
+
+ it('should create an action to remove a notification', () => {
+ const expectedAction = { type: REMOVE_FLASH_NOTIFICATION, payload: { id: 1 } };
+
+ expect(removeFlashNotification(1)).toEqual(expectedAction);
+ });
+ });
+
+ describe('reducer', () => {
+ it('should return the initial state', () => {
+ const initial = reducer(undefined, {});
+ expect(initial).toEqual([]);
+ });
+
+ it('should handle ADD_FLASH_NOTIFICATION', () => {
+ const addFlashNote1 = addFlashNotification('error', 'Run the tests');
+ const addFlashNote2 = addFlashNotification('error', 'Run the tests again');
+ const reduced = reducer([], addFlashNote1);
+ const reducedWithInitialContent = reducer([ addFlashNote1.payload ], addFlashNote2);
+ expect(reduced).toEqual([ addFlashNote1.payload ]);
+ expect(reducedWithInitialContent).toEqual([ addFlashNote1.payload, addFlashNote2.payload ]);
+ });
+
+ it('should handle duplicates ADD_FILTER', () => {
+ const addFlashNote = addFlashNotification('error', 'Run the tests');
+ const reducedWithInitialContent = reducer([ addFlashNote.payload ], addFlashNote);
+ expect(reducedWithInitialContent).toEqual([ addFlashNote.payload ]);
+ });
+ });
+});
diff --git a/src/ducks/index.js b/src/ducks/index.js
new file mode 100644
index 00000000..739c6c30
--- /dev/null
+++ b/src/ducks/index.js
@@ -0,0 +1,9 @@
+/**
+ * Import reducers from shared ducks modules (default export)
+ * We are following Ducks module proposition:
+ * https://github.com/erikras/ducks-modular-redux
+ */
+
+import FlashNotification from './FlashNotification.ducks';
+
+export { FlashNotification }; // eslint-disable-line import/prefer-default-export
diff --git a/src/index.js b/src/index.js
index d5c92c40..90e229be 100644
--- a/src/index.js
+++ b/src/index.js
@@ -14,12 +14,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { ClientApp, renderApp } from './app';
+import configureStore from './store';
import './index.css';
// If we're in a browser already, render the client application.
if (typeof window !== 'undefined') {
- ReactDOM.render(
, document.getElementById('root'));
+ const preloadedState = window.__PRELOADED_STATE__ || {}; // eslint-disable-line no-underscore-dangle
+ const store = configureStore(preloadedState);
+
+ ReactDOM.render(
, document.getElementById('root'));
}
// Export the function for server side rendering.
diff --git a/src/reducers.js b/src/reducers.js
new file mode 100644
index 00000000..6fa17e41
--- /dev/null
+++ b/src/reducers.js
@@ -0,0 +1,16 @@
+import { combineReducers } from 'redux';
+import * as globalReducers from './ducks';
+import * as pageReducers from './containers/reducers';
+
+/**
+ * Function _createReducer_ combines global reducers (reducers that are used in
+ * multiple pages) and reducers that are handling actions happening inside one page container.
+ * Since we combineReducers, pageReducers will get page specific key (e.g. SearchPage)
+ * which is page specific.
+ * Future: this structure could take in asyncReducers, which are changed when you navigate pages.
+ */
+const createReducer = function createReducer() {
+ return combineReducers({ ...globalReducers, ...pageReducers });
+};
+
+export default createReducer;
diff --git a/src/store.js b/src/store.js
new file mode 100644
index 00000000..2d0f79e3
--- /dev/null
+++ b/src/store.js
@@ -0,0 +1,10 @@
+import { createStore } from 'redux';
+import createReducer from './reducers';
+
+/**
+ * configureStore creates a new store with initial state and possible enhancers
+ * (like redux-saga or redux-thunk middleware)
+ */
+export default function configureStore(initialState) {
+ return createStore(createReducer(), initialState);
+}
diff --git a/yarn.lock b/yarn.lock
index 52556dc9..02c6ac0b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2608,6 +2608,10 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+hoist-non-react-statics@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
+
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -2801,7 +2805,7 @@ interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
-invariant@^2.2.0, invariant@^2.2.1:
+invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
dependencies:
@@ -3455,6 +3459,14 @@ loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.
json5 "^0.5.0"
object-assign "^4.0.1"
+lodash, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+lodash-es@^4.2.0, lodash-es@^4.2.1:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
+
lodash._arraycopy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1"
@@ -3600,10 +3612,6 @@ lodash.uniq@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.16.2, lodash@^4.16.4, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
- version "4.17.4"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -4792,7 +4800,7 @@ qs@6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
-qs@~6.3.0:
+qs@^6.3.0, qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
@@ -4869,6 +4877,16 @@ react-helmet@^4.0.0:
object-assign "^4.0.1"
react-side-effect "^1.1.0"
+react-redux@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.2.tgz#3d9878f5f71c6fafcd45de1fbb162ea31f389814"
+ dependencies:
+ hoist-non-react-statics "^1.0.3"
+ invariant "^2.0.0"
+ lodash "^4.2.0"
+ lodash-es "^4.2.0"
+ loose-envify "^1.1.0"
+
react-router@4.0.0-alpha.6:
version "4.0.0-alpha.6"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.0.0-alpha.6.tgz#239fcf9a6ba7997021022c9b51d72d370f7b6bf4"
@@ -5010,6 +5028,15 @@ reduce-function-call@^1.0.1:
dependencies:
balanced-match "^0.4.2"
+redux@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"
+ dependencies:
+ lodash "^4.2.1"
+ lodash-es "^4.2.1"
+ loose-envify "^1.1.0"
+ symbol-observable "^1.0.2"
+
referrer-policy@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.1.0.tgz#35774eb735bf50fb6c078e83334b472350207d79"
@@ -5598,6 +5625,10 @@ svgo@^0.7.0:
sax "~1.2.1"
whet.extend "~0.9.9"
+symbol-observable@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
+
"symbol-tree@>= 3.1.0 < 4.0.0":
version "3.2.1"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.1.tgz#8549dd1d01fa9f893c18cc9ab0b106b4d9b168cb"