mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 13:06:03 +10:00
Merge pull request #230 from sharetribe/responsive-image
ResponsiveImage component
This commit is contained in:
commit
a53e61b111
8 changed files with 454 additions and 0 deletions
42
src/components/ResponsiveImage/NoImageIcon.js
Normal file
42
src/components/ResponsiveImage/NoImageIcon.js
Normal 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;
|
||||
36
src/components/ResponsiveImage/ResponsiveImage.css
Normal file
36
src/components/ResponsiveImage/ResponsiveImage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
32
src/components/ResponsiveImage/ResponsiveImage.example.css
Normal file
32
src/components/ResponsiveImage/ResponsiveImage.example.css
Normal 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;
|
||||
}
|
||||
}
|
||||
241
src/components/ResponsiveImage/ResponsiveImage.example.js
Normal file
241
src/components/ResponsiveImage/ResponsiveImage.example.js
Normal 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',
|
||||
},
|
||||
};
|
||||
98
src/components/ResponsiveImage/ResponsiveImage.js
Normal file
98
src/components/ResponsiveImage/ResponsiveImage.js
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue