Merge pull request #69 from sharetribe/searchpage-show-images

Add images to SearchPage results
This commit is contained in:
Vesa Luusua 2017-03-14 23:09:37 +02:00 committed by GitHub
commit b0b9a34a85
15 changed files with 150 additions and 23 deletions

View file

@ -3,6 +3,7 @@
"version": "0.2.0",
"private": true,
"dependencies": {
"array.prototype.find": "^2.0.1",
"basic-auth": "^1.1.0",
"classnames": "^2.2.5",
"compression": "^1.6.2",

View file

@ -31,8 +31,7 @@ class AddImagesTest extends Component {
// Fake image uploaded state: show image thumbnail
setTimeout(
() => {
this.setState((prevState) => {
this.setState(prevState => {
const images = prevState.images;
const imageIndex = findIndex(images, i => i.id === fileId);
const updatedImage = { ...imageData, imageId: fileId };
@ -44,9 +43,10 @@ class AddImagesTest extends Component {
return {
images: updatedImages,
};
});
},
1000);
});
},
1000,
);
}
onSortEnd({ oldIndex, newIndex }) {

View file

@ -37,7 +37,9 @@ class Thumbnail extends Component {
render() {
const { file, id, imageId } = this.props;
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !imageId ? <div className={css.thumbnailLoading}>Uploading</div> : null;
const uploadingOverlay = !imageId
? <div className={css.thumbnailLoading}>Uploading</div>
: null;
return (
<Promised
key={id}
@ -54,7 +56,7 @@ class Thumbnail extends Component {
/>
);
}
};
}
Thumbnail.defaultProps = { imageId: null };

View file

@ -12,9 +12,10 @@
/* Firefox doesn't support image aspect ratio inside flexbox */
.aspectWrapper {
padding-bottom: 75%; /* 4:3 Aspect Ratio */
padding-bottom: 100%; /* 1:1 Aspect Ratio */
}
.noImageContainer,
.thumbnail {
position: absolute;
top: 0;
@ -25,6 +26,19 @@
height: 100%;
}
.noImageContainer,
.noImageWrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #eee;
}
.noImageText {
color: #666;
}
.info {
padding: 1rem;

View file

@ -2,6 +2,7 @@ import React from 'react';
import { NamedLink } from '../../components';
import * as propTypes from '../../util/propTypes';
import css from './ListingCard.css';
import noImageIcon from './images/noImageIcon.svg';
const ListingCard = props => {
const { listing } = props;
@ -18,12 +19,39 @@ const ListingCard = props => {
const review = { rating: 3, count: 10 };
const authorAvatar = 'http://placehold.it/44x44';
const authorReview = { rating: 4 };
const images = listing.images
? listing.images.map(i => ({ id: i.id, sizes: i.attributes.sizes }))
: [];
const mainImage = images.length > 0 ? images[0] : null;
const squareImageURL = mainImage ? mainImage.sizes.find(i => i.name === 'square').url : null;
const square2XImageURL = mainImage
? mainImage.sizes.find(i => i.name === 'square2x').url
: null;
const higherRes = square2XImageURL ? { srcSet: `${square2XImageURL} 2x` } : null;
// TODO: svg should have own loading strategy
// Now noImageIcon is imported with default configuration (gives url)
/* eslint-disable jsx-a11y/img-redundant-alt */
const noListingImage = (
<div className={css.noImageContainer}>
<div className={css.noImageWrapper}>
<img className={css.noImageIcon} src={noImageIcon} alt="No image" />
<div className={css.noImageText}>No image</div>
</div>
</div>
);
/* eslint-enable jsx-a11y/img-redundant-alt */
const listingImage = (
<img className={css.thumbnail} src={squareImageURL} alt="Listing Title" {...higherRes} />
);
const imageOrPlaceholder = squareImageURL ? listingImage : noListingImage;
return (
<div className={css.listing}>
<div className={css.squareWrapper}>
<div className={css.aspectWrapper}>
<img className={css.thumbnail} src="http://placehold.it/400x300" alt="Listing Title" />
{imageOrPlaceholder}
</div>
</div>
<div className={css.info}>

View file

@ -2,9 +2,16 @@ exports[`ListingCard matches snapshot 1`] = `
<div>
<div>
<div>
<img
alt="Listing Title"
src="http://placehold.it/400x300" />
<div>
<div>
<img
alt="No image"
src="noImageIcon.svg" />
<div>
No image
</div>
</div>
</div>
</div>
</div>
<div>

View file

@ -0,0 +1 @@
<svg width="48" height="49" viewBox="0 -1 48 49" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M0 47.2h48V0H0z"/></defs><g fill="none" fill-rule="evenodd"><path stroke="#525961" stroke-width="2" stroke-linejoin="round" d="M13.6 9.6L24 .8l10.4 8.8z"/><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path stroke="#525961" stroke-width="2" mask="url(#b)" d="M.8 46.4h46.4V9.6H.8z"/><path stroke="#525961" stroke-width="2" mask="url(#b)" 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" stroke="#525961" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" mask="url(#b)"/><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" fill="#525961" mask="url(#b)"/></g></svg>

After

Width:  |  Height:  |  Size: 915 B

View file

@ -4,6 +4,8 @@
flex-direction: column;
border-radius: 4px;
margin-right: 1rem;
flex-basis: 130px;
flex-shrink: 0;
}
.squareWrapper {
@ -14,9 +16,10 @@
/* Firefox doesn't support image aspect ratio inside flexbox */
.aspectWrapper {
padding-bottom: 75%; /* 4:3 Aspect Ratio */
padding-bottom: 100%; /* 4:3 Aspect Ratio */
}
.noImageContainer,
.thumbnail {
position: absolute;
top: 0;
@ -26,6 +29,18 @@
width: 100%;
height: 100%;
}
.noImageContainer,
.noImageWrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #eee;
}
.noImageText {
color: #666;
}
.info {
width: 100%;

View file

@ -2,6 +2,7 @@ import React from 'react';
import { NamedLink } from '../../components';
import * as propTypes from '../../util/propTypes';
import css from './ListingCardSmall.css';
import noImageIcon from './images/noImageIcon.svg';
const ListingCardSmall = props => {
const { listing } = props;
@ -13,12 +14,39 @@ const ListingCardSmall = props => {
// to do with them
const price = '55\u20AC / day';
const review = { rating: 3, count: 10 };
const images = listing.images
? listing.images.map(i => ({ id: i.id, sizes: i.attributes.sizes }))
: [];
const mainImage = images.length > 0 ? images[0] : null;
const squareImageURL = mainImage ? mainImage.sizes.find(i => i.name === 'square').url : null;
const square2XImageURL = mainImage
? mainImage.sizes.find(i => i.name === 'square2x').url
: null;
const higherRes = square2XImageURL ? { srcSet: `${square2XImageURL} 2x` } : null;
// TODO: svg should have own loading strategy
// Now noImageIcon is imported with default configuration (gives url)
/* eslint-disable jsx-a11y/img-redundant-alt */
const noListingImage = (
<div className={css.noImageContainer}>
<div className={css.noImageWrapper}>
<img className={css.noImageIcon} src={noImageIcon} alt="No image" />
<div className={css.noImageText}>No image</div>
</div>
</div>
);
/* eslint-enable jsx-a11y/img-redundant-alt */
const listingImage = (
<img className={css.thumbnail} src={squareImageURL} alt="Listing Title" {...higherRes} />
);
const imageOrPlaceholder = squareImageURL ? listingImage : noListingImage;
return (
<div className={css.listing}>
<div className={css.squareWrapper}>
<div className={css.aspectWrapper}>
<img className={css.thumbnail} src="http://placehold.it/200x150" alt="Listing Title" />
{imageOrPlaceholder}
</div>
</div>
<div className={css.info}>

View file

@ -2,9 +2,16 @@ exports[`ListingCardSmall matches snapshot 1`] = `
<div>
<div>
<div>
<img
alt="Listing Title"
src="http://placehold.it/200x150" />
<div>
<div>
<img
alt="No image"
src="noImageIcon.svg" />
<div>
No image
</div>
</div>
</div>
</div>
</div>
<div>

View file

@ -0,0 +1 @@
<svg width="48" height="49" viewBox="0 -1 48 49" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M0 47.2h48V0H0z"/></defs><g fill="none" fill-rule="evenodd"><path stroke="#525961" stroke-width="2" stroke-linejoin="round" d="M13.6 9.6L24 .8l10.4 8.8z"/><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path stroke="#525961" stroke-width="2" mask="url(#b)" d="M.8 46.4h46.4V9.6H.8z"/><path stroke="#525961" stroke-width="2" mask="url(#b)" 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" stroke="#525961" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" mask="url(#b)"/><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" fill="#525961" mask="url(#b)"/></g></svg>

After

Width:  |  Height:  |  Size: 915 B

View file

@ -1,5 +1,5 @@
.mapContainer {
height: 70vh;
height: calc(100vh - 237px);
width: 100%;
display: flex;
align-items: center;
@ -9,8 +9,9 @@
font-size: 2rem;
}
.mapListingsContainer {
height: 30vh;
width: 100%;
height: 237px;
width: auto;
overflow-x: scroll;
background-color: #fff;
display: flex;
flex-direction: row;

View file

@ -8,7 +8,14 @@ import EditListingForm from './EditListingForm';
describe('EditListingForm', () => {
it('matches snapshot', () => {
const tree = renderShallow(<EditListingForm images={[]} onImageUpload={v => v} onSubmit={v => v} onUpdateImageOrder={v => v} />);
const tree = renderShallow(
<EditListingForm
images={[]}
onImageUpload={v => v}
onSubmit={v => v}
onUpdateImageOrder={v => v}
/>,
);
expect(tree).toMatchSnapshot();
});
});

View file

@ -51,7 +51,7 @@ export class SearchPageComponent extends Component {
}
SearchPageComponent.loadData = dispatch => {
dispatch(searchListings({ origin: new LatLng(40, 70), include: ['author'] }));
dispatch(searchListings({ origin: new LatLng(40, 70), include: ['author', 'images'] }));
};
SearchPageComponent.defaultProps = { initialListingsLoaded: false, listings: [], tab: 'listings' };

View file

@ -21,7 +21,7 @@ import { PropTypes } from 'react';
import { types as sdkTypes } from 'sharetribe-sdk';
const { UUID, LatLng, LatLngBounds } = sdkTypes;
const { oneOf, string, bool, func, shape, instanceOf } = PropTypes;
const { arrayOf, bool, func, instanceOf, number, oneOf, shape, string } = PropTypes;
// Fixed value
export const value = val => oneOf([val]);
@ -62,6 +62,20 @@ export const user = shape({
}),
});
// Denormalised image object
export const image = shape({
id: uuid.isRequired,
type: value('image').isRequired,
attributes: shape({
sizes: arrayOf(shape({
width: number.isRequired,
height: number.isRequired,
name: string.isRequired,
url: string.isRequired,
})).isRequired,
}),
});
// Denormalised listing object
export const listing = shape({
id: uuid.isRequired,
@ -73,4 +87,5 @@ export const listing = shape({
geolocation: latlng.isRequired,
}),
author: user,
images: arrayOf(image),
});