diff --git a/package.json b/package.json
index 8334977d..bb6b7612 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,10 @@
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-helmet": "^4.0.0",
+ "react-redux": "^5.0.2",
"react-router": "4.0.0-alpha.6",
"react-test-renderer": "^15.4.2",
+ "redux": "^3.6.0",
"sharetribe-scripts": "0.8.6",
"source-map-support": "^0.4.10"
},
diff --git a/src/app.js b/src/app.js
index 33bb6b69..7e24a06f 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2,30 +2,45 @@ import React, { PropTypes } from 'react';
import ReactDOMServer from 'react-dom/server';
import Helmet from 'react-helmet';
import { BrowserRouter, ServerRouter } from 'react-router';
+import { Provider } from 'react-redux';
+import configureStore from './store';
import Routes from './Routes';
-const RoutesWithRouterProp = ({ router }) => ;
+export const ClientApp = props => {
+ const { store } = props;
+ return (
+
+ {
+ ({router}) => (
+
+
+
+ )
+ }
+
+ );
+};
const { any, string } = PropTypes;
-RoutesWithRouterProp.propTypes = { router: any.isRequired };
-
-export const ClientApp = () => (
-
- {RoutesWithRouterProp}
-
-);
+ClientApp.propTypes = { store: any.isRequired };
export const ServerApp = props => {
const { url, context } = props;
return (
- {RoutesWithRouterProp}
+ {
+ ({router}) => (
+
+
+
+ )
+ }
);
};
-ServerApp.propTypes = { url: string.isRequired, context: any.isRequired };
+ServerApp.propTypes = { url: string.isRequired, context: any.isRequired, store: any.isRequired };
/**
* Render the given route.
diff --git a/src/app.test.js b/src/app.test.js
index ad415995..b5707d7a 100644
--- a/src/app.test.js
+++ b/src/app.test.js
@@ -4,14 +4,17 @@ import ReactDOMServer from 'react-dom/server';
import { forEach } from 'lodash';
import { createServerRenderContext } from 'react-router';
import { ClientApp, ServerApp } from './app';
+import configureStore from './store';
+
+const store = configureStore({});
const render = (url, context) =>
- ReactDOMServer.renderToString();
+ ReactDOMServer.renderToString();
describe('Application', () => {
it('renders in the client without crashing', () => {
const div = document.createElement('div');
- ReactDOM.render(, div);
+ ReactDOM.render(, div);
});
it('renders in the server without crashing', () => {
diff --git a/src/containers/SearchPage/SearchPage.js b/src/containers/SearchPage/SearchPage.js
index 8b0ba9dd..51d8daf6 100644
--- a/src/containers/SearchPage/SearchPage.js
+++ b/src/containers/SearchPage/SearchPage.js
@@ -1,9 +1,28 @@
import React from 'react';
import { Link } from 'react-router';
+import { connect } from 'react-redux';
import { Page } from '../../components';
+import { addFlashNotification } from '../../ducks/FlashNotification';
+import { addFilter } from './SearchPageDucks';
-export default () => (
-
+export const SearchPage = props => (
+
Nice studio in Helsinki
-
-)
+
+);
+
+/**
+ * Container functions.
+ */
+const mapStateToProps = function mapStateToProps(state) {
+ return state;
+};
+
+const mapDispatchToProps = function mapDispatchToProps(dispatch) {
+ return {
+ addNotice: msg => dispatch(addFlashNotification('notice', msg)),
+ addFilter: (k, v) => dispatch(addFilter(k, v)),
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(SearchPage);
diff --git a/src/containers/SearchPage/SearchPage.test.js b/src/containers/SearchPage/SearchPage.test.js
index 893ea3c5..81419d85 100644
--- a/src/containers/SearchPage/SearchPage.test.js
+++ b/src/containers/SearchPage/SearchPage.test.js
@@ -1,16 +1,14 @@
import React from 'react';
import { BrowserRouter } from 'react-router';
import renderer from 'react-test-renderer';
-import SearchPage from './SearchPage';
+import { SearchPage } from './SearchPage';
describe('SearchPage', () => {
it('matches snapshot', () => {
const component = renderer.create(
- (
-
-
-
- ),
+
+
+ ,
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
diff --git a/src/containers/SearchPage/SearchPageDucks.js b/src/containers/SearchPage/SearchPageDucks.js
new file mode 100644
index 00000000..8f2a3ecd
--- /dev/null
+++ b/src/containers/SearchPage/SearchPageDucks.js
@@ -0,0 +1,26 @@
+/**
+ * This file contains Action constants, Action creators, and reducer of a page
+ * container. We are following Ducks module proposition:
+ * https://github.com/erikras/ducks-modular-redux
+ */
+import unionBy from 'lodash/unionBy';
+
+// Actions
+const ADD_FILTER = 'ADD_FILTER';
+
+// Reducer
+export default function reducer(state = {}, action = {}) {
+ const { type, payload } = action;
+ switch (type) {
+ case ADD_FILTER:
+ const stateFilters = state.filters || [];
+ return Object.assign({}, state, { filters: unionBy(stateFilters, [ payload ]) });
+ default:
+ return state;
+ }
+}
+
+// Action Creators
+export const addFilter = (key, value) => {
+ return { type: ADD_FILTER, payload: { [key]: value } };
+};
diff --git a/src/containers/reducers.js b/src/containers/reducers.js
new file mode 100644
index 00000000..577a391b
--- /dev/null
+++ b/src/containers/reducers.js
@@ -0,0 +1,9 @@
+/**
+ * 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
+ */
+
+import SearchPage from './SearchPage/SearchPageDucks';
+
+export { SearchPage };
diff --git a/src/ducks/FlashNotification.js b/src/ducks/FlashNotification.js
new file mode 100644
index 00000000..c5aea686
--- /dev/null
+++ b/src/ducks/FlashNotification.js
@@ -0,0 +1,52 @@
+/**
+ * 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 from 'lodash/find';
+import findIndex from 'lodash/findIndex';
+
+// Actions: system notifications
+const ADD_FLASH_NOTIFICATION = 'FLASH::ADD_NOTIFICATION';
+const REMOVE_FLASH_NOTIFICATION = 'FLASH::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/index.js b/src/ducks/index.js
new file mode 100644
index 00000000..3f31556a
--- /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';
+
+export { FlashNotification };
diff --git a/src/reducers.js b/src/reducers.js
new file mode 100644
index 00000000..a54211b2
--- /dev/null
+++ b/src/reducers.js
@@ -0,0 +1,14 @@
+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.
+ * 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..722e55f8 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.15.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"
@@ -4869,6 +4881,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 +5032,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 +5629,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"