Extract ImageFromFile component away from AddImages

This commit is contained in:
Vesa Luusua 2017-09-07 17:18:00 +03:00
parent d4310d4a0f
commit 02da6a5de9
5 changed files with 131 additions and 82 deletions

View file

@ -7,12 +7,10 @@
* <input type="file" accept="images/*" onChange={handleChange} />
* </AddImages>
*/
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import React, { PropTypes } from 'react';
import { SortableContainer } from 'react-sortable-hoc';
import classNames from 'classnames';
import { Promised, ResponsiveImage } from '../../components';
import { uuid } from '../../util/propTypes';
import { ImageFromFile, ResponsiveImage, SpinnerIcon } from '../../components';
import css from './AddImages.css';
const RemoveImageButton = props => {
@ -41,89 +39,38 @@ const RemoveImageButton = props => {
);
};
const { any, array, func, node, string, object } = PropTypes;
const { array, func, node, string, object } = PropTypes;
RemoveImageButton.propTypes = { onClick: func.isRequired };
// 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);
});
// Create sortable elments out of given thumbnail file
class Thumbnail extends Component {
constructor(props) {
super(props);
this.state = {
promisedImage: readImage(this.props.file),
};
}
render() {
const { className, file, id, imageId, onRemoveImage } = this.props;
const handleRemoveClick = e => {
e.preventDefault();
onRemoveImage(id);
};
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !imageId
? <div className={css.thumbnailLoading}><FormattedMessage id="AddImages.upload" /></div>
: null;
const removeButton = imageId ? <RemoveImageButton onClick={handleRemoveClick} /> : null;
const classes = classNames(css.thumbnail, className);
return (
<Promised
key={id}
promise={this.state.promisedImage}
renderFulfilled={dataURL => {
return (
<div className={classes}>
<div className={css.aspectWrapper}>
<img src={dataURL} alt={file.name} className={css.rootForImage} />
</div>
{removeButton}
{uploadingOverlay}
</div>
);
}}
renderRejected={() => (
<div className={css.thumbnail}><FormattedMessage id="AddImages.couldNotReadFile" /></div>
)}
/>
);
}
}
Thumbnail.defaultProps = { className: null, imageId: null };
Thumbnail.propTypes = {
className: string,
file: any.isRequired,
id: string.isRequired,
imageId: uuid,
onRemoveImage: func.isRequired,
};
const ThumbnailWrapper = props => {
const { className, image, savedImageAltText, onRemoveImage } = props;
const handleRemoveClick = e => {
e.preventDefault();
onRemoveImage(image.id);
};
if (image.file) {
return <Thumbnail className={className} onRemoveImage={onRemoveImage} {...image} />;
// Remove this image if file has been uploaded
const removeButton = image.imageId ? <RemoveImageButton onClick={handleRemoveClick} /> : null;
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !image.imageId
? <div className={css.thumbnailLoading}><SpinnerIcon /></div>
: null;
return (
<ImageFromFile
id={image.id}
className={className}
rootClassName={css.thumbnail}
file={image.file}
>
{removeButton}
{uploadingOverlay}
</ImageFromFile>
);
} else {
const handleRemoveClick = e => {
e.preventDefault();
onRemoveImage(image.id);
};
const classes = classNames(css.thumbnail, className);
return (
<div className={classes}>

View file

@ -0,0 +1,31 @@
@import '../../marketplace.css';
.root {
display: block;
position: relative;
width: 100%;
margin: 0;
overflow: hidden;
background-color: var(--matterColorNegative); /* Loading BG color */
}
.threeToTwoWrapper {
position: relative;
}
.aspectWrapper {
padding-bottom: calc(100% * (2 / 3));
}
.rootForImage {
/* Layout - image will take space defined by aspect ratio wrapper */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--borderRadius);
}

View file

@ -0,0 +1,70 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Promised } from '../../components';
import css from './ImageFromFile.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);
});
// Create elements out of given thumbnail file
class ImageFromFile extends Component {
constructor(props) {
super(props);
this.state = {
promisedImage: readImage(this.props.file),
};
}
render() {
const { className, rootClassName, file, id, children } = this.props;
const classes = classNames(rootClassName || css.root, className);
return (
<Promised
key={id}
promise={this.state.promisedImage}
renderFulfilled={dataURL => {
return (
<div className={classes}>
<div className={css.threeToTwoWrapper}>
<div className={css.aspectWrapper}>
<img src={dataURL} alt={file.name} className={css.rootForImage} />
</div>
</div>
{children}
</div>
);
}}
renderRejected={() => (
<div className={classes}><FormattedMessage id="ImageFromFile.couldNotReadFile" /></div>
)}
/>
);
}
}
ImageFromFile.defaultProps = { className: null, children: null, rootClassName: null };
const { any, node, string } = PropTypes;
ImageFromFile.propTypes = {
className: string,
rootClassName: string,
file: any.isRequired,
id: string.isRequired,
children: node,
};
export default ImageFromFile;

View file

@ -18,6 +18,7 @@ import ExpandingTextarea from './ExpandingTextarea/ExpandingTextarea';
import FilterPanel from './FilterPanel/FilterPanel';
import HeroSection from './HeroSection/HeroSection';
import ImageCarousel from './ImageCarousel/ImageCarousel';
import ImageFromFile from './ImageFromFile/ImageFromFile';
import ListingCard from './ListingCard/ListingCard';
import LocationAutocompleteInput, {
LocationAutocompleteInputField,
@ -85,6 +86,7 @@ export {
FilterPanel,
HeroSection,
ImageCarousel,
ImageFromFile,
InlineTextButton,
ListingCard,
LocationAutocompleteInput,

View file

@ -1,6 +1,4 @@
{
"AddImages.couldNotReadFile": "Could not read file",
"AddImages.upload": "Uploading",
"AuthenticationPage.emailAlreadyInUse": "An account already exists with this email address. Try logging in instead.",
"AuthenticationPage.loginFailed": "The email and password you entered did not match our records. Please double-check and try again.",
"AuthenticationPage.loginLinkText": "Login",
@ -125,6 +123,7 @@
"HeroSection.subTitle": "The largest online community to rent saunas in Finland.",
"HeroSection.title": "Book saunas everywhere.",
"ImageCarousel.imageAltText": "Image {index}/{count}",
"ImageFromFile.couldNotReadFile": "Could not read file",
"InboxPage.fetchFailed": "Could not load all messages. Please try again.",
"InboxPage.noOrdersFound": "You haven't made any bookings.",
"InboxPage.noSalesFound": "Nobody has booked anything from you yet.",