mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 20:53:24 +10:00
Merge pull request #69 from sharetribe/searchpage-show-images
Add images to SearchPage results
This commit is contained in:
commit
b0b9a34a85
15 changed files with 150 additions and 23 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
1
src/components/ListingCard/images/noImageIcon.svg
Normal file
1
src/components/ListingCard/images/noImageIcon.svg
Normal 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 |
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
1
src/components/ListingCardSmall/images/noImageIcon.svg
Normal file
1
src/components/ListingCardSmall/images/noImageIcon.svg
Normal 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 |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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' };
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue