├── src ├── favicon.ico ├── images │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ ├── 9.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 15.jpg │ └── 16.jpg ├── fonts │ └── icons │ │ ├── turn-arrow.eot │ │ ├── turn-arrow.ttf │ │ ├── turn-arrow.woff │ │ └── turn-arrow.svg ├── index.html ├── data │ └── imageDatas.json ├── styles │ └── main.scss └── components │ └── GalleryByReactApp.js ├── .travis.yml ├── .yo-rc.json ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jshintrc ├── test ├── spec │ └── components │ │ └── GalleryByReactApp.js ├── .jshintrc └── helpers │ ├── createComponent.js │ └── pack │ └── phantomjs-shims.js ├── LICENSE ├── package.json ├── README.md ├── webpack.dist.config.js ├── webpack.config.js ├── karma.conf.js └── Gruntfile.js /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/1.jpg -------------------------------------------------------------------------------- /src/images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/2.jpg -------------------------------------------------------------------------------- /src/images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/3.jpg -------------------------------------------------------------------------------- /src/images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/4.jpg -------------------------------------------------------------------------------- /src/images/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/5.jpg -------------------------------------------------------------------------------- /src/images/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/6.jpg -------------------------------------------------------------------------------- /src/images/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/7.jpg -------------------------------------------------------------------------------- /src/images/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/8.jpg -------------------------------------------------------------------------------- /src/images/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/9.jpg -------------------------------------------------------------------------------- /src/images/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/10.jpg -------------------------------------------------------------------------------- /src/images/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/11.jpg -------------------------------------------------------------------------------- /src/images/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/12.jpg -------------------------------------------------------------------------------- /src/images/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/13.jpg -------------------------------------------------------------------------------- /src/images/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/14.jpg -------------------------------------------------------------------------------- /src/images/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/15.jpg -------------------------------------------------------------------------------- /src/images/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/images/16.jpg -------------------------------------------------------------------------------- /src/fonts/icons/turn-arrow.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/fonts/icons/turn-arrow.eot -------------------------------------------------------------------------------- /src/fonts/icons/turn-arrow.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/fonts/icons/turn-arrow.ttf -------------------------------------------------------------------------------- /src/fonts/icons/turn-arrow.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/materliu/gallery-by-react/HEAD/src/fonts/icons/turn-arrow.woff -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | before_script: 7 | - npm install grunt-cli -g 8 | script: grunt 9 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-react-webpack": { 3 | "app-name": "galleryByReact", 4 | "architecture": false, 5 | "styles-language": "sass", 6 | "component-suffix": "js" 7 | } 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "react" 4 | ], 5 | "ecmaFeatures": { 6 | "jsx": true, 7 | "modules": true 8 | }, 9 | "env": { 10 | "browser": true, 11 | "amd": true, 12 | "es6": true 13 | }, 14 | "rules": { 15 | "quotes": [ 1, "single" ], 16 | "no-undef": false, 17 | "global-strict": false, 18 | "no-extra-semi": 1, 19 | "no-underscore-dangle": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### SublimeText ### 2 | *.sublime-workspace 3 | 4 | ### OSX ### 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | # Files that might appear on external disk 14 | .Spotlight-V100 15 | .Trashes 16 | 17 | ### Windows ### 18 | # Windows image file caches 19 | Thumbs.db 20 | ehthumbs.db 21 | 22 | # Folder config file 23 | Desktop.ini 24 | 25 | # Recycle Bin used on file shares 26 | $RECYCLE.BIN/ 27 | 28 | # App specific 29 | 30 | node_modules/ 31 | .tmp 32 | dist 33 | /src/main.js 34 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "false", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "white": true, 22 | "newcap": false, 23 | "globals": { 24 | "React": true 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /test/spec/components/GalleryByReactApp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('GalleryByReactApp', () => { 4 | let React = require('react/addons'); 5 | let GalleryByReactApp, component; 6 | 7 | beforeEach(() => { 8 | let container = document.createElement('div'); 9 | container.id = 'content'; 10 | document.body.appendChild(container); 11 | 12 | GalleryByReactApp = require('components/GalleryByReactApp.js'); 13 | component = React.createElement(GalleryByReactApp); 14 | }); 15 | 16 | it('should create a new instance of GalleryByReactApp', () => { 17 | expect(component).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/fonts/icons/turn-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "false", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "white": true, 22 | "newcap": false, 23 | "globals": { 24 | "after": false, 25 | "afterEach": false, 26 | "react": false, 27 | "before": false, 28 | "beforeEach": false, 29 | "browser": false, 30 | "describe": false, 31 | "expect": false, 32 | "inject": false, 33 | "it": false, 34 | "spyOn": false, 35 | "jasmine": false, 36 | "spyOnConstructor": false, 37 | "React": true 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 |
15 |

If you can see this, something is broken (or JS is not enabled)!!.

16 |
17 | 18 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/helpers/createComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to get the shallow output for a given component 3 | * As we are using phantom.js, we also need to include the fn.proto.bind shim! 4 | * 5 | * @see http://simonsmith.io/unit-testing-react-components-without-a-dom/ 6 | * @author somonsmith 7 | */ 8 | 9 | // Add missing methods to phantom.js 10 | import './pack/phantomjs-shims'; 11 | 12 | import React from 'react/addons'; 13 | const TestUtils = React.addons.TestUtils; 14 | 15 | /** 16 | * Get the shallow rendered component 17 | * 18 | * @param {Object} component The component to return the output for 19 | * @param {Object} props [optional] The components properties 20 | * @param {Mixed} ...children [optional] List of children 21 | * @return {Object} Shallow rendered output 22 | */ 23 | export default function createComponent(component, props = {}, ...children) { 24 | const shallowRenderer = TestUtils.createRenderer(); 25 | shallowRenderer.render(React.createElement(component, props, children.length > 1 ? children : children[0])); 26 | return shallowRenderer.getRenderOutput(); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 刘炬光 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 | 23 | -------------------------------------------------------------------------------- /test/helpers/pack/phantomjs-shims.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var Ap = Array.prototype; 4 | var slice = Ap.slice; 5 | var Fp = Function.prototype; 6 | 7 | if (!Fp.bind) { 8 | // PhantomJS doesn't support Function.prototype.bind natively, so 9 | // polyfill it whenever this module is required. 10 | Fp.bind = function(context) { 11 | var func = this; 12 | var args = slice.call(arguments, 1); 13 | 14 | function bound() { 15 | var invokedAsConstructor = func.prototype && (this instanceof func); 16 | return func.apply( 17 | // Ignore the context parameter when invoking the bound function 18 | // as a constructor. Note that this includes not only constructor 19 | // invocations using the new keyword but also calls to base class 20 | // constructors such as BaseClass.call(this, ...) or super(...). 21 | !invokedAsConstructor && context || this, 22 | args.concat(slice.call(arguments)) 23 | ); 24 | } 25 | 26 | // The bound function must share the .prototype of the unbound 27 | // function so that any object created by one constructor will count 28 | // as an instance of both constructors. 29 | bound.prototype = func.prototype; 30 | 31 | return bound; 32 | }; 33 | } 34 | })(); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gallerybyreact", 3 | "version": "0.0.0", 4 | "description": "", 5 | "repository": "", 6 | "private": true, 7 | "src": "src", 8 | "test": "test", 9 | "dist": "dist", 10 | "mainInput": "GalleryByReactApp", 11 | "mainOutput": "main", 12 | "dependencies": { 13 | "react": "0.13.x", 14 | "normalize.css": "~3.0.3" 15 | }, 16 | "devDependencies": { 17 | "autoprefixer-loader": "^2.0.0", 18 | "babel": "^5.0.0", 19 | "babel-loader": "^5.0.0", 20 | "css-loader": "~0.9.0", 21 | "eslint": "^0.21.2", 22 | "eslint-loader": "^0.11.2", 23 | "eslint-plugin-react": "^2.4.0", 24 | "grunt": "~0.4.5", 25 | "grunt-contrib-clean": "~0.6.0", 26 | "grunt-contrib-connect": "~0.8.0", 27 | "grunt-contrib-copy": "~0.5.0", 28 | "grunt-karma": "~0.8.3", 29 | "grunt-open": "~0.2.3", 30 | "grunt-webpack": "~1.0.8", 31 | "jasmine-core": "^2.3.4", 32 | "json-loader": "^0.5.2", 33 | "karma": "~0.12.21", 34 | "karma-jasmine": "^0.3.5", 35 | "karma-phantomjs-launcher": "~0.1.3", 36 | "karma-script-launcher": "~0.1.0", 37 | "karma-webpack": "^1.5.0", 38 | "load-grunt-tasks": "~0.6.0", 39 | "react-hot-loader": "^1.0.7", 40 | "sass-loader": "^1.0.1", 41 | "style-loader": "~0.8.0", 42 | "url-loader": "~0.5.5", 43 | "webpack": "~1.10.0", 44 | "webpack-dev-server": "~1.10.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gallery By React 2 | ===== 3 | 4 | [![Build Status](https://secure.travis-ci.org/materliu/gallery-by-react.png)](https://travis-ci.org/materliu/gallery-by-react) [![Dependency Status](https://gemnasium.com/materliu/gallery-by-react.svg)](https://gemnasium.com/materliu/gallery-by-react) 5 | 6 | 使用ReactJS构架的图片画廊应用-为慕课网《React实战》课程对应的实战项目 7 | 8 | ## Demo & Examples 9 | 10 | Live demo: [http://materliu.github.io/gallery-by-react/](http://materliu.github.io/gallery-by-react/) 11 | 12 | To build the examples locally, run: 13 | 14 | ``` 15 | npm install 16 | grunt serve 17 | ``` 18 | 19 | Then open [`http://localhost:8000/webpack-dev-server/`](http://localhost:8000/webpack-dev-server/) in a browser. 20 | 21 | ## Guideline 22 | 23 | 不建议大家直接照搬源代码,可利用其中的一些资源文件,比如说图片,Icon Font文件等,最好的实践过程是跟着课程一步步构建你自己的图片画廊应用。 24 | 25 | ## 求赞 26 | 27 | 来吧,小伙伴们,不要吝啬你们的赞了,这样才能让更多的同学看到并学习本项目。 28 | 29 | ## Gratitude 30 | 31 | * lyn 32 | * codrops/ScatteredPolaroidsGallery 33 | * 有问题可发issue, 或者微博沟通: [http://weibo.com/jasonandliu/](http://weibo.com/jasonandliu/) 34 | 35 | ## Browser Support 36 | 37 | ![IE](https://raw.github.com/alrra/browser-logos/master/internet-explorer/internet-explorer_48x48.png) | ![Chrome](https://raw.github.com/alrra/browser-logos/master/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/firefox/firefox_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/opera/opera_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/safari/safari_48x48.png) 38 | --- | --- | --- | --- | --- | 39 | IE 10+ ✔ | Chrome 4.0+ ✔ | Firefox 10.0+ ✔ | Opera 15.0+ ✔ | Safari 4.0+ ✔ | 40 | -------------------------------------------------------------------------------- /webpack.dist.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Webpack distribution configuration 3 | * 4 | * This file is set up for serving the distribution version. It will be compiled to dist/ by default 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var webpack = require('webpack'); 10 | 11 | module.exports = { 12 | 13 | output: { 14 | publicPath: '/assets/', 15 | path: 'dist/assets/', 16 | filename: 'main.js' 17 | }, 18 | 19 | debug: false, 20 | devtool: false, 21 | entry: './src/components/GalleryByReactApp.js', 22 | 23 | stats: { 24 | colors: true, 25 | reasons: false 26 | }, 27 | 28 | plugins: [ 29 | new webpack.optimize.DedupePlugin(), 30 | new webpack.optimize.UglifyJsPlugin(), 31 | new webpack.optimize.OccurenceOrderPlugin(), 32 | new webpack.optimize.AggressiveMergingPlugin(), 33 | new webpack.NoErrorsPlugin() 34 | ], 35 | 36 | resolve: { 37 | extensions: ['', '.js', '.jsx'], 38 | alias: { 39 | 'styles': __dirname + '/src/styles', 40 | 'mixins': __dirname + '/src/mixins', 41 | 'components': __dirname + '/src/components/' 42 | } 43 | }, 44 | 45 | module: { 46 | preLoaders: [{ 47 | test: /\.(js|jsx)$/, 48 | exclude: /node_modules/, 49 | loader: 'eslint-loader' 50 | }], 51 | loaders: [{ 52 | test: /\.(js|jsx)$/, 53 | exclude: /node_modules/, 54 | loader: 'babel-loader' 55 | }, { 56 | test: /\.css$/, 57 | loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:["last 2 version", "firefox 15"]}' 58 | }, { 59 | test: /\.scss/, 60 | loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:["last 2 version", "firefox 15"]}!sass-loader?outputStyle=expanded' 61 | }, { 62 | test: /\.json$/, 63 | loader: 'json-loader' 64 | }, { 65 | test: /\.(png|jpg|woff|woff2|eot|ttf|svg)$/, 66 | loader: 'url-loader?limit=8192' 67 | }] 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Webpack development server configuration 3 | * 4 | * This file is set up for serving the webpack-dev-server, which will watch for changes and recompile as required if 5 | * the subfolder /webpack-dev-server/ is visited. Visiting the root will not automatically reload. 6 | */ 7 | 'use strict'; 8 | var webpack = require('webpack'); 9 | 10 | module.exports = { 11 | 12 | output: { 13 | filename: 'main.js', 14 | publicPath: '/assets/' 15 | }, 16 | 17 | cache: true, 18 | debug: true, 19 | devtool: 'sourcemap', 20 | entry: [ 21 | 'webpack/hot/only-dev-server', 22 | './src/components/GalleryByReactApp.js' 23 | ], 24 | 25 | stats: { 26 | colors: true, 27 | reasons: true 28 | }, 29 | resolve: { 30 | extensions: ['', '.js', '.jsx'], 31 | alias: { 32 | 'styles': __dirname + '/src/styles', 33 | 'mixins': __dirname + '/src/mixins', 34 | 'components': __dirname + '/src/components/' 35 | } 36 | }, 37 | module: { 38 | preLoaders: [{ 39 | test: /\.(js|jsx)$/, 40 | exclude: /node_modules/, 41 | loader: 'eslint-loader' 42 | }], 43 | loaders: [{ 44 | test: /\.(js|jsx)$/, 45 | exclude: /node_modules/, 46 | loader: 'react-hot!babel-loader'// transpiling compiling 47 | }, { 48 | test: /\.scss/, 49 | loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:["last 2 version", "firefox 15"]}!sass-loader?outputStyle=expanded' 50 | }, { 51 | test: /\.css$/, 52 | loader: 'style-loader!css-loader!autoprefixer-loader?{browsers:["last 2 version", "firefox 15"]}' 53 | }, { 54 | test: /\.json$/, 55 | loader: 'json-loader' 56 | }, { 57 | test: /\.(png|jpg|woff|woff2|eot|ttf|svg)$/, 58 | loader: 'url-loader?limit=8192' 59 | }] 60 | }, 61 | 62 | plugins: [ 63 | new webpack.HotModuleReplacementPlugin() 64 | ] 65 | 66 | }; 67 | 68 | -------------------------------------------------------------------------------- /src/data/imageDatas.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fileName": "1.jpg", 4 | "title": "Heaven of time", 5 | "desc": "Here he comes Here comes Speed Racer." 6 | }, 7 | { 8 | "fileName": "2.jpg", 9 | "title": "Heaven of time", 10 | "desc": "Here he comes Here comes Speed Racer." 11 | }, 12 | { 13 | "fileName": "3.jpg", 14 | "title": "Heaven of time", 15 | "desc": "Here he comes Here comes Speed Racer." 16 | }, 17 | { 18 | "fileName": "4.jpg", 19 | "title": "Heaven of time", 20 | "desc": "Here he comes Here comes Speed Racer. " 21 | }, 22 | { 23 | "fileName": "5.jpg", 24 | "title": "Heaven of time", 25 | "desc": "Here he comes Here comes Speed Racer. " 26 | }, 27 | { 28 | "fileName": "6.jpg", 29 | "title": "Heaven of time", 30 | "desc": "Here he comes Here comes Speed Racer. " 31 | }, 32 | { 33 | "fileName": "7.jpg", 34 | "title": "Heaven of time", 35 | "desc": "Here he comes Here comes Speed Racer. " 36 | }, 37 | { 38 | "fileName": "8.jpg", 39 | "title": "Heaven of time", 40 | "desc": "Here he comes Here comes Speed Racer. " 41 | }, 42 | { 43 | "fileName": "9.jpg", 44 | "title": "Heaven of time", 45 | "desc": "Here he comes Here comes Speed Racer. " 46 | }, 47 | { 48 | "fileName": "10.jpg", 49 | "title": "Heaven of time", 50 | "desc": "Here he comes Here comes Speed Racer. " 51 | }, 52 | { 53 | "fileName": "11.jpg", 54 | "title": "Heaven of time", 55 | "desc": "Here he comes Here comes Speed Racer. " 56 | }, 57 | { 58 | "fileName": "12.jpg", 59 | "title": "Heaven of time", 60 | "desc": "Here he comes Here comes Speed Racer. " 61 | }, 62 | { 63 | "fileName": "13.jpg", 64 | "title": "Heaven of time", 65 | "desc": "Here he comes Here comes Speed Racer. " 66 | }, 67 | { 68 | "fileName": "14.jpg", 69 | "title": "Heaven of time", 70 | "desc": "Here he comes Here comes Speed Racer. " 71 | }, 72 | { 73 | "fileName": "15.jpg", 74 | "title": "Heaven of time", 75 | "desc": "Here he comes Here comes Speed Racer. " 76 | }, 77 | { 78 | "fileName": "16.jpg", 79 | "title": "Heaven of time", 80 | "desc": "Here he comes Here comes Speed Racer. " 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['jasmine'], 9 | files: [ 10 | 'test/helpers/pack/**/*.js', 11 | 'test/helpers/react/**/*.js', 12 | 'test/spec/components/**/*.js' 13 | ], 14 | preprocessors: { 15 | 'test/helpers/createComponent.js': ['webpack'], 16 | 'test/spec/components/**/*.js': ['webpack'], 17 | 'test/spec/components/**/*.jsx': ['webpack'] 18 | }, 19 | webpack: { 20 | cache: true, 21 | module: { 22 | loaders: [{ 23 | test: /\.gif/, 24 | loader: 'url-loader?limit=10000&mimetype=image/gif' 25 | }, { 26 | test: /\.jpg/, 27 | loader: 'url-loader?limit=10000&mimetype=image/jpg' 28 | }, { 29 | test: /\.png/, 30 | loader: 'url-loader?limit=10000&mimetype=image/png' 31 | }, { 32 | test: /\.(js|jsx)$/, 33 | loader: 'babel-loader', 34 | exclude: /node_modules/ 35 | }, { 36 | test: /\.sass/, 37 | loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded' 38 | }, { 39 | test: /\.css$/, 40 | loader: 'style-loader!css-loader' 41 | }, { 42 | test: /\.woff/, 43 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 44 | }, { 45 | test: /\.woff2/, 46 | loader: 'url-loader?limit=10000&mimetype=application/font-woff2' 47 | }] 48 | }, 49 | resolve: { 50 | alias: { 51 | 'styles': path.join(process.cwd(), './src/styles/'), 52 | 'components': path.join(process.cwd(), './src/components/'), 53 | 'helpers': path.join(process.cwd(), './test/helpers/') 54 | } 55 | } 56 | }, 57 | webpackMiddleware: { 58 | noInfo: true, 59 | stats: { 60 | colors: true 61 | } 62 | }, 63 | exclude: [], 64 | port: 8080, 65 | logLevel: config.LOG_INFO, 66 | colors: true, 67 | autoWatch: false, 68 | browsers: ['PhantomJS'], 69 | reporters: ['dots'], 70 | captureTimeout: 60000, 71 | singleRun: true, 72 | plugins: [ 73 | require('karma-webpack'), 74 | require('karma-jasmine'), 75 | require('karma-phantomjs-launcher') 76 | ] 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mountFolder = function (connect, dir) { 4 | return connect.static(require('path').resolve(dir)); 5 | }; 6 | 7 | var webpackDistConfig = require('./webpack.dist.config.js'), 8 | webpackDevConfig = require('./webpack.config.js'); 9 | 10 | module.exports = function (grunt) { 11 | // Let *load-grunt-tasks* require everything 12 | require('load-grunt-tasks')(grunt); 13 | 14 | // Read configuration from package.json 15 | var pkgConfig = grunt.file.readJSON('package.json'); 16 | 17 | grunt.initConfig({ 18 | pkg: pkgConfig, 19 | 20 | webpack: { 21 | options: webpackDistConfig, 22 | dist: { 23 | cache: false 24 | } 25 | }, 26 | 27 | 'webpack-dev-server': { 28 | options: { 29 | hot: true, 30 | port: 8000, 31 | webpack: webpackDevConfig, 32 | publicPath: '/assets/', 33 | contentBase: './<%= pkg.src %>/' 34 | }, 35 | 36 | start: { 37 | keepAlive: true 38 | } 39 | }, 40 | 41 | connect: { 42 | options: { 43 | port: 8000 44 | }, 45 | 46 | dist: { 47 | options: { 48 | keepalive: true, 49 | middleware: function (connect) { 50 | return [ 51 | mountFolder(connect, pkgConfig.dist) 52 | ]; 53 | } 54 | } 55 | } 56 | }, 57 | 58 | open: { 59 | options: { 60 | delay: 500 61 | }, 62 | dev: { 63 | path: 'http://localhost:<%= connect.options.port %>/webpack-dev-server/' 64 | }, 65 | dist: { 66 | path: 'http://localhost:<%= connect.options.port %>/' 67 | } 68 | }, 69 | 70 | karma: { 71 | unit: { 72 | configFile: 'karma.conf.js' 73 | } 74 | }, 75 | 76 | copy: { 77 | dist: { 78 | files: [ 79 | // includes files within path 80 | { 81 | flatten: true, 82 | expand: true, 83 | src: ['<%= pkg.src %>/*'], 84 | dest: '<%= pkg.dist %>/', 85 | filter: 'isFile' 86 | }, 87 | { 88 | flatten: true, 89 | expand: true, 90 | src: ['<%= pkg.src %>/images/*'], 91 | dest: '<%= pkg.dist %>/images/' 92 | } 93 | ] 94 | } 95 | }, 96 | 97 | clean: { 98 | dist: { 99 | files: [{ 100 | dot: true, 101 | src: [ 102 | '<%= pkg.dist %>' 103 | ] 104 | }] 105 | } 106 | } 107 | }); 108 | 109 | grunt.registerTask('serve', function (target) { 110 | if (target === 'dist') { 111 | return grunt.task.run(['build', 'open:dist', 'connect:dist']); 112 | } 113 | 114 | grunt.task.run([ 115 | 'open:dev', 116 | 'webpack-dev-server' 117 | ]); 118 | }); 119 | 120 | grunt.registerTask('test', ['karma']); 121 | 122 | grunt.registerTask('build', ['clean', 'copy', 'webpack']); 123 | 124 | grunt.registerTask('default', []); 125 | }; 126 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "icons-turn-arrow"; 3 | src: url("../fonts/icons/turn-arrow.eot") format("embedded-opentype"), url("../fonts/icons/turn-arrow.woff") format("woff"), url("../fonts/icons/turn-arrow.ttf") format("truetype"), url("../fonts/icons/turn-arrow.svg") format("svg"); 4 | } 5 | 6 | html, body { 7 | width: 100%; 8 | height: 100%; 9 | 10 | background-color: #222; 11 | } 12 | 13 | .content { 14 | width: 100%; 15 | height: 100%; 16 | } 17 | 18 | /* stage -- start */ 19 | .stage { 20 | position: relative; 21 | 22 | width: 100%; 23 | height: 680px; 24 | } 25 | /* stage -- end */ 26 | 27 | /* image -- start */ 28 | .img-sec { 29 | position: relative; 30 | 31 | width: 100%; 32 | height: 100%; 33 | overflow: hidden; 34 | 35 | background-color: #ddd; 36 | 37 | perspective: 1800px; 38 | 39 | @at-root { 40 | .img-figure { 41 | position: absolute; 42 | 43 | width: 320px; 44 | height: 360px; 45 | margin: 0; 46 | padding: 40px; 47 | 48 | background-color: #fff; 49 | 50 | box-sizing: border-box; 51 | cursor: pointer; 52 | transform-origin: 0 50% 0; 53 | transform-style: preserve-3d; 54 | transition: transform .6s ease-in-out, left .6s ease-in-out, top .6s ease-in-out; 55 | 56 | &.is-inverse { 57 | transform: translate(320px) rotateY(180deg); 58 | } 59 | } 60 | 61 | figcaption { 62 | text-align: center; 63 | 64 | .img-title { 65 | margin: 20px 0 0 0; 66 | 67 | color: #a7a0a2; 68 | font-size: 16px; 69 | } 70 | 71 | .img-back { 72 | position: absolute; 73 | top: 0; 74 | left: 0; 75 | 76 | width: 100%; 77 | height: 100%; 78 | padding: 50px 40px; 79 | overflow: auto; 80 | 81 | color: #a7a0a2; 82 | font-size: 22px; 83 | line-height: 1.25; 84 | text-align: left; 85 | 86 | background-color: #fff; 87 | 88 | box-sizing: border-box; 89 | transform: rotateY(180deg) translateZ(1px); 90 | backface-visibility: hidden; 91 | 92 | p { 93 | margin: 0; 94 | } 95 | } 96 | } 97 | } 98 | 99 | } 100 | /* image -- end */ 101 | 102 | /* controller -- start */ 103 | .controller-nav { 104 | position: absolute; 105 | left: 0; 106 | bottom: 30px; 107 | z-index: 101; 108 | 109 | width: 100%; 110 | 111 | text-align: center; 112 | 113 | @at-root { 114 | .controller-unit { 115 | display: inline-block; 116 | margin: 0 5px; 117 | width: 30px; 118 | height: 30px; 119 | 120 | text-align: center; 121 | vertical-align: middle; 122 | 123 | cursor: pointer; 124 | background-color: #aaa; 125 | border-radius: 50%; 126 | 127 | transform: scale(.5); 128 | transition: transform .6s ease-in-out, background-color .3s; 129 | 130 | &.is-center { 131 | background-color: #888; 132 | 133 | transform: scale(1); 134 | 135 | &::after { 136 | color: #fff; 137 | font-family: "icons-turn-arrow"; 138 | font-size: 80%; 139 | line-height: 30px; 140 | 141 | content: "\e600"; 142 | 143 | -webkit-font-smoothing: antialiased; 144 | -moz-osx-font-smoothing: grayscale; 145 | } 146 | 147 | &.is-inverse { 148 | background-color: #555; 149 | 150 | transform: rotateY(180deg); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | /* controller -- end */ 157 | -------------------------------------------------------------------------------- /src/components/GalleryByReactApp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | 5 | // CSS 6 | require('normalize.css'); 7 | require('../styles/main.scss'); 8 | 9 | // 获取图片相关的数据 10 | var imageDatas = require('../data/imageDatas.json'); 11 | 12 | // 利用自执行函数, 将图片名信息转成图片URL路径信息 13 | imageDatas = (function genImageURL(imageDatasArr) { 14 | for (var i = 0, j = imageDatasArr.length; i < j; i++) { 15 | var singleImageData = imageDatasArr[i]; 16 | 17 | singleImageData.imageURL = require('../images/' + singleImageData.fileName); 18 | 19 | imageDatasArr[i] = singleImageData; 20 | } 21 | 22 | return imageDatasArr; 23 | })(imageDatas); 24 | 25 | /* 26 | * 获取区间内的一个随机值 27 | */ 28 | function getRangeRandom(low, high) { 29 | return Math.ceil(Math.random() * (high - low) + low); 30 | } 31 | 32 | /* 33 | * 获取 0~30° 之间的一个任意正负值 34 | */ 35 | function get30DegRandom() { 36 | return ((Math.random() > 0.5 ? '' : '-') + Math.ceil(Math.random() * 30)); 37 | } 38 | 39 | var ImgFigure = React.createClass({ 40 | 41 | /* 42 | * imgFigure 的点击处理函数 43 | */ 44 | handleClick: function (e) { 45 | 46 | if (this.props.arrange.isCenter) { 47 | this.props.inverse(); 48 | } else { 49 | this.props.center(); 50 | } 51 | 52 | e.stopPropagation(); 53 | e.preventDefault(); 54 | }, 55 | 56 | render: function () { 57 | 58 | var styleObj = {}; 59 | 60 | // 如果props属性中指定了这张图片的位置,则使用 61 | if (this.props.arrange.pos) { 62 | styleObj = this.props.arrange.pos; 63 | } 64 | 65 | // 如果图片的旋转角度有值并且不为0, 添加旋转角度 66 | if (this.props.arrange.rotate) { 67 | (['MozTransform', 'msTransform', 'WebkitTransform', 'transform']).forEach(function (value) { 68 | styleObj[value] = 'rotate(' + this.props.arrange.rotate + 'deg)'; 69 | }.bind(this)); 70 | } 71 | 72 | // 如果是居中的图片, z-index设为11 73 | if (this.props.arrange.isCenter) { 74 | styleObj.zIndex = 11; 75 | } 76 | 77 | var imgFigureClassName = 'img-figure'; 78 | imgFigureClassName += this.props.arrange.isInverse ? ' is-inverse' : ''; 79 | 80 | return ( 81 |
82 | {this.props.data.title} 85 |
86 |

{this.props.data.title}

87 |
88 |

89 | {this.props.data.desc} 90 |

91 |
92 |
93 |
94 | ); 95 | } 96 | }); 97 | 98 | // 控制组件 99 | var ControllerUnit = React.createClass({ 100 | handleClick: function (e) { 101 | 102 | // 如果点击的是当前正在选中态的按钮,则翻转图片,否则将对应的图片居中 103 | if (this.props.arrange.isCenter) { 104 | this.props.inverse(); 105 | } else { 106 | this.props.center(); 107 | } 108 | 109 | e.preventDefault(); 110 | e.stopPropagation(); 111 | }, 112 | render: function () { 113 | var controlelrUnitClassName = "controller-unit"; 114 | 115 | // 如果对应的是居中的图片,显示控制按钮的居中态 116 | if (this.props.arrange.isCenter) { 117 | controlelrUnitClassName += " is-center"; 118 | 119 | // 如果同时对应的是翻转图片, 显示控制按钮的翻转态 120 | if (this.props.arrange.isInverse) { 121 | controlelrUnitClassName += " is-inverse"; 122 | } 123 | } 124 | 125 | return ( 126 | 127 | ); 128 | } 129 | }); 130 | 131 | var GalleryByReactApp = React.createClass({ 132 | Constant: { 133 | centerPos: { 134 | left: 0, 135 | right: 0 136 | }, 137 | hPosRange: { // 水平方向的取值范围 138 | leftSecX: [0, 0], 139 | rightSecX: [0, 0], 140 | y: [0, 0] 141 | }, 142 | vPosRange: { // 垂直方向的取值范围 143 | x: [0, 0], 144 | topY: [0, 0] 145 | } 146 | }, 147 | 148 | /* 149 | * 翻转图片 150 | * @param index 传入当前被执行inverse操作的图片对应的图片信息数组的index值 151 | * @returns {Function} 这是一个闭包函数, 其内return一个真正待被执行的函数 152 | */ 153 | inverse: function (index) { 154 | return function () { 155 | var imgsArrangeArr = this.state.imgsArrangeArr; 156 | 157 | imgsArrangeArr[index].isInverse = !imgsArrangeArr[index].isInverse; 158 | 159 | this.setState({ 160 | imgsArrangeArr: imgsArrangeArr 161 | }); 162 | }.bind(this); 163 | }, 164 | 165 | /* 166 | * 重新布局所有图片 167 | * @param centerIndex 指定居中排布哪个图片 168 | */ 169 | rearrange: function (centerIndex) { 170 | var imgsArrangeArr = this.state.imgsArrangeArr, 171 | Constant = this.Constant, 172 | centerPos = Constant.centerPos, 173 | hPosRange = Constant.hPosRange, 174 | vPosRange = Constant.vPosRange, 175 | hPosRangeLeftSecX = hPosRange.leftSecX, 176 | hPosRangeRightSecX = hPosRange.rightSecX, 177 | hPosRangeY = hPosRange.y, 178 | vPosRangeTopY = vPosRange.topY, 179 | vPosRangeX = vPosRange.x, 180 | 181 | imgsArrangeTopArr = [], 182 | topImgNum = Math.floor(Math.random() * 2), // 取一个或者不取 183 | topImgSpliceIndex = 0, 184 | 185 | imgsArrangeCenterArr = imgsArrangeArr.splice(centerIndex, 1); 186 | 187 | // 首先居中 centerIndex 的图片, 居中的 centerIndex 的图片不需要旋转 188 | imgsArrangeCenterArr[0] = { 189 | pos: centerPos, 190 | rotate: 0, 191 | isCenter: true 192 | }; 193 | 194 | // 取出要布局上侧的图片的状态信息 195 | topImgSpliceIndex = Math.ceil(Math.random() * (imgsArrangeArr.length - topImgNum)); 196 | imgsArrangeTopArr = imgsArrangeArr.splice(topImgSpliceIndex, topImgNum); 197 | 198 | // 布局位于上侧的图片 199 | imgsArrangeTopArr.forEach(function (value, index) { 200 | imgsArrangeTopArr[index] = { 201 | pos: { 202 | top: getRangeRandom(vPosRangeTopY[0], vPosRangeTopY[1]), 203 | left: getRangeRandom(vPosRangeX[0], vPosRangeX[1]) 204 | }, 205 | rotate: get30DegRandom(), 206 | isCenter: false 207 | }; 208 | }); 209 | 210 | // 布局左右两侧的图片 211 | for (var i = 0, j = imgsArrangeArr.length, k = j / 2; i < j; i++) { 212 | var hPosRangeLORX = null; 213 | 214 | // 前半部分布局左边, 右半部分布局右边 215 | if (i < k) { 216 | hPosRangeLORX = hPosRangeLeftSecX; 217 | } else { 218 | hPosRangeLORX = hPosRangeRightSecX; 219 | } 220 | 221 | imgsArrangeArr[i] = { 222 | pos: { 223 | top: getRangeRandom(hPosRangeY[0], hPosRangeY[1]), 224 | left: getRangeRandom(hPosRangeLORX[0], hPosRangeLORX[1]) 225 | }, 226 | rotate: get30DegRandom(), 227 | isCenter: false 228 | }; 229 | 230 | } 231 | 232 | if (imgsArrangeTopArr && imgsArrangeTopArr[0]) { 233 | imgsArrangeArr.splice(topImgSpliceIndex, 0, imgsArrangeTopArr[0]); 234 | } 235 | 236 | imgsArrangeArr.splice(centerIndex, 0, imgsArrangeCenterArr[0]); 237 | 238 | this.setState({ 239 | imgsArrangeArr: imgsArrangeArr 240 | }); 241 | }, 242 | 243 | /* 244 | * 利用arrange函数, 居中对应index的图片 245 | * @param index, 需要被居中的图片对应的图片信息数组的index值 246 | * @returns {Function} 247 | */ 248 | center: function (index) { 249 | return function () { 250 | this.rearrange(index); 251 | }.bind(this); 252 | }, 253 | 254 | getInitialState: function () { 255 | return { 256 | imgsArrangeArr: [ 257 | /*{ 258 | pos: { 259 | left: '0', 260 | top: '0' 261 | }, 262 | rotate: 0, // 旋转角度 263 | isInverse: false, // 图片正反面 264 | isCenter: false, // 图片是否居中 265 | }*/ 266 | ] 267 | }; 268 | }, 269 | 270 | // 组件加载以后, 为每张图片计算其位置的范围 271 | componentDidMount: function () { 272 | 273 | // 首先拿到舞台的大小 274 | var stageDOM = React.findDOMNode(this.refs.stage), 275 | stageW = stageDOM.scrollWidth, 276 | stageH = stageDOM.scrollHeight, 277 | halfStageW = Math.ceil(stageW / 2), 278 | halfStageH = Math.ceil(stageH / 2); 279 | 280 | // 拿到一个imageFigure的大小 281 | var imgFigureDOM = React.findDOMNode(this.refs.imgFigure0), 282 | imgW = imgFigureDOM.scrollWidth, 283 | imgH = imgFigureDOM.scrollHeight, 284 | halfImgW = Math.ceil(imgW / 2), 285 | halfImgH = Math.ceil(imgH / 2); 286 | 287 | // 计算中心图片的位置点 288 | this.Constant.centerPos = { 289 | left: halfStageW - halfImgW, 290 | top: halfStageH - halfImgH 291 | }; 292 | 293 | // 计算左侧,右侧区域图片排布位置的取值范围 294 | this.Constant.hPosRange.leftSecX[0] = -halfImgW; 295 | this.Constant.hPosRange.leftSecX[1] = halfStageW - halfImgW * 3; 296 | this.Constant.hPosRange.rightSecX[0] = halfStageW + halfImgW; 297 | this.Constant.hPosRange.rightSecX[1] = stageW - halfImgW; 298 | this.Constant.hPosRange.y[0] = -halfImgH; 299 | this.Constant.hPosRange.y[1] = stageH - halfImgH; 300 | 301 | // 计算上侧区域图片排布位置的取值范围 302 | this.Constant.vPosRange.topY[0] = -halfImgH; 303 | this.Constant.vPosRange.topY[1] = halfStageH - halfImgH * 3; 304 | this.Constant.vPosRange.x[0] = halfStageW - imgW; 305 | this.Constant.vPosRange.x[1] = halfStageW; 306 | 307 | this.rearrange(0); 308 | 309 | }, 310 | 311 | render: function() { 312 | 313 | var controllerUnits = [], 314 | imgFigures = []; 315 | 316 | imageDatas.forEach(function (value, index) { 317 | 318 | if (!this.state.imgsArrangeArr[index]) { 319 | this.state.imgsArrangeArr[index] = { 320 | pos: { 321 | left: 0, 322 | top: 0 323 | }, 324 | rotate: 0, 325 | isInverse: false, 326 | isCenter: false 327 | }; 328 | } 329 | 330 | imgFigures.push(); 331 | 332 | controllerUnits.push(); 333 | }.bind(this)); 334 | 335 | return ( 336 |
337 |
338 | {imgFigures} 339 |
340 | 343 |
344 | ); 345 | } 346 | }); 347 | React.render(, document.getElementById('content')); // jshint ignore:line 348 | 349 | module.exports = GalleryByReactApp; 350 | --------------------------------------------------------------------------------