Merge pull request #230 from sharetribe/responsive-image

ResponsiveImage component
This commit is contained in:
Vesa Luusua 2017-06-21 12:37:13 +03:00 committed by GitHub
commit a53e61b111
8 changed files with 454 additions and 0 deletions

View file

@ -0,0 +1,42 @@
import React, { PropTypes } from 'react';
const NoImageIcon = props => {
const { className } = props;
return (
<svg
className={className}
width="48"
height="49"
viewBox="0 -1 48 49"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd">
<path strokeWidth="2" strokeLinejoin="round" d="M13.6 9.6L24 .8l10.4 8.8z" />
<path strokeWidth="2" d="M.8 46.4h46.4V9.6H.8z" />
<path strokeWidth="2" d="M5.6 41.6h36.8V14.4H5.6z" />
<path
d="M19.998 31.636l.835-.835a4.035 4.035 0 0 1 2.853-1.183h.693c1.07 0 2.097.426 2.853 1.183l.835.835"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M21.599 23.983a1.009 1.009 0 1 0 0 2.018 1.009 1.009 0 0 0 0-2.018M26.402 23.983a1.009 1.009 0 1 0 0 2.018 1.009 1.009 0 0 0 0-2.018"
/>
</g>
</svg>
);
};
const { string } = PropTypes;
NoImageIcon.defaultProps = {
className: null,
};
NoImageIcon.propTypes = {
className: string,
};
export default NoImageIcon;

View file

@ -0,0 +1,36 @@
@import '../../marketplace.css';
.root {
width: 100%;
}
.noImageContainer {
display: flex;
flex-direction: column;
justify-content: center;
background-color: var(--matterColorNegative);
}
.noImageWrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.noImageIcon {
padding-top: 24px;
box-sizing: content-box;
stroke: var(--matterColor);
}
.noImageText {
@apply --marketplaceH6FontStyles;
color: var(--matterColor);
padding-bottom: 24px;
margin-bottom: 0;
@media (--desktopViewport) {
margin-bottom: 0;
}
}

View file

@ -0,0 +1,32 @@
.root {
display: block;
position: relative;
width: 200px;
margin-bottom: 70px;
}
/* Firefox doesn't support image aspect ratio inside flexbox */
.aspectWrapper {
padding-bottom: 66.6667%; /* 3:2 Aspect Ratio */
}
.rootForImageWithAspectRatio {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
}
.rootForSizes {
display: block;
position: relative;
width: 200px;
margin-bottom: 70px;
@media (min-width: 601px) {
width: 400px;
}
}

View file

@ -0,0 +1,241 @@
import React from 'react';
import ResponsiveImage from './ResponsiveImage';
import { types as sdkTypes } from '../../util/sdkLoader';
import css from './ResponsiveImage.example.css';
const { UUID } = sdkTypes;
const ResponsiveImageWrapper = props => {
return (
<div className={css.root}>
<ResponsiveImage {...props} />
</div>
);
};
const ResponsiveImageWrapperWithAspectRatio = props => {
return (
<div className={css.root}>
<div className={css.aspectWrapper}>
<ResponsiveImage {...props} rootClassName={css.rootForImageWithAspectRatio} />
</div>
</div>
);
};
/* Image without aspect ratio wrapper */
export const Image2X = {
component: ResponsiveImageWrapper,
props: {
alt: 'img',
image: {
id: new UUID('empty'),
type: 'image',
attributes: {
sizes: [
{
name: 'crop',
width: 200,
height: 133,
url: 'https://via.placeholder.com/200x133',
},
{
name: 'crop2x',
width: 400,
height: 266,
url: 'https://via.placeholder.com/400x266',
},
],
},
},
nameSet: [
{
name: 'crop',
size: '1x',
},
{
name: 'crop2x',
size: '2x',
},
],
},
};
/* Image with aspect ratio wrapper */
export const Image2XAspect = {
component: ResponsiveImageWrapperWithAspectRatio,
props: {
alt: 'img',
image: {
id: new UUID('empty'),
type: 'image',
attributes: {
sizes: [
{
name: 'crop',
width: 200,
height: 133,
url: 'https://via.placeholder.com/200x133',
},
{
name: 'crop2x',
width: 400,
height: 266,
url: 'https://via.placeholder.com/400x266',
},
],
},
},
nameSet: [
{
name: 'crop',
size: '1x',
},
{
name: 'crop2x',
size: '2x',
},
],
},
};
/* Image with aspect ratio wrapper and wrong aspect */
export const Image2XWrongAspect = {
component: ResponsiveImageWrapperWithAspectRatio,
props: {
alt: 'img',
image: {
id: new UUID('empty'),
type: 'image',
attributes: {
sizes: [
{
name: 'crop',
width: 200,
height: 133,
url: 'https://via.placeholder.com/200x200',
},
{
name: 'crop2x',
width: 400,
height: 266,
url: 'https://via.placeholder.com/400x400',
},
],
},
},
nameSet: [
{
name: 'crop',
size: '1x',
},
{
name: 'crop2x',
size: '2x',
},
],
},
};
export const Image2XWrongAspectNoWrapper = {
component: ResponsiveImage,
props: {
alt: 'img',
image: {
id: new UUID('empty'),
type: 'image',
attributes: {
sizes: [
{
name: 'crop',
width: 200,
height: 133,
url: 'https://via.placeholder.com/200x200',
},
{
name: 'crop2x',
width: 400,
height: 266,
url: 'https://via.placeholder.com/400x400',
},
],
},
},
nameSet: [
{
name: 'crop',
size: '1x',
},
{
name: 'crop2x',
size: '2x',
},
],
},
};
/* No image without aspect ratio wrapper */
export const ImageEmpty = {
component: ResponsiveImageWrapper,
props: {
alt: 'img',
image: null,
},
};
/* No image with aspect ratio wrapper */
export const ImageEmptyWithAspect = {
component: ResponsiveImageWrapperWithAspectRatio,
props: {
alt: 'img',
image: null,
},
};
/* Image without aspect ratio wrapper usign sizes */
const ResponsiveImageWrapperForSizes = props => (
<div className={css.rootForSizes}>
<div className={css.aspectWrapper}>
<ResponsiveImage {...props} />
</div>
</div>
);
export const ImageWithSizes = {
component: ResponsiveImageWrapperForSizes,
props: {
alt: 'img',
image: {
id: new UUID('empty'),
type: 'image',
attributes: {
sizes: [
{
name: 'crop',
width: 200,
height: 133,
url: 'https://via.placeholder.com/200x133',
},
{
name: 'crop2x',
width: 400,
height: 266,
url: 'https://via.placeholder.com/400x266',
},
],
},
},
nameSet: [
{
name: 'crop',
size: '200w',
},
{
name: 'crop2x',
size: '400w',
},
],
sizes: '(max-width: 600px) 200px, 400px',
},
};

View file

@ -0,0 +1,98 @@
/**
* Usage without sizes:
* <ResponsiveImage
* alt="ListingX"
* image={imageDataFromSDK}
* nameSet={[{ name: 'landscape-crop', size: '1x'}, { name: 'landscape-crop2x', size: '2x'}]}
* />
* // produces:
* <img
* alt="ListingX"
* src="url/to/landscape-crop.jpg"
* srcSet="url/to/landscape-crop.jpg 1x, url/to/landscape-crop2x.jpg 2x" />
*
* Usage with sizes:
* <ResponsiveImage
* alt="ListingX"
* image={imageDataFromSDK}
* nameSet={[{ name: 'landscape-crop', size: '400w'}, { name: 'landscape-crop2x', size: '800w'}]}
* sizes="(max-width: 600px) 100vw, 50vw"
* />
* // produces:
* <img
* alt="ListingX"
* src="url/to/landscape-crop.jpg"
* srcSet="url/to/landscape-crop.jpg 400w, url/to/landscape-crop2x.jpg 800w"
* sizes="(max-width: 600px) 100vw, 50vw" />
*
* // This means that below 600px image will take as many pixels there are available on current
* // viewport width (100vw) - and above that image will only take 50% of the page width.
* // Browser decides which image it will fetch based on current screen size.
*/
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import * as propTypes from '../../util/propTypes';
import NoImageIcon from './NoImageIcon';
import css from './ResponsiveImage.css';
const ResponsiveImage = props => {
const { className, rootClassName, alt, image, nameSet, sizes } = props;
const classes = classNames(rootClassName || css.root, className);
if (image == null || nameSet.length === 0) {
const noImageClasses = classNames(rootClassName || css.root, css.noImageContainer, className);
/* eslint-disable jsx-a11y/img-redundant-alt */
return (
<div className={noImageClasses}>
<div className={css.noImageWrapper}>
<NoImageIcon className={css.noImageIcon} />
<div className={css.noImageText}><FormattedMessage id="ResponsiveImage.noImage" /></div>
</div>
</div>
);
/* eslint-enable jsx-a11y/img-redundant-alt */
}
const defaultURL = nameSet[0].url;
const imageSizes = image.attributes.sizes;
const srcSet = nameSet
.map(v => {
const url = imageSizes.find(i => i.name === v.name).url;
return `${url} ${v.size}`;
})
.join(', ');
const sizesProp = sizes ? { sizes } : {};
return <img alt={alt} className={classes} src={defaultURL} srcSet={srcSet} {...sizesProp} />;
};
const { arrayOf, shape, string } = PropTypes;
ResponsiveImage.defaultProps = {
className: null,
rootClassName: null,
image: null,
nameSet: [],
sizes: null,
};
ResponsiveImage.propTypes = {
className: string,
rootClassName: string,
alt: string.isRequired,
image: propTypes.image,
nameSet: arrayOf(
shape({
name: string.isRequired,
size: string.isRequired,
})
),
sizes: string,
};
export default ResponsiveImage;

View file

@ -34,6 +34,7 @@ import OrderDiscussionPanel from './OrderDiscussionPanel/OrderDiscussionPanel';
import PageLayout from './PageLayout/PageLayout';
import PaginationLinks from './PaginationLinks/PaginationLinks';
import Promised from './Promised/Promised';
import ResponsiveImage from './ResponsiveImage/ResponsiveImage';
import RoutesProvider from './RoutesProvider/RoutesProvider';
import SaleDetailsPanel from './SaleDetailsPanel/SaleDetailsPanel';
import SearchIcon from './SearchIcon/SearchIcon';
@ -85,6 +86,7 @@ export {
PageLayout,
PaginationLinks,
Promised,
ResponsiveImage,
RoutesProvider,
SaleDetailsPanel,
SearchIcon,

View file

@ -15,6 +15,7 @@ import * as NamedLink from './components/NamedLink/NamedLink.example';
import * as LocationAutocompleteInput
from './components/LocationAutocompleteInput/LocationAutocompleteInput.example';
import * as PaginationLinks from './components/PaginationLinks/PaginationLinks.example';
import * as ResponsiveImage from './components/ResponsiveImage/ResponsiveImage.example';
import * as StripeBankAccountToken
from './components/StripeBankAccountToken/StripeBankAccountToken.example';
import * as TabNav from './components/TabNav/TabNav.example';
@ -71,6 +72,7 @@ export {
PaginationLinks,
PasswordForgottenForm,
PayoutDetailsForm,
ResponsiveImage,
SignupForm,
StripeBankAccountToken,
StripePaymentForm,

View file

@ -119,6 +119,7 @@
"PayoutDetailsForm.streetAddressRequired": "This field is required",
"PayoutDetailsForm.submitButtonText": "Save details & publish listing",
"PayoutDetailsForm.title": "One more thing: payout preferences",
"ResponsiveImage.noImage": "No image",
"SaleDetailsPanel.listingAcceptedTitle": "{customerName} has booked {title}.",
"SaleDetailsPanel.listingDeliveredTitle": "{customerName} booked {title}.",
"SaleDetailsPanel.listingRejectedTitle": "{customerName} requested to book {title}.",