diff --git a/package.json b/package.json index c5d7aeef..e4420155 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "redux-saga": "^0.14.3", "sanitize.css": "^4.1.0", "sharetribe-scripts": "0.8.6", - "source-map-support": "^0.4.11" + "source-map-support": "^0.4.11", + "url": "^0.11.0" }, "devDependencies": { "nodemon": "^1.11.0", diff --git a/server/index.js b/server/index.js index a2cc2d6c..5be3f781 100644 --- a/server/index.js +++ b/server/index.js @@ -22,6 +22,7 @@ const compression = require('compression'); const path = require('path'); const fs = require('fs'); const qs = require('qs'); +const url = require('url'); const _ = require('lodash'); const React = require('react'); const { createServerRenderContext } = require('react-router'); @@ -33,7 +34,9 @@ const buildPath = path.resolve(__dirname, '..', 'build'); const manifestPath = path.join(buildPath, 'asset-manifest.json'); const manifest = require(manifestPath); const mainJsPath = path.join(buildPath, manifest['main.js']); -const renderApp = require(mainJsPath).default; +const mainJs = require(mainJsPath); +const renderApp = mainJs.default; +const matchPathname = mainJs.matchPathname; // The HTML build file is generated from the `public/index.html` file // and used as a template for server side rendering. The application @@ -66,8 +69,21 @@ const template = _.template(indexHtml, { escape: reNoMatch, }); -function render(url, context, preloadedState) { - const { head, body } = renderApp(url, context, preloadedState); +function render(requestUrl, context, preloadedState) { + const { head, body } = renderApp(requestUrl, context, preloadedState); + + const pathname = url.parse(requestUrl).pathname; + const { matchedRoutes, params } = matchPathname(pathname); + const component = matchedRoutes[0].component; + + if (component.loadData) { + component + .loadData() + .then((val) => { + console.log('Data fetch resolved', val); + }); + } + // Preloaded state needs to be passed for client side too. // For security reasons we ensure that preloaded state is considered as a string diff --git a/src/containers/SearchPage/SearchPage.js b/src/containers/SearchPage/SearchPage.js index 5aeb1d17..938a512d 100644 --- a/src/containers/SearchPage/SearchPage.js +++ b/src/containers/SearchPage/SearchPage.js @@ -1,4 +1,4 @@ -import React, { PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; import classNames from 'classnames'; import { connect } from 'react-redux'; import { includes } from 'lodash'; @@ -39,41 +39,53 @@ const fakeListings = [ }, ]; -export const SearchPageComponent = props => { - const { tab } = props; - const selectedTab = includes(['filters','listings', 'map'], tab) - ? tab - : 'listings'; +export class SearchPageComponent extends Component { - const filtersClassName = classNames(css.filters, { - [css.open]: selectedTab === 'filters', - }); - const listingsClassName = classNames(css.filters, { - [css.open]: selectedTab === 'listings', - }); - const mapClassName = classNames(css.filters, { - [css.open]: selectedTab === 'map', - }); + componentWillMount() { + /* eslint-disable no-console */ + console.log('Client loads data'); + SearchPageComponent.loadData().then((val) => { + console.log('Client fetched data', val); + }); + /* eslint-enable no-console */ + } - return ( - -
-
- + render() { + const { tab } = this.props; + const selectedTab = includes(['filters','listings', 'map'], tab) + ? tab + : 'listings'; + + const filtersClassName = classNames(css.filters, { + [css.open]: selectedTab === 'filters', + }); + const listingsClassName = classNames(css.filters, { + [css.open]: selectedTab === 'listings', + }); + const mapClassName = classNames(css.filters, { + [css.open]: selectedTab === 'map', + }); + + return ( + +
+
+ +
+
+ + {fakeListings.map(l => )} + +
+
+ + {fakeListings.map(l => )} + +
-
- - {fakeListings.map(l => )} - -
-
- - {fakeListings.map(l => )} - -
-
- - ); + + ); + } }; SearchPageComponent.defaultProps = { tab: 'listings' }; @@ -82,6 +94,15 @@ const { string } = PropTypes; SearchPageComponent.propTypes = { tab: string }; +SearchPageComponent.loadData = () => ( + new Promise((resolve) => ( + // also node has a 'setTimeout' -> it's good for testing async call. + setTimeout(() => { + resolve(fakeListings); + }, (Math.random() * 2000) + 1000) + )) +); + /** * Container functions. * Since we add this to global store state with combineReducers, this will only get partial state diff --git a/src/index.js b/src/index.js index 30f46bd5..7f782c02 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { ClientApp, renderApp } from './app'; import configureStore from './store'; +import { matchLocation } from './routesConfiguration'; import './index.css'; @@ -29,3 +30,5 @@ if (typeof window !== 'undefined') { // Export the function for server side rendering. export default renderApp; + +export { matchLocation }; diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js index a57a7144..0c1367a6 100644 --- a/src/routesConfiguration.js +++ b/src/routesConfiguration.js @@ -1,6 +1,10 @@ import React from 'react'; import { find } from 'lodash'; import { Redirect } from 'react-router'; + +// This will change to `matchPath` soonish +import matchPattern from 'react-router/matchPattern' + import pathToRegexp from 'path-to-regexp'; import { AuthenticationPage, @@ -269,7 +273,42 @@ const toPathByRouteName = (nameToFind, routes) => { const pathByRouteName = (nameToFind, routes, params = {}) => toPathByRouteName(nameToFind, routes)(params); +/** + * matchRoutesToLocation helps to figure out which routes are related to given location. + */ +const matchRoutesToLocation = (routes, location, matchedRoutes=[], params={}) => { + const parameters = { ...params }; + routes.forEach((route) => { + const { exactly = false } = route; + const match = !route.pattern ? true : matchPattern(route.pattern, location, exactly); + + if (match) { + matchedRoutes.push(route); + + if (match.params) { + Object.keys(match.params).forEach(key => { parameters[key] = match.params[key]; }); + } + } + + if (route.routes) { + matchRoutesToLocation(route.routes, location, matchedRoutes, parameters); + } + }); + + return { matchedRoutes, params: parameters }; +} + +const matchPathnameCreator = routes => ( + (location, matchedRoutes=[], params={}) => + matchRoutesToLocation(routes, { pathname: location }, matchedRoutes, params) +); + +/** + * matchRoutesToLocation helps to figure out which routes are related to given location. + */ +const matchPathname = matchPathnameCreator(routesConfiguration); + // Exported helpers -export { findRouteByName, flattenRoutes, toPathByRouteName, pathByRouteName }; +export { findRouteByName, flattenRoutes, matchPathname, toPathByRouteName, pathByRouteName }; export default routesConfiguration;