Merge pull request #74 from sharetribe/new-listing-location

Add location field to new listing form
This commit is contained in:
Kimmo Puputti 2017-03-17 14:31:24 +02:00 committed by GitHub
commit 17cfe340d8
9 changed files with 108 additions and 37 deletions

View file

@ -8,6 +8,7 @@
.predictions {
position: absolute;
margin: 0;
top: 30px;
width: 100%;
background-color: #fff;

View file

@ -5,6 +5,7 @@ import FilterPanel from './FilterPanel/FilterPanel';
import HeroSection from './HeroSection/HeroSection';
import ListingCard from './ListingCard/ListingCard';
import ListingCardSmall from './ListingCardSmall/ListingCardSmall';
import LocationAutocompleteInput from './LocationAutocompleteInput/LocationAutocompleteInput';
import MapPanel from './MapPanel/MapPanel';
import Menu from './Menu/Menu';
import NamedLink from './NamedLink/NamedLink';
@ -24,6 +25,7 @@ export {
HeroSection,
ListingCard,
ListingCardSmall,
LocationAutocompleteInput,
MapPanel,
Menu,
NamedLink,

View file

@ -3,43 +3,66 @@ import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import { isEqual } from 'lodash';
import { arrayMove } from 'react-sortable-hoc';
import { noEmptyArray, maxLength, required } from '../../util/validators';
import { AddImages } from '../../components';
import {
noEmptyArray,
maxLength,
required,
autocompleteSearchRequired,
autocompletePlaceSelected,
} from '../../util/validators';
import { AddImages, LocationAutocompleteInput } from '../../components';
import css from './EditListingForm.css';
const ACCEPT_IMAGES = 'image/*';
const TITLE_MAX_LENGTH = 60;
// Custom inputs with validator messages
const RenderField = ({ input, label, type, meta }) => {
const { touched, error } = meta;
const component = type === 'textarea'
? <textarea {...input} placeholder={label} />
: <input {...input} placeholder={label} type={type} />;
return (
<div>
<label htmlFor={input.name}>{label}</label>
const { bool, func, object, shape, string } = PropTypes;
/**
* Hoc to convert a component used within a Field to one that renders
* a label and possible errors.
*
* @param {Component|String} Comp component or a String that is used
* to create an input element
*
* @return {Component} new component that wraps the given one with a
* label and possible errors
*/
const enhancedField = Comp => {
const EnhancedField = props => {
const { input, label, meta } = props;
const { touched, error } = meta;
let component = null;
if (typeof Comp !== 'string') {
component = <Comp {...props} />;
} else if (Comp === 'textarea') {
component = <textarea {...input} placeholder={label} />;
} else {
component = <input {...input} placeholder={label} type={Comp} />;
}
return (
<div>
<label htmlFor={input.name}>{label}</label>
{component}
{touched && error ? <span className={css.error}>{error}</span> : null}
</div>
</div>
);
};
);
};
EnhancedField.displayName = `EnhancedField(${Comp.displayName || Comp.name})`;
const { bool, func, object, shape, string } = PropTypes;
EnhancedField.propTypes = {
input: shape({
onChange: func.isRequired,
name: string.isRequired,
}).isRequired,
label: string.isRequired,
meta: shape({
touched: bool,
error: string,
}).isRequired,
};
RenderField.propTypes = {
input: shape({
onChange: func.isRequired,
name: string.isRequired,
}).isRequired,
label: string.isRequired,
type: string.isRequired,
meta: shape({
touched: bool,
error: string,
}).isRequired,
return EnhancedField;
};
// Add image wrapper. Label is the only visible element, file input is hidden.
@ -120,14 +143,19 @@ class EditListingForm extends Component {
);
const maxLength60 = maxLength(maxLengthStr, TITLE_MAX_LENGTH);
const imageRequiredStr = intl.formatMessage({ id: 'EditListingForm.imageRequired' });
const locationRequiredMessage = intl.formatMessage({
id: 'EditListingForm.locationRequired',
});
const locationNotRecognizedMessage = intl.formatMessage({
id: 'EditListingForm.locationNotRecognized',
});
return (
<form onSubmit={handleSubmit}>
<Field
name="title"
label="Title"
component={RenderField}
type="text"
component={enhancedField('text')}
validate={[required(requiredStr), maxLength60]}
/>
@ -160,11 +188,21 @@ class EditListingForm extends Component {
/>
</AddImages>
<Field
name="location"
label="Location"
format={null}
component={enhancedField(LocationAutocompleteInput)}
validate={[
autocompleteSearchRequired(locationRequiredMessage),
autocompletePlaceSelected(locationNotRecognizedMessage),
]}
/>
<Field
name="description"
label="Description"
component={RenderField}
type="textarea"
component={enhancedField('textarea')}
validate={[required(requiredStr)]}
/>
<button type="submit" disabled={pristine || submitting || disabled}>{saveActionMsg}</button>

View file

@ -14,14 +14,13 @@ import {
} from './EditListingPage.duck';
const formatRequestData = values => {
const { description, images, title } = values;
const { description, images, title, location } = values;
const { selectedPlace: { address, origin } } = location;
return {
address: 'Bulevardi 14, 00200 Helsinki, Finland',
address,
description,
geolocation: {
lat: 40.6,
lng: 73.9,
},
geolocation: origin,
images: images.map(i => i.imageId),
title,
};

View file

@ -30,7 +30,8 @@
height: 100%;
}
.description {
.description,
.address {
margin: 2rem;
}

View file

@ -56,6 +56,8 @@ export class ListingPageComponent extends Component {
const title = currentListing ? currentListing.attributes.title : '';
const description = currentListing ? currentListing.attributes.description : '';
const address = currentListing ? currentListing.attributes.address : '';
const geolocation = currentListing ? currentListing.attributes.geolocation : null;
// TODO Responsive image-objects need to be thought through when final image sizes are know
const images = currentListing && currentListing.images
@ -88,6 +90,9 @@ export class ListingPageComponent extends Component {
{/* eslint-disable react/no-danger */}
<div className={css.description} dangerouslySetInnerHTML={{ __html: description }} />
{/* eslint-enable react/no-danger */}
<div className={css.address}>
{address} {geolocation ? `(${geolocation.lat}, ${geolocation.lng})` : null}
</div>
<div className={css.filterSection}>
<h1>Here will be filters (or dragons)</h1>
<h2>Studio type</h2>

View file

@ -7,6 +7,11 @@ exports[`ListingPage matches snapshot 1`] = `
"__html": "listing1 description",
}
} />
<div>
listing1 address
(40, 60)
</div>
<div>
<h1>
Here will be filters (or dragons)

View file

@ -8,6 +8,8 @@
"EditListingForm.required": "Required",
"EditListingForm.imageRequired": "You need to add at least one image.",
"EditListingForm.maxLength": "Must be {maxLength} characters or less",
"EditListingForm.locationRequired": "You need to provide a location",
"EditListingForm.locationNotRecognized": "We didn't recognize this location. Please try another location.",
"ListingPage.loadingListingData": "Loading listing data",
"ListingPage.noListingData": "Could not find listing data",
"PageLayout.authInfoFailed": "Could not get authentication information.",

View file

@ -1,3 +1,7 @@
import { types } from 'sharetribe-sdk';
const { LatLng } = types;
/**
* Validator functions and helpers for Redux Forms
*/
@ -19,3 +23,17 @@ export const noEmptyArray = message =>
value => {
return value && Array.isArray(value) && value.length > 0 ? VALID : message;
};
export const autocompleteSearchRequired = message =>
value => {
return value && value.search ? VALID : message;
};
export const autocompletePlaceSelected = message =>
value => {
const selectedPlaceIsValid = value &&
value.selectedPlace &&
value.selectedPlace.address &&
value.selectedPlace.origin instanceof LatLng;
return selectedPlaceIsValid ? VALID : message;
};