├── 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 |
--------------------------------------------------------------------------------
/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 | [](https://travis-ci.org/materliu/gallery-by-react) [](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 |  |  |  |  | 
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 |
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 |
340 |
343 |
344 | );
345 | }
346 | });
347 | React.render(, document.getElementById('content')); // jshint ignore:line
348 |
349 | module.exports = GalleryByReactApp;
350 |
--------------------------------------------------------------------------------