From 327ea45b80a85938dc22424b151859a08057e722 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 23 Jan 2017 19:52:43 +0200 Subject: [PATCH] Add routes configuration --- src/Routes.js | 222 +++++------------ src/app.js | 5 +- .../RouterProvider/RouterProvider.js | 21 ++ .../RouterProvider/RouterProvider.test.js | 18 ++ .../RoutesProvider/RoutesProvider.js | 21 ++ .../RoutesProvider/RoutesProvider.test.js | 18 ++ src/components/index.js | 5 +- src/routesConfiguration.js | 230 ++++++++++++++++++ 8 files changed, 370 insertions(+), 170 deletions(-) create mode 100644 src/components/RouterProvider/RouterProvider.js create mode 100644 src/components/RouterProvider/RouterProvider.test.js create mode 100644 src/components/RoutesProvider/RoutesProvider.js create mode 100644 src/components/RoutesProvider/RoutesProvider.test.js create mode 100644 src/routesConfiguration.js diff --git a/src/Routes.js b/src/Routes.js index c5be2328..17e2b086 100644 --- a/src/Routes.js +++ b/src/Routes.js @@ -1,31 +1,8 @@ import React, { PropTypes } from 'react'; import { Match, Miss, Redirect } from 'react-router'; -import { - AuthenticationPage, - CheckoutPage, - ConversationPage, - ContactDetailsPage, - EditProfilePage, - InboxPage, - LandingPage, - ListingPage, - ManageListingsPage, - NotFoundPage, - NotificationSettingsPage, - OrderPage, - PasswordChangePage, - PasswordForgottenPage, - PaymentMethodsPage, - PayoutPreferencesPage, - ProfilePage, - SalesConversationPage, - SearchPage, - SecurityPage, -} from './containers'; - -// This is only used for testing that redirects work correct in the -// client and when rendering in the server. -const RedirectLandingPage = () => ; +import { RouterProvider, RoutesProvider } from './components'; +import { NotFoundPage } from './containers'; +import routesConfiguration, { flattenRoutes, pathByRouteName } from './routesConfiguration'; // Fake authentication module // An example from react-router v4 repository @@ -43,152 +20,63 @@ export const fakeAuth = { }, }; -/* eslint-disable react/prop-types, arrow-body-style */ -// User must be authenticated before he can see certain pages -export const MatchWhenAuthorized = ({ component: Component, ...rest }) => ( - { - return fakeAuth.isAuthenticated - ? - : ; - }} - /> -); -/* eslint-enable react/prop-types, arrow-body-style */ +// wrap `Match` and use this everywhere instead, then when +// sub routes are added to any route it'll work +// This will also check if route needs authentication. +/* eslint-disable arrow-body-style */ +const MatchWithSubRoutes = props => { + const { auth, component: Component, ...rest } = props; + const canShowComponent = !auth || (auth && fakeAuth.isAuthenticated); + return ( + { + return canShowComponent + ? + : ( + + ); + }} + /> + ); +}; +/* eslint-enable arrow-body-style */ -class Routes extends React.Component { - getChildContext() { - return { router: this.props.router }; - } +MatchWithSubRoutes.defaultProps = { auth: false, exactly: false }; - render() { - return ( -
- +const { any, array, bool, func, node, oneOfType, string } = PropTypes; - {/* Search view */} - +MatchWithSubRoutes.propTypes = { + pattern: string.isRequired, + auth: bool, + exactly: bool, + name: string.isRequired, + component: oneOfType([ func, node ]).isRequired, +}; - {/* Listing view */} - - +const Routes = props => { + const flattenedRoutes = flattenRoutes(props.routes); + const matches = flattenedRoutes.map(route => ); - {/* profile / storefront view */} - - - + return ( + + +
+ {matches} + +
+
+
+ ); +}; - {/* checkout */} - - - {/* Login and signup */} - } - /> - } - /> - - {/* Password forgotten */} - - - {/* Change password */} - - - {/* Inbox and filtered views */} - } - /> - } - /> - } - /> - - {/* Order/Conversation and mobile views */} - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - {/* Manage listings */} - - - {/* Account settings */} - } - /> - - - - - - -
- ); - } -} - -const { any } = PropTypes; - -Routes.propTypes = { router: any.isRequired }; -Routes.childContextTypes = { router: React.PropTypes.object }; +Routes.propTypes = { router: any.isRequired, routes: array.isRequired }; export default Routes; diff --git a/src/app.js b/src/app.js index 4c82629b..ff1438c1 100644 --- a/src/app.js +++ b/src/app.js @@ -5,6 +5,7 @@ import { BrowserRouter, ServerRouter } from 'react-router'; import { Provider } from 'react-redux'; import configureStore from './store'; import Routes from './Routes'; +import routesConfiguration from './routesConfiguration'; export const ClientApp = props => { const { store } = props; @@ -12,7 +13,7 @@ export const ClientApp = props => { {({ router }) => ( - + )} @@ -29,7 +30,7 @@ export const ServerApp = props => { {({ router }) => ( - + )} diff --git a/src/components/RouterProvider/RouterProvider.js b/src/components/RouterProvider/RouterProvider.js new file mode 100644 index 00000000..01b35c8a --- /dev/null +++ b/src/components/RouterProvider/RouterProvider.js @@ -0,0 +1,21 @@ +import React, { Component, PropTypes } from 'react'; + +class RouterProvider extends Component { + getChildContext() { + return { router: this.props.router }; + } + + render() { + return React.Children.only(this.props.children); + } +} + +const { any, node, object } = PropTypes; + +RouterProvider.childContextTypes = { router: object }; + +RouterProvider.defaultProps = { children: {} }; + +RouterProvider.propTypes = { router: any.isRequired, children: node }; + +export default RouterProvider; diff --git a/src/components/RouterProvider/RouterProvider.test.js b/src/components/RouterProvider/RouterProvider.test.js new file mode 100644 index 00000000..49703700 --- /dev/null +++ b/src/components/RouterProvider/RouterProvider.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import RouterProvider from './RouterProvider'; +import renderer from 'react-test-renderer'; + +describe('RouterProvider', () => { + it('should contain routes from context', () => { + const router = { name: 'router in context' }; + const Child = (props, context) => { + return
{context.router.name}
; + }; + Child.contextTypes = { router: React.PropTypes.object }; + + const rendered = renderer + .create() + .toJSON(); + expect(rendered.children).toContain('router in context'); + }); +}); diff --git a/src/components/RoutesProvider/RoutesProvider.js b/src/components/RoutesProvider/RoutesProvider.js new file mode 100644 index 00000000..55a1a98f --- /dev/null +++ b/src/components/RoutesProvider/RoutesProvider.js @@ -0,0 +1,21 @@ +import React, { Component, PropTypes } from 'react'; + +class RoutesProvider extends Component { + getChildContext() { + return { routes: this.props.routes }; + } + + render() { + return React.Children.only(this.props.children); + } +} + +const { array, node } = PropTypes; + +RoutesProvider.childContextTypes = { routes: array }; + +RoutesProvider.defaultProps = { children: {} }; + +RoutesProvider.propTypes = { routes: array.isRequired, children: node }; + +export default RoutesProvider; diff --git a/src/components/RoutesProvider/RoutesProvider.test.js b/src/components/RoutesProvider/RoutesProvider.test.js new file mode 100644 index 00000000..889b78c6 --- /dev/null +++ b/src/components/RoutesProvider/RoutesProvider.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import RoutesProvider from './RoutesProvider'; +import renderer from 'react-test-renderer'; + +describe('RoutesProvider', () => { + it('should contain routes from context', () => { + const routesConf = [ { name: 'SomePage' } ]; + const Child = (props, context) => { + return
{context.routes[0].name}
; + }; + Child.contextTypes = { routes: React.PropTypes.array }; + + const rendered = renderer + .create() + .toJSON(); + expect(rendered.children).toContain('SomePage'); + }); +}); diff --git a/src/components/index.js b/src/components/index.js index 3c747810..01b7ea37 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,4 +1,7 @@ /* eslint-disable import/prefer-default-export */ +import NamedLink from './NamedLink/NamedLink'; import PageLayout from './PageLayout/PageLayout'; +import RouterProvider from './RouterProvider/RouterProvider'; +import RoutesProvider from './RoutesProvider/RoutesProvider'; -export { PageLayout }; +export { NamedLink, PageLayout, RouterProvider, RoutesProvider }; diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js new file mode 100644 index 00000000..21e6057b --- /dev/null +++ b/src/routesConfiguration.js @@ -0,0 +1,230 @@ +import React from 'react'; +import { find } from 'lodash'; +import { Redirect } from 'react-router'; +import pathToRegexp from 'path-to-regexp'; +import { + AuthenticationPage, + CheckoutPage, + ContactDetailsPage, + EditProfilePage, + InboxPage, + LandingPage, + ListingPage, + ManageListingsPage, + OrderPage, + PasswordChangePage, + PasswordForgottenPage, + PayoutPreferencesPage, + ProfilePage, + SalesConversationPage, + SearchPage, + SecurityPage, +} from './containers'; + +// This is only used for testing that redirects work correct in the +// client and when rendering in the server. +const RedirectLandingPage = () => ; + +const routesConfiguration = [ + { pattern: '/', exactly: true, name: 'LandingPage', component: LandingPage }, + { pattern: '/s', exactly: true, name: 'SearchPage', component: SearchPage }, + { + pattern: '/l', + exactly: true, + name: 'ListingBasePage', + component: RedirectLandingPage, + routes: [ + { pattern: '/l/:slug/:id', exactly: true, name: 'ListingPage', component: ListingPage }, + ], + }, + { + pattern: '/u', + exactly: true, + name: 'ProfileBasePage', + component: RedirectLandingPage, + routes: [ + { + pattern: '/u/:displayName', + exactly: true, + name: 'ProfilePage', + component: ProfilePage, + routes: [ + { + pattern: '/u/:displayName/edit', + auth: true, + exactly: true, + name: 'EditProfilePage', + component: EditProfilePage, + }, + ], + }, + ], + }, + { + pattern: '/checkout', + exactly: true, + name: 'CheckoutBasePage', + component: RedirectLandingPage, + routes: [ + { + pattern: '/checkout/:listingId', + exactly: true, + name: 'CheckoutPage', + component: CheckoutPage, + }, + ], + }, + { + pattern: '/login', + exactly: true, + name: 'LogInPage', + component: props => , + }, + { + pattern: '/signup', + exactly: true, + name: 'SignUpPage', + component: props => , + }, + { pattern: '/password', exactly: true, name: 'PasswordPage', component: PasswordForgottenPage }, + { + pattern: '/password/forgotten', + exactly: true, + name: 'PasswordForgottenPage', + component: PasswordForgottenPage, + }, + { + pattern: '/password/change', + exactly: true, + name: 'PasswordChangePage', + component: PasswordChangePage, + }, + { + pattern: '/orders', + auth: true, + exactly: true, + name: 'OrdersPage', + component: props => , + }, + { + pattern: '/sales', + auth: true, + exactly: true, + name: 'SalesPage', + component: props => , + }, + { + pattern: '/order/:id', + auth: true, + exactly: true, + name: 'OrderPage', + component: props => , + routes: [ + { + pattern: '/order/:id/details', + auth: true, + exactly: true, + name: 'OrderDetailsPage', + component: props => , + }, + { + pattern: '/order/:id/discussion', + auth: true, + exactly: true, + name: 'OrderDiscussionPage', + component: props => , + }, + ], + }, + { + pattern: '/sale/:id', + auth: true, + exactly: true, + name: 'SalePage', + component: props => , + routes: [ + { + pattern: '/sale/:id/details', + auth: true, + exactly: true, + name: 'SaleDetailsPage', + component: props => , + }, + { + pattern: '/sale/:id/discussion', + auth: true, + exactly: true, + name: 'SaleDiscussionPage', + component: props => , + }, + ], + }, + { + pattern: '/listings', + auth: true, + exactly: true, + name: 'ManageListingsPage', + component: ManageListingsPage, + }, + { + pattern: '/account', + auth: true, + exactly: true, + name: 'AccountPage', + component: () => , + routes: [ + { + pattern: '/account/contact-details', + auth: true, + exactly: true, + name: 'ContactDetailsPage', + component: ContactDetailsPage, + }, + { + pattern: '/account/payout-preferences', + auth: true, + exactly: true, + name: 'PayoutPreferencesPage', + component: PayoutPreferencesPage, + }, + { + pattern: '/account/security', + auth: true, + exactly: true, + name: 'SecurityPage', + component: SecurityPage, + }, + ], + }, +]; + +const flattenRoutes = routesArray => + routesArray.reduce((a, b) => a.concat(b.routes ? [ b ].concat(flattenRoutes(b.routes)) : b), []); + +const findRouteByName = (nameToFind, routes) => { + const flattenedRoutes = flattenRoutes(routes); + return find(flattenedRoutes, route => route.name === nameToFind); +}; + +/** + * E.g. ```const toListingPath = toPathByRouteName('ListingPage', routes);``` + * Then we can generate listing paths with given params (```toListingPath({ id: uuidX })```) + */ +const toPathByRouteName = (nameToFind, routes) => { + const route = findRouteByName(nameToFind, routes); + if (!route) { + throw new Error(`Path "${nameToFind}" was not found.`); + } + return pathToRegexp.compile(route.pattern); +}; + +/** + * Shorthand for single path call. (```pathByRouteName('ListingPage', routes, { id: uuidX });```) + */ +const pathByRouteName = (nameToFind, routes, params = {}) => + toPathByRouteName(nameToFind, routes)(params); + +// Exported helpers +export { findRouteByName, flattenRoutes, toPathByRouteName, pathByRouteName }; + +export default routesConfiguration;