diff --git a/src/components/HeroSection/HeroSection.css b/src/components/HeroSection/HeroSection.css
new file mode 100644
index 00000000..783564e2
--- /dev/null
+++ b/src/components/HeroSection/HeroSection.css
@@ -0,0 +1,37 @@
+.section {
+ width: 100%;
+ min-height: calc(100vh - 80px);
+ background-image: url('http://placehold.it/700x300');
+ background-size: cover;
+ background-position: center;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.titleWrapper {
+ text-align: center;
+}
+
+.title {
+ font-size: 2em;
+}
+.subTitle {
+ font-size: 1em;
+}
+
+.ctaWrapper {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ align-items: center;
+ width: 90vw;
+}
+
diff --git a/src/components/HeroSection/HeroSection.js b/src/components/HeroSection/HeroSection.js
new file mode 100644
index 00000000..e830d48a
--- /dev/null
+++ b/src/components/HeroSection/HeroSection.js
@@ -0,0 +1,32 @@
+import React, { PropTypes } from 'react';
+import { FormattedMessage } from 'react-intl';
+import css from './HeroSection.css';
+
+const HeroSection = props => (
+
+
+
+
+ {props.children}
+
+
+
+);
+
+HeroSection.defaultProps = { children: [] };
+
+const { any } = PropTypes;
+
+HeroSection.propTypes = { children: any };
+
+export default HeroSection;
diff --git a/src/components/HeroSection/HeroSection.test.js b/src/components/HeroSection/HeroSection.test.js
new file mode 100644
index 00000000..e1ab55f1
--- /dev/null
+++ b/src/components/HeroSection/HeroSection.test.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { TestProvider } from '../../util/test-helpers';
+import HeroSection from './HeroSection';
+
+describe('HeroSection', () => {
+ it('matches snapshot', () => {
+ const component = renderer.create(
+ (
+
+
+ test
+
+
+ ),
+ );
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/src/components/HeroSection/__snapshots__/HeroSection.test.js.snap b/src/components/HeroSection/__snapshots__/HeroSection.test.js.snap
new file mode 100644
index 00000000..4c13bbdd
--- /dev/null
+++ b/src/components/HeroSection/__snapshots__/HeroSection.test.js.snap
@@ -0,0 +1,27 @@
+exports[`HeroSection matches snapshot 1`] = `
+
+
+
+
+
+ Book Studiotime anywhere
+
+
+
+
+ The largest online community to rent music studios
+
+
+
+
+ test
+
+
+
+`;
diff --git a/src/components/NamedRedirect/NamedRedirect.js b/src/components/NamedRedirect/NamedRedirect.js
new file mode 100644
index 00000000..7e9c0f53
--- /dev/null
+++ b/src/components/NamedRedirect/NamedRedirect.js
@@ -0,0 +1,31 @@
+/**
+ * This component wraps React-Router's Redirect by providing name-based routing.
+ * This is also special component that gets routes from context.
+ * (Helps to narrow down the scope of possible format changes to routes.)
+ */
+import React, { PropTypes } from 'react';
+import { Redirect } from 'react-router';
+import { pathByRouteName } from '../../routesConfiguration';
+
+const NamedRedirect = (props, context) => {
+ const { name, params, query, hash, state, ...rest } = props;
+ const pathname = pathByRouteName(name, context.routes, params);
+ const locationDescriptor = { pathname, query, hash, state };
+ return ;
+};
+
+const { array, object, string } = PropTypes;
+
+NamedRedirect.contextTypes = { routes: array };
+
+NamedRedirect.defaultProps = { hash: '', params: {}, query: {}, state: {} };
+
+NamedRedirect.propTypes = {
+ hash: string,
+ name: string.isRequired,
+ params: object,
+ query: object,
+ state: object,
+};
+
+export default NamedRedirect;
diff --git a/src/components/index.js b/src/components/index.js
index 6629c16f..082c9dc5 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -1,6 +1,8 @@
+import HeroSection from './HeroSection/HeroSection';
import NamedLink from './NamedLink/NamedLink';
+import NamedRedirect from './NamedRedirect/NamedRedirect';
import PageLayout from './PageLayout/PageLayout';
import RouterProvider from './RouterProvider/RouterProvider';
import RoutesProvider from './RoutesProvider/RoutesProvider';
-export { NamedLink, PageLayout, RouterProvider, RoutesProvider };
+export { HeroSection, NamedLink, NamedRedirect, PageLayout, RouterProvider, RoutesProvider };
diff --git a/src/containers/HeroSearchForm/HeroSearchForm.css b/src/containers/HeroSearchForm/HeroSearchForm.css
new file mode 100644
index 00000000..c0f27222
--- /dev/null
+++ b/src/containers/HeroSearchForm/HeroSearchForm.css
@@ -0,0 +1,11 @@
+.locationInput {
+ width: 100%;
+ height: 50px;
+ background-color: white;
+}
+
+.locationButton {
+ width: 100%;
+ height: 50px;
+ margin: 0;
+}
diff --git a/src/containers/HeroSearchForm/HeroSearchForm.example.js b/src/containers/HeroSearchForm/HeroSearchForm.example.js
new file mode 100644
index 00000000..6f763010
--- /dev/null
+++ b/src/containers/HeroSearchForm/HeroSearchForm.example.js
@@ -0,0 +1,11 @@
+/* eslint-disable no-console, import/prefer-default-export */
+import HeroSearchForm from './HeroSearchForm';
+
+export const Empty = {
+ component: HeroSearchForm,
+ props: {
+ onSubmit(values) {
+ console.log('submit search query:', values);
+ },
+ },
+};
diff --git a/src/containers/HeroSearchForm/HeroSearchForm.js b/src/containers/HeroSearchForm/HeroSearchForm.js
new file mode 100644
index 00000000..19972a95
--- /dev/null
+++ b/src/containers/HeroSearchForm/HeroSearchForm.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
+import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
+import css from './HeroSearchForm.css';
+
+const HeroSearchForm = props => {
+ const { className, intl, handleSubmit, pristine, submitting } = props;
+ const addClassName = className ? { className } : {};
+ const placeholderMsg = {
+ id: 'HeroSearchForm.placeholder',
+ defaultMessage: 'Location search (soon)',
+ };
+
+ return (
+
+ );
+};
+
+HeroSearchForm.propTypes = { ...formPropTypes, intl: intlShape.isRequired };
+
+export default reduxForm({ form: 'herosearchform' })(injectIntl(HeroSearchForm))
diff --git a/src/containers/HeroSearchForm/HeroSearchForm.test.js b/src/containers/HeroSearchForm/HeroSearchForm.test.js
new file mode 100644
index 00000000..0c4f607d
--- /dev/null
+++ b/src/containers/HeroSearchForm/HeroSearchForm.test.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { Provider } from 'react-redux';
+import { injectIntl, IntlProvider } from 'react-intl';
+import configureStore from '../../store';
+import { TestProvider } from '../../util/test-helpers';
+import HeroSearchForm from './HeroSearchForm';
+
+describe('HeroSearchForm', () => {
+ it('matches snapshot', () => {
+ const store = configureStore();
+ const component = renderer.create(
+ (
+
+
+
+ ),
+ );
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/src/containers/HeroSearchForm/__snapshots__/HeroSearchForm.test.js.snap b/src/containers/HeroSearchForm/__snapshots__/HeroSearchForm.test.js.snap
new file mode 100644
index 00000000..b6c2ed38
--- /dev/null
+++ b/src/containers/HeroSearchForm/__snapshots__/HeroSearchForm.test.js.snap
@@ -0,0 +1,24 @@
+exports[`HeroSearchForm matches snapshot 1`] = `
+
+`;
diff --git a/src/containers/LandingPage/LandingPage.css b/src/containers/LandingPage/LandingPage.css
new file mode 100644
index 00000000..b2e54dc8
--- /dev/null
+++ b/src/containers/LandingPage/LandingPage.css
@@ -0,0 +1,3 @@
+.form {
+ width: 100%;
+}
diff --git a/src/containers/LandingPage/LandingPage.js b/src/containers/LandingPage/LandingPage.js
index c08581dd..8a13ec55 100644
--- a/src/containers/LandingPage/LandingPage.js
+++ b/src/containers/LandingPage/LandingPage.js
@@ -1,14 +1,46 @@
-import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { NamedLink, PageLayout } from '../../components';
+import React, { PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { HeroSection, NamedRedirect, PageLayout } from '../../components';
+import { HeroSearchForm } from '../../containers';
+import { changeLocationFilter } from '../../ducks/LocationFilter.ducks';
+import css from './LandingPage.css';
-export default () => (
-
-
-
-
-
-)
+const createSubmitHandler = onLocationChanged => formData => {
+ onLocationChanged(formData.location);
+};
+
+export const LandingPageComponent = props => {
+ const handleSubmit = createSubmitHandler(props.onLocationChanged);
+ const componentOrRedirect = props.LocationFilter && props.LocationFilter.length > 0
+ ?
+ : (
+
+
+
+
+
+ );
+
+ return componentOrRedirect;
+};
+
+const { func, string } = PropTypes;
+
+LandingPageComponent.defaultProps = { LocationFilter: '' };
+
+LandingPageComponent.propTypes = { onLocationChanged: func.isRequired, LocationFilter: string };
+
+/**
+ * Container functions.
+ * Since we add this to global store state with combineReducers, this will only get partial state
+ * which is page specific.
+ */
+const mapStateToProps = function mapStateToProps(state) {
+ return state;
+};
+
+const mapDispatchToProps = function mapDispatchToProps(dispatch) {
+ return { onLocationChanged: v => dispatch(changeLocationFilter(v)) };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(LandingPageComponent)
diff --git a/src/containers/LandingPage/LandingPage.test.js b/src/containers/LandingPage/LandingPage.test.js
index eb61d60b..4614d5b8 100644
--- a/src/containers/LandingPage/LandingPage.test.js
+++ b/src/containers/LandingPage/LandingPage.test.js
@@ -1,7 +1,7 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { TestProvider } from '../../util/test-helpers';
-import LandingPage from './LandingPage';
+import { LandingPageComponent } from './LandingPage';
import { RoutesProvider } from '../../components';
import routesConfiguration from '../../routesConfiguration';
@@ -11,7 +11,7 @@ describe('LandingPage', () => {
(
-
+ v} />
),
diff --git a/src/containers/LandingPage/__snapshots__/LandingPage.test.js.snap b/src/containers/LandingPage/__snapshots__/LandingPage.test.js.snap
index 2e55d7ee..1be63bdb 100644
--- a/src/containers/LandingPage/__snapshots__/LandingPage.test.js.snap
+++ b/src/containers/LandingPage/__snapshots__/LandingPage.test.js.snap
@@ -119,14 +119,51 @@ exports[`LandingPage matches snapshot 1`] = `
Landing page
-
-
- Show nice studios! (default)
-
-
+
+
+
+
+
+ Book Studiotime anywhere
+
+
+
+
+ The largest online community to rent music studios
+
+
+
+
+
+
+
+
`;
diff --git a/src/containers/index.js b/src/containers/index.js
index 451075e1..0942c704 100644
--- a/src/containers/index.js
+++ b/src/containers/index.js
@@ -4,6 +4,7 @@ import ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm';
import CheckoutPage from './CheckoutPage/CheckoutPage';
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
import EditProfilePage from './EditProfilePage/EditProfilePage';
+import HeroSearchForm from './HeroSearchForm/HeroSearchForm';
import InboxPage from './InboxPage/InboxPage';
import LandingPage from './LandingPage/LandingPage';
import ListingPage from './ListingPage/ListingPage';
@@ -30,6 +31,7 @@ export {
CheckoutPage,
ContactDetailsPage,
EditProfilePage,
+ HeroSearchForm,
InboxPage,
LandingPage,
ListingPage,
diff --git a/src/ducks/LocationFilter.ducks.js b/src/ducks/LocationFilter.ducks.js
new file mode 100644
index 00000000..78f259ec
--- /dev/null
+++ b/src/ducks/LocationFilter.ducks.js
@@ -0,0 +1,24 @@
+/**
+ * This file contains Action types, Action creators, and reducer of global
+ * LocationFilter. Global actions can be used in multiple pages.
+ * We are following Ducks module proposition:
+ * https://github.com/erikras/ducks-modular-redux
+ */
+
+// Actions
+export const CHANGE_LOCATION = 'app/LocationFilter/CHANGE_LOCATION';
+
+// Reducer
+export default function reducer(state = '', action = {}) {
+ const { type, payload } = action;
+ switch (type) {
+ case CHANGE_LOCATION: {
+ return payload;
+ }
+ default:
+ return state;
+ }
+}
+
+// Action types
+export const changeLocationFilter = location => ({ type: CHANGE_LOCATION, payload: location });
diff --git a/src/ducks/LocationFilter.test.js b/src/ducks/LocationFilter.test.js
new file mode 100644
index 00000000..036e1ec1
--- /dev/null
+++ b/src/ducks/LocationFilter.test.js
@@ -0,0 +1,27 @@
+import reducer, { CHANGE_LOCATION, changeLocationFilter } from './LocationFilter.ducks';
+
+describe('LocationFilterDucks', () => {
+ describe('actions', () => {
+ it('should create an action to change the filter', () => {
+ const expectedAction = { type: CHANGE_LOCATION, payload: 'helsinki' };
+ expect(changeLocationFilter('helsinki')).toEqual(expectedAction);
+ });
+ });
+
+ describe('reducer', () => {
+ it('should return the initial state', () => {
+ const initial = reducer(undefined, {});
+ expect(initial).toEqual('');
+ });
+
+ it('should handle CHANGE_LOCATION', () => {
+ const addFilter1 = changeLocationFilter('Helsinki');
+ const addFilter2 = changeLocationFilter('Espoo');
+ const reduced = reducer('', addFilter1);
+
+ const reducedWithInitialContent = reducer(addFilter1.payload, addFilter2);
+ expect(reduced).toEqual(addFilter1.payload);
+ expect(reducedWithInitialContent).toEqual(addFilter2.payload);
+ });
+ });
+});
diff --git a/src/ducks/index.js b/src/ducks/index.js
index 6d774136..7a760a4d 100644
--- a/src/ducks/index.js
+++ b/src/ducks/index.js
@@ -6,5 +6,6 @@
import { reducer as formReducer } from 'redux-form';
import FlashNotification from './FlashNotification.ducks';
+import LocationFilter from './LocationFilter.ducks';
-export { formReducer as form, FlashNotification };
+export { formReducer as form, FlashNotification, LocationFilter };
diff --git a/src/examples.js b/src/examples.js
index 8881760b..3d6bb477 100644
--- a/src/examples.js
+++ b/src/examples.js
@@ -1,5 +1,6 @@
import * as ChangeAccountPasswordForm from './containers/ChangeAccountPasswordForm/ChangeAccountPasswordForm.example';
import * as ChangePasswordForm from './containers/ChangePasswordForm/ChangePasswordForm.example';
+import * as HeroSearchForm from './containers/HeroSearchForm/HeroSearchForm.example';
import * as LoginForm from './containers/LoginForm/LoginForm.example';
import * as PasswordForgottenForm from './containers/PasswordForgottenForm/PasswordForgottenForm.example';
import * as SignUpForm from './containers/SignUpForm/SignUpForm.example';
@@ -7,6 +8,7 @@ import * as SignUpForm from './containers/SignUpForm/SignUpForm.example';
export {
ChangeAccountPasswordForm,
ChangePasswordForm,
+ HeroSearchForm,
LoginForm,
PasswordForgottenForm,
SignUpForm,
diff --git a/src/translations/en.json b/src/translations/en.json
index e8eb922f..5ce5a195 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1,3 +1,7 @@
{
- "landingpage.examplelink": "Show nice studios! (from json)"
+ "landingpage.examplelink": "Show nice studios! (from json)",
+ "HeroSearchForm.placeholder": "Location search (soon)",
+ "HeroSearchForm.search": "Search",
+ "HeroSection.title": "Book Studiotime anywhere",
+ "HeroSection.subTitle": "The largest online community to rent music studios"
}