diff --git a/package.json b/package.json index 1ba5c0fd..95d1983a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/AddImages/AddImages.example.js b/src/components/AddImages/AddImages.example.js index 68e47571..d9af74c9 100644 --- a/src/components/AddImages/AddImages.example.js +++ b/src/components/AddImages/AddImages.example.js @@ -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 }) { diff --git a/src/components/AddImages/AddImages.js b/src/components/AddImages/AddImages.js index 6e546b23..92aafd65 100644 --- a/src/components/AddImages/AddImages.js +++ b/src/components/AddImages/AddImages.js @@ -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 ?
Uploading
: null; + const uploadingOverlay = !imageId + ?
Uploading
+ : null; return ( ); } -}; +} Thumbnail.defaultProps = { imageId: null }; diff --git a/src/components/ListingCard/ListingCard.css b/src/components/ListingCard/ListingCard.css index bafa1e92..3080a4d1 100644 --- a/src/components/ListingCard/ListingCard.css +++ b/src/components/ListingCard/ListingCard.css @@ -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; diff --git a/src/components/ListingCard/ListingCard.js b/src/components/ListingCard/ListingCard.js index bd5b5448..cc32c067 100644 --- a/src/components/ListingCard/ListingCard.js +++ b/src/components/ListingCard/ListingCard.js @@ -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 = ( +
+
+ No image +
No image
+
+
+ ); + /* eslint-enable jsx-a11y/img-redundant-alt */ + const listingImage = ( + Listing Title + ); + + const imageOrPlaceholder = squareImageURL ? listingImage : noListingImage; return (
- Listing Title + {imageOrPlaceholder}
diff --git a/src/components/ListingCard/__snapshots__/ListingCard.test.js.snap b/src/components/ListingCard/__snapshots__/ListingCard.test.js.snap index 856d6c65..ae592643 100644 --- a/src/components/ListingCard/__snapshots__/ListingCard.test.js.snap +++ b/src/components/ListingCard/__snapshots__/ListingCard.test.js.snap @@ -2,9 +2,16 @@ exports[`ListingCard matches snapshot 1`] = `
- Listing Title +
+
+ No image +
+ No image +
+
+
diff --git a/src/components/ListingCard/images/noImageIcon.svg b/src/components/ListingCard/images/noImageIcon.svg new file mode 100644 index 00000000..c168d5cd --- /dev/null +++ b/src/components/ListingCard/images/noImageIcon.svg @@ -0,0 +1 @@ + diff --git a/src/components/ListingCardSmall/ListingCardSmall.css b/src/components/ListingCardSmall/ListingCardSmall.css index 4ac667ed..8d7527af 100644 --- a/src/components/ListingCardSmall/ListingCardSmall.css +++ b/src/components/ListingCardSmall/ListingCardSmall.css @@ -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%; diff --git a/src/components/ListingCardSmall/ListingCardSmall.js b/src/components/ListingCardSmall/ListingCardSmall.js index 4c08e8ae..940013f8 100644 --- a/src/components/ListingCardSmall/ListingCardSmall.js +++ b/src/components/ListingCardSmall/ListingCardSmall.js @@ -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 = ( +
+
+ No image +
No image
+
+
+ ); + /* eslint-enable jsx-a11y/img-redundant-alt */ + const listingImage = ( + Listing Title + ); + + const imageOrPlaceholder = squareImageURL ? listingImage : noListingImage; return (
- Listing Title + {imageOrPlaceholder}
diff --git a/src/components/ListingCardSmall/__snapshots__/ListingCardSmall.test.js.snap b/src/components/ListingCardSmall/__snapshots__/ListingCardSmall.test.js.snap index c89265a6..1de934e6 100644 --- a/src/components/ListingCardSmall/__snapshots__/ListingCardSmall.test.js.snap +++ b/src/components/ListingCardSmall/__snapshots__/ListingCardSmall.test.js.snap @@ -2,9 +2,16 @@ exports[`ListingCardSmall matches snapshot 1`] = `
- Listing Title +
+
+ No image +
+ No image +
+
+
diff --git a/src/components/ListingCardSmall/images/noImageIcon.svg b/src/components/ListingCardSmall/images/noImageIcon.svg new file mode 100644 index 00000000..c168d5cd --- /dev/null +++ b/src/components/ListingCardSmall/images/noImageIcon.svg @@ -0,0 +1 @@ + diff --git a/src/components/MapPanel/MapPanel.css b/src/components/MapPanel/MapPanel.css index 3ff8f8e6..01812a91 100644 --- a/src/components/MapPanel/MapPanel.css +++ b/src/components/MapPanel/MapPanel.css @@ -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; diff --git a/src/containers/EditListingForm/EditListingForm.test.js b/src/containers/EditListingForm/EditListingForm.test.js index 23e56029..5ed55006 100644 --- a/src/containers/EditListingForm/EditListingForm.test.js +++ b/src/containers/EditListingForm/EditListingForm.test.js @@ -8,7 +8,14 @@ import EditListingForm from './EditListingForm'; describe('EditListingForm', () => { it('matches snapshot', () => { - const tree = renderShallow( v} onSubmit={v => v} onUpdateImageOrder={v => v} />); + const tree = renderShallow( + v} + onSubmit={v => v} + onUpdateImageOrder={v => v} + />, + ); expect(tree).toMatchSnapshot(); }); }); diff --git a/src/containers/SearchPage/SearchPage.js b/src/containers/SearchPage/SearchPage.js index 51c2463d..95c28f0d 100644 --- a/src/containers/SearchPage/SearchPage.js +++ b/src/containers/SearchPage/SearchPage.js @@ -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' }; diff --git a/src/util/propTypes.js b/src/util/propTypes.js index 688f8b93..5a180601 100644 --- a/src/util/propTypes.js +++ b/src/util/propTypes.js @@ -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), });