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