From 00757859426592281af34e9afefb5beb056b5334 Mon Sep 17 00:00:00 2001 From: Shachar16290 Date: Sat, 16 Nov 2019 11:55:31 +0200 Subject: [PATCH 1/2] Flickr gallery --- src/components/App/App.js | 2 +- src/components/App/App.scss | 4 + .../ExpandedImageView/ExpandedImageView.js | 69 ++++++++++++++ .../ExpandedImageView/ExpandedImageView.scss | 39 ++++++++ src/components/ExpandedImageView/index.js | 3 + src/components/Gallery/Gallery.js | 89 ++++++++++++++----- src/components/Gallery/Gallery.scss | 3 + src/components/Image/Image.js | 74 ++++++++------- src/components/Image/Image.scss | 4 +- src/utils/dtoUtils.js | 3 + 10 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 src/components/ExpandedImageView/ExpandedImageView.js create mode 100644 src/components/ExpandedImageView/ExpandedImageView.scss create mode 100644 src/components/ExpandedImageView/index.js create mode 100644 src/utils/dtoUtils.js diff --git a/src/components/App/App.js b/src/components/App/App.js index 16c23ab..338a864 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -9,7 +9,7 @@ class App extends React.Component { constructor() { super(); this.state = { - tag: 'art' + tag: 'dogs' }; } diff --git a/src/components/App/App.scss b/src/components/App/App.scss index b8866ea..fbfa731 100644 --- a/src/components/App/App.scss +++ b/src/components/App/App.scss @@ -24,3 +24,7 @@ body, html { width: 400px; margin-bottom: 30px; } + +[data-hidden="true"] { + display: none; +} \ No newline at end of file diff --git a/src/components/ExpandedImageView/ExpandedImageView.js b/src/components/ExpandedImageView/ExpandedImageView.js new file mode 100644 index 0000000..a88c2c1 --- /dev/null +++ b/src/components/ExpandedImageView/ExpandedImageView.js @@ -0,0 +1,69 @@ +import React from 'react'; +import {urlFromDto} from '../../utils/dtoUtils' +import FontAwesome from 'react-fontawesome'; +import './ExpandedImageView.scss'; + +class ExpandedImageView extends React.Component { + + componentDidMount(){ + document.addEventListener('keydown', this.keyDownHandler); + } + componentWillUnmount(){ + document.removeEventListener('keydown', this.keyDownHandler); + } + + // Use arrow keys to navigate in the expanded view. ESC to exit + keyDownHandler = (e) => { + switch (e.keyCode){ + // "Escape" + case(27): { + this.closeView() + return + } + // "ArrowRight" + case(39): { + if (this.props.imageIndex !== this.props.imageCount - 1) { + this.selectNextImage() + } + return + } + // "ArrowLeft" + case(37): { + if(this.props.imageIndex !== 0) { + this.selectPrevImage() + } + return + } + } + } + + closeView = () => { + this.props.onImageSelected ? this.props.onImageSelected(null) : null + } + + selectNextImage = () => { + this.props.onImageSelected ? this.props.onImageSelected(this.props.imageIndex + 1) : null + } + + selectPrevImage = () => { + this.props.onImageSelected ? this.props.onImageSelected(this.props.imageIndex - 1) : null + } + + render() { + return ( +
+
+ + + +
+ ); + } +} + +export default ExpandedImageView; diff --git a/src/components/ExpandedImageView/ExpandedImageView.scss b/src/components/ExpandedImageView/ExpandedImageView.scss new file mode 100644 index 0000000..f0e9071 --- /dev/null +++ b/src/components/ExpandedImageView/ExpandedImageView.scss @@ -0,0 +1,39 @@ +.expanded-image-view { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + } + + .image-container { + background-repeat: no-repeat; + background-position: center center; + height: 100%; + } + + .exit-icon { + position: absolute; + color: white; + font-size: 21px; + top: 15px; + right: 15px; + cursor: pointer; +} + +.next-icon, .previous-icon { + position: absolute; + color: white; + font-size: 25px; + cursor: pointer; + top: 50%; +} + +.next-icon { + right: 15px; +} + +.previous-icon { + left: 15px; +} \ No newline at end of file diff --git a/src/components/ExpandedImageView/index.js b/src/components/ExpandedImageView/index.js new file mode 100644 index 0000000..a4837c3 --- /dev/null +++ b/src/components/ExpandedImageView/index.js @@ -0,0 +1,3 @@ +import ExpandedImageView from './ExpandedImageView'; + +export default ExpandedImageView; diff --git a/src/components/Gallery/Gallery.js b/src/components/Gallery/Gallery.js index 8ba8699..9ed93f4 100644 --- a/src/components/Gallery/Gallery.js +++ b/src/components/Gallery/Gallery.js @@ -1,29 +1,22 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import debounce from 'lodash/debounce'; import axios from 'axios'; import Image from '../Image'; +import ExpandedImageView from '../ExpandedImageView' + import './Gallery.scss'; class Gallery extends React.Component { - static propTypes = { - tag: PropTypes.string - }; - constructor(props) { super(props); this.state = { images: [], - galleryWidth: this.getGalleryWidth() + selectedImage: null, + selectedImageAngle: 0, + draggedImageIndex: null }; } - getGalleryWidth(){ - try { - return document.body.clientWidth; - } catch (e) { - return 1000; - } - } getImages(tag) { const getImagesUrl = `services/rest/?method=flickr.photos.search&api_key=522c1f9009ca3609bcbaf08545f067ad&tags=${tag}&tag_mode=any&per_page=100&format=json&nojsoncallback=1`; const baseUrl = 'https://api.flickr.com/'; @@ -40,28 +33,84 @@ class Gallery extends React.Component { res.photos.photo && res.photos.photo.length > 0 ) { - this.setState({images: res.photos.photo}); + this.setState({images: this.state.images.concat(res.photos.photo)}); } }); } componentDidMount() { this.getImages(this.props.tag); - this.setState({ - galleryWidth: document.body.clientWidth - }); + + document.addEventListener('scroll', debounce(this.handleScroll, 500)) + } + + componentWillUnmount(){ + document.removeEventListener('scroll', this.handleScroll, false) } componentWillReceiveProps(props) { this.getImages(props.tag); } + onDelete = index => { + let cloned = this.state.images.slice() + cloned.splice(index, 1) + this.setState({images: cloned}); + + } + + // Set the selected image index + rotation angle. null - no selection + onImageSelected = (index = null, angle = 0) => { + this.setState({selectedImage: index, selectedImageAngle: angle}); + } + + + // Get more images only if the scroll is near the bottom of the window + handleScroll = () => { + if (window.scrollY + 1500 > this.galleryRootElement.scrollHeight) { + this.getImages(this.props.tag); + } + } + + onDropImage = e => { + // Check if the dropped component is valid + if(e.target.id === '') { + return + } + + const droppedIndex = parseInt(e.target.id) + + // Check if we drag an image to a smaller index + const isAfter = droppedIndex > this.state.draggedImageIndex + + const draggedImage = this.state.images[this.state.draggedImageIndex] + + let cloned = this.state.images.slice() + + // Insert the image after the dropped index location if the dragged image is smaller than the dropped index + cloned.splice(isAfter ? droppedIndex + 1 : droppedIndex, 0, draggedImage); + // Remove the dragged image from the array + cloned.splice(isAfter ? this.state.draggedImageIndex : this.state.draggedImageIndex + 1, 1) + + this.setState({draggedImageIndex: null, images: cloned}) + } + + onDragImage = draggedImageIndex => { + this.setState({draggedImageIndex: draggedImageIndex}) + } + render() { return ( -
- {this.state.images.map(dto => { - return ; +
this.galleryRootElement = galleryRootElement}> + {this.state.images.map((dto, i) => { + return ; })} + {this.state.selectedImage !== null && }
); } diff --git a/src/components/Gallery/Gallery.scss b/src/components/Gallery/Gallery.scss index ba4e3cb..6d6dda5 100644 --- a/src/components/Gallery/Gallery.scss +++ b/src/components/Gallery/Gallery.scss @@ -1,5 +1,8 @@ .gallery-root { text-align: center; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-gap: 0rem; } .gallery-header { diff --git a/src/components/Image/Image.js b/src/components/Image/Image.js index f96462e..9f99ce7 100644 --- a/src/components/Image/Image.js +++ b/src/components/Image/Image.js @@ -1,54 +1,66 @@ import React from 'react'; -import PropTypes from 'prop-types'; import FontAwesome from 'react-fontawesome'; +import {urlFromDto} from '../../utils/dtoUtils' + import './Image.scss'; class Image extends React.Component { - static propTypes = { - dto: PropTypes.object, - galleryWidth: PropTypes.number - }; - constructor(props) { super(props); - this.calcImageSize = this.calcImageSize.bind(this); + this.deleteImage = this.deleteImage.bind(this); + this.rotateImage = this.rotateImage.bind(this); + this.expandImage = this.expandImage.bind(this); + this.allowDrop = this.allowDrop.bind(this); + this.onDrag = this.onDrag.bind(this) this.state = { - size: 200 + angle: 0 }; } - calcImageSize() { - const {galleryWidth} = this.props; - const targetSize = 200; - const imagesPerRow = Math.round(galleryWidth / targetSize); - const size = (galleryWidth / imagesPerRow); - this.setState({ - size - }); + // Deletes the image by the given index + deleteImage() { + this.props.onDelete ? this.props.onDelete(this.props.imageIndex) : null + } + + // Rotates the image by 90 degree + rotateImage() { + const newAngle = this.state.angle + 90 + this.setState({angle: newAngle === 360 ? 0 : newAngle }) + } + + // Expand the image to a larger view + expandImage() { + this.props.onImageSelected ? this.props.onImageSelected(this.props.imageIndex, this.state.angle) : null } - componentDidMount() { - this.calcImageSize(); + allowDrop(e) { + e.preventDefault(); } - urlFromDto(dto) { - return `https://farm${dto.farm}.staticflickr.com/${dto.server}/${dto.id}_${dto.secret}.jpg`; + // Drag the image by its index + onDrag () { + this.props.onDragImage ? this.props.onDragImage(this.props.imageIndex) : null } render() { return (
-
- - - + draggable="true" + id={this.props.imageIndex} + className="image-root" + style={{ + backgroundImage: `url(${urlFromDto(this.props.dto)})`, + transform: `rotate(${this.state.angle}deg)` + }} + onDrop={this.props.onDropImage} + onDragOver={this.allowDrop} + onDragStart={this.onDrag}> +
+ + +
); diff --git a/src/components/Image/Image.scss b/src/components/Image/Image.scss index f9e7fe8..ff05ace 100644 --- a/src/components/Image/Image.scss +++ b/src/components/Image/Image.scss @@ -1,11 +1,13 @@ .image-root { background-size: cover; background-position: center center; - display: inline-block; + vertical-align: top; box-sizing: border-box; position: relative; border: 1px solid white; + padding-top: 100%; + overflow: hidden; > div { visibility: hidden; diff --git a/src/utils/dtoUtils.js b/src/utils/dtoUtils.js new file mode 100644 index 0000000..ccdf5dc --- /dev/null +++ b/src/utils/dtoUtils.js @@ -0,0 +1,3 @@ +export const urlFromDto = (dto) => { + return `https://farm${dto.farm}.staticflickr.com/${dto.server}/${dto.id}_${dto.secret}.jpg`; + } \ No newline at end of file From a82c6fbdaca65ce566b07ec8d498a9475f07b428 Mon Sep 17 00:00:00 2001 From: Shachar16290 Date: Sat, 16 Nov 2019 18:40:02 +0200 Subject: [PATCH 2/2] fix --- src/components/Image/Image.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/Image/Image.js b/src/components/Image/Image.js index 9f99ce7..e375aa8 100644 --- a/src/components/Image/Image.js +++ b/src/components/Image/Image.js @@ -17,6 +17,11 @@ class Image extends React.Component { }; } + componentWillReceiveProps(props) { + if (this.props.dto.id !== props.dto.id) { + this.setState({angle: 0}) + } + } // Deletes the image by the given index deleteImage() { this.props.onDelete ? this.props.onDelete(this.props.imageIndex) : null