├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .travis.yml
├── README.md
├── bin
├── README.dist.template.md
├── clean-dist.js
├── generate-dist-readme.js
├── nock
│ └── call-nock.js
└── test-build.sh
├── common.js
├── karma.conf.js
├── package.json
├── protractor.config.js
├── src
├── assets
│ └── images
│ │ ├── github-retina.png
│ │ ├── github.png
│ │ ├── react-logo.svg
│ │ ├── twitter-retina.png
│ │ └── twitter.png
├── bootstrap.js
├── components
│ ├── CounterButton
│ │ ├── CounterButton.js
│ │ └── __tests__
│ │ │ └── CounterButton.spec.js
│ ├── Footer
│ │ └── Footer.js
│ ├── Header
│ │ └── Header.js
│ ├── IntroBox
│ │ └── IntroBox.js
│ ├── Profile
│ │ ├── Profile.js
│ │ └── __tests__
│ │ │ └── Profile.spec.js
│ ├── ProfileBox
│ │ ├── ProfileBox.js
│ │ └── __tests__
│ │ │ └── ProfileBox.spec.js
│ ├── ProfileList
│ │ ├── ProfileList.js
│ │ └── __tests__
│ │ │ └── ProfileList.spec.js
│ ├── Repos
│ │ ├── Repos.js
│ │ └── __tests__
│ │ │ └── Repos.spec.js
│ ├── ReposPaginator
│ │ ├── ReposPaginator.js
│ │ └── __tests__
│ │ │ └── ReposPaginator.spec.js
│ ├── SearchBox
│ │ └── SearchBox.js
│ ├── TwitterButton
│ │ └── TwitterButton.js
│ └── common
│ │ ├── DisplayInfosPanel.js
│ │ ├── DisplayStars.js
│ │ ├── Panel.js
│ │ ├── Spinner.js
│ │ ├── Tr.js
│ │ └── __tests__
│ │ ├── DisplayInfosPanel.spec.js
│ │ ├── DisplayStars.spec.js
│ │ ├── Panel.spec.js
│ │ ├── Spinner.spec.js
│ │ └── Tr.spec.js
├── containers
│ ├── App
│ │ └── App.js
│ ├── Github
│ │ └── Github.js
│ ├── GithubUser
│ │ └── GithubUser.js
│ ├── Home
│ │ └── Home.js
│ ├── Lazy
│ │ └── Lazy.js
│ ├── LazyHome
│ │ └── LazyHome.js
│ ├── Redux
│ │ └── Redux.js
│ └── index.js
├── index.ejs
├── redux
│ ├── DevTools.js
│ ├── configure-store.js
│ ├── middleware
│ │ ├── clientMiddleware.js
│ │ └── logger.js
│ └── modules
│ │ ├── __tests__
│ │ ├── counter.spec.js
│ │ └── singleUser.spec.js
│ │ ├── counter.js
│ │ ├── multipleUsers.js
│ │ ├── reducer.js
│ │ └── singleUser.js
├── routes.js
├── services
│ ├── github.js
│ ├── httpService.js
│ ├── httpService
│ │ ├── http.js
│ │ └── http.mock.js
│ └── localStorageWrapper.js
└── style
│ ├── footer.scss
│ ├── header.scss
│ ├── main.scss
│ └── spinner.scss
├── test
├── e2e
│ └── spec
│ │ ├── home.spec.js
│ │ └── search.spec.js
├── fixtures
│ └── http.json
└── unit
│ ├── init.js
│ └── setup.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": ["transform-class-properties", "transform-es2015-destructuring", "transform-object-rest-spread", "add-module-exports"],
4 | "env": {
5 | // only enable it when process.env.NODE_ENV is 'development' or undefined
6 | "development": {
7 | "presets": ["react-hmre"]
8 | },
9 | // configuration for babel-plugin-__coverage__ - see https://github.com/dtinth/babel-plugin-__coverage__#readme
10 | // only enables the coverage loader when process.env.NODE_ENV=mock (used by karma-coverage to create coverage reports)
11 | // I use "mock", you might use "test"
12 | "mock": {
13 | "plugins": [ [ "__coverage__", { "ignore": "src/**/*.spec.js" } ] ]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | # change these settings to your own preference
6 | indent_style = space
7 | indent_size = 2
8 |
9 | # it's recommend to keep these unchanged
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
18 | [{package,bower}.json]
19 | indent_style = space
20 | indent_size = 2
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true,
4 | "jasmine": true,
5 | "protractor": true
6 | },
7 | "globals": {
8 | "goToUrl": true,
9 | "waitUntilIsElementPresent": true,
10 | "sinon": true
11 | },
12 | "extends": "airbnb",
13 | "parser": "babel-eslint",
14 | "rules": {
15 | // disable requiring trailing commas because it might be nice to revert to
16 | // being JSON at some point, and I don't want to make big changes now.
17 | "comma-dangle": 0,
18 | "brace-style": [2, "stroustrup"],
19 | "no-console": 0,
20 | "padded-blocks": 0,
21 | "indent": [2, 2, {"SwitchCase": 1}],
22 | "spaced-comment": 1
23 | },
24 | "plugins":[
25 | "react"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node_modules.bak
3 | .idea
4 | build
5 | *.log
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | env:
5 | global:
6 | - CXX=g++-4.8
7 | - SAUCE_USERNAME=react-es6-redux
8 | - SAUCE_ACCESS_KEY=e5996a82-a35b-4707-b324-79fe310d5ab3
9 | addons:
10 | sauce_connect: true
11 | apt:
12 | sources:
13 | - ubuntu-toolchain-r-test
14 | packages:
15 | - g++-4.8
16 | before_install:
17 | - echo "Updating npm (need at least v3), because of the following problem - https://github.com/npm/npm/issues/11088"
18 | - npm update -g
19 | - npm --version
20 | - time npm i -g yarn@0.24.x --cache-min 999999999
21 | install:
22 | - time yarn
23 | before_script:
24 | - npm run build-prod-all-owner
25 | - npm run build-travis
26 | - "npm run serve-dist > /dev/null &"
27 | script:
28 | - echo "UNIT TESTS - via karma - with code coverage"
29 | - npm test
30 | - echo "UNIT TESTS - via mocha"
31 | - npm run mocha
32 | - echo "END 2 END TESTS - via protractor / through SauceLabs"
33 | - npm run test-e2e -- --port 3000
34 | after_success:
35 | - cat ./build/reports/coverage/**/lcov.info | ./node_modules/.bin/coveralls
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-es6-redux
2 | ===============
3 |
4 | [](https://travis-ci.org/topheman/react-es6-redux)
5 | [](https://coveralls.io/github/topheman/react-es6-redux?branch=master)
6 | [](https://saucelabs.com/u/react-es6-redux)
7 |
8 | 
9 |
10 | This project started as a POC for **React** and has now become my own sandbox for testing the latest technologies. You'll find documentation across the code, the commits and the READMEs helping you implement the following I'm using:
11 |
12 | * [React](https://github.com/facebook/react)
13 | * [React Router](https://github.com/reactjs/react-router)
14 | * [Babel v6](http://babeljs.io/) to transpile ES6+
15 | * [Webpack](http://webpack.github.io/) for bundling
16 | * [Redux](https://github.com/reactjs/redux) for state management
17 | * [Redux Dev Tools](https://github.com/gaearon/redux-devtools) please watch [Dan Abramov about Time Travel at React-Europe](https://www.youtube.com/watch?v=xsSnOQynTHs)
18 | * [React Router Redux](https://github.com/reactjs/react-router-redux) Redux/React Router bindings
19 | * [Eslint](http://eslint.org/) (with [eslint-config-airbnb](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb))
20 | * [style-loader](https://github.com/webpack/style-loader), [sass-loader](https://github.com/jtangelder/sass-loader)
21 | * [babel-preset-react-hmre](https://github.com/danmartinez101/babel-preset-react-hmre) (react-hot-reload for babel v6 - thanks to [react-transform-hmr](https://github.com/gaearon/react-transform-hmr))
22 | * [Karma](https://karma-runner.github.io) Test runner / [PhantomJS](http://phantomjs.org/) Scripted, headless browser
23 | * [Mocha](https://mochajs.org/) / [Chai](http://chaijs.com/) / [Sinon](http://sinonjs.org/) Test framework / Assertion Library / Test spies
24 | * [Enzyme](http://airbnb.io/enzyme/) Testing utilities for React from Airbnb
25 | * [babel-plugin-\_\_coverage\_\_](https://github.com/dtinth/babel-plugin-__coverage__) used with [karma-coverage](https://github.com/karma-runner/karma-coverage), spits out coverage reports **directly on es6 source code**
26 | * [karma-coveralls](https://github.com/caitp/karma-coveralls) (coverage reports in CI mode)
27 | * [Protractor](https://angular.github.io/protractor/) (e2e tests run with [Selenium WebDriver](http://www.seleniumhq.org/) - on [SauceLabs](https://saucelabs.com/u/react-es6-redux) in CI mode)
28 | * [nock](https://github.com/node-nock/nock) / [superagent-mocker](https://github.com/A/superagent-mocker) to record & mock http requests
29 |
30 | The **development / build / deploy workflow** is based on [topheman/webpack-babel-starter](https://github.com/topheman/webpack-babel-starter), which allows to have online both:
31 |
32 | * [production version](https://topheman.github.io/react-es6-redux/) (minified js/css ...)
33 | * [development version](https://topheman.github.io/react-es6-redux/devtools/) (with sourcemaps, so that users could see the original es6 source code, even online, just by opening the sources panel in the devtools console)
34 |
35 | **Support for [Travis CI](https://travis-ci.org/topheman/react-es6-redux)** (see [.travis.yml](https://github.com/topheman/react-es6-redux/blob/master/.travis.yml) file):
36 |
37 | * builds are tested
38 | * source code is linted
39 | * unit tests are run
40 | * code coverage is sent to [coveralls.io](https://coveralls.io/github/topheman/react-es6-redux)
41 | * e2e tests are run through [SauceLabs](https://saucelabs.com/u/react-es6-redux) (a cross-browser automation tool built on top of Selenium WebDriver)
42 |
43 | **Previous versions** (checkout the [releases sections](https://github.com/topheman/react-es6-redux/releases)):
44 |
45 | * The version without redux remains on the [v1.x branch](https://github.com/topheman/react-es6-redux/tree/v1.x).
46 | * The version in babel v5 remains on the [v2.x branch](https://github.com/topheman/react-es6-redux/tree/v2.x)
47 | * You can see the isomorphic (universal if you will) version (with server-side rendering) at [topheman/react-es6-isomorphic](https://github.com/topheman/react-es6-isomorphic/) (based on v1 - not yet with redux at this time).
48 |
49 | To **read further** about this project and its evolution:
50 |
51 | * [Read the WIKI](https://github.com/topheman/react-es6-redux/wiki)
52 | * [Blog post about the upgrade to react v0.14](http://dev.topheman.com/upgraded-to-react-v0-14/)
53 | * [Blog post about the original version](http://dev.topheman.com/playing-with-es6-and-react/)
54 | * [Slides of the ReactJsParis meetup about this project (nov 2015)](http://slides.com/topheman/react-es6-redux)
55 | * [Slides of the ParisJS meetup about this project (jan 2016)](https://topheman.github.io/talks/react-es6-redux/)
56 | * [Blog post about ES6+ code coverage with Babel plugin](http://dev.topheman.com/es6-code-coverage-with-babel-plugin)
57 |
58 | **[ONLINE DEMO](https://topheman.github.io/react-es6-redux/)**
59 |
60 | ### Setup
61 |
62 | This project now follows the same development workflow as the one explained in [topheman/webpack-babel-starter](https://github.com/topheman/webpack-babel-starter) (with some additions, specific to the project).
63 |
64 | #### Install
65 |
66 | If you don't have [yarn](https://yarnpkg.com/lang/en/) yet, just `npm install yarn -g`
67 |
68 | ```shell
69 | git clone https://github.com/topheman/react-es6-redux.git
70 | cd react-es6-redux
71 | yarn
72 | ```
73 |
74 | *Note:* Installing the [topheman-apis-proxy](#with-topheman-apis-proxy) backend is **no longer mandatory** (I changed the code so that you could do unauthenticated request to the github API - you will be [rate limited to 10 requests per minute](https://developer.github.com/v3/search/#rate-limit) though).
75 |
76 | #### Run
77 |
78 | ##### Dev mode
79 |
80 | * `npm start`
81 | * Open [http://localhost:8080/](http://localhost:8080/)
82 |
83 | You're good to go with hot-reload / redux-devtools / time-travel / sourcemaps ...!
84 |
85 | ##### Mock mode
86 |
87 | You can also run the app in mock mode (useful for tests):
88 |
89 | * `npm run webpack-mock`
90 | * Open [http://localhost:8080/](http://localhost:8080/)
91 |
92 |
93 | #### Build
94 |
95 | At the root of the project :
96 |
97 | * `npm run build`: for debug (like in dev - with sourceMaps and all)
98 | * `npm run build-prod`: for production (minified/optimized ...)
99 | * `npm run build-prod-all`: both at once in the same build (with redux devtools & sourcemaps on dev version)
100 |
101 | A `/build/dist` folder will be created with your project built in it.
102 |
103 | You can run it with `npm run serve-build`
104 |
105 | #### Test
106 |
107 | ##### Unit tests
108 |
109 | `npm test` will launch:
110 |
111 | * linting of `/src` & `/test` folders via `eslint`
112 | * the unit-tests files are located in `/src` inside `__tests__` folders, named like `*.spec.js`
113 | * those tests files are run by karma
114 |
115 | This task is launched on `pre-commit` hook & on [Travis CI](https://travis-ci.org/topheman/react-es6-redux).
116 |
117 | If you wish to generate coverage reports, just `npm run karma-coverage` (those reports are generated on Travis CI anyway and available on [coveralls.io](https://coveralls.io/github/topheman/react-es6-redux)), you will find them in local at `/build/reports/coverage`.
118 |
119 | *Note:* Unit-tests are run through karma in PhantomJS (the `webpack.config.js` being injected), they can also be run directly via mocha ([see wiki](https://github.com/topheman/react-es6-redux/wiki/Advanced-tasks#test-tasks)).
120 |
121 | ##### End to end tests
122 |
123 | e2e tests are located in `/test/e2e/spec`.
124 |
125 | Open two terminal tabs, on each one:
126 |
127 | * `npm run webpack-mock`: will launch the project in mock mode
128 | * `npm run test-e2e`: will run the e2e tests in `/test/e2e` via `protractor` against your local server
129 |
130 | Those tests are run on Travis CI, via [SauceLabs](https://saucelabs.com/u/react-es6-redux).
131 |
132 | #### Linter
133 |
134 | I'm using eslint, based on [eslint-config-airbnb](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb), a preset for `.eslintrc` configuration. For more infos, checkout the release it was implemented: [v2.5.0](https://github.com/topheman/react-es6-redux/releases/tag/v2.5.0).
135 |
136 | * `npm run lint`: single run linting of `/src` & `/test` folders
137 | * `npm run lint-watch`: same in watch mode
138 |
139 | #### Specific commands
140 |
141 | You may want some granularity, the `DEVTOOLS`, `SHOW_DEVTOOLS`, `NODE_ENV` & `LINTER` variables are at your disposal:
142 |
143 | * `DEVTOOLS=true npm run build`: will build a debug version with the devtools
144 | * `DEVTOOLS=false npm run webpack`: will launch a webpack dev server without the devtools (if you find it annoying)
145 | * `LINTER=false npm start` (if you don't want to be bothered by the linter - at your own risks! the pre-commit hook will run the linter and the tests anyway)
146 | * `SHOW_DEVTOOLS=false npm start` (if you want to hide the redux-devtools - you'll still be able to show them by `ctrl+H`)
147 | * `DASHBOARD=true npm start`: will use [webpack-dashboard](https://github.com/FormidableLabs/webpack-dashboard) in dev-server mode
148 | * ... you can mix and match ;-)
149 |
150 | **Read the ["Advanced tasks" wiki section](https://github.com/topheman/react-es6-redux/wiki/Advanced-tasks) for more infos ...**
151 |
152 | #### With topheman-apis-proxy
153 |
154 | **This part is optional**
155 |
156 | [topheman-apis-proxy](https://github.com/topheman/topheman-apis-proxy) is a proxy that lets you do authenticated requests to github / twitter APIs (that way you have a much higher rate limit). For the install, please follow the [installation steps](https://github.com/topheman/topheman-apis-proxy#installation) README section.
157 |
158 | Then your workflow will be:
159 |
160 | * Open a terminal in the react-es6-redux folder and `npm run webpack-dev`
161 | * Open a terminal in the topheman-apis-proxy folder and `grunt serve` (see more in the [run in local](https://github.com/topheman/topheman-apis-proxy#run-in-local) README section)
162 | * Go to [http://localhost:8080/](http://localhost:8080/)
163 |
164 | ### Deploy
165 |
166 | I'm using github pages for hosting (free https, easy deploy via git - a good deal since I don't need any server-side logic). You'll find a [gh-pages orphan branch](https://github.com/topheman/react-es6-redux/tree/gh-pages) where the deployed builds are stored.
167 |
168 | My deployment routine is described on the [topheman/webpack-babel-starter Wiki](https://github.com/topheman/webpack-babel-starter/wiki).
169 |
170 | ### Notes
171 |
172 | * `build-prod-all-owner`: build task for [topheman.github.io/react-es6-redux](https://topheman.github.io/react-es6-redux/)
173 |
174 | ### License
175 |
176 | This software is distributed under an MIT licence.
177 |
178 | Copyright 2015-2016 © Christophe Rosset
179 |
180 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software
181 | > and associated documentation files (the "Software"), to deal in the Software without
182 | > restriction, including without limitation the rights to use, copy, modify, merge, publish,
183 | > distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
184 | > Software is furnished to do so, subject to the following conditions:
185 | > The above copyright notice and this permission notice shall be included in all copies or
186 | > substantial portions of the Software.
187 | > The Software is provided "as is", without warranty of any kind, express or implied, including
188 | > but not limited to the warranties of merchantability, fitness for a particular purpose and
189 | > noninfringement. In no event shall the authors or copyright holders be liable for any claim,
190 | > damages or other liability, whether in an action of contract, tort or otherwise, arising from,
191 | > out of or in connection with the software or the use or other dealings in the Software.
--------------------------------------------------------------------------------
/bin/README.dist.template.md:
--------------------------------------------------------------------------------
1 | ## react-es6-redux - distribution version (gh-pages branch)
2 |
3 | This is the distribution version of [topheman/react-es6-redux](https://github.com/topheman/react-es6-redux) - v<%= pkg.version %><% if (urlToCommit !== null) { %> - [#<%= gitRevisionShort %>](<%= urlToCommit %>)<% } %>.
4 |
5 | **Warning**: This is the **generated** code, versionned on the `gh-pages` branch, testable online [here](https://topheman.github.io/react-es6-redux/). If you wish to see the original source code, switch to the [master branch](https://github.com/topheman/react-es6-redux).
6 |
7 | ### Infos:
8 |
9 | Those informations are available on the [topheman/webpack-babel-starter](https://github.com/topheman/webpack-babel-starter) project:
10 |
11 | * [How those files were generated (Readme - build section)](https://github.com/topheman/webpack-babel-starter#build)
12 | * [How to deploy your generated version (Wiki - deploy section)](https://github.com/topheman/webpack-babel-starter/wiki#deploy)
13 |
14 | As explained in the [README](https://github.com/topheman/react-es6-redux#build), when you `npm run build-prod-all`, two versions will be generated:
15 |
16 | * One at the root (the production version)
17 | * One in the [devtools folder](https://github.com/topheman/react-es6-redux/tree/gh-pages/devtools), which contains as you'll see sourcemaps and are not minified.
18 |
19 | Test the demo [here](https://topheman.github.io/react-es6-redux/).
20 |
21 | ------
22 |
23 | You can disable the generation of this file by removing the following line in the `package.json`:
24 |
25 | ```js
26 | "postbuild-prod-all": "npm run generate-dist-readme"
27 | ```
28 |
29 | You can customize the output of this file, the template is located at `bin/README.dist.template.md`.
30 |
--------------------------------------------------------------------------------
/bin/clean-dist.js:
--------------------------------------------------------------------------------
1 | const log = require('npmlog');
2 | log.level = 'silly';
3 | const common = require('../common');
4 |
5 | const ROOT_DIR = common.getRootDir();
6 |
7 | /** run */
8 |
9 | log.info('clean-dist', `Cleaning ...`);
10 | const deleted = require('del').sync([
11 | ROOT_DIR + '/build/dist/*',
12 | ROOT_DIR + '/build/dist/**/*',
13 | ROOT_DIR + '/build/dist/!.git/**/*'
14 | ]);
15 | deleted.forEach(function(e){
16 | console.log(e);
17 | });
18 |
--------------------------------------------------------------------------------
/bin/generate-dist-readme.js:
--------------------------------------------------------------------------------
1 | const log = require('npmlog');
2 | log.level = 'silly';
3 | const fs = require('fs');
4 | const path = require('path');
5 | const common = require('../common');
6 | const _ = {template: require('lodash.template')};
7 | var template;
8 |
9 | const OUTPUT_DIR = './build/dist';
10 |
11 | try {
12 | template = fs.readFileSync(__dirname + '/README.dist.template.md', 'utf8').toString();
13 | }
14 | catch (e) {
15 | log.error('generate-dist-readme', e.message);
16 | process.exit(1);
17 | }
18 |
19 | const infos = common.getInfos();
20 |
21 | template = _.template(template);
22 |
23 | const compiled = template(infos);
24 |
25 | try {
26 | fs.writeFileSync(path.resolve(__dirname, '..', OUTPUT_DIR, 'README.md'), compiled);
27 | log.info('generate-dist-readme', 'Create README.md file for gh-pages at ' + path.resolve(__dirname, '..', OUTPUT_DIR, 'README.md'));
28 | }
29 | catch(e) {
30 | log.error('generate-dist-readme', e.message);
31 | process.exit(1);
32 | }
33 |
--------------------------------------------------------------------------------
/bin/nock/call-nock.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This script records the http requests (requests / response with body and headers)
3 | * so that they could be mocked for unit test.
4 | * This task is automated, this way, you don't have to bother to do it manually ! ;-)
5 | *
6 | * For the moment, I request the github API via topheman-apis-proxy (direct request gives encoded results, which might
7 | * be because of https or gzip which aren't managed yet by nock).
8 | */
9 |
10 | const nock = require('nock');
11 | const request = require('superagent');
12 | const fs = require('fs');
13 | const path = require('path');
14 |
15 | const OUTPUT_PATH = './test/fixtures/http.json';
16 |
17 | nock.recorder.rec({
18 | output_objects: true,
19 | enable_reqheaders_recording: true,
20 | dont_print: true
21 | });
22 |
23 | const uris = [
24 | '/users/topheman',
25 | '/users/topheman/repos?page=1&per_page=15&sort=stars',
26 | '/users/topheman/repos?page=2&per_page=15&sort=stars',
27 | '/users/topheman/repos?page=3&per_page=15&sort=stars',
28 | '/users/topheman/followers',
29 | '/search/users?q=tophe',
30 | '/search/users?q=topheman', // this one is to return "one result"
31 | '/search/users?q=aaazzzeeerrrtttyyyuuuiiioooppp' // this one is to return "no results"
32 | ];
33 |
34 | const promises = uris.map((uri) => {
35 | return request.get('http://localhost:8000/github' + uri)
36 | });
37 |
38 | Promise.all(promises)
39 | .then(() => {
40 | const nockCallObjects = nock.recorder.play();
41 | if (nockCallObjects.length > 0) {
42 | // bellow some processing to cleanup the mock so that they could be correctly used (according to your use-case, it could differ a little)
43 | const output = nockCallObjects.map((item) => {
44 | item.scope = 'http://localhost'; // change the host name (avoid CORS), and protocol (superagent-mocker considers ":" as wildcard, so you can't put a port)
45 | item.path = item.path.replace(/^\/github/,''); // remove the leading "/github" (in topheman-apis-proxy), so that relative url will be used
46 | item.path = item.path.replace('aaazzzeeerrrtttyyyuuuiiioooppp',':username'); // this one is to return "no results"
47 | console.log(item.path);
48 | return item;
49 | });
50 | fs.writeFileSync(OUTPUT_PATH, JSON.stringify(output));
51 | console.log('Saved in ', OUTPUT_PATH);
52 | process.exit(0);
53 | }
54 | throw new Error('No results');
55 | })
56 | .catch(error => console.log('[ERROR]', error.message));
57 |
--------------------------------------------------------------------------------
/bin/test-build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script is inspired from my previous project topheman/vanilla-es6-jspm
4 | #
5 | # https://github.com/topheman/vanilla-es6-jspm/blob/master/bin/test-build.sh
6 | #
7 | # This script will launch the `npm run build` task (based on the env vars)
8 | #
9 | # If your build/ folder is under git management,
10 | # it will git stash your modifications before doing anything and restore them
11 | # at the end of the test (wether it passed or not)
12 |
13 | # don't put this flag, we need to go through
14 | # always stop on errors
15 | # set -e
16 |
17 | echo "This script is deprecated - I maybe upgrade it in the future."
18 | exit 1
19 |
20 | WEBPACK_PATH="$(npm bin)/webpack"
21 |
22 | BUILD_IS_GIT=0
23 | BUILD_IS_GIT_DIRTY=0
24 |
25 | # vars retrieving the exit codes of the commands run
26 | NPM_RUN_BUILD_EXIT_CODE=0
27 | WEBPACK_CLEAN_EXIT_CODE=0
28 |
29 | echo "###### TEST npm run build"
30 |
31 | # If build folder is under git, stash modification - fail if can't stash
32 | if [ -d $(dirname $0)/../build/.git ]
33 | then
34 | BUILD_IS_GIT=1
35 | echo "[INFO] build folder is under git management"
36 | cd $(dirname $0)/../build
37 | echo "[INFO] $(pwd)"
38 |
39 | if [[ -n $(git status --porcelain) ]]
40 | then
41 | BUILD_IS_GIT_DIRTY=1
42 | echo "[INFO] build folder has un-committed changes, stashing them"
43 |
44 | cmd="git stash save -u"
45 | echo "[RUN] $cmd"
46 | eval $cmd
47 | if [ $? -gt 0 ]
48 | then
49 | echo "[WARN] Couldn't stash modifications please commit your files in build folder before proceeding"
50 | exit 1
51 | fi
52 | else
53 | echo "[INFO] build folder repo is clean, nothing to stash"
54 | fi
55 | fi
56 |
57 | cmd="npm run build"
58 | echo "[RUN] $cmd"
59 | eval $cmd
60 | NPM_RUN_BUILD_EXIT_CODE=$?
61 | echo "[DEBUG] npm run build exit code : $NPM_RUN_BUILD_EXIT_CODE";
62 |
63 | cmd="npm run clean"
64 | echo "[RUN] $cmd"
65 | eval $cmd
66 | WEBPACK_CLEAN_EXIT_CODE=$?
67 | echo "[DEBUG] npm run clean exit code : $WEBPACK_CLEAN_EXIT_CODE";
68 |
69 | if [ $WEBPACK_CLEAN_EXIT_CODE -gt 0 ] && [ $BUILD_IS_GIT_DIRTY -gt 0 ]
70 | then
71 | echo "[WARN] Couldn't clean the build folder repo before git unstash"
72 | echo "[WARN] Run the following commands manually to get back your repo in build folder"
73 | echo "[INFO] ./node_modules/.bin/webpack --clean-only"
74 | echo "[INFO] git reset --hard HEAD"
75 | echo "[INFO] git stash pop --index"
76 | exit 1
77 | fi
78 |
79 | # After cleaning build folder, if it is a git repo, point it back to the HEAD
80 | if [ $BUILD_IS_GIT -gt 0 ]
81 | then
82 | echo "[INFO] build folder is under git management, pointing back to HEAD"
83 |
84 | cmd="git reset --hard HEAD"
85 | echo "[RUN] $cmd"
86 | eval $cmd
87 | if [ $? -gt 0 ]
88 | then
89 | echo "[WARN] Couldn't reset --hard HEAD build folder repo"
90 | echo "[WARN] Run the following command manually to get back your repo in build folder"
91 | echo "[INFO] git reset --hard HEAD"
92 | echo "[INFO] git stash pop --index"
93 | exit 1
94 | fi
95 | fi
96 |
97 | # If build folder is a git repo and was dirty, retrieve the stash
98 | if [ $BUILD_IS_GIT_DIRTY -gt 0 ]
99 | then
100 | echo "[INFO] build folder is under git management & has stashed files, retrieving stash"
101 |
102 | cmd="git stash pop --index"
103 | echo "[RUN] $cmd"
104 | eval $cmd
105 | if [ $? -gt 0 ]
106 | then
107 | echo "[WARN] Couldn't unstash build folder repo"
108 | echo "[WARN] Run the following command manually to get back your repo in build folder"
109 | echo "[INFO] git stash pop --index"
110 | exit 1
111 | fi
112 | else
113 | if [ $BUILD_IS_GIT -gt 0 ]
114 | then
115 | echo "[INFO] build folder is under git management but directory was clean at start, nothing to unstash"
116 | fi
117 | fi
118 |
119 | #finally return an exit code according to the npm run build task
120 | if [ $NPM_RUN_BUILD_EXIT_CODE -gt 0 ]
121 | then
122 | echo "[FAILED] npm run build failed. Exiting with code $NPM_RUN_BUILD_EXIT_CODE"
123 | echo "###### END TEST npm run build"
124 | exit $NPM_RUN_BUILD_EXIT_CODE
125 | else
126 | echo "[PASSED] npm run build passed"
127 | echo "###### END TEST npm run build"
128 | exit 0
129 | fi
130 |
--------------------------------------------------------------------------------
/common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | function getRootDir() {
3 | return __dirname;
4 | }
5 |
6 | function getInfos() {
7 | const gitActive = projectIsGitManaged();
8 | const gitRev = require('git-rev-sync');
9 | const moment = require('moment');
10 | const pkg = require('./package.json');
11 | const infos = {
12 | pkg: pkg,
13 | today: moment(new Date()).format('DD/MM/YYYY'),
14 | year: new Date().toISOString().substr(0, 4),
15 | gitRevisionShort: gitActive ? gitRev.short() : null,
16 | gitRevisionLong: gitActive ? gitRev.long() : null,
17 | author: (pkg.author && pkg.author.name) ? pkg.author.name : (pkg.author || null),
18 | urlToCommit: null
19 | };
20 | infos.urlToCommit = gitActive ? _getUrlToCommit(pkg, infos.gitRevisionLong) : null;
21 | return infos;
22 | }
23 |
24 | /**
25 | * Called in default mode by webpack (will format it correctly in comments)
26 | * Called in formatted mode by gulp (for html comments)
27 | * @param {String} mode default/formatted
28 | * @returns {String}
29 | */
30 | function getBanner(mode) {
31 | const infos = getInfos();
32 | const compiled = [
33 | infos.pkg.name,
34 | '',
35 | infos.pkg.description,
36 | '',
37 | `@version v${infos.pkg.version} - ${infos.today}`,
38 | (infos.gitRevisionShort !== null ? `@revision #${infos.gitRevisionShort}` : '') + (infos.urlToCommit !== null ? ` - ${infos.urlToCommit}` : ''),
39 | (infos.author !== null ? `@author ${infos.author}` : ''),
40 | `@copyright ${infos.year}(c)` + (infos.author !== null ? ` ${infos.author}` : ''),
41 | (infos.pkg.license ? `@license ${infos.pkg.license}` : ''),
42 | ''
43 | ].join(mode === 'formatted' ? '\n * ' : '\n');
44 | return compiled;
45 | }
46 |
47 | function getBannerHtml() {
48 | return '\n';
49 | }
50 |
51 | function projectIsGitManaged() {
52 | const fs = require('fs');
53 | const path = require('path');
54 | try {
55 | // Query the entry
56 | const stats = fs.lstatSync(path.join(__dirname,'.git'));
57 |
58 | // Is it a directory?
59 | if (stats.isDirectory()) {
60 | return true;
61 | }
62 | return false;
63 | }
64 | catch (e) {
65 | return false;
66 | }
67 | }
68 |
69 | function _getUrlToCommit(pkg, gitRevisionLong){
70 | let urlToCommit = null;
71 | // if no repository return null
72 | if (typeof pkg.repository === 'undefined') {
73 | return urlToCommit;
74 | }
75 | //retrieve and reformat repo url from package.json
76 | if (typeof(pkg.repository) === 'string') {
77 | urlToCommit = pkg.repository;
78 | }
79 | else if (typeof(pkg.repository.url) === 'string') {
80 | urlToCommit = pkg.repository.url;
81 | }
82 | //check that there is a git repo specified in package.json & it is a github one
83 | if (urlToCommit !== null && /^https:\/\/github.com/.test(urlToCommit)) {
84 | urlToCommit = urlToCommit.replace(/.git$/, '/tree/' + gitRevisionLong);//remove the .git at the end
85 | }
86 | return urlToCommit;
87 | }
88 |
89 | module.exports.getRootDir = getRootDir;
90 | module.exports.getInfos = getInfos;
91 | module.exports.getBanner = getBanner;
92 | module.exports.getBannerHtml = getBannerHtml;
93 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // retrieve args
2 | const argv = require('minimist')(process.argv.slice(2));
3 | const TRAVIS = process.env.TRAVIS;
4 | const COVERAGE = argv.coverage === true || TRAVIS;// code coverage on by default on TRAVIS, or activated by flag --coverage
5 |
6 | // the following env vars are used in webpack.config.js
7 | process.env.UNIT_TEST = true;
8 |
9 | const path = require('path');
10 | const webpackConfig = require('./webpack.config');
11 | const log = require('npmlog');
12 | log.level = 'silly';
13 |
14 | const plugins = [
15 | 'karma-webpack',
16 | 'karma-sinon',
17 | 'karma-mocha',
18 | 'karma-mocha-reporter',
19 | 'karma-sourcemap-loader',
20 | 'karma-chrome-launcher',
21 | 'karma-phantomjs-launcher'
22 | ];
23 | const reporters = ['mocha'];
24 | // default coverage reporter (we may want different reporters between local & CI)
25 | var coverageReporter = {
26 | reporters: [
27 | {type: 'lcov', dir: './build/reports/coverage'}
28 | ]
29 | };
30 |
31 | if (COVERAGE) {
32 | log.info('karma', 'COVERAGE mode enabled');
33 | reporters.push('coverage');
34 | plugins.push('karma-coverage');
35 | }
36 | if (COVERAGE && TRAVIS) {
37 | log.info('karma', 'TRAVIS mode - will send coverage reports to coveralls.io');
38 | reporters.push('coveralls');
39 | plugins.push('karma-coveralls');
40 | coverageReporter = { type: 'lcovonly', dir: './build/reports/coverage' };
41 | }
42 |
43 | module.exports = function(config) {
44 | config.set({
45 | basePath: '',
46 | frameworks: ['mocha', 'sinon'],
47 | files: [
48 | 'test/unit/init.js',// include the throw on console.error
49 | 'src/**/*.spec.js'// unit-test files
50 | ],
51 |
52 | preprocessors: {
53 | // add webpack as preprocessor
54 | 'src/**/*.js': ['webpack', 'sourcemap']
55 | },
56 |
57 | webpack: { //kind of a copy of your webpack config
58 | devtool: 'inline-source-map', //just do inline source maps instead of the default
59 | module: {
60 | loaders: webpackConfig.module.loaders // re-use the exact same loaders declared in webpack.config.js
61 | },
62 | externals: {
63 | 'cheerio': 'window',
64 | 'react/addons': true,
65 | 'react/lib/ExecutionEnvironment': true,
66 | 'react/lib/ReactContext': true
67 | }
68 | },
69 |
70 | webpackServer: {
71 | noInfo: true //please don't spam the console when running in karma!
72 | },
73 |
74 | plugins: plugins,
75 |
76 |
77 | //babelPreprocessor: {
78 | // options: {
79 | // presets: ['airbnb']
80 | // }
81 | //},
82 | coverageReporter: coverageReporter,
83 | reporters: reporters,
84 | port: 9876,
85 | colors: true,
86 | logLevel: config.LOG_INFO,
87 | autoWatch: true,
88 | browsers: ['PhantomJS'],
89 | singleRun: false
90 | })
91 | };
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-es6-redux",
3 | "version": "3.3.0",
4 | "description": "A simple app to try React / ES6 & redux, using topheman-apis-proxy as data api backend",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "DEVTOOLS=true npm run webpack",
8 | "postinstall": "npm run webdriver-manager-update",
9 | "test": "npm run lint && npm run karma",
10 | "test-build": "./bin/test-build.sh",
11 | "karma": "npm run karma-watch -- --single-run",
12 | "karma-watch": "NODE_ENV=mock LINTER=false ./node_modules/karma/bin/karma start",
13 | "karma-coverage": "npm run karma -- --coverage",
14 | "mocha": "NODE_ENV=mock mocha 'src/**/*.spec.js' --compilers js:babel-core/register --recursive --require ./test/unit/setup.js --require ./test/unit/setup.js --require ./test/unit/init.js",
15 | "mocha-watch": "npm run mocha -- --watch",
16 | "unit-test": "echo 'Deprecated, please use \"npm run karma\"' && exit 0",
17 | "unit-test-watch": "echo 'Deprecated, please use \"npm run karma-watch\"' && exit 0",
18 | "test-e2e": "./node_modules/.bin/protractor protractor.config.js",
19 | "lint": "./node_modules/.bin/eslint --ext .js --ext .jsx src test",
20 | "lint-watch": "./node_modules/.bin/esw --watch --ext .js --ext .jsx src test",
21 | "clean-dist": "node ./bin/clean-dist.js",
22 | "serve-build": "echo 'Serving distribution folder build/dist' && npm run serve-dist",
23 | "serve-dist": "./node_modules/.bin/serve build/dist",
24 | "build": "npm run clean-dist && NODE_ENV=production OPTIMIZE=false DEVTOOLS=true SHOW_DEVTOOLS=false npm run webpack-build",
25 | "build-prod": "npm run clean-dist && NODE_ENV=production npm run webpack-build-prod",
26 | "build-prod-owner": "API_ROOT_URL='https://topheman-apis-proxy.herokuapp.com/github' npm run build-prod",
27 | "build-prod-all": "DEVTOOLS=false npm run build-prod && NODE_ENV=production OPTIMIZE=false DEVTOOLS=true DIST_DIR=dist/devtools npm run webpack-build",
28 | "build-prod-all-owner": "API_ROOT_URL='https://topheman-apis-proxy.herokuapp.com/github' npm run build-prod-all",
29 | "postbuild-prod-all": "npm run generate-dist-readme",
30 | "postbuild-prod-all-owner": "npm run generate-dist-readme",
31 | "build-travis": "NODE_ENV=mock API_ROOT_URL='http://localhost' ./node_modules/.bin/webpack --progress -p",
32 | "webpack": "./node_modules/.bin/webpack-dev-server --progress --colors --hot --inline",
33 | "webpack-build": "./node_modules/.bin/webpack --progress",
34 | "webpack-build-prod": "./node_modules/.bin/webpack --progress -p",
35 | "webpack-dev": "API_ROOT_URL='http://localhost:8000/github' DEVTOOLS=true npm run webpack",
36 | "webpack-mock": "NODE_ENV=mock API_ROOT_URL='http://localhost' npm run webpack",
37 | "generate-dist-readme": "node bin/generate-dist-readme.js",
38 | "generate-http-fixtures": "node ./bin/nock/call-nock.js",
39 | "webdriver-manager-update": "./node_modules/.bin/webdriver-manager update"
40 | },
41 | "pre-commit": [
42 | "test"
43 | ],
44 | "repository": {
45 | "type": "git",
46 | "url": "https://github.com/topheman/react-es6-redux.git"
47 | },
48 | "keywords": [
49 | "react",
50 | "ES6"
51 | ],
52 | "author": "Christophe Rosset",
53 | "license": "MIT",
54 | "bugs": {
55 | "url": "https://github.com/topheman/react-es6-redux/issues"
56 | },
57 | "homepage": "https://github.com/topheman/react-es6-redux",
58 | "devDependencies": {
59 | "babel-core": "^6.7.4",
60 | "babel-eslint": "^5.0.0",
61 | "babel-loader": "^6.2.4",
62 | "babel-plugin-__coverage__": "^0.111111.1",
63 | "babel-plugin-add-module-exports": "^0.1.2",
64 | "babel-plugin-transform-class-properties": "^6.6.0",
65 | "babel-plugin-transform-es2015-destructuring": "^6.6.5",
66 | "babel-plugin-transform-object-rest-spread": "^6.6.5",
67 | "babel-preset-es2015": "^6.6.0",
68 | "babel-preset-react": "^6.5.0",
69 | "babel-preset-react-hmre": "^1.1.1",
70 | "chai": "^3.5.0",
71 | "chromedriver": "^2.27.3",
72 | "css-loader": "^0.23.1",
73 | "del": "^2.0.2",
74 | "enzyme": "^2.2.0",
75 | "eslint": "^1.10.3",
76 | "eslint-config-airbnb": "^1.0.2",
77 | "eslint-loader": "^1.3.0",
78 | "eslint-plugin-react": "^4.2.3",
79 | "eslint-watch": "^2.1.7",
80 | "expect": "^1.13.4",
81 | "extract-text-webpack-plugin": "^1.0.1",
82 | "file-loader": "^0.8.4",
83 | "git-rev-sync": "^1.4.0",
84 | "html-webpack-plugin": "^2.9.0",
85 | "jasmine-spec-reporter": "^2.4.0",
86 | "jsdom": "^8.0.1",
87 | "json-loader": "^0.5.3",
88 | "karma": "^0.13.22",
89 | "karma-babel-preprocessor": "^6.0.1",
90 | "karma-chrome-launcher": "^0.2.3",
91 | "karma-coverage": "^0.5.5",
92 | "karma-coveralls": "^1.1.2",
93 | "karma-mocha": "^0.2.2",
94 | "karma-mocha-reporter": "^2.0.0",
95 | "karma-phantomjs-launcher": "^1.0.0",
96 | "karma-sinon": "^1.0.4",
97 | "karma-sourcemap-loader": "^0.3.7",
98 | "karma-webpack": "^1.7.0",
99 | "lodash.template": "^4.2.2",
100 | "minimist": "^1.2.0",
101 | "mocha": "^2.4.5",
102 | "moment": "^2.11.0",
103 | "my-local-ip": "^1.0.0",
104 | "nock": "^7.0.2",
105 | "node-sass": "^3.3.3",
106 | "npmlog": "^2.0.2",
107 | "phantomjs-prebuilt": "^2.1.7",
108 | "pre-commit": "^1.1.2",
109 | "protractor": "^3.0.0",
110 | "react-addons-test-utils": "15.4.x",
111 | "sass-loader": "^3.1.2",
112 | "serve": "^1.4.0",
113 | "sinon": "^1.17.3",
114 | "style-loader": "^0.13.0",
115 | "url-loader": "^0.5.7",
116 | "webpack": "^1.12.14",
117 | "webpack-dashboard": "0.0.1",
118 | "webpack-dev-server": "^1.14.1"
119 | },
120 | "dependencies": {
121 | "bootstrap-sass": "^3.3.5",
122 | "es6-promise": "^3.0.2",
123 | "lscache": "^1.0.5",
124 | "react": "15.4.x",
125 | "react-dom": "15.4.x",
126 | "react-redux": "^4.4.5",
127 | "react-router": "^2.2.4",
128 | "react-router-redux": "^4.0.2",
129 | "redux": "^3.4.0",
130 | "redux-devtools": "^3.2.0",
131 | "redux-devtools-dock-monitor": "^1.1.1",
132 | "redux-devtools-log-monitor": "^1.0.10",
133 | "superagent": "^1.8.3",
134 | "superagent-mocker": "^0.4.0"
135 | },
136 | "private": true
137 | }
138 |
--------------------------------------------------------------------------------
/protractor.config.js:
--------------------------------------------------------------------------------
1 | //require('babel-core/register');// write tests in es6 // removing transpilation (babel transformers are messing with protractor config ...)
2 | var SpecReporter = require('jasmine-spec-reporter');
3 | var pkg = require('./package.json');
4 |
5 | /**
6 | * The default port on which the test will be run is the one specified in package.json under config.port
7 | *
8 | * To overload this port, just pass the flag --port
9 | *
10 | * Use the global goToUrl(relativeUrl) helper (which will use what ever port you defined)
11 | *
12 | */
13 | var argv = require('minimist')(process.argv.slice(2));
14 | var PORT = argv.port || (pkg.config ? (pkg.config.port ? pkg.config.port : null) : null) || 8080;
15 | var BASE_URL = argv['base-url'] || 'http://localhost';
16 | console.log('[INFOS] Testing on ' + BASE_URL + ':' + PORT);
17 |
18 | var specs = [
19 | 'test/e2e/**/*.spec.js'
20 | ];
21 |
22 | var config = {
23 | framework: 'jasmine2',
24 | specs: specs,
25 | chromeDriver: './node_modules/chromedriver/lib/chromedriver/chromedriver',
26 | onPrepare: function () {
27 | browser.ignoreSynchronization = true;
28 | /**
29 | * Helper to use instead of directly `browser.get` so that you don't bother about the port
30 | * baseUrl and port are optional and can be overriden globally when launching protractor
31 | * with the flags --base-url and --port
32 | * @param relativeUrl
33 | * @param baseUrl
34 | * @param port
35 | */
36 | global.goToUrl = function (relativeUrl, baseUrl, port) {
37 | baseUrl = typeof baseUrl === 'undefined' ? BASE_URL : baseUrl;
38 | port = typeof port === 'undefined' ? PORT : port;
39 | return browser.get(baseUrl + ':' + port + relativeUrl);
40 | };
41 | global.waitUntilIsElementPresent = function(element, timeout) {
42 | timeout = typeof timeout !== 'undefined' ? timeout : 4000;
43 | return browser.driver.wait(() => {
44 | return browser.driver.isElementPresent(element);
45 | }, timeout);
46 | };
47 | jasmine.getEnv().addReporter(new SpecReporter());
48 | }
49 | };
50 |
51 | if (process.env.TRAVIS) {
52 | config.sauceUser = process.env.SAUCE_USERNAME;
53 | config.sauceKey = process.env.SAUCE_ACCESS_KEY;
54 | config.capabilities = {
55 | 'name': 'react-es6-redux E2E node v' + process.env.TRAVIS_NODE_VERSION,
56 | 'browserName': 'chrome',
57 | 'seleniumVersion': '2.48.2',
58 | 'chromedriverVersion': '2.20',
59 | 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
60 | 'build': process.env.TRAVIS_BUILD_NUMBER
61 | };
62 | }
63 |
64 | module.exports.config = exports.config = config;
65 |
--------------------------------------------------------------------------------
/src/assets/images/github-retina.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/topheman/react-es6-redux/6daace633102b2d68d2682ab90b21dd509fd9ca5/src/assets/images/github-retina.png
--------------------------------------------------------------------------------
/src/assets/images/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/topheman/react-es6-redux/6daace633102b2d68d2682ab90b21dd509fd9ca5/src/assets/images/github.png
--------------------------------------------------------------------------------
/src/assets/images/react-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
--------------------------------------------------------------------------------
/src/assets/images/twitter-retina.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/topheman/react-es6-redux/6daace633102b2d68d2682ab90b21dd509fd9ca5/src/assets/images/twitter-retina.png
--------------------------------------------------------------------------------
/src/assets/images/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/topheman/react-es6-redux/6daace633102b2d68d2682ab90b21dd509fd9ca5/src/assets/images/twitter.png
--------------------------------------------------------------------------------
/src/bootstrap.js:
--------------------------------------------------------------------------------
1 | global.Promise = global.Promise || require('es6-promise').Promise;
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | import { Router, hashHistory } from 'react-router';
7 | import { syncHistoryWithStore } from 'react-router-redux';
8 | import { Provider } from 'react-redux';
9 |
10 | import routes from './routes.js';
11 |
12 | import configureStore from './redux/configure-store.js';
13 |
14 | // init httpService
15 | import httpService from './services/httpService.js';
16 | httpService.getInstance();
17 |
18 | const initialState = {};
19 |
20 | /**
21 | * The whole store/reducers/actions creators configuration is done inside configureStore
22 | */
23 | const store = configureStore(initialState);
24 |
25 | /**
26 | * Router initialization
27 | * API has changed since react-router-redux v4 - checkout this commit for migration v3 -> v4: https://github.com/davezuko/react-redux-starter-kit/commit/0df26907
28 | */
29 | const syncedHistory = syncHistoryWithStore(hashHistory, store);
30 |
31 | const component = (
32 |
33 | {routes}
34 |
35 | );
36 |
37 | /**
38 | * The linter can be disabled via DISABLE_LINTER env var - show a message in console to inform if it's on or off
39 | * Won't show in production
40 | */
41 | if (process.env.NODE_ENV !== 'production') {
42 | if (!process.env.LINTER) {
43 | console.warn('Linter disabled, make sure to run your code against the linter, otherwise, if it fails, your commit will be rejected');
44 | }
45 | else {
46 | console.info('Linter active, if you meet some problems, you can still run without linter :', 'set the env var LINTER=false', 'More infos in the README');
47 | }
48 | }
49 |
50 | let rootElement = null;
51 |
52 | /**
53 | * Thanks to webpack.DefinePlugin which lets you inject variables at transpile time,
54 | * everything inside the if statement will be dropped at minification if process.env.DEVTOOLS is set to false.
55 | * This is why I don't use static ES6 import but CommonJS import (so that it will only get required in that particular case)
56 | *
57 | * Cause: since the following are debug tools, they are not meant to be a part of the production bundle (which makes it lighter)
58 | *
59 | * https://webpack.github.io/docs/list-of-plugins.html#defineplugin
60 | */
61 | if (process.env.DEVTOOLS) {
62 | console.info('redux devtools active, to hide the panel: ctrl+H, to change position: ctrl+Q - for more infos', 'https://github.com/gaearon/redux-devtools');
63 | const DevTools = require('./redux/DevTools');
64 | rootElement = (
65 |
66 |
67 | {component}
68 |
69 |
70 |
71 | );
72 | }
73 | else {
74 | console.info('redux devtools not active, you can test them online at', 'https://topheman.github.io/react-es6-redux/devtools/');
75 | console.info('if you\'re testing the project in local, please refer to the README to activate them');
76 | rootElement = (
77 |
78 | {component}
79 |
80 | );
81 | }
82 |
83 | if (process.env.NODE_ENV === 'mock') {
84 | console.info('MOCK mode');
85 | }
86 |
87 | ReactDOM.render(rootElement, document.getElementById('app-container'));
88 |
--------------------------------------------------------------------------------
/src/components/CounterButton/CounterButton.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | import { bindActionCreators } from 'redux';
4 | import { connect } from 'react-redux';
5 |
6 | import { increment } from '../../redux/modules/counter.js';// import action creators
7 |
8 | export class CounterButton extends React.Component {
9 |
10 | static propTypes = {
11 | increment: PropTypes.func.isRequired,
12 | counter: PropTypes.number.isRequired
13 | }
14 |
15 | render() {
16 | const { counter, increment: incrementCounter } = this.props;
17 | return (
18 |
21 | );
22 | }
23 | }
24 |
25 | /**
26 | * The connect from react-redux can also be used as an ES7 decorator (babel stage 0)
27 | * 1rst argument: mapStateToProps - (state) => props
28 | * 2nd argument: mapDispatchToProps - (dispatch, [ownProps]) => dispatchProps
29 | *
30 | * Using bindActionCreators : Turns an object whose values are action creators, into an object with the same keys,
31 | * but with every action creator wrapped into a dispatch call so they may be invoked directly.
32 | */
33 | export default connect(
34 | (state) => ({counter: state.counter}), // mapStateToProps - signature : (state) => props
35 | (dispatch) => bindActionCreators({ increment }, dispatch)// mapDispatchToProps (using bindActionCreators helper) - http://rackt.org/redux/docs/api/bindActionCreators.html
36 | // The bindActionCreators results to the following - dispatch in param - wraps the actions in dispatch in a key value object
37 | // (dispatch) => ({
38 | // increment: function(){
39 | // return dispatch(CounterActions.increment());
40 | // }
41 | // })
42 | )(CounterButton);
43 |
--------------------------------------------------------------------------------
/src/components/CounterButton/__tests__/CounterButton.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions, react/prop-types */
2 | import { expect } from 'chai';
3 |
4 | import React from 'react';
5 | import { shallow } from 'enzyme';
6 | import { CounterButton } from '../CounterButton';
7 |
8 | function noop() {}
9 |
10 | describe('components/CounterButton', () => {
11 | describe('state/render', () => {
12 | it('should render the badge from props', () => {
13 | const wrapper = shallow();
14 | expect(wrapper.find('span').text()).to.be.equal('4');
15 | });
16 | });
17 | describe('state/behaviour', () => {
18 | it('should call props.increment onClick on button', () => {
19 | const onButtonClick = sinon.spy();
20 | const wrapper = shallow();
21 | wrapper.find('button').simulate('click');
22 | expect(onButtonClick.calledOnce).to.be.true;
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import TwitterButton from './../TwitterButton/TwitterButton.js';
4 |
5 | const Footer = ({year}) => (
6 |
12 | );
13 |
14 | Footer.propTypes = {
15 | year: React.PropTypes.number.isRequired
16 | };
17 |
18 | export default Footer;
19 |
--------------------------------------------------------------------------------
/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IndexLink, Link } from 'react-router';
3 |
4 | export default class Header extends React.Component {
5 |
6 | static propTypes = {
7 | title: React.PropTypes.string
8 | }
9 |
10 | constructor(props) {
11 |
12 | super(props);
13 |
14 | // init context bindings - due to diff between React.createClass and ES6 class
15 | this.handleClick = this.handleClick.bind(this);
16 |
17 | // init state
18 | this.state = {
19 | collapsed: true
20 | };
21 |
22 | }
23 | handleClick() {
24 | const { collapsed } = this.state;
25 | this.setState({collapsed: !collapsed});
26 | }
27 | render() {
28 |
29 | const collapsedMenuClassName = 'collapse navbar-collapse' + (this.state.collapsed === true ? '' : ' in');
30 | const { title } = this.props;
31 |
32 | return (
33 |
34 |
57 |
58 |
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/IntroBox/IntroBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IntroBox = () => (
4 |
5 |
This is only a simple feature, search Github users ...
6 |
All the UI is coded in React and ES6, using only isomorphic techs like superagent for the AJAX request (so that it could also work server-side).
7 |
8 | );
9 |
10 | export default IntroBox;
11 |
--------------------------------------------------------------------------------
/src/components/Profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Panel from '../common/Panel.js';
4 | import Tr from '../common/Tr.js';
5 | import DisplayInfosPanel from '../common/DisplayInfosPanel.js';
6 |
7 | const Profile = ({profile}) => {
8 | if (profile && profile.data) {
9 | const user = profile.data;
10 | user.$githubProfileHref = user.html_url;
11 | user.$githubProfileHrefTitle = 'Visit ' + user.login + ' profile on Github';
12 | user.$avatar_url = user.avatar_url + '&s=130';
13 | return (
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 | return (
41 |
42 | );
43 | };
44 |
45 | Profile.propTypes = {
46 | profile: React.PropTypes.object.isRequired
47 | };
48 |
49 | export default Profile;
50 |
--------------------------------------------------------------------------------
/src/components/Profile/__tests__/Profile.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions, react/prop-types */
2 | import { expect } from 'chai';
3 |
4 | import React from 'react';
5 | import { shallow } from 'enzyme';
6 | import Profile from '../Profile';
7 | import Panel from '../../common/Panel';
8 | import DisplayInfosPanel from '../../common/DisplayInfosPanel';
9 |
10 | const mockProfile = {
11 | data: {
12 | html_url: 'https://github.com/topheman',
13 | login: 'topheman',
14 | avatar_url: 'https://avatars.githubusercontent.com/u/985982?v=3',
15 | name: 'Christophe Rosset',
16 | location: 'Paris',
17 | created_at: '2011-08-17T11:59:42Z',
18 | blog: 'http://labs.topheman.com/',
19 | followers: 37,
20 | following: 2,
21 | bio: 'JavaScript FTW'
22 | }
23 | };
24 |
25 | describe('components/Profile', () => {
26 | describe('state/render', () => {
27 | describe('profile fully hydrated', () => {
28 | const wrapper = shallow();
29 | it('should render a Panel with props.profile.data.login as title', () => {
30 | expect(wrapper.find(Panel).props().title).to.be.equal('topheman');
31 | });
32 | it('should render a proper anchor with link & title', () => {
33 | const a = wrapper.find('a').first();
34 | expect(a.props().href).to.be.equal('https://github.com/topheman');
35 | expect(a.props().title).to.be.equal('Visit topheman profile on Github');
36 | });
37 | it('should render a proper image profile with correct avatar url (adapted)', () => {
38 | expect(wrapper.find('img').first().props().src).to.be.equal('https://avatars.githubusercontent.com/u/985982?v=3&s=130');
39 | });
40 | });
41 | describe('profile not fully hydrated', () => {
42 | it('should return a placeholder panel', () => {
43 | const wrapper = shallow();
44 | expect(wrapper.equals()).to.be.true;
45 | });
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/components/ProfileBox/ProfileBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Link } from 'react-router';
4 |
5 | const ProfileBox = ({user}) => {
6 | const link = '/github/user/' + user.login;
7 | return (
8 |
9 |
{user.login}
10 |
11 | );
12 | };
13 |
14 | ProfileBox.propTypes = {
15 | user: React.PropTypes.shape({
16 | login: React.PropTypes.string.isRequired,
17 | $avatar_url: React.PropTypes.string.isRequired
18 | })
19 | };
20 |
21 | export default ProfileBox;
22 |
--------------------------------------------------------------------------------
/src/components/ProfileBox/__tests__/ProfileBox.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions, react/prop-types */
2 | import { expect } from 'chai';
3 |
4 | import React from 'react';
5 | import { shallow } from 'enzyme';
6 | import ProfileBox from '../ProfileBox';
7 | import { Link } from 'react-router';
8 |
9 | const mockUser = {
10 | login: 'topheman',
11 | $avatar_url: 'https://avatars.githubusercontent.com/u/985982?v=3&s=130'
12 | };
13 |
14 | describe('components/ProfileBox', () => {
15 | describe('state/render', () => {
16 | const wrapper = shallow();
17 | it('should render link with correct props', () => {
18 | expect(wrapper.find(Link).prop('to')).to.be.equal('/github/user/topheman');
19 | });
20 | it('should render an img with src same as props.user.$avatar_url', () => {
21 | expect(wrapper.contains(
)).to.be.true;
22 | });
23 | it('should render the user login', () => {
24 | expect(wrapper.contains(topheman)).to.be.true;
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/ProfileList/ProfileList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | import ProfileBox from './../ProfileBox/ProfileBox.js';
5 |
6 | const ProfileList = ({results}) => {
7 | if (results === null) {
8 | return (
9 |
10 | Just search for a Github user or organization ... or access directly to my profile.
11 |
12 | );
13 | }
14 | else if (results.error) {
15 | return (
16 |
17 | {results.error}
18 |
19 | );
20 | }
21 | else if (results.total_count === 0) {
22 | return (
23 |
24 | No results.
25 |
26 | );
27 | }
28 | return (
29 |
30 |
Total result{results.total_count > 1 ? 's' : ''} : {results.total_count} / showing : {results.items.length}
31 |
32 | {results.items.map((user) => {
33 | user.$avatar_url = user.avatar_url + '&s=40';
34 | return
;
35 | })}
36 |
37 |
38 | );
39 | };
40 |
41 | ProfileList.propTypes = {
42 | results: React.PropTypes.object
43 | };
44 |
45 | export default ProfileList;
46 |
--------------------------------------------------------------------------------
/src/components/ProfileList/__tests__/ProfileList.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions, react/prop-types */
2 | import { expect } from 'chai';
3 |
4 | import React from 'react';
5 | import { shallow } from 'enzyme';
6 | import ProfileList from '../ProfileList';
7 | import ProfileBox from '../../ProfileBox/ProfileBox';
8 |
9 | describe('components/ProfileList', () => {
10 | describe('state/render', () => {
11 | describe('case props.results not hydrated with data', () => {
12 | it('should render default message if props.results = null', () => {
13 | const wrapper = shallow();
14 | expect(wrapper.type()).to.be.equal('p');
15 | });
16 | it('should render the error if props.results.error is set', () => {
17 | const wrapper = shallow();
18 | expect(wrapper.equals(Something went wrong
)).to.be.true;
19 | });
20 | it('should show "No results" if props.results.total_count = 0', () => {
21 | const wrapper = shallow();
22 | expect(wrapper.equals(No results.
)).to.be.true;
23 | });
24 | });
25 | describe('case props.results hydrated with data', () => {
26 | it('header should show "Total result" in case props.results.total_count = 1', () => {
27 | const wrapper = shallow();
28 | expect(wrapper.find('.panel-heading').text()).to.equal('Total result : 1 / showing : 1');
29 | });
30 | it('header should show "Total results" in case props.results.total_count > 1', () => {
31 | const wrapper = shallow();
32 | expect(wrapper.find('.panel-heading').text()).to.equal('Total results : 2 / showing : 2');
33 | });
34 | it('should render correctly the ProfileBox in the list', () => {
35 | const wrapper = shallow();
36 | expect(wrapper.find(ProfileBox).first().prop('user').id).to.equal(42);
37 | });
38 | it('should render correctly multiple ProfileBox in the list', () => {
39 | const wrapper = shallow();
40 | expect(wrapper.find(ProfileBox).length).to.equal(2);
41 | });
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/components/Repos/Repos.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Panel from '../common/Panel.js';
4 | import DisplayInfosPanel from '../common/DisplayInfosPanel.js';
5 | import DisplayStars from '../common/DisplayStars.js';
6 | import ReposPaginator from '../ReposPaginator/ReposPaginator.js';
7 |
8 | export default class Repos extends React.Component {
9 |
10 | static propTypes = {
11 | repositories: React.PropTypes.object.isRequired,
12 | reposGotoPage: React.PropTypes.func.isRequired
13 | }
14 |
15 | render() {
16 | const { repositories, reposGotoPage } = this.props;
17 | const fetching = repositories.fetching;
18 | const originalTitle = repositories.pristineLogin + "'s repositories";
19 | if (repositories && repositories.data) {
20 | const repos = repositories.data;
21 | return (
22 |
23 |
39 |
40 | );
41 | }
42 | return (
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Repos/__tests__/Repos.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions, react/prop-types */
2 | import { expect } from 'chai';
3 |
4 | import React from 'react';
5 | import { shallow } from 'enzyme';
6 | import Repos from '../Repos';
7 | import DisplayInfosPanel from '../../common/DisplayInfosPanel';
8 | import DisplayStars from '../../common/DisplayStars';
9 |
10 | function noop() {}
11 |
12 | describe('components/ProfileBox', () => {
13 | describe('state/render', () => {
14 | it('should render a placeholder if props.repositories.data not hydrated', () => {
15 | const mockRepositories = { fetching: false, pristineLogin: 'topheman' };
16 | const wrapper = shallow();
17 | expect(wrapper.equals()).to.be.true;
18 | });
19 | describe('check render for list of repositories', () => {
20 | const mockRepositories = {
21 | fetching: false,
22 | infos: {},
23 | pristineLogin: 'topheman',
24 | data: [
25 | { html_url: 'https://github.com/topheman/react-es6-redux', name: 'react-es6-redux', stargazers_count: 57, full_name: 'topheman/react-es6-redux' },
26 | { html_url: 'https://github.com/topheman/webpack-babel-starter', name: 'webpack-babel-starter', stargazers_count: 39, full_name: 'topheman/webpack-babel-starter' },
27 | { html_url: 'https://github.com/topheman/rxjs-experiments', name: 'rxjs-experiments', stargazers_count: 19, full_name: 'topheman/rxjs-experiments' }
28 | ]
29 | };
30 | const wrapper = shallow();
31 | it('should render the correct amount of links to repos', () => {
32 | expect(wrapper.find('a').length).to.equal(3);
33 | });
34 | it('should render links properly', () => {
35 | const anchor = wrapper.find('a').at(1);
36 | expect(anchor.prop('href')).to.equal('https://github.com/topheman/webpack-babel-starter');
37 | expect(anchor.prop('title')).to.equal('topheman/webpack-babel-starter');
38 | });
39 | it('should render stargazer properly', () => {
40 | const displayStars = wrapper.find(DisplayStars).at(1);
41 | expect(displayStars.prop('number')).to.equal(39);
42 | });
43 | });
44 | describe('check paginators', () => {
45 |
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/components/ReposPaginator/ReposPaginator.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Spinner from '../common/Spinner.js';
4 |
5 | export default class ReposPaginator extends React.Component {
6 |
7 | static propTypes = {
8 | infos: React.PropTypes.object.isRequired,
9 | reposGotoPage: React.PropTypes.func.isRequired,
10 | fetching: React.PropTypes.bool.isRequired
11 | }
12 |
13 | constructor(props) {
14 | super(props);
15 | this.gotoPage = this.gotoPage.bind(this);
16 | this.gotoNextPage = this.gotoNextPage.bind(this);
17 | this.gotoPreviousPage = this.gotoPreviousPage.bind(this);
18 | this.gotoFirstPage = this.gotoFirstPage.bind(this);
19 | this.gotoLastPage = this.gotoLastPage.bind(this);
20 | this.getClickGotoPageHandler = this.getClickGotoPageHandler.bind(this);
21 | }
22 |
23 | getClickGotoPageHandler(methodName) {
24 | return function gotoPageHandler(e) {
25 | e.preventDefault();
26 | this[methodName]();
27 | }.bind(this);
28 | }
29 |
30 | gotoPage(pageNum) {
31 | this.props.reposGotoPage(pageNum);
32 | }
33 |
34 | gotoNextPage() {
35 | this.gotoPage(this.props.infos.page + 1);
36 | }
37 |
38 | gotoPreviousPage() {
39 | this.gotoPage(this.props.infos.page - 1);
40 | }
41 |
42 | gotoFirstPage() {
43 | this.gotoPage(1);
44 | }
45 |
46 | gotoLastPage() {
47 | this.gotoPage(this.props.infos.totalPages);
48 | }
49 |
50 | render() {
51 | const { infos, fetching } = this.props;
52 | if (infos.totalPages > 1) {
53 | let firstPage;
54 | let previousPage;
55 | let nextPage;
56 | let lastPage;
57 | if (infos.page > 1) {
58 | firstPage = (
59 |
60 |
61 | ←←
62 |
63 |
64 | );
65 | }
66 | if (infos.page > 2) {
67 | previousPage = (
68 |
69 |
70 | ←
71 |
72 |
73 | );
74 | }
75 | if (infos.page < (infos.totalPages - 1) ) {
76 | nextPage = (
77 |
78 |
79 | →
80 |
81 |
82 | );
83 | }
84 | if (infos.page <= (infos.totalPages - 1) ) {
85 | lastPage = (
86 |
87 |
88 | →→
89 |
90 |
91 | );
92 | }
93 | return (
94 |
95 |
96 |
104 |
105 | );
106 | }
107 | return (
108 |
109 | );
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/components/ReposPaginator/__tests__/ReposPaginator.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions, react/prop-types */
2 | import { expect } from 'chai';
3 |
4 | import React from 'react';
5 | import { shallow } from 'enzyme';
6 | import ReposPaginator from '../ReposPaginator';
7 |
8 | function noop() {}
9 | const syntheticEventData = {
10 | preventDefault: function preventDefault() {}
11 | };
12 |
13 | describe('components/ReposPaginator', () => {
14 | describe('state/render', () => {
15 | it('should render empty p if props.infos.totalPages < 1', () => {
16 | const wrapper = shallow();
17 | expect(wrapper.equals()).to.be.true;
18 | });
19 | it('should render link to first page if props.infos.page > 1', () => {
20 | const wrapper = shallow();
21 | const li = wrapper.find('li').first();
22 | expect(li.prop('className')).to.be.equal('previous');
23 | expect(li.find('a').prop('title')).to.be.equal('First page');
24 | });
25 | it('should render link to previous page if props.infos.page > 2', () => {
26 | const wrapper = shallow();
27 | const li = wrapper.find('li').at(1);
28 | expect(li.prop('className')).to.be.equal('previous');
29 | expect(li.find('a').prop('title')).to.be.equal('Previous page');
30 | });
31 | it('should render link to next page if props.infos.page {
32 | const wrapper = shallow();
33 | const li = wrapper.find('li').at(3);
34 | expect(li.prop('className')).to.be.equal('next');
35 | expect(li.find('a').prop('title')).to.be.equal('Next page');
36 | });
37 | it('should render link to last page if props.infos.page<=props.infos.totalPages - 1', () => {
38 | const wrapper = shallow();
39 | const li = wrapper.find('li').at(2);
40 | expect(li.prop('className')).to.be.equal('next');
41 | expect(li.find('a').prop('title')).to.be.equal('Last page');
42 | });
43 | });
44 | describe('state/behaviour', () => {
45 | it('click on "first page" should call props.reposGotoPage(1)', () => {
46 | const spy = sinon.spy();
47 | const wrapper = shallow();
48 | const first = wrapper.find('li').at(0).find('a');
49 | first.simulate('click', syntheticEventData);
50 | expect(spy.calledOnce).to.be.true;
51 | expect(spy.calledWith(1)).to.be.true;
52 | });
53 | it('click on "previous page" should call props.reposGotoPage(props.infos.page - 1)', () => {
54 | const spy = sinon.spy();
55 | const wrapper = shallow();
56 | const first = wrapper.find('li').at(1).find('a');
57 | first.simulate('click', syntheticEventData);
58 | expect(spy.calledOnce).to.be.true;
59 | expect(spy.calledWith(4)).to.be.true;
60 | });
61 | it('click on "next page" should call props.reposGotoPage(props.infos.page + 1)', () => {
62 | const spy = sinon.spy();
63 | const wrapper = shallow();
64 | const first = wrapper.find('li').at(3).find('a');
65 | first.simulate('click', syntheticEventData);
66 | expect(spy.calledOnce).to.be.true;
67 | expect(spy.calledWith(6)).to.be.true;
68 | });
69 | it('click on "last page" should call props.reposGotoPage(props.infos.totalPages)', () => {
70 | const spy = sinon.spy();
71 | const wrapper = shallow();
72 | const first = wrapper.find('li').at(2).find('a');
73 | first.simulate('click', syntheticEventData);
74 | expect(spy.calledOnce).to.be.true;
75 | expect(spy.calledWith(10)).to.be.true;
76 | });
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/src/components/SearchBox/SearchBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProfileList from './../ProfileList/ProfileList.js';
3 | import Spinner from '../common/Spinner.js';
4 |
5 | /**
6 | * This component doesn't have state nor it has to know about connect or redux.
7 | * Everything it needs is passed down via props (and lets it update the state of its parent)
8 | */
9 |
10 | class SearchBox extends React.Component {
11 |
12 | static propTypes = {
13 | changeUsername: React.PropTypes.func.isRequired,
14 | findUsers: React.PropTypes.func.isRequired,
15 | username: React.PropTypes.string.isRequired,
16 | fetching: React.PropTypes.bool.isRequired,
17 | results: React.PropTypes.object
18 | }
19 |
20 | constructor(props) {
21 |
22 | super(props);
23 |
24 | // init context bindings - due to diff between React.createClass and ES6 class
25 | this.handleSubmit = this.handleSubmit.bind(this);
26 | this.handleChange = this.handleChange.bind(this);
27 |
28 | }
29 | handleFocus(e) {
30 | const target = e.target;
31 | // dirty but curiously in React this is a known bug and workaround ...
32 | setTimeout(() => {
33 | target.select();
34 | }, 0);
35 | }
36 | handleSubmit(e) {
37 | e.preventDefault();
38 | document.getElementById('user-name').blur();
39 | const currentUser = this.props.username;
40 | // prevent submiting empty user
41 | if (currentUser !== '') {
42 | this.props.findUsers(currentUser);
43 | }
44 | }
45 | handleChange() {
46 | const node = this.refs.input;
47 | const username = node.value.trim();
48 | this.props.changeUsername(username);
49 | }
50 | render() {
51 | const { username, results, fetching } = this.props;
52 | return (
53 |
70 | );
71 | }
72 | }
73 |
74 | export default SearchBox;
75 |
--------------------------------------------------------------------------------
/src/components/TwitterButton/TwitterButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /**
4 | * This component renders directly the iframe of twitter without running external script
5 | * to avoid messing up with react's internal DOM and break react hot loader
6 | *
7 | * @todo make a more generic version
8 | * @note : Tweet
9 | */
10 | export default class TwitterButton extends React.Component {
11 | render() {
12 | return (
13 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/common/DisplayInfosPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Panel from './Panel.js';
4 | import Spinner from './Spinner.js';
5 |
6 | const DisplayInfosPanel = ({infos, originalTitle}) => {
7 | const fetching = infos ? infos.fetching : false;
8 | if (infos && infos.error) {
9 | const { error } = infos;
10 | return (
11 |
12 |
13 |
14 |
15 | {error}
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | // initial case before xhr
23 | // better speed loading perception if username already present
24 | return (
25 |
26 |
31 |
32 | );
33 | };
34 |
35 | DisplayInfosPanel.propTypes = {
36 | infos: React.PropTypes.object,
37 | originalTitle: React.PropTypes.string
38 | };
39 |
40 | export default DisplayInfosPanel;
41 |
--------------------------------------------------------------------------------
/src/components/common/DisplayStars.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const DisplayStars = ({number}) => {
4 | if (number > 0) {
5 | return (
6 |
7 | {number}
8 |
9 | );
10 | }
11 | return null;
12 | };
13 |
14 | DisplayStars.propTypes = {
15 | number: React.PropTypes.number
16 | };
17 |
18 | export default DisplayStars;
19 |
--------------------------------------------------------------------------------
/src/components/common/Panel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Panel = ({title, children}) => (
4 |
5 |
6 |
7 |
{title}
8 |
9 | {children}
10 |
11 |
12 | );
13 |
14 | Panel.propTypes = {
15 | title: React.PropTypes.string.isRequired,
16 | children: React.PropTypes.element.isRequired,
17 | };
18 |
19 | export default Panel;
20 |
--------------------------------------------------------------------------------
/src/components/common/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Spinner = ({fetching}) => {
4 | if (fetching === true) {
5 | return (
6 |
11 | );
12 | }
13 | return (
14 |
15 | );
16 | };
17 |
18 | Spinner.propTypes = {
19 | fetching: React.PropTypes.bool
20 | };
21 |
22 | export default Spinner;
23 |
--------------------------------------------------------------------------------
/src/components/common/Tr.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Tr = ({label, value, type, display}) => {
4 | let _value = value;
5 | if (typeof _value !== 'undefined' && !!_value) {
6 | if (_value instanceof Date) {
7 | _value = _value.toString().split(' ').slice(0, 4).join(' ');// ok I took a very simple way ;-)
8 | }
9 | if (type === 'link') {
10 | _value = {_value};
11 | }
12 |
13 | if (display !== 'colspan') {
14 | return (
15 |
16 | {label} |
17 | {_value} |
18 |
19 | );
20 | }
21 | return (
22 |
23 | {_value} |
24 |
25 | );
26 | }
27 | // won't return if so, getting following : Warning: validateDOMNesting(...):