├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src ├── actions └── index.js ├── api └── flickr.js ├── components ├── Gallery.js ├── GalleryImage.js └── GalleryThumbs.js ├── index.html ├── main.js ├── reducers └── index.js ├── sagas ├── index.js └── sagas.spec.js └── styles └── styles.css /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | .idea 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yassine Elouafi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egghead-redux-image-gallery 2 | > This is an extension of the [redux-sage beginner tutorial](http://yelouafi.github.io/redux-saga/docs/introduction/BeginnerTutorial.html) from [Yassine Elouafi](https://github.com/yelouafi) who created [redux-saga](https://github.com/yelouafi/redux-saga) for us. 3 | 4 | :star: Check out **[my blog post](http://joelhooks.com/blog/2016/03/20/build-an-image-gallery-using-redux-saga/)** that details this repository. 5 | 6 | # Instructions 7 | 8 | Setup 9 | 10 | ``` 11 | // clone the repo 12 | git clone https://github.com/joelhooks/egghead-react-redux-image-gallery.git 13 | 14 | cd egghead-react-redux-image-gallery 15 | 16 | npm install 17 | ``` 18 | 19 | Run the demo 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | Run tests 26 | 27 | ``` 28 | npm test 29 | ``` 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egghead-react-redux-image-gallery", 3 | "version": "0.0.1", 4 | "description": "Redux Saga beginner tutorial", 5 | "main": "src/main.js.js", 6 | "scripts": { 7 | "test": "babel-node ./src/sagas/sagas.spec.js | tap-spec", 8 | "start": "budo ./src/main.js:build.js --dir ./src --verbose --live -- -t babelify" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/joelhooks/egghead-react-redux-image-gallery.git" 13 | }, 14 | "author": "Joel Hooks ", 15 | "license": "MIT", 16 | "dependencies": { 17 | "babel-polyfill": "6.3.14", 18 | "react": "^0.14.3", 19 | "react-dom": "^0.14.3", 20 | "react-redux": "^4.4.1", 21 | "redux": "^3.3.1", 22 | "redux-saga": "^0.8.0" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.1.18", 26 | "babel-core": "6.4.0", 27 | "babel-preset-es2015": "^6.1.18", 28 | "babel-preset-react": "^6.1.18", 29 | "babel-preset-stage-2": "^6.1.18", 30 | "babelify": "^7.2.0", 31 | "browserify": "^13.0.0", 32 | "budo": "^8.0.4", 33 | "tap-spec": "^4.1.1", 34 | "tape": "^4.2.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | const LOAD_IMAGES = 'LOAD_IMAGES'; 2 | const SELECT_IMAGE = 'SELECT_IMAGE'; 3 | 4 | export function selectImage(image) { 5 | return { 6 | type: SELECT_IMAGE, 7 | image 8 | } 9 | } 10 | 11 | export function loadImages() { 12 | return { 13 | type: LOAD_IMAGES 14 | } 15 | } -------------------------------------------------------------------------------- /src/api/flickr.js: -------------------------------------------------------------------------------- 1 | const API_KEY = 'a46a979f39c49975dbdd23b378e6d3d5'; 2 | const API_ENDPOINT = `https://api.flickr.com/services/rest/?method=flickr.interestingness.getList&api_key=${API_KEY}&format=json&nojsoncallback=1&per_page=5`; 3 | 4 | export const fetchImages = () => { 5 | return fetch(API_ENDPOINT).then(function (response) { 6 | return response.json().then(function (json) { 7 | return json.photos.photo.map( 8 | ({farm, server, id, secret}) => `https://farm${farm}.staticflickr.com/${server}/${id}_${secret}.jpg` 9 | ); 10 | }) 11 | }) 12 | }; -------------------------------------------------------------------------------- /src/components/Gallery.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | 5 | import GalleryImage from './GalleryImage'; 6 | import GalleryThumbs from './GalleryThumbs'; 7 | 8 | import * as ImageGalleryActions from "../actions"; 9 | 10 | export class Gallery extends Component { 11 | componentDidMount() { 12 | this.props.loadImages(); 13 | } 14 | render() { 15 | const {images, selectImage, selectedImage} = this.props; 16 | return ( 17 | 21 | ) 22 | } 23 | } 24 | 25 | export default connect( 26 | state => ({images: state.images, selectedImage: state.selectedImage}), 27 | dispatch => bindActionCreators(ImageGalleryActions, dispatch) 28 | )(Gallery) -------------------------------------------------------------------------------- /src/components/GalleryImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export default function GalleryImage({image}) { 3 | return ( 4 |
5 |
6 | {image ? : null} 7 |
8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/GalleryThumbs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export default function GalleryThumbs({images, selectImage}) { 3 | return ( 4 |
5 | {images.map((image, index) => ( 6 |
7 | 8 |
9 | ))} 10 |
11 | ) 12 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | egghead: React Redux Image Gallery 7 | 8 | 9 | 10 |
11 | 12 |

Egghead Image Gallery

13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import "babel-polyfill" 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import { createStore, applyMiddleware } from 'redux' 6 | import {Provider} from 'react-redux'; 7 | import createSagaMiddleware from 'redux-saga' 8 | 9 | import Gallery from './components/Gallery' 10 | import reducer from './reducers' 11 | 12 | import {watchLoadImages} from './sagas'; 13 | 14 | const store = createStore( 15 | reducer, 16 | applyMiddleware(createSagaMiddleware(watchLoadImages)) 17 | ); 18 | 19 | ReactDOM.render( 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ); 25 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export default function images(state = {images:[]}, action) { 2 | switch (action.type) { 3 | case 'IMAGES_RECEIVED': 4 | console.log(JSON.stringify(action.images)); 5 | return {...state, images: action.images}; 6 | case 'LOAD_IMAGES_FAILURE': 7 | return state; 8 | case 'SELECT_IMAGE': 9 | return {...state, selectedImage: action.image}; 10 | default: 11 | return state 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import {call, put, take} from 'redux-saga/effects'; 2 | import {fetchImages} from '../api/flickr'; 3 | 4 | export function* loadImages() { 5 | try { 6 | const images = yield call(fetchImages); 7 | yield put({type: 'IMAGES_RECEIVED', images}); 8 | yield put({type: 'SELECT_IMAGE', image: images[0]}) 9 | } catch (error) { 10 | yield put({type: 'LOAD_IMAGES_FAILURE', error}) 11 | } 12 | } 13 | 14 | export function* watchLoadImages() { 15 | while (true) { 16 | yield take('LOAD_IMAGES'); 17 | yield call(loadImages); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/sagas/sagas.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import {put, call, take} from 'redux-saga/effects' 3 | import {watchLoadImages, loadImages} from './index'; 4 | import {fetchImages} from '../api/flickr'; 5 | 6 | test('watchLoadImages', assert => { 7 | const gen = watchLoadImages(); 8 | 9 | assert.deepEqual( 10 | gen.next().value, 11 | take('LOAD_IMAGES'), 12 | 'watchLoadImages should be waiting for LOAD_IMAGES action' 13 | ); 14 | 15 | assert.deepEqual( 16 | gen.next().value, 17 | call(loadImages), 18 | 'watchLoadImages should call loadImages after LOAD_IMAGES action is received' 19 | ); 20 | 21 | assert.end(); 22 | }); 23 | 24 | test('loadImages', assert => { 25 | const gen = loadImages(); 26 | 27 | assert.deepEqual( 28 | gen.next().value, 29 | call(fetchImages), 30 | 'loadImages should call the fetchImages api' 31 | ); 32 | 33 | const images = [0]; 34 | 35 | assert.deepEqual( 36 | gen.next(images).value, 37 | put({type: 'IMAGES_RECEIVED', images}), 38 | 'loadImages should dispatch an IMAGES_RECEIVED action with the images' 39 | ); 40 | 41 | assert.deepEqual( 42 | gen.next(images).value, 43 | put({type: 'SELECT_IMAGE', image: images[0]}), 44 | 'loadImages should dispatch an SELECT_IMAGE action with the first image' 45 | ); 46 | 47 | const error = 'error'; 48 | 49 | assert.deepEqual( 50 | gen.throw(error).value, 51 | put({type: 'LOAD_IMAGES_FAILURE', error}), 52 | 'loadImages should dispatch an LOAD_IMAGES_FAILURE if an error is thrown' 53 | ); 54 | 55 | assert.end(); 56 | }); -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, Sans-Serif, sans-serif; 3 | background: white; 4 | } 5 | 6 | .title { 7 | display: flex; 8 | padding: 2px; 9 | } 10 | 11 | .egghead { 12 | width: 30px; 13 | padding: 5px; 14 | } 15 | 16 | .image-gallery { 17 | width: 300px; 18 | display: flex; 19 | flex-direction: column; 20 | border: 1px solid darkgray; 21 | } 22 | 23 | .gallery-image { 24 | height: 250px; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | } 29 | 30 | .gallery-image img { 31 | width: 100%; 32 | max-height: 250px; 33 | } 34 | 35 | .image-scroller { 36 | display: flex; 37 | justify-content: space-around; 38 | overflow: auto; 39 | overflow-y: hidden; 40 | } 41 | 42 | .image-scroller img { 43 | width: 50px; 44 | height: 50px; 45 | padding: 1px; 46 | border: 1px solid black; 47 | } --------------------------------------------------------------------------------