From 0238dd38f8decba788a096cd5330903e3d82a4bf Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 10 Mar 2017 00:49:41 +0200 Subject: [PATCH] Add images on EditListingForm (form does not yet handle image state) --- src/components/Promised/Promised.js | 48 ++++++++ src/components/index.js | 2 + .../EditListingForm/EditListingForm.css | 58 +++++++++ .../EditListingForm.example.js | 6 +- .../EditListingForm/EditListingForm.js | 96 ++++++++++++++- .../EditListingForm/EditListingForm.test.js | 2 +- .../EditListingForm.test.js.snap | 27 +++++ .../EditListingPage/EditListingPage.duck.js | 114 +++++++++++------- .../EditListingPage/EditListingPage.js | 50 +++++--- .../EditListingPage/EditListingPage.test.js | 21 ++++ .../EditListingPage.test.js.snap | 11 ++ src/util/data.test.js | 1 + 12 files changed, 371 insertions(+), 65 deletions(-) create mode 100644 src/components/Promised/Promised.js create mode 100644 src/containers/EditListingPage/EditListingPage.test.js create mode 100644 src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap diff --git a/src/components/Promised/Promised.js b/src/components/Promised/Promised.js new file mode 100644 index 00000000..e834c711 --- /dev/null +++ b/src/components/Promised/Promised.js @@ -0,0 +1,48 @@ +/** + * Promised component makes it easier to render content that + * depends on resolution of a Promise. + * + * How to use: + *
{v}
} /> + */ + +import { Component, PropTypes } from 'react'; + +class Promised extends Component { + constructor(props) { + super(props); + + // success value is string to be more useful when rendering texts. + this.state = { + value: '', + error: null, + }; + } + + componentDidMount() { + this.props.promise + .then(value => { + this.setState({ value }); + }) + .catch(error => { + this.setState({ error }); + }); + } + + render() { + const { onError, onSuccess } = this.props; + return this.state.error ? onError(this.state.error) : onSuccess(this.state.value); + } +} + +Promised.defaultProps = { onError: e => e }; + +const { func, object } = PropTypes; + +Promised.propTypes = { + promise: object.isRequired, + onSuccess: func.isRequired, + onError: func, +}; + +export default Promised; diff --git a/src/components/index.js b/src/components/index.js index b450a3a4..940f786c 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -11,6 +11,7 @@ import NamedRedirect from './NamedRedirect/NamedRedirect'; import OrderDetailsPanel from './OrderDetailsPanel/OrderDetailsPanel'; import OrderDiscussionPanel from './OrderDiscussionPanel/OrderDiscussionPanel'; import PageLayout from './PageLayout/PageLayout'; +import Promised from './Promised/Promised'; import RoutesProvider from './RoutesProvider/RoutesProvider'; import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel'; @@ -28,6 +29,7 @@ export { OrderDetailsPanel, OrderDiscussionPanel, PageLayout, + Promised, RoutesProvider, SearchResultsPanel, }; diff --git a/src/containers/EditListingForm/EditListingForm.css b/src/containers/EditListingForm/EditListingForm.css index ee7dba23..dcbb04ed 100644 --- a/src/containers/EditListingForm/EditListingForm.css +++ b/src/containers/EditListingForm/EditListingForm.css @@ -1,3 +1,61 @@ .error { color: red; } + +.imagesContainer { + width: 100%; + min-height: 150px; +} + +.addImageWrapper { + float: left; + width: 110px; + height: 110px; + margin: 5px; + + &::after { + content: "."; + visibility: hidden; + display: block; + height: 0; + clear: both; + } +} + +.addImage { + width: 110px; + height: 110px; + background-color: lightgrey; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + margin-top: 0; +} + +.thumbnail { + float: left; + position: relative; + width: 110px; + height: 110px; + margin: 5px; + overflow: hidden; +} + +.thumbnailImage { + width: 100%; + height: 100%; + object-fit: cover; +} + +.thumbnailLoading { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(211, 211, 211, 0.8); + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/containers/EditListingForm/EditListingForm.example.js b/src/containers/EditListingForm/EditListingForm.example.js index b6300b72..91d193de 100644 --- a/src/containers/EditListingForm/EditListingForm.example.js +++ b/src/containers/EditListingForm/EditListingForm.example.js @@ -4,8 +4,12 @@ import EditListingForm from './EditListingForm'; export const Empty = { component: EditListingForm, props: { + images: [], + onImageUpload(values) { + console.log(`onImageUpload with id (${values.id}) and file name (${values.file.name})`); + }, onSubmit(values) { - console.log('submit new password form values:', values); + console.log('Submit EditListingForm with (unformatted) values:', values); }, }, }; diff --git a/src/containers/EditListingForm/EditListingForm.js b/src/containers/EditListingForm/EditListingForm.js index 16c84f3a..ea431edf 100644 --- a/src/containers/EditListingForm/EditListingForm.js +++ b/src/containers/EditListingForm/EditListingForm.js @@ -2,10 +2,23 @@ import React, { Component, PropTypes } from 'react'; import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form'; import { intlShape, injectIntl } from 'react-intl'; import { maxLength, required } from '../../util/validators'; +import { Promised } from '../../components'; import css from './EditListingForm.css'; -const MAX_LENGTH = 60; +const ACCEPT_IMAGES = 'image/*'; +const TITLE_MAX_LENGTH = 60; +// readImage returns a promise which is resolved +// when FileReader has loaded given file as dataURL +const readImage = file => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => resolve(e.target.result); + reader.onerror = e => reject(new Error(`Error reading ${file.name}: ${e.target.result}`)); + reader.readAsDataURL(file); +}); + + +// Custom inputs with validator messages const RenderField = ({ input, label, type, meta: { touched, error } }) => { const component = type === 'textarea' ?