Merge pull request #15 from sharetribe/routes-as-config

Routes in separate config
This commit is contained in:
Vesa Luusua 2017-01-24 19:13:31 +02:00 committed by GitHub
commit fd86a9b3db
46 changed files with 850 additions and 1316 deletions

View file

@ -9,6 +9,7 @@
"helmet": "^3.3.0",
"lodash": "^4.17.4",
"qs": "^6.3.0",
"path-to-regexp": "^1.5.3",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-helmet": "^4.0.0",

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

@ -25,7 +25,7 @@ describe('Application', () => {
const urlTitles = {
'/': 'Landing page',
'/s': 'Search page',
'/l/1234': 'Listing page with listing id: #1234',
'/l/listing-title-slug/1234': 'Listing page with listing id: #1234',
'/u/1234': 'Profile page with display name: 1234',
'/checkout/1234': 'Checkout page: 1234',
'/login': 'Authentication page: login tab',
@ -41,10 +41,8 @@ describe('Application', () => {
});
const urlRedirects = {
'/inbox': '/login',
'/orders': '/login',
'/sales': '/login',
'/conversation/1234': '/login',
'/order/1234': '/login',
'/order/1234/discussion': '/login',
'/order/1234/details': '/login',
@ -54,8 +52,6 @@ describe('Application', () => {
'/listings': '/login',
'/account': '/login',
'/account/contact-details': '/login',
'/account/notifications': '/login',
'/account/payment-methods': '/login',
'/account/payout-preferences': '/login',
'/account/security': '/login',
};

View file

@ -0,0 +1,20 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { pathByRouteName } from '../../routesConfiguration';
const NamedLink = (props, context) => {
const { name, params, ...rest } = props;
const path = pathByRouteName(name, context.routes, params);
return <Link to={path} {...rest} />;
};
const { array, object, string } = PropTypes;
NamedLink.contextTypes = { routes: array };
NamedLink.defaultProps = { params: {} };
NamedLink.propTypes = { name: string.isRequired, params: object };
export default NamedLink;

View file

@ -0,0 +1,28 @@
import React from 'react';
import { BrowserRouter } from 'react-router';
import { RoutesProvider } from '../index';
import Routes from '../../Routes';
import NamedLink from './NamedLink';
import renderer from 'react-test-renderer';
describe('NamedLink', () => {
it('should contain correct link', () => {
const id = 12;
const routesConf = [
{ pattern: '/somepage/:id', name: 'SomePage', component: () => <div>blaa</div> },
];
const component = renderer.create(
(
<BrowserRouter>
<RoutesProvider routes={routesConf}>
<NamedLink name="SomePage" params={{ id }}>to SomePage</NamedLink>
</RoutesProvider>
</BrowserRouter>
),
);
const json = component.toJSON();
expect(json.type).toEqual('a');
expect(json.props.href).toEqual(`/somepage/${id}`);
expect(json.children).toEqual([ 'to SomePage' ]);
});
});

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

View file

@ -19,7 +19,7 @@ exports[`AuthenticationPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`AuthenticationPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`CheckoutPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`CheckoutPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -1,17 +0,0 @@
import React, { PropTypes } from 'react';
import { PageLayout } from '../../components';
const ConversationPage = props => {
const { params } = props;
return (
<PageLayout title="Conversation page">
<p>Conversation id: {params.id}</p>
</PageLayout>
);
};
const { shape, string } = PropTypes;
ConversationPage.propTypes = { params: shape({ id: string.isRequired }).isRequired };
export default ConversationPage;

View file

@ -1,18 +0,0 @@
import React from 'react';
import { BrowserRouter } from 'react-router';
import renderer from 'react-test-renderer';
import ConversationPage from './ConversationPage';
describe('ConversationPage', () => {
it('matches snapshot', () => {
const component = renderer.create(
(
<BrowserRouter>
<ConversationPage params={{ id: 'some-conversation-id' }} />
</BrowserRouter>
),
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,145 +0,0 @@
exports[`ConversationPage matches snapshot 1`] = `
<div
className="">
<div
className={undefined}>
<a
className=""
href="/"
onClick={[Function]}
style={Object {}}>
Home
</a>
<a
className=""
href="/s"
onClick={[Function]}
style={Object {}}>
Search
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
onClick={[Function]}
style={Object {}}>
Listing page
</a>
<a
className=""
href="/u/Bikerrs"
onClick={[Function]}
style={Object {}}>
Profile
</a>
<a
className=""
href="/u/Bikerrs/edit"
onClick={[Function]}
style={Object {}}>
Edit profile
</a>
<a
className=""
href="/checkout/lid1234"
onClick={[Function]}
style={Object {}}>
Checkout
</a>
<a
className=""
href="/inbox"
onClick={[Function]}
style={Object {}}>
Inbox
</a>
<a
className=""
href="/orders"
onClick={[Function]}
style={Object {}}>
Orders
</a>
<a
className=""
href="/sales"
onClick={[Function]}
style={Object {}}>
Sales
</a>
<a
className=""
href="/password/forgotten"
onClick={[Function]}
style={Object {}}>
Request password
</a>
<a
className=""
href="/password/change"
onClick={[Function]}
style={Object {}}>
Change password
</a>
<a
className=""
href="/listings"
onClick={[Function]}
style={Object {}}>
Manage listings
</a>
<a
className=""
href="/account"
onClick={[Function]}
style={Object {}}>
Account settings
</a>
<a
className=""
href="/account/contact-details"
onClick={[Function]}
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"
onClick={[Function]}
style={Object {}}>
Payout preferences
</a>
<a
className=""
href="/account/security"
onClick={[Function]}
style={Object {}}>
Security
</a>
<button
onClick={[Function]}>
Sign out
</button>
</div>
<h1>
Conversation page
</h1>
<p>
Conversation id:
some-conversation-id
</p>
</div>
`;

View file

@ -19,7 +19,7 @@ exports[`EditProfilePage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`EditProfilePage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`InboxPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`InboxPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`LandingPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`LandingPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -2,21 +2,12 @@ import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import { PageLayout } from '../../components';
const ListingPage = ({ params }) => {
// Listing id should be located either in the end of slug
// - https://example.com/l/listing-title-as-slug-12345
// - https://example.com/l/12345
const slugAndId = params.slug.split('-');
const id = slugAndId[slugAndId.length - 1];
// TODO: Fetch data from SDK if no data is passed through props
return (
<PageLayout title={`Listing page with listing id: #${id}`}>
<p>Slug: {params.slug}</p>
<Link to={`/checkout/${id}`}><button>Book</button></Link>
</PageLayout>
);
};
const ListingPage = ({ params }) => (
<PageLayout title={`Listing page with listing id: #${params.id}`}>
<p>Slug: {params.slug}</p>
<Link to={`/checkout/${params.id}`}><button>Book</button></Link>
</PageLayout>
);
const { shape, string } = PropTypes;

View file

@ -8,7 +8,7 @@ describe('ListingPage', () => {
const component = renderer.create(
(
<BrowserRouter>
<ListingPage params={{ slug: 'really-nice-house-1234' }} />
<ListingPage params={{ slug: 'really-nice-house-1234', id: 1234 }} />
</BrowserRouter>
),
);

View file

@ -19,7 +19,7 @@ exports[`ListingPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`ListingPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`ManageListingsPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`ManageListingsPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`NotFoundPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`NotFoundPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -1,4 +0,0 @@
import React from 'react';
import { PageLayout } from '../../components';
export default () => <PageLayout title="Notification settings" />

View file

@ -1,18 +0,0 @@
import React from 'react';
import { BrowserRouter } from 'react-router';
import renderer from 'react-test-renderer';
import NotificationSettingsPage from './NotificationSettingsPage';
describe('NotificationSettingsPage', () => {
it('matches snapshot', () => {
const component = renderer.create(
(
<BrowserRouter>
<NotificationSettingsPage />
</BrowserRouter>
),
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,141 +0,0 @@
exports[`NotificationSettingsPage matches snapshot 1`] = `
<div
className="">
<div
className={undefined}>
<a
className=""
href="/"
onClick={[Function]}
style={Object {}}>
Home
</a>
<a
className=""
href="/s"
onClick={[Function]}
style={Object {}}>
Search
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
onClick={[Function]}
style={Object {}}>
Listing page
</a>
<a
className=""
href="/u/Bikerrs"
onClick={[Function]}
style={Object {}}>
Profile
</a>
<a
className=""
href="/u/Bikerrs/edit"
onClick={[Function]}
style={Object {}}>
Edit profile
</a>
<a
className=""
href="/checkout/lid1234"
onClick={[Function]}
style={Object {}}>
Checkout
</a>
<a
className=""
href="/inbox"
onClick={[Function]}
style={Object {}}>
Inbox
</a>
<a
className=""
href="/orders"
onClick={[Function]}
style={Object {}}>
Orders
</a>
<a
className=""
href="/sales"
onClick={[Function]}
style={Object {}}>
Sales
</a>
<a
className=""
href="/password/forgotten"
onClick={[Function]}
style={Object {}}>
Request password
</a>
<a
className=""
href="/password/change"
onClick={[Function]}
style={Object {}}>
Change password
</a>
<a
className=""
href="/listings"
onClick={[Function]}
style={Object {}}>
Manage listings
</a>
<a
className=""
href="/account"
onClick={[Function]}
style={Object {}}>
Account settings
</a>
<a
className=""
href="/account/contact-details"
onClick={[Function]}
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"
onClick={[Function]}
style={Object {}}>
Payout preferences
</a>
<a
className=""
href="/account/security"
onClick={[Function]}
style={Object {}}>
Security
</a>
<button
onClick={[Function]}>
Sign out
</button>
</div>
<h1>
Notification settings
</h1>
</div>
`;

View file

@ -19,7 +19,7 @@ exports[`OrderPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`OrderPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`PasswordChangePage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`PasswordChangePage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`PasswordForgottenPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`PasswordForgottenPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -1,4 +0,0 @@
import React from 'react';
import { PageLayout } from '../../components';
export default () => <PageLayout title="Payment methods" />

View file

@ -1,18 +0,0 @@
import React from 'react';
import { BrowserRouter } from 'react-router';
import renderer from 'react-test-renderer';
import PaymentMethodsPage from './PaymentMethodsPage';
describe('PaymentMethodsPage', () => {
it('matches snapshot', () => {
const component = renderer.create(
(
<BrowserRouter>
<PaymentMethodsPage />
</BrowserRouter>
),
);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View file

@ -1,141 +0,0 @@
exports[`PaymentMethodsPage matches snapshot 1`] = `
<div
className="">
<div
className={undefined}>
<a
className=""
href="/"
onClick={[Function]}
style={Object {}}>
Home
</a>
<a
className=""
href="/s"
onClick={[Function]}
style={Object {}}>
Search
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
onClick={[Function]}
style={Object {}}>
Listing page
</a>
<a
className=""
href="/u/Bikerrs"
onClick={[Function]}
style={Object {}}>
Profile
</a>
<a
className=""
href="/u/Bikerrs/edit"
onClick={[Function]}
style={Object {}}>
Edit profile
</a>
<a
className=""
href="/checkout/lid1234"
onClick={[Function]}
style={Object {}}>
Checkout
</a>
<a
className=""
href="/inbox"
onClick={[Function]}
style={Object {}}>
Inbox
</a>
<a
className=""
href="/orders"
onClick={[Function]}
style={Object {}}>
Orders
</a>
<a
className=""
href="/sales"
onClick={[Function]}
style={Object {}}>
Sales
</a>
<a
className=""
href="/password/forgotten"
onClick={[Function]}
style={Object {}}>
Request password
</a>
<a
className=""
href="/password/change"
onClick={[Function]}
style={Object {}}>
Change password
</a>
<a
className=""
href="/listings"
onClick={[Function]}
style={Object {}}>
Manage listings
</a>
<a
className=""
href="/account"
onClick={[Function]}
style={Object {}}>
Account settings
</a>
<a
className=""
href="/account/contact-details"
onClick={[Function]}
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"
onClick={[Function]}
style={Object {}}>
Payout preferences
</a>
<a
className=""
href="/account/security"
onClick={[Function]}
style={Object {}}>
Security
</a>
<button
onClick={[Function]}>
Sign out
</button>
</div>
<h1>
Payment methods
</h1>
</div>
`;

View file

@ -19,7 +19,7 @@ exports[`PayoutPreferencesPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`PayoutPreferencesPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`ProfilePage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`ProfilePage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -19,7 +19,7 @@ exports[`SalesConversationPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`SalesConversationPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -1,13 +1,16 @@
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { PageLayout } from '../../components';
import { NamedLink, PageLayout } from '../../components';
import { addFlashNotification } from '../../ducks/FlashNotification.ducks';
import { addFilter } from './SearchPage.ducks';
export const SearchPageComponent = () => (
<PageLayout title="Search page">
<Link to="/l/Nice+studio-in-Helsinki-345">Nice studio in Helsinki</Link>
<NamedLink name="ListingPage" params={{ slug: 'Nice-studio-in-Helsinki', id: 345 }}>
Nice studio in Helsinki
</NamedLink>
<br />
<NamedLink name="LandingPage">LandingPage</NamedLink>
</PageLayout>
);

View file

@ -3,13 +3,17 @@ import { BrowserRouter } from 'react-router';
import renderer from 'react-test-renderer';
import { SearchPageComponent } from './SearchPage';
import reducer, { ADD_FILTER, addFilter } from './SearchPage.ducks';
import { RoutesProvider } from '../../components';
import routesConfiguration from '../../routesConfiguration';
describe('SearchPageComponent', () => {
it('matches snapshot', () => {
const component = renderer.create(
(
<BrowserRouter>
<SearchPageComponent />
<RoutesProvider routes={routesConfiguration}>
<SearchPageComponent />
</RoutesProvider>
</BrowserRouter>
),
);
@ -38,7 +42,9 @@ describe('SearchPageDucs', () => {
const reduced = reducer([], addFilter1);
const reducedWithInitialContent = reducer({ filters: [ addFilter1.payload ] }, addFilter2);
expect(reduced).toEqual({ filters: [ addFilter1.payload ] });
expect(reducedWithInitialContent).toEqual({ filters: [ addFilter1.payload, addFilter2.payload ] });
expect(
reducedWithInitialContent,
).toEqual({ filters: [ addFilter1.payload, addFilter2.payload ] });
});
it('should handle duplicates ADD_FILTER', () => {

View file

@ -19,7 +19,7 @@ exports[`SearchPageComponent matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`SearchPageComponent matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"
@ -139,10 +125,18 @@ exports[`SearchPageComponent matches snapshot 1`] = `
</h1>
<a
className=""
href="/l/Nice+studio-in-Helsinki-345"
href="/l/Nice-studio-in-Helsinki/345"
onClick={[Function]}
style={Object {}}>
Nice studio in Helsinki
</a>
<br />
<a
className=""
href="/"
onClick={[Function]}
style={Object {}}>
LandingPage
</a>
</div>
`;

View file

@ -19,7 +19,7 @@ exports[`SecurityPage matches snapshot 1`] = `
</a>
<a
className=""
href="/l/Bike-Pelago-12345"
href="/l/Bike-Pelago/12345"
onClick={[Function]}
style={Object {}}>
Listing page
@ -101,20 +101,6 @@ exports[`SecurityPage matches snapshot 1`] = `
style={Object {}}>
Contact details
</a>
<a
className=""
href="/account/notifications"
onClick={[Function]}
style={Object {}}>
Notification settings
</a>
<a
className=""
href="/account/payment-methods"
onClick={[Function]}
style={Object {}}>
Payment methods
</a>
<a
className=""
href="/account/payout-preferences"

View file

@ -24,7 +24,7 @@ const Topbar = (props, context) => {
<div className={css.container}>
<Link to="/" {...linkProps}>Home</Link>
<Link to="/s" {...linkProps}>Search</Link>
<Link to="/l/Bike-Pelago-12345" {...linkProps}>Listing page</Link>
<Link to="/l/Bike-Pelago/12345" {...linkProps}>Listing page</Link>
<Link to="/u/Bikerrs" {...linkProps}>Profile</Link>
<Link to="/u/Bikerrs/edit" {...linkProps}>Edit profile</Link>
<Link to="/checkout/lid1234" {...linkProps}>Checkout</Link>
@ -36,8 +36,6 @@ const Topbar = (props, context) => {
<Link to="/listings" {...linkProps}>Manage listings</Link>
<Link to="/account" {...linkProps}>Account settings</Link>
<Link to="/account/contact-details" {...linkProps}>Contact details</Link>
<Link to="/account/notifications" {...linkProps}>Notification settings</Link>
<Link to="/account/payment-methods" {...linkProps}>Payment methods</Link>
<Link to="/account/payout-preferences" {...linkProps}>Payout preferences</Link>
<Link to="/account/security" {...linkProps}>Security</Link>
<SignoutButton router={context.router} />

View file

@ -1,17 +1,14 @@
import AuthenticationPage from './AuthenticationPage/AuthenticationPage';
import CheckoutPage from './CheckoutPage/CheckoutPage';
import ConversationPage from './ConversationPage/ConversationPage';
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
import EditProfilePage from './EditProfilePage/EditProfilePage';
import InboxPage from './InboxPage/InboxPage';
import LandingPage from './LandingPage/LandingPage';
import ListingPage from './ListingPage/ListingPage';
import ManageListingsPage from './ManageListingsPage/ManageListingsPage';
import NotificationSettingsPage from './NotificationSettingsPage/NotificationSettingsPage';
import OrderPage from './OrderPage/OrderPage';
import PasswordChangePage from './PasswordChangePage/PasswordChangePage';
import PasswordForgottenPage from './PasswordForgottenPage/PasswordForgottenPage';
import PaymentMethodsPage from './PaymentMethodsPage/PaymentMethodsPage';
import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage';
import ProfilePage from './ProfilePage/ProfilePage';
import SalesConversationPage from './SalesConversationPage/SalesConversationPage';
@ -23,7 +20,6 @@ import Topbar from './Topbar/Topbar';
export {
AuthenticationPage,
CheckoutPage,
ConversationPage,
ContactDetailsPage,
EditProfilePage,
InboxPage,
@ -31,11 +27,9 @@ export {
ListingPage,
ManageListingsPage,
NotFoundPage,
NotificationSettingsPage,
OrderPage,
PasswordChangePage,
PasswordForgottenPage,
PaymentMethodsPage,
PayoutPreferencesPage,
ProfilePage,
SalesConversationPage,

View file

@ -20,7 +20,8 @@ import './index.css';
// If we're in a browser already, render the client application.
if (typeof window !== 'undefined') {
const preloadedState = window.__PRELOADED_STATE__ || {}; // eslint-disable-line no-underscore-dangle
// eslint-disable-next-line no-underscore-dangle
const preloadedState = window.__PRELOADED_STATE__ || {};
const store = configureStore(preloadedState);
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));

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;

728
yarn.lock

File diff suppressed because it is too large Load diff