mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 12:43:11 +10:00
EditListingPhotosPanel and form within
This commit is contained in:
parent
dcfac16948
commit
73d98b5689
11 changed files with 368 additions and 0 deletions
|
|
@ -0,0 +1,6 @@
|
|||
.root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { EditListingPhotosForm } from '../../containers';
|
||||
|
||||
import css from './EditListingPhotosPanel.css';
|
||||
|
||||
const EditListingPhotosPanel = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
images,
|
||||
onImageUpload,
|
||||
onSubmit,
|
||||
onUpdateImageOrder,
|
||||
stripeConnected,
|
||||
} = props;
|
||||
|
||||
const rootClass = rootClassName || css.root;
|
||||
const classes = classNames(rootClass, className);
|
||||
|
||||
// TODO This will be defined in the stripe popup form later.
|
||||
const country = 'US';
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<h1><FormattedMessage id="EditListingPhotosPanel.title" /></h1>
|
||||
<EditListingPhotosForm
|
||||
initialValues={{ country, images }}
|
||||
images={images}
|
||||
onImageUpload={onImageUpload}
|
||||
onSubmit={onSubmit}
|
||||
onUpdateImageOrder={onUpdateImageOrder}
|
||||
stripeConnected={stripeConnected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { array, bool, func, string } = PropTypes;
|
||||
|
||||
EditListingPhotosPanel.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
images: [],
|
||||
};
|
||||
|
||||
EditListingPhotosPanel.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
images: array,
|
||||
onImageUpload: func.isRequired,
|
||||
onUpdateImageOrder: func.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
stripeConnected: bool.isRequired,
|
||||
};
|
||||
|
||||
export default EditListingPhotosPanel;
|
||||
|
|
@ -9,6 +9,7 @@ import DateInput from './DateInput/DateInput';
|
|||
import Discussion from './Discussion/Discussion';
|
||||
import EditListingDescriptionPanel from './EditListingDescriptionPanel/EditListingDescriptionPanel';
|
||||
import EditListingLocationPanel from './EditListingLocationPanel/EditListingLocationPanel';
|
||||
import EditListingPhotosPanel from './EditListingPhotosPanel/EditListingPhotosPanel';
|
||||
import EditListingPricingPanel from './EditListingPricingPanel/EditListingPricingPanel';
|
||||
import EditListingWizard from './EditListingWizard/EditListingWizard';
|
||||
import ExternalLink from './ExternalLink/ExternalLink';
|
||||
|
|
@ -53,6 +54,7 @@ export {
|
|||
Discussion,
|
||||
EditListingDescriptionPanel,
|
||||
EditListingLocationPanel,
|
||||
EditListingPhotosPanel,
|
||||
EditListingPricingPanel,
|
||||
EditListingWizard,
|
||||
ExternalLink,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
.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;
|
||||
}
|
||||
|
||||
.addImageInput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.imageRequiredWrapper {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
}
|
||||
.imageRequiredError {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/* eslint-disable no-console */
|
||||
import EditListingPhotosForm from './EditListingPhotosForm';
|
||||
|
||||
export const Empty = {
|
||||
component: EditListingPhotosForm,
|
||||
props: {
|
||||
initialValues: { country: 'US', images: [] },
|
||||
stripeConnected: false,
|
||||
onImageUpload: values => {
|
||||
console.log(`onImageUpload with id (${values.id}) and file name (${values.file.name})`);
|
||||
},
|
||||
onSubmit: values => {
|
||||
console.log('Submit EditListingPhotosForm with (unformatted) values:', values);
|
||||
},
|
||||
onUpdateImageOrder: imageOrder => {
|
||||
console.log('onUpdateImageOrder with new imageOrder:', imageOrder);
|
||||
},
|
||||
},
|
||||
};
|
||||
160
src/containers/EditListingPhotosForm/EditListingPhotosForm.js
Normal file
160
src/containers/EditListingPhotosForm/EditListingPhotosForm.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
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 config from '../../config';
|
||||
import { noEmptyArray, required } from '../../util/validators';
|
||||
import {
|
||||
AddImages,
|
||||
Button,
|
||||
Input,
|
||||
StripeBankAccountToken,
|
||||
ValidationError
|
||||
} from '../../components';
|
||||
|
||||
import css from './EditListingPhotosForm.css';
|
||||
|
||||
const ACCEPT_IMAGES = 'image/*';
|
||||
|
||||
// Add image wrapper. Label is the only visible element, file input is hidden.
|
||||
const RenderAddImage = props => {
|
||||
const { accept, input, label, type } = props;
|
||||
const { name, onChange } = input;
|
||||
const inputProps = { accept, id: name, name, onChange, type };
|
||||
return (
|
||||
<div className={css.addImageWrapper}>
|
||||
<Input {...inputProps} className={css.addImageInput} />
|
||||
<label htmlFor={name} className={css.addImage}>{label}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { bool, func, object, shape, string } = PropTypes;
|
||||
|
||||
RenderAddImage.propTypes = {
|
||||
accept: string.isRequired,
|
||||
input: shape({
|
||||
value: object,
|
||||
onChange: func.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
label: string.isRequired,
|
||||
type: string.isRequired,
|
||||
};
|
||||
|
||||
export class EditListingPhotosFormComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onImageUploadHandler = this.onImageUploadHandler.bind(this);
|
||||
this.onSortEnd = this.onSortEnd.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!isEqual(this.props.images, nextProps.images)) {
|
||||
nextProps.change('images', nextProps.images);
|
||||
}
|
||||
}
|
||||
|
||||
onImageUploadHandler(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.props.onImageUpload({ id: `${file.name}_${Date.now()}`, file });
|
||||
}
|
||||
}
|
||||
|
||||
onSortEnd({ oldIndex, newIndex }) {
|
||||
const images = arrayMove(this.props.images, oldIndex, newIndex);
|
||||
this.props.onUpdateImageOrder(images.map(i => i.id));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
disabled,
|
||||
handleSubmit,
|
||||
images,
|
||||
intl,
|
||||
invalid,
|
||||
saveActionMsg,
|
||||
submitting,
|
||||
stripeConnected,
|
||||
} = this.props;
|
||||
|
||||
const imageRequiredMessage = intl.formatMessage({ id: 'EditListingPhotosForm.imageRequired' });
|
||||
const bankAccountNumberRequiredMessage = intl.formatMessage({
|
||||
id: 'EditListingPhotosForm.bankAccountNumberRequired',
|
||||
});
|
||||
|
||||
// TODO This will be defined in the stripe popup form later.
|
||||
// When that's ready, remove country from initialValues and hidden field.
|
||||
const country = this.props.initialValues.country;
|
||||
const showBankAccountField = !stripeConnected;
|
||||
const currency = config.currencyConfig.currency;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
||||
<AddImages images={images} onSortEnd={this.onSortEnd}>
|
||||
<Field
|
||||
accept={ACCEPT_IMAGES}
|
||||
component={RenderAddImage}
|
||||
label="+ Add image"
|
||||
name="addImage"
|
||||
onChange={this.onImageUploadHandler}
|
||||
type="file"
|
||||
/>
|
||||
|
||||
<Field
|
||||
component={props => {
|
||||
const { input, type, meta } = props;
|
||||
return (
|
||||
<div className={css.imageRequiredWrapper}>
|
||||
<Input {...input} type={type} />
|
||||
<ValidationError className={css.imageRequiredError} fieldMeta={meta} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
name="images"
|
||||
type="hidden"
|
||||
validate={[noEmptyArray(imageRequiredMessage)]}
|
||||
/>
|
||||
</AddImages>
|
||||
|
||||
<Field component="input" name="country" type="hidden" value={country} />
|
||||
{showBankAccountField
|
||||
? <Field
|
||||
name="bankAccountToken"
|
||||
component={StripeBankAccountToken}
|
||||
props={{ country, currency }}
|
||||
validate={required(bankAccountNumberRequiredMessage)}
|
||||
/>
|
||||
: null}
|
||||
|
||||
<Button
|
||||
className={css.submitButton}
|
||||
type="submit"
|
||||
disabled={invalid || submitting || disabled}
|
||||
>
|
||||
{saveActionMsg}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditListingPhotosFormComponent.defaultProps = { saveActionMsg: 'Publish listing' };
|
||||
|
||||
EditListingPhotosFormComponent.propTypes = {
|
||||
...formPropTypes,
|
||||
intl: intlShape.isRequired,
|
||||
onImageUpload: func.isRequired,
|
||||
onUpdateImageOrder: func.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
saveActionMsg: string,
|
||||
stripeConnected: bool.isRequired,
|
||||
};
|
||||
|
||||
const formName = 'EditListingPhotosForm';
|
||||
|
||||
export default compose(reduxForm({ form: formName }), injectIntl)(EditListingPhotosFormComponent);
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// TODO: renderdeep doesn't work due to
|
||||
// "Invariant Violation: getNodeFromInstance: Invalid argument."
|
||||
// refs and findDOMNode are not supported by react-test-renderer
|
||||
// (react-sortable-hoc uses them)
|
||||
import React from 'react';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { fakeIntl, fakeFormProps } from '../../util/test-data';
|
||||
import { EditListingPhotosFormComponent } from './EditListingPhotosForm';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
describe('EditListingPhotosForm', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<EditListingPhotosFormComponent
|
||||
{...fakeFormProps}
|
||||
initialValues={{ country: 'US', images: [] }}
|
||||
intl={fakeIntl}
|
||||
dispatch={noop}
|
||||
onImageUpload={v => v}
|
||||
onSubmit={v => v}
|
||||
onUpdateImageOrder={v => v}
|
||||
stripeConnected={false}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
exports[`EditListingPhotosForm matches snapshot 1`] = `
|
||||
<form
|
||||
onSubmit={[Function]}>
|
||||
<AddImages
|
||||
images={Array []}
|
||||
onSortEnd={[Function]}>
|
||||
<Field
|
||||
accept="image/*"
|
||||
component={[Function]}
|
||||
label="+ Add image"
|
||||
name="addImage"
|
||||
onChange={[Function]}
|
||||
type="file" />
|
||||
<Field
|
||||
component={[Function]}
|
||||
name="images"
|
||||
type="hidden"
|
||||
validate={
|
||||
Array [
|
||||
[Function],
|
||||
]
|
||||
} />
|
||||
</AddImages>
|
||||
<Field
|
||||
component="input"
|
||||
name="country"
|
||||
type="hidden"
|
||||
value="US" />
|
||||
<Field
|
||||
component={[Function]}
|
||||
name="bankAccountToken"
|
||||
props={
|
||||
Object {
|
||||
"country": "US",
|
||||
"currency": "USD",
|
||||
}
|
||||
}
|
||||
validate={[Function]} />
|
||||
<Button
|
||||
className={null}
|
||||
rootClassName=""
|
||||
type="submit">
|
||||
Publish listing
|
||||
</Button>
|
||||
</form>
|
||||
`;
|
||||
|
|
@ -6,6 +6,7 @@ import CheckoutPage from './CheckoutPage/CheckoutPage';
|
|||
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
|
||||
import EditListingDescriptionForm from './EditListingDescriptionForm/EditListingDescriptionForm';
|
||||
import EditListingLocationForm from './EditListingLocationForm/EditListingLocationForm';
|
||||
import EditListingPhotosForm from './EditListingPhotosForm/EditListingPhotosForm';
|
||||
import EditListingPricingForm from './EditListingPricingForm/EditListingPricingForm';
|
||||
import EditListingPage from './EditListingPage/EditListingPage';
|
||||
import EditProfilePage from './EditProfilePage/EditProfilePage';
|
||||
|
|
@ -39,6 +40,7 @@ export {
|
|||
ContactDetailsPage,
|
||||
EditListingDescriptionForm,
|
||||
EditListingLocationForm,
|
||||
EditListingPhotosForm,
|
||||
EditListingPricingForm,
|
||||
EditListingPage,
|
||||
EditProfilePage,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import * as EditListingDescriptionForm
|
|||
from './containers/EditListingDescriptionForm/EditListingDescriptionForm.example';
|
||||
import * as EditListingLocationForm
|
||||
from './containers/EditListingLocationForm/EditListingLocationForm.example';
|
||||
import * as EditListingPhotosForm
|
||||
from './containers/EditListingPhotosForm/EditListingPhotosForm.example';
|
||||
import * as EditListingPricingForm
|
||||
from './containers/EditListingPricingForm/EditListingPricingForm.example';
|
||||
import * as SearchForm from './containers/SearchForm/SearchForm.example';
|
||||
|
|
@ -47,6 +49,7 @@ export {
|
|||
DateInput,
|
||||
EditListingDescriptionForm,
|
||||
EditListingLocationForm,
|
||||
EditListingPhotosForm,
|
||||
EditListingPricingForm,
|
||||
EditListingWizard,
|
||||
ListingCard,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@
|
|||
"EditListingLocationForm.locationRequired": "You need to provide a location",
|
||||
"EditListingLocationForm.locationNotRecognized": "We didn't recognize this location. Please try another location.",
|
||||
"EditListingLocationPanel.title": "Where's your sauna located?",
|
||||
"EditListingPhotosForm.imageRequired": "You need to add at least one image.",
|
||||
"EditListingPhotosForm.bankAccountNumberRequired": "You need to add a bank account number.",
|
||||
"EditListingPhotosPanel.title": "Add photos",
|
||||
"EditListingPricingForm.price": "Price",
|
||||
"EditListingPricingForm.pricePlaceholder": "$0.00",
|
||||
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue