├── .gitignore ├── README.md ├── debug └── index.html ├── example └── src │ ├── actions │ ├── api │ │ ├── flickr.js │ │ └── yandex.js │ ├── constants.js │ └── photos.js │ ├── app.css │ ├── app.js │ ├── components │ ├── Container.css │ ├── Container.js │ ├── FeedView.css │ └── FeedView.js │ ├── constants │ └── flickr.js │ ├── dispatcher │ └── index.js │ ├── stores │ ├── PhotoStore.js │ ├── SimpleStore.js │ └── index.js │ └── utils │ └── url.js ├── index.html ├── index.js ├── library ├── photogrid.js └── style.css ├── package.json ├── src └── components │ ├── DefaultInfoElement.css │ ├── DefaultInfoElement.js │ ├── PhotoGrid.css │ ├── PhotoGrid.js │ ├── RadioButtonGroup.css │ └── RadioButtonGroup.js ├── static ├── app.min.js ├── styles.css └── vendors.min.js ├── webpack.config.debug.js ├── webpack.config.library.js └── webpack.config.production.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-photo-feed 2 | Photo gallery, example with public photos feed from Flickr, Yandex 3 | 4 | Simple example of photo gallery with responsive image grid, columns customizing, one-column view with description, fullscreen preview with 5 | one click. Pure CSS for that. 6 | 7 | 8 | ## Installation 9 | You can use PhotoGrid in your app, just install it from npm 10 | 11 | `npm install react-photo-feed` 12 | 13 | ## Usage 14 | 15 | ````js 16 | import React from "react"; 17 | import ReactDOM from "react-dom"; 18 | import PhotoGrid from "react-photo-feed"; 19 | import "react-photo-feed/library/style.css"; 20 | 21 | const demoPhotos = [ 22 | { 23 | id : 1, src : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_n.jpg", 24 | bigSrc : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_b.jpg" 25 | }, 26 | { 27 | id : 2, src : "https://farm5.staticflickr.com/4240/35527849422_25a0a67df6_n.jpg", 28 | bigSrc : "https://farm5.staticflickr.com/4240/35527849422_25a0a67df6_b.jpg" 29 | }, 30 | { 31 | id : 3, src : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_n.jpg", 32 | bigSrc : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_b.jpg" 33 | }, 34 | { 35 | id : 4, src : "https://farm5.staticflickr.com/4240/35527849422_25a0a67df6_n.jpg", 36 | bigSrc : "https://farm5.staticflickr.com/4240/35527849422_25a0a67df6_b.jpg" 37 | }, 38 | { 39 | id : 5, src : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_n.jpg", 40 | bigSrc : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_b.jpg" 41 | }, 42 | { 43 | id : 6, src : "https://farm5.staticflickr.com/4240/35527849422_25a0a67df6_n.jpg", 44 | bigSrc : "https://farm5.staticflickr.com/4240/35527849422_25a0a67df6_b.jpg" 45 | }, 46 | { 47 | id : 7, src : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_n.jpg", 48 | bigSrc : "https://farm5.staticflickr.com/4077/34824083444_f5f050e31c_b.jpg" 49 | } 50 | ]; 51 | ReactDOM.render( 52 |
53 | 54 |
, 55 | document.getElementById('root') 56 | ); 57 | ````` 58 | 59 | ### Prop Types 60 | | Property | Type | Required? | Description | 61 | |:---|:---|:---:|:---| 62 | | photos | Array | ✓ | Array of objects, like `[{id: 1, src: 'http://url_to_small_image', bigSrc: 'http://url_to_big_image', title: 'Caption of photo'}]` | 63 | | columns | Number | ✓ | Grid columns, like `columns={1}`, also can be 2,3,5 | 64 | | InformationElement | Function | | Component used for one-column view | 65 | 66 | 67 | 68 | Also you can see toggle|radio button group. 69 | ```javascript 70 | 71 | ``` 72 | ## [Demo](http://lkazberova.github.io/react-photo-feed/) 73 | 74 | ![Preview](https://s31.postimg.org/e2eejik3v/Untitled_GIF.gif) 75 | 76 | Some ideas were inspired by [react-rpg](https://github.com/James-Oldfield/react-rpg) 77 | -------------------------------------------------------------------------------- /debug/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flickr/Yandex Public Feed 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/src/actions/api/flickr.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { LOAD_PUBLIC_FEED, _SUCCESS, _FAIL, _START } from './../constants'; 3 | import {FLICKR_PUBLIC_FEED_URL, SUFFIX_SMALL_240, SUFFIX_LARGE_1024,SUFFIX_SMALL_320} from '../../constants/flickr'; 4 | import {getLastPartOfUrl} from '../../utils/url'; 5 | import AppDispatcher from '../../dispatcher'; 6 | 7 | 8 | export function loadPublicFeed() { 9 | 10 | AppDispatcher.dispatch({ 11 | type : LOAD_PUBLIC_FEED + _START 12 | }); 13 | 14 | $.getJSON(FLICKR_PUBLIC_FEED_URL) 15 | .done(response => { 16 | let photos = response.items.map(item => ({ 17 | title : item.title, 18 | id : getLastPartOfUrl(item.link), 19 | link : item.link, 20 | src : item.media.m.replace(SUFFIX_SMALL_240, SUFFIX_SMALL_320), 21 | bigSrc : item.media.m.replace(SUFFIX_SMALL_240, SUFFIX_LARGE_1024), 22 | author : item.author, 23 | created : Date.parse(item.published), 24 | tags : item.tags 25 | })); 26 | 27 | AppDispatcher.dispatch({ 28 | type : LOAD_PUBLIC_FEED + _SUCCESS, 29 | response : photos 30 | }); 31 | }) 32 | .fail((error) => { 33 | AppDispatcher.dispatch({ 34 | type : LOAD_PUBLIC_FEED + _FAIL, 35 | error 36 | }) 37 | }); 38 | 39 | } -------------------------------------------------------------------------------- /example/src/actions/api/yandex.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import { LOAD_PUBLIC_FEED, _SUCCESS, _FAIL, _START } from './../constants'; 3 | import AppDispatcher from '../../dispatcher'; 4 | 5 | 6 | export function loadPublicFeed() { 7 | AppDispatcher.dispatch({ 8 | type : LOAD_PUBLIC_FEED + _START 9 | }); 10 | 11 | $.getJSON('http://api-fotki.yandex.ru/api/podhistory/poddate/?format=json&callback=?') 12 | .done(response => { 13 | console.log(response.entries); 14 | 15 | let photos = response.entries.map(item => ({ 16 | title : item.title, 17 | id : item.id, 18 | link : item.links.alternate, 19 | src : item.img.L.href, 20 | bigSrc : item.img.XXL.href, 21 | author : item.author, 22 | created : Date.parse(item.published), 23 | tags : Object.keys(item.tags).join(',') 24 | })); 25 | console.log(photos); 26 | 27 | AppDispatcher.dispatch({ 28 | type : LOAD_PUBLIC_FEED + _SUCCESS, 29 | response : photos 30 | }); 31 | }) 32 | .fail((error) => { 33 | AppDispatcher.dispatch({ 34 | type : LOAD_PUBLIC_FEED + _FAIL, 35 | error 36 | }) 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /example/src/actions/constants.js: -------------------------------------------------------------------------------- 1 | export const LOAD_PUBLIC_FEED = 'LOAD_PUBLIC_FEED'; 2 | 3 | export const _START = '_START'; 4 | export const _SUCCESS = '_SUCCESS'; 5 | export const _FAIL = '_FAIL'; -------------------------------------------------------------------------------- /example/src/actions/photos.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../dispatcher' 2 | import { LOAD_PUBLIC_FEED } from './constants' 3 | import { loadPublicFeed as loadFlickrPF} from './api/flickr'; 4 | import { loadPublicFeed as loadYandexPF} from './api/yandex'; 5 | 6 | export const loadFlickrPublicFeed = loadFlickrPF; 7 | export const loadYandexPublicFeed = loadYandexPF; 8 | -------------------------------------------------------------------------------- /example/src/app.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,300,500); 2 | html { 3 | font-family: 'Roboto', sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | } 6 | body { 7 | width: 80%; 8 | margin: 30px auto; 9 | } -------------------------------------------------------------------------------- /example/src/app.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import Container from './components/Container'; 4 | import './app.css'; 5 | 6 | ReactDOM.render(, document.getElementById('container')); -------------------------------------------------------------------------------- /example/src/components/Container.css: -------------------------------------------------------------------------------- 1 | h1 small { 2 | font-size:65%; 3 | color:#777; 4 | font-weight: 400; 5 | } -------------------------------------------------------------------------------- /example/src/components/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {loadFlickrPublicFeed, loadYandexPublicFeed} from '../actions/photos'; 3 | import {photoStore} from '../stores'; 4 | import FeedView from './FeedView'; 5 | import styles from './Container.css'; 6 | 7 | 8 | class Container extends React.Component { 9 | constructor() { 10 | super(); 11 | 12 | this.state = { 13 | photos : photoStore.getAll() 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | photoStore.addChangeListener(this.change); 19 | 20 | loadYandexPublicFeed(); 21 | loadFlickrPublicFeed(); 22 | } 23 | 24 | componentWillUnmount() { 25 | photoStore.removeChangeListener(this.change); 26 | } 27 | 28 | change = () => { 29 | this.setState({ 30 | photos : photoStore.getAll() 31 | }) 32 | }; 33 | 34 | 35 | render() { 36 | const {photos} = this.state; 37 | return ( 38 |
39 |

PUBLIC FEED from Flickr, Yandex

40 | 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default Container; -------------------------------------------------------------------------------- /example/src/components/FeedView.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lkazberova/react-photo-feed/6119b53182717d964f758972cdcb76df4fb75c37/example/src/components/FeedView.css -------------------------------------------------------------------------------- /example/src/components/FeedView.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import PhotoGrid from './../../../src/components/PhotoGrid'; 4 | import RadioButtonGroup from './../../../src/components/RadioButtonGroup'; 5 | import styles from './FeedView.css'; 6 | 7 | const columnsData = [ 8 | {value : 1, label : 'x1'}, 9 | {value : 2, label : 'x2'}, 10 | {value : 3, label : 'x3'}, 11 | {value : 5, label : 'x5'}]; 12 | const sortParams = [ 13 | {label : 'oldest', value : 'created-asc'}, 14 | {label : 'newest', value : 'created-desc'}, 15 | {label : 'title asc', value : 'title-asc'}, 16 | {label : 'title desc', value : 'title-desc'}]; 17 | 18 | 19 | class Feed extends React.Component { 20 | static propTypes = { 21 | photos : PropTypes.array 22 | }; 23 | constructor () { 24 | super (); 25 | this.state = { 26 | columns : 2, 27 | order : null 28 | } 29 | } 30 | 31 | render() { 32 | const {photos} = this.props; 33 | const { columns, order } = this.state; 34 | const sortedPhotos = order ? this.getSorted() : photos; 35 | 36 | return ( 37 |
38 | 40 | 42 | 43 | 44 |
45 | ); 46 | } 47 | 48 | getSorted() { 49 | const {photos} = this.props; 50 | 51 | const {order} = this.state; 52 | const [field, type] = order.split('-'); 53 | const sign = type == 'asc' ? 1 : -1; 54 | return photos.slice().sort((a, b) => (+(a[field] > b[field]) || +(a[field] === b[field]) - 1) * sign); 55 | } 56 | 57 | onSortClick(item) { 58 | this.setState({ 59 | order : item == this.state.order ? null : item 60 | }); 61 | } 62 | 63 | onClick(value) { 64 | this.setState({ 65 | columns : value 66 | }); 67 | 68 | } 69 | } 70 | 71 | export default Feed; 72 | -------------------------------------------------------------------------------- /example/src/constants/flickr.js: -------------------------------------------------------------------------------- 1 | export const FLICKR_PUBLIC_FEED_URL = 'https://api.flickr.com/services/feeds/photos_public.gne?format=json&jsoncallback=?'; 2 | 3 | // see https://www.flickr.com/services/api/misc.urls.html 4 | export const SUFFIX_MEDIUM_640x640 = "_z"; 5 | export const SUFFIX_SMALL_240 = "_m"; 6 | export const SUFFIX_SMALL_320 = "_n"; 7 | export const SUFFIX_MEDIUM_500 = ''; 8 | export const SUFFIX_LARGE_1024 = '_b'; 9 | -------------------------------------------------------------------------------- /example/src/dispatcher/index.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | 3 | const AppDispatcher = new Dispatcher; 4 | 5 | AppDispatcher.register(console.log.bind(console)); 6 | 7 | export default AppDispatcher; -------------------------------------------------------------------------------- /example/src/stores/PhotoStore.js: -------------------------------------------------------------------------------- 1 | import SimpleStore from './SimpleStore'; 2 | import { LOAD_PUBLIC_FEED, _SUCCESS, _FAIL, _START } from '../actions/constants'; 3 | import AppDispatcher from '../dispatcher'; 4 | 5 | class PhotoStore extends SimpleStore { 6 | constructor(...args) { 7 | super(...args) 8 | this.dispatchToken = AppDispatcher.register((action) => { 9 | const { type, data, response } = action; 10 | 11 | switch (type) { 12 | case LOAD_PUBLIC_FEED + _SUCCESS: 13 | response.forEach(this.add); 14 | this.emitChange(); 15 | 16 | break; 17 | default: return; 18 | } 19 | }) 20 | } 21 | } 22 | 23 | export default PhotoStore; -------------------------------------------------------------------------------- /example/src/stores/SimpleStore.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | const CHANGE_EVENT = 'CHANGE_EVENT'; 3 | 4 | class SimpleStore extends EventEmitter { 5 | constructor() { 6 | super(); 7 | this.__items = []; 8 | } 9 | 10 | emitChange() { 11 | this.emit(CHANGE_EVENT); 12 | } 13 | 14 | addChangeListener(callback) { 15 | this.on(CHANGE_EVENT, callback); 16 | } 17 | 18 | removeChangeListener(callback) { 19 | this.removeListener(CHANGE_EVENT, callback); 20 | } 21 | 22 | getAll() { 23 | return this.__items.slice(); 24 | } 25 | 26 | getById = (id) => { 27 | let result = this.__items.filter((item) => item.id == id); 28 | return result && result.length > 0 ? result[0] : null; 29 | }; 30 | 31 | add = (item) => { 32 | this.delete(item.id); 33 | this.__items.push(item); 34 | }; 35 | 36 | delete = (id) => { 37 | this.__items = this.__items.filter(item => item.id != id); 38 | }; 39 | 40 | 41 | } 42 | 43 | export default SimpleStore; -------------------------------------------------------------------------------- /example/src/stores/index.js: -------------------------------------------------------------------------------- 1 | import PhotoStore from './PhotoStore'; 2 | 3 | export const photoStore = new PhotoStore(); 4 | -------------------------------------------------------------------------------- /example/src/utils/url.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function getLastPartOfUrl (url) { 4 | let matches = url.match(/\/([^/]*)([/]*)$/); 5 | return matches && matches.length > 1 ? matches [1] : null; 6 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flickr/Yandex Public Feed 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = require('./src/components/PhotoGrid.js'); -------------------------------------------------------------------------------- /library/photogrid.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.PhotoFeed=t(require("react")):e.PhotoFeed=t(e.React)}(this,function(e){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=6)}([function(e,t){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(e){if(f===setTimeout)return setTimeout(e,0);if((f===n||!f)&&setTimeout)return f=setTimeout,setTimeout(e,0);try{return f(e,0)}catch(t){try{return f.call(null,e,0)}catch(t){return f.call(this,e,0)}}}function i(e){if(s===clearTimeout)return clearTimeout(e);if((s===r||!s)&&clearTimeout)return s=clearTimeout,clearTimeout(e);try{return s(e)}catch(t){try{return s.call(null,e)}catch(t){return s.call(this,e)}}}function u(){h&&d&&(h=!1,d.length?m=d.concat(m):y=-1,m.length&&a())}function a(){if(!h){var e=o(u);h=!0;for(var t=m.length;t;){for(d=m,m=[];++y1)for(var n=1;n1?t-1:0),r=1;r2?r-2:0),i=2;it+1?t+1:0},e.getPreviousPhotoIndex=function(t){return t-1>=0?t-1:e.props.photos.length-1},e.getPhoto=function(t){return e.props.photos.length>t?e.props.photos[t]:null},e.state={fullScreenImage:null,fullScreenImageIndex:null},e}return u(t,e),a(t,[{key:"render",value:function(){return s.default.createElement("div",null,this.getGridElements(),this.getFullScreenImage(this.state.fullScreenImage))}},{key:"getGridElements",value:function(){var e=this,t=this.props.photos,n=this.isShowInfo()?[d.default.imageGridItem,d.default.column1]:[d.default.imageGridItem],r=this.isShowInfo()?{}:{width:this.getPercentWidth()+"%"};return t.map(function(t,o){return s.default.createElement("div",{className:n.join(" "),style:r,key:t.id},e.getImageElement(t,o))})}}]),t}(s.default.Component);h.propTypes={photos:l.default.array,columns:l.default.number,InformationElement:l.default.func},t.default=h},function(e,t,n){(function(t){if("production"!==t.env.NODE_ENV){var r="function"==typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,o=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===r};e.exports=n(10)(o,!0)}else e.exports=n(12)()}).call(t,n(0))},function(e,t,n){"use strict";(function(t){var r=n(1),o=n(2),i=n(4),u=n(3),a=n(11);e.exports=function(e,n){function c(e){var t=e&&(T&&e[T]||e[O]);if("function"==typeof t)return t}function l(e,t){return e===t?0!==e||1/e==1/t:e!==e&&t!==t}function f(e){this.message=e,this.stack=""}function s(e){function r(r,l,s,p,d,m,h){if(p=p||P,m=m||s,h!==u)if(n)o(!1,"Calling PropTypes validators directly is not supported by the `prop-types` package. Use `PropTypes.checkPropTypes()` to call them. Read more at http://fb.me/use-check-prop-types");else if("production"!==t.env.NODE_ENV&&"undefined"!=typeof console){var y=p+":"+s;!a[y]&&c<3&&(i(!1,"You are manually calling a React.PropTypes validation function for the `%s` prop on `%s`. This is deprecated and will throw in the standalone `prop-types` package. You may be seeing this warning due to a third-party PropTypes library. See https://fb.me/react-warning-dont-call-proptypes for details.",m,p),a[y]=!0,c++)}return null==l[s]?r?new f(null===l[s]?"The "+d+" `"+m+"` is marked as required in `"+p+"`, but its value is `null`.":"The "+d+" `"+m+"` is marked as required in `"+p+"`, but its value is `undefined`."):null:e(l,s,p,d,m)}if("production"!==t.env.NODE_ENV)var a={},c=0;var l=r.bind(null,!1);return l.isRequired=r.bind(null,!0),l}function p(e){function t(t,n,r,o,i,u){var a=t[n];if(E(a)!==e)return new f("Invalid "+o+" `"+i+"` of type `"+x(a)+"` supplied to `"+r+"`, expected `"+e+"`.");return null}return s(t)}function d(e){function t(t,n,r,o,i){if("function"!=typeof e)return new f("Property `"+i+"` of component `"+r+"` has invalid PropType notation inside arrayOf.");var a=t[n];if(!Array.isArray(a)){return new f("Invalid "+o+" `"+i+"` of type `"+E(a)+"` supplied to `"+r+"`, expected an array.")}for(var c=0;c