Add routes configuration

This commit is contained in:
Vesa Luusua 2017-01-23 19:52:43 +02:00
parent c0367780b6
commit 327ea45b80
8 changed files with 370 additions and 170 deletions

View file

@ -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 = () => <Redirect to="/" />;
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 }) => (
<Match
{...rest}
render={props => {
return fakeAuth.isAuthenticated
? <Component {...props} />
: <Redirect to={{ pathname: '/login', state: { from: props.location } }} />;
}}
/>
);
/* 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 (
<Match
{...rest}
render={matchProps => {
return canShowComponent
? <Component {...matchProps} />
: (
<Redirect
to={
{
pathname: pathByRouteName('LogInPage', routesConfiguration, {}),
state: { from: matchProps.location },
}
}
/>
);
}}
/>
);
};
/* eslint-enable arrow-body-style */
class Routes extends React.Component {
getChildContext() {
return { router: this.props.router };
}
MatchWithSubRoutes.defaultProps = { auth: false, exactly: false };
render() {
return (
<div>
<Match exactly pattern="/" component={LandingPage} />
const { any, array, bool, func, node, oneOfType, string } = PropTypes;
{/* Search view */}
<Match exactly pattern="/s" component={SearchPage} />
MatchWithSubRoutes.propTypes = {
pattern: string.isRequired,
auth: bool,
exactly: bool,
name: string.isRequired,
component: oneOfType([ func, node ]).isRequired,
};
{/* Listing view */}
<Match exactly pattern="/l/:slug" component={ListingPage} />
<Match exactly pattern="/l" component={RedirectLandingPage} />
const Routes = props => {
const flattenedRoutes = flattenRoutes(props.routes);
const matches = flattenedRoutes.map(route => <MatchWithSubRoutes key={route.name} {...route} />);
{/* profile / storefront view */}
<Match exactly pattern="/u/:displayName" component={ProfilePage} />
<MatchWhenAuthorized exactly pattern="/u/:displayName/edit" component={EditProfilePage} />
<Match exactly pattern="/u" component={RedirectLandingPage} />
return (
<RouterProvider router={props.router}>
<RoutesProvider routes={props.routes}>
<div>
{matches}
<Miss component={NotFoundPage} />
</div>
</RoutesProvider>
</RouterProvider>
);
};
{/* checkout */}
<Match exactly pattern="/checkout/:listingId" component={CheckoutPage} />
{/* Login and signup */}
<Match
exactly
pattern="/login"
component={props => <AuthenticationPage {...props} tab="login" />}
/>
<Match
exactly
pattern="/signup"
component={props => <AuthenticationPage {...props} tab="signup" />}
/>
{/* Password forgotten */}
<Match exactly pattern="/password/forgotten" component={PasswordForgottenPage} />
{/* Change password */}
<Match exactly pattern="/password/change" component={PasswordChangePage} />
{/* Inbox and filtered views */}
<MatchWhenAuthorized
exactly
pattern="/inbox"
component={props => <InboxPage {...props} filter="inbox" />}
/>
<MatchWhenAuthorized
exactly
pattern="/orders"
component={props => <InboxPage {...props} filter="orders" />}
/>
<MatchWhenAuthorized
exactly
pattern="/sales"
component={props => <InboxPage {...props} filter="sales" />}
/>
{/* Order/Conversation and mobile views */}
<MatchWhenAuthorized exactly pattern="/conversation/:id" component={ConversationPage} />
<MatchWhenAuthorized
exactly
pattern="/order/:id"
component={props => <OrderPage {...props} tab="discussion" />}
/>
<MatchWhenAuthorized
exactly
pattern="/order/:id/discussion"
component={props => <OrderPage {...props} tab="discussion" />}
/>
<MatchWhenAuthorized
exactly
pattern="/order/:id/details"
component={props => <OrderPage {...props} tab="details" />}
/>
<MatchWhenAuthorized
exactly
pattern="/sale/:id"
component={props => <SalesConversationPage {...props} tab="discussion" />}
/>
<MatchWhenAuthorized
exactly
pattern="/sale/:id/discussion"
component={props => <SalesConversationPage {...props} tab="discussion" />}
/>
<MatchWhenAuthorized
exactly
pattern="/sale/:id/details"
component={props => <SalesConversationPage {...props} tab="details" />}
/>
{/* Manage listings */}
<MatchWhenAuthorized exactly pattern="/listings" component={ManageListingsPage} />
{/* Account settings */}
<MatchWhenAuthorized
exactly
pattern="/account"
component={() => <Redirect to="/account/contact-details" />}
/>
<MatchWhenAuthorized
exactly
pattern="/account/contact-details"
component={ContactDetailsPage}
/>
<MatchWhenAuthorized
exactly
pattern="/account/notifications"
component={NotificationSettingsPage}
/>
<MatchWhenAuthorized
exactly
pattern="/account/payment-methods"
component={PaymentMethodsPage}
/>
<MatchWhenAuthorized
exactly
pattern="/account/payout-preferences"
component={PayoutPreferencesPage}
/>
<MatchWhenAuthorized exactly pattern="/account/security" component={SecurityPage} />
<Miss component={NotFoundPage} />
</div>
);
}
}
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;

View file

@ -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 => {
<BrowserRouter>
{({ router }) => (
<Provider store={store}>
<Routes router={router} />
<Routes router={router} routes={routesConfiguration} />
</Provider>
)}
</BrowserRouter>
@ -29,7 +30,7 @@ export const ServerApp = props => {
<ServerRouter location={url} context={context}>
{({ router }) => (
<Provider store={store}>
<Routes router={router} />
<Routes router={router} routes={routesConfiguration} />
</Provider>
)}
</ServerRouter>

View file

@ -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;

View file

@ -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 <div>{context.router.name}</div>;
};
Child.contextTypes = { router: React.PropTypes.object };
const rendered = renderer
.create(<RouterProvider router={router}><Child /></RouterProvider>)
.toJSON();
expect(rendered.children).toContain('router in context');
});
});

View file

@ -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;

View file

@ -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 <div>{context.routes[0].name}</div>;
};
Child.contextTypes = { routes: React.PropTypes.array };
const rendered = renderer
.create(<RoutesProvider routes={routesConf}><Child /></RoutesProvider>)
.toJSON();
expect(rendered.children).toContain('SomePage');
});
});

View file

@ -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 };

230
src/routesConfiguration.js Normal file
View file

@ -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 = () => <Redirect to="/" />;
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 => <AuthenticationPage {...props} tab="login" />,
},
{
pattern: '/signup',
exactly: true,
name: 'SignUpPage',
component: props => <AuthenticationPage {...props} tab="signup" />,
},
{ 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 => <InboxPage {...props} filter="orders" />,
},
{
pattern: '/sales',
auth: true,
exactly: true,
name: 'SalesPage',
component: props => <InboxPage {...props} filter="sales" />,
},
{
pattern: '/order/:id',
auth: true,
exactly: true,
name: 'OrderPage',
component: props => <OrderPage {...props} tab="discussion" />,
routes: [
{
pattern: '/order/:id/details',
auth: true,
exactly: true,
name: 'OrderDetailsPage',
component: props => <OrderPage {...props} tab="details" />,
},
{
pattern: '/order/:id/discussion',
auth: true,
exactly: true,
name: 'OrderDiscussionPage',
component: props => <OrderPage {...props} tab="discussion" />,
},
],
},
{
pattern: '/sale/:id',
auth: true,
exactly: true,
name: 'SalePage',
component: props => <SalesConversationPage {...props} tab="discussion" />,
routes: [
{
pattern: '/sale/:id/details',
auth: true,
exactly: true,
name: 'SaleDetailsPage',
component: props => <SalesConversationPage {...props} tab="discussion" />,
},
{
pattern: '/sale/:id/discussion',
auth: true,
exactly: true,
name: 'SaleDiscussionPage',
component: props => <SalesConversationPage {...props} tab="discussion" />,
},
],
},
{
pattern: '/listings',
auth: true,
exactly: true,
name: 'ManageListingsPage',
component: ManageListingsPage,
},
{
pattern: '/account',
auth: true,
exactly: true,
name: 'AccountPage',
component: () => <Redirect to="/account/contact-details" />,
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;