mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
Add routes configuration
This commit is contained in:
parent
c0367780b6
commit
327ea45b80
8 changed files with 370 additions and 170 deletions
222
src/Routes.js
222
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 = () => <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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
21
src/components/RouterProvider/RouterProvider.js
Normal file
21
src/components/RouterProvider/RouterProvider.js
Normal 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;
|
||||
18
src/components/RouterProvider/RouterProvider.test.js
Normal file
18
src/components/RouterProvider/RouterProvider.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
21
src/components/RoutesProvider/RoutesProvider.js
Normal file
21
src/components/RoutesProvider/RoutesProvider.js
Normal 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;
|
||||
18
src/components/RoutesProvider/RoutesProvider.test.js
Normal file
18
src/components/RoutesProvider/RoutesProvider.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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
230
src/routesConfiguration.js
Normal 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;
|
||||
Loading…
Add table
Reference in a new issue