Separate images container to a component

This commit is contained in:
Vesa Luusua 2017-03-13 19:10:01 +02:00
parent 8931a701a1
commit c0e3113ce6
9 changed files with 230 additions and 70 deletions

View file

@ -0,0 +1,33 @@
.imagesContainer {
width: 100%;
min-height: 120px;
padding-left: 0;
}
.thumbnail {
display: block;
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;
}

View file

@ -0,0 +1,29 @@
.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;
}

View file

@ -0,0 +1,83 @@
import React, { Component } from 'react';
import { findIndex } from 'lodash';
import { arrayMove } from 'react-sortable-hoc';
import AddImages from './AddImages';
import css from './AddImages.example.css';
let id = 0;
const getId = () => {
id += 1;
return id;
};
class AddImagesTest extends Component {
constructor(props, state) {
super(props, state);
this.state = {
images: [],
};
this.onChange = this.onChange.bind(this);
this.onSortEnd = this.onSortEnd.bind(this);
}
onChange(event) {
const file = event.target.files[0];
const fileId = getId();
const imageData = { file, id: fileId, imageId: null };
// Show loading overlay
this.setState({
images: this.state.images.concat([imageData]),
});
// Fake image uploaded state: show image thumbnail
setTimeout(
() => {
this.setState((prevState, props) => {
const images = prevState.images;
const imageIndex = findIndex(images, i => i.id === fileId);
const updatedImage = { ...imageData, imageId: fileId };
const updatedImages = [
...images.slice(0, imageIndex),
updatedImage,
...images.slice(imageIndex + 1),
];
return {
images: updatedImages,
};
});
},
1000);
}
onSortEnd({ oldIndex, newIndex }) {
const { images } = this.state;
this.setState({
images: arrayMove(images, oldIndex, newIndex),
});
}
render() {
return (
<div>
<AddImages images={this.state.images} onSortEnd={this.onSortEnd}>
<div className={css.addImageWrapper}>
<label className={css.addImage} htmlFor="addImageExampleInput">+ Add image</label>
<input
id="addImageExampleInput"
type="file"
accept="images/*"
onChange={this.onChange}
className={css.addImageInput}
/>
</div>
</AddImages>
</div>
);
}
}
export const Empty = {
component: AddImagesTest,
};

View file

@ -0,0 +1,70 @@
/**
* Creates a sortable image grid with children added to the end of the created grid.
*
* Example:
* // images = [{ id: 'tempId', imageId: 'realIdFromAPI', file: File }];
* <AddImages images={images}>
* <input type="file" accept="images/*" onChange={handleChange} />
* </AddImages>
*/
import React, { PropTypes } from 'react';
import { Promised } from '../../components';
import css from './AddImages.css';
// 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 => {
// eslint-disable-next-line
console.error(`Error ${e} happened while reading ${file.name}: ${e.target.result}`);
reject(new Error(`Error reading ${file.name}: ${e.target.result}`));
};
reader.readAsDataURL(file);
});
const AddImages = props => {
const { children, images } = props;
return (
<div className={css.imagesContainer}>
{images.map(i => {
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !i.imageId
? <div className={css.thumbnailLoading}>Uploading</div>
: null;
return (
<Promised
key={i.id}
promise={readImage(i.file)}
renderFulfilled={dataURL => {
return (
<div className={css.thumbnail}>
<img
src={dataURL}
alt={encodeURIComponent(i.file.name)}
className={css.thumbnailImage}
/>
{uploadingOverlay}
</div>
);
}}
renderRejected={() => <div className={css.thumbnail}>Could not read file</div>}
/>
);
})}
{children}
</div>
);
};
AddImages.defaultProps = { images: [] };
const { array, node } = PropTypes;
AddImages.propTypes = {
images: array,
children: node.isRequired,
};
export default AddImages;

View file

@ -1,3 +1,4 @@
import AddImages from './AddImages/AddImages';
import BookingInfo from './BookingInfo/BookingInfo';
import Discussion from './Discussion/Discussion';
import FilterPanel from './FilterPanel/FilterPanel';
@ -16,6 +17,7 @@ import RoutesProvider from './RoutesProvider/RoutesProvider';
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
export {
AddImages,
BookingInfo,
Discussion,
FilterPanel,

View file

@ -2,11 +2,6 @@
color: red;
}
.imagesContainer {
width: 100%;
min-height: 110px;
}
.addImageWrapper {
float: left;
width: 110px;
@ -33,31 +28,8 @@
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;
.addImageInput {
display: none;
}
.imageRequiredWrapper {

View file

@ -5,11 +5,14 @@ export const Empty = {
component: EditListingForm,
props: {
images: [],
onImageUpload(values) {
onImageUpload: values => {
console.log(`onImageUpload with id (${values.id}) and file name (${values.file.name})`);
},
onSubmit(values) {
onSubmit: values => {
console.log('Submit EditListingForm with (unformatted) values:', values);
},
onUpdateImageOrder: imageOrder => {
console.log('onUpdateImageOrder with new imageOrder:', imageOrder);
},
},
};

View file

@ -3,25 +3,12 @@ import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import { isEqual } from 'lodash';
import { noEmptyArray, maxLength, required } from '../../util/validators';
import { Promised } from '../../components';
import { AddImages } from '../../components';
import css from './EditListingForm.css';
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 => {
// eslint-disable-next-line
console.error(`Error ${e} happened while reading ${file.name}: ${e.target.result}`);
reject(new Error(`Error reading ${file.name}: ${e.target.result}`));
};
reader.readAsDataURL(file);
});
// Custom inputs with validator messages
const RenderField = ({ input, label, type, meta }) => {
const { touched, error } = meta;
@ -61,7 +48,7 @@ const RenderAddImage = props => {
const inputProps = { accept, id: name, name, onChange, type };
return (
<div className={css.addImageWrapper}>
<input {...inputProps} style={{ display: 'none' }} />
<input {...inputProps} className={css.addImageInput} />
<label htmlFor={name} className={css.addImage}>{label}</label>
</div>
);
@ -135,28 +122,7 @@ class EditListingForm extends Component {
/>
<h3>Images</h3>
<div className={css.imagesContainer}>
{images.map(i => {
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !i.imageId
? <div className={css.thumbnailLoading}>Uploading</div>
: null;
return (
<Promised
key={i.id}
promise={readImage(i.file)}
renderFulfilled={dataURL => {
return (
<div className={css.thumbnail}>
<img src={dataURL} alt={i.file.name} className={css.thumbnailImage} />
{uploadingOverlay}
</div>
);
}}
renderRejected={() => <div className={css.thumbnail}>Could not read file</div>}
/>
);
})}
<AddImages images={images}>
<Field
accept={ACCEPT_IMAGES}
component={RenderAddImage}
@ -182,7 +148,7 @@ class EditListingForm extends Component {
type="hidden"
validate={[noEmptyArray(imageRequiredStr)]}
/>
</div>
</AddImages>
<Field
name="description"

View file

@ -1,4 +1,5 @@
// components
import * as AddImages from './components/AddImages/AddImages.example';
import * as BookingInfo from './components/BookingInfo/BookingInfo.example';
import * as ListingCard from './components/ListingCard/ListingCard.example';
import * as NamedLink from './components/NamedLink/NamedLink.example';
@ -17,6 +18,7 @@ import * as PasswordForgottenForm
import * as SignUpForm from './containers/SignUpForm/SignUpForm.example';
export {
AddImages,
BookingInfo,
ChangeAccountPasswordForm,
ChangePasswordForm,