├── .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 | [![Build Status](https://travis-ci.org/topheman/react-es6-redux.svg?branch=master)](https://travis-ci.org/topheman/react-es6-redux) 5 | [![Coverage Status](https://coveralls.io/repos/github/topheman/react-es6-redux/badge.svg?branch=master)](https://coveralls.io/github/topheman/react-es6-redux?branch=master) 6 | [![Sauce Test Status](https://saucelabs.com/buildstatus/react-es6-redux)](https://saucelabs.com/u/react-es6-redux) 7 | 8 | ![image](http://dev.topheman.com/wp-content/uploads/2015/04/logo-reactjs.png) 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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 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 |
18 | 19 | User Pic 20 | 21 |
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 |
24 | 25 |
26 | {repos.map((repo) => { 27 | return ( 28 | 29 | {repo.name} 30 |
31 | 32 |
33 |
34 | ); 35 | })} 36 |
37 | 38 |
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 |
    54 |
    55 |
    56 | 57 |
    58 | 59 |
    60 |
    61 |
    62 |
    63 | 64 | 65 |
    66 |
    67 |
    68 | 69 |
    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 |