├── .eslintignore ├── .eslintrc-base ├── .eslintrc-client ├── .eslintrc-client-test ├── .eslintrc-server ├── .eslintrc-server-test ├── .gitignore ├── .istanbul.func.yml ├── .istanbul.server-rest.yml ├── .istanbul.server-unit.yml ├── .travis.yml ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── README.md ├── appveyor.yml ├── client ├── actions │ └── index.js ├── app.jsx ├── components │ ├── convert.jsx │ ├── error-panel.jsx │ ├── input.jsx │ ├── output-panel.jsx │ ├── output.jsx │ ├── types-title.jsx │ └── types.jsx ├── containers │ └── page.jsx ├── reducers │ └── index.js ├── store │ └── create-store.js ├── styles │ └── app.css └── utils │ ├── api.js │ ├── query.js │ └── types.js ├── heroku ├── doc │ ├── _tmpl │ │ └── layout.jade │ ├── contributing.jade │ ├── development.jade │ ├── index.jade │ └── public │ │ ├── site.css │ │ └── site.js └── scripts │ ├── cluster.js │ ├── install.js │ ├── not-heroku.js │ └── server.js ├── karma.conf.coverage.js ├── karma.conf.dev.js ├── karma.conf.js ├── package.json ├── server ├── converter.js ├── index-dev.js ├── index-hot.js ├── index.js └── middleware.js ├── templates └── index.jsx ├── test ├── client │ ├── main.js │ ├── spec │ │ ├── base.spec.js │ │ └── components │ │ │ └── types-title.spec.jsx │ └── test.html ├── func │ ├── mocha.dev.opts │ ├── mocha.opts │ ├── setup.dev.js │ ├── setup.js │ ├── spec │ │ ├── application.spec.js │ │ └── base.spec.js │ └── util │ │ └── promise-done.js └── server │ ├── mocha.opts │ ├── rest │ ├── api.spec.js │ └── base.spec.js │ ├── setup.js │ └── spec │ ├── base.spec.js │ └── converter.spec.js ├── webpack.config.coverage.js ├── webpack.config.dev.js ├── webpack.config.hot.js ├── webpack.config.js └── webpack.config.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* 3 | -------------------------------------------------------------------------------- /.eslintrc-base: -------------------------------------------------------------------------------- 1 | --- 2 | # Any base overrides can go here. -------------------------------------------------------------------------------- /.eslintrc-client: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "defaults/configurations/walmart/es6-react" 4 | - ".eslintrc-base" 5 | -------------------------------------------------------------------------------- /.eslintrc-client-test: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "defaults/configurations/walmart/es6-react" 4 | - ".eslintrc-base" 5 | 6 | env: 7 | mocha: true 8 | 9 | globals: 10 | expect: false 11 | sandbox: false 12 | 13 | rules: 14 | no-unused-expressions: 0 # Disable for Chai expression assertions. -------------------------------------------------------------------------------- /.eslintrc-server: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "defaults/configurations/walmart/es5-node" 4 | - ".eslintrc-base" 5 | 6 | globals: 7 | fetch: false 8 | -------------------------------------------------------------------------------- /.eslintrc-server-test: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "defaults/configurations/walmart/es5-node" 4 | - ".eslintrc-base" 5 | 6 | env: 7 | mocha: true 8 | 9 | globals: 10 | fetch: false 11 | expect: false 12 | sandbox: false 13 | 14 | rules: 15 | no-unused-expressions: 0 # Disable for Chai expression assertions. 16 | max-nested-callbacks: 0 # Disable for nested describes. 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \.git 2 | \.hg 3 | 4 | \.DS_Store 5 | \.project 6 | bower_components 7 | node_modules 8 | npm-debug\.log 9 | 10 | # Build 11 | dist 12 | coverage 13 | Procfile 14 | -------------------------------------------------------------------------------- /.istanbul.func.yml: -------------------------------------------------------------------------------- 1 | reporting: 2 | dir: coverage/func 3 | reports: 4 | - lcov 5 | - json 6 | - text-summary 7 | -------------------------------------------------------------------------------- /.istanbul.server-rest.yml: -------------------------------------------------------------------------------- 1 | reporting: 2 | dir: coverage/server/rest 3 | reports: 4 | - lcov 5 | - json 6 | - text-summary 7 | -------------------------------------------------------------------------------- /.istanbul.server-unit.yml: -------------------------------------------------------------------------------- 1 | reporting: 2 | dir: coverage/server/unit 3 | reports: 4 | - lcov 5 | - json 6 | - text-summary 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | - 0.12 6 | 7 | # Use container-based Travis infrastructure. 8 | sudo: false 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | before_install: 15 | # GUI for real browsers. 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | 19 | before_script: 20 | # Install dev. stuff (e.g., selenium drivers). 21 | - npm run install-dev 22 | 23 | env: 24 | # NOTE: **Cannot** have a space after `:` character in JSON string or else 25 | # YAML parser will fail to parse correctly. 26 | global: 27 | # PhantomJS fails currently. (ROWDY_SETTINGS="local.phantomjs") 28 | # https://github.com/FormidableLabs/converter-react/issues/34 29 | - ROWDY_SETTINGS="local.firefox" 30 | 31 | script: 32 | # Run all base checks (with FF browser for functional tests). 33 | - npm run check-ci 34 | 35 | # Manually send coverage reports to coveralls. 36 | # - Aggregate client results 37 | # - Single server and func test results 38 | - ls coverage/client/*/lcov.info coverage/server/{rest,unit}/lcov.info coverage/func/lcov.info | cat 39 | - cat coverage/client/*/lcov.info coverage/server/{rest,unit}/lcov.info coverage/func/lcov.info | ./node_modules/.bin/coveralls || echo "Coveralls upload failed" 40 | 41 | # Prune deps to just production and ensure we can still build 42 | - npm prune --production 43 | - npm install --production 44 | - npm run build 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thanks for helping out! 5 | 6 | ## Development 7 | 8 | Run `npm run dev` to run the dev. application. 9 | 10 | ## Checks, Tests 11 | 12 | Run `npm run check` before committing. 13 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | ## Development 5 | 6 | All development tasks consist of watching the demo bundle and the test bundle. 7 | 8 | Run the application with watched rebuilds: 9 | 10 | ```sh 11 | $ npm run dev # dev test/app server (OR) 12 | $ npm run hot # hot reload test/app server (OR) 13 | $ npm run prod # run the "REAL THING" with watchers 14 | ``` 15 | 16 | From there you can see: 17 | 18 | * Demo app: [127.0.0.1:3000](http://127.0.0.1:3000/) 19 | * Client tests: [127.0.0.1:3001/test/client/test.html](http://127.0.0.1:3001/test/client/test.html) 20 | 21 | 22 | ## General Checks 23 | 24 | ### In Development 25 | 26 | During development, you are expected to be running either: 27 | 28 | ```sh 29 | $ npm run dev 30 | ``` 31 | 32 | to build the lib and test files. With these running, you can run the faster 33 | 34 | ```sh 35 | $ npm run check-dev 36 | ``` 37 | 38 | Command. It is comprised of: 39 | 40 | ```sh 41 | $ npm run lint 42 | $ npm run test-dev 43 | ``` 44 | 45 | Note that the tests here are not instrumented for code coverage and are thus 46 | more development / debugging friendly. 47 | 48 | ### Continuous Integration 49 | 50 | CI doesn't have source / test file watchers, so has to _build_ the test files 51 | via the commands: 52 | 53 | ```sh 54 | $ npm run check # PhantomJS only 55 | $ npm run check-cov # (OR) PhantomJS w/ coverage 56 | $ npm run check-ci # (OR) PhantomJS,Firefox + coverage - available on Travis. 57 | ``` 58 | 59 | Which is currently comprised of: 60 | 61 | ```sh 62 | $ npm run lint # AND ... 63 | 64 | $ npm run test # PhantomJS only 65 | $ npm run test-cov # (OR) PhantomJS w/ coverage 66 | $ npm run test-ci # (OR) PhantomJS,Firefox + coverage 67 | ``` 68 | 69 | Note that `(test|check)-(cov|ci)` run code coverage and thus the 70 | test code may be harder to debug because it is instrumented. 71 | 72 | ### Client Tests 73 | 74 | The client tests rely on webpack dev server to create and serve the bundle 75 | of the app/test code at: http://127.0.0.1:3001/assets/main.js which is done 76 | with the task `npm run server-test` (part of `npm dev`). 77 | 78 | #### Code Coverage 79 | 80 | Code coverage reports are outputted to: 81 | 82 | ``` 83 | coverage/ 84 | client/ 85 | BROWSER_STRING/ 86 | lcov-report/index.html # Viewable web report. 87 | ``` 88 | 89 | ## Tests 90 | 91 | The test suites in this project can be found in the following locations: 92 | 93 | ``` 94 | test/server 95 | test/client 96 | test/func 97 | ``` 98 | 99 | ### Backend Tests 100 | 101 | `test/server` 102 | 103 | Server-side (aka "backend") tests have two real flavors -- *unit* and *REST* 104 | tests. To run all the server-side tests, try: 105 | 106 | ```sh 107 | $ npm run test-server 108 | ``` 109 | 110 | #### Server-side Unit Tests 111 | 112 | `test/server/spec` 113 | 114 | Pure JavaScript tests that import the server code and test it in isolation. 115 | 116 | * Extremely fast to execute. 117 | * Typically test pure code logic in isolation. 118 | * Contains a Sinon [sandbox][] **with** fake timers. 119 | 120 | Run the tests with: 121 | 122 | ```sh 123 | $ npm run test-server-unit 124 | ``` 125 | 126 | #### Server-side REST Tests 127 | 128 | `test/server/rest` 129 | 130 | REST tests rely on spinning up the backend web application and using an HTTP 131 | client to make real network requests to the server and validate responses. 132 | 133 | * Must set up / tear down the application web server. 134 | * Issue real REST requests against server and verify responses. 135 | * Fairly fast to execute (localhost network requests). 136 | * Cover more of an "end-to-end" perspective on validation. 137 | 138 | Programming notes: 139 | 140 | * Contains a Sinon [sandbox][] _without_ fake timers. 141 | * Test against a remote server with environment variables: 142 | * `TEST_REST_IS_REMOTE=true` (tests should only stub/spy if not remote) 143 | * `TEST_REST_BASE_URL=http://example.com/` 144 | 145 | Run the tests with: 146 | 147 | ```sh 148 | $ npm run test-server-rest 149 | ``` 150 | 151 | ### Frontend Tests 152 | 153 | `test/client/spec` 154 | 155 | Client-side (aka "frontend") unit tests focus on one or more client application 156 | files in isolation. Some aspects of these tests: 157 | 158 | * Extremely fast to execute. 159 | * Execute via a test HTML driver page, not the web application HTML. 160 | * Must create mock DOM and data fixtures. 161 | * Mock out real browser network requests / time. 162 | * Typically test some aspect of the UI from the user perspective. 163 | * Run tests in the browser or from command line. 164 | * May need to be bundled like your application code. 165 | 166 | Programming notes: 167 | 168 | * Contains a Sinon [sandbox][] **with** fake timers and servers. 169 | 170 | Build, then run the tests from the command line with: 171 | 172 | ```sh 173 | $ npm run test-client 174 | $ npm run test-client-cov # With coverage 175 | $ npm run test-client-dev # (Faster) Use existing `npm run dev` watchers. 176 | ``` 177 | 178 | ### Functional Tests 179 | 180 | `test/func` 181 | 182 | Functional (aka "integration", "end-to-end") tests rely on a full, working 183 | instance of the entire web application. These tests typically: 184 | 185 | * Are slower than the other test types. 186 | * Take a "black box" approach to the application and interact only via the 187 | actual web UI. 188 | * Test user behaviors in an end-to-end manner. 189 | 190 | Programming notes: 191 | 192 | * Use the [webdriverio][] Selenium client libraries. 193 | * Use the [rowdy][] configuration wrapper for webdriverio / Selenium 194 | * Test against a remote server with environment variables: 195 | * `TEST_FUNC_IS_REMOTE=true` (tests should only stub/spy if not remote) 196 | * `TEST_FUNC_BASE_URL=http://example.com/` 197 | 198 | Run the tests with: 199 | 200 | ```sh 201 | $ npm run test-func 202 | $ npm run test-func-cov # With coverage 203 | $ npm run test-func-dev # (Faster) Use existing `npm run dev` watchers. 204 | ``` 205 | 206 | You can override settings and browser selections from the environment per 207 | the [rowdy](https://github.com/FormidableLabs/rowdy) documentation. E.g., 208 | 209 | ```sh 210 | # Client and server logging. 211 | $ ROWDY_OPTIONS='{ "client":{ "logger":true }, "server":{ "logger":true } }' \ 212 | npm run test-func 213 | 214 | # Switch to Chrome 215 | $ ROWDY_SETTINGS="local.chrome" \ 216 | npm run test-func 217 | ``` 218 | 219 | ## Releases 220 | 221 | **IMPORTANT - NPM**: To correctly run `preversion` your first step is to make 222 | sure that you have a very modern `npm` binary: 223 | 224 | ```sh 225 | $ npm install -g npm 226 | ``` 227 | 228 | The basic workflow is: 229 | 230 | ```sh 231 | # Make sure you have a clean, up-to-date `master` 232 | $ git pull 233 | $ git status # (should be no changes) 234 | 235 | # Choose a semantic update for the new version. 236 | # If you're unsure, read about semantic versioning at http://semver.org/ 237 | $ npm version major|minor|patch -m "Version %s - INSERT_REASONS" 238 | 239 | # `package.json` is updated, and files are committed to git (but unpushed). 240 | 241 | # Check that everything looks good in last commit and push. 242 | $ git diff HEAD^ HEAD 243 | $ git push && git push --tags 244 | # ... the project is now pushed to GitHub. 245 | 246 | # And finally publish to `npm`! 247 | $ npm publish 248 | ``` 249 | 250 | And you've published! 251 | 252 | [sandbox]: http://sinonjs.org/docs/#sinon-sandbox 253 | [webdriverio]: http://webdriver.io/ 254 | [rowdy]: https://github.com/FormidableLabs/rowdy 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Converter - React 2 | ================= 3 | 4 | [![Build Status][trav_img]][trav_site] 5 | [![Appveyor Status][av_img]][av_site] 6 | [![Coverage Status][cov_img]][cov_site] 7 | 8 | A simple app written using [React][react] and [CommonJS][cjs], built with 9 | [Webpack][webpack]. Based on 10 | [full-stack-testing.formidablelabs.com/app/](http://full-stack-testing.formidablelabs.com/app/) 11 | from our "[Full. Stack. Testing](http://full-stack-testing.formidablelabs.com/)" 12 | training project. 13 | 14 | ## Overview 15 | 16 | The converter app has a simple Express-based REST backend that serves string 17 | conversions. The frontend app is a React app, crafted with the following: 18 | 19 | * [ES6](https://kangax.github.io/compat-table/es6/) via 20 | [Babel](https://babeljs.io/) for client code. 21 | * Components from [react-bootstrap](http://react-bootstrap.github.io/) 22 | * [Redux](https://github.com/rackt/redux) for data layer. 23 | * [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) for 24 | AJAX requests. 25 | * Server-side rendering and SPA bootstrap. 26 | 27 | See the app hard at work! 28 | 29 | * [`127.0.0.1:3000/`](http://127.0.0.1:3000/): Server-side bootstrap, then client-side. 30 | * [`127.0.0.1:3000/?__mode=noss`](http://127.0.0.1:3000/?__mode=noss): Pure client-side. 31 | * [`127.0.0.1:3000/?__mode=nojs`](http://127.0.0.1:3000/?__mode=nojs): Pure server-side. 32 | 33 | ## Notes 34 | 35 | ### Size 36 | 37 | To test out how optimized the build is, here are some useful curl commands: 38 | 39 | ```sh 40 | # Run production build 41 | $ npm run build 42 | 43 | # Minified size 44 | $ wc -c dist/js/*.js 45 | 286748 dist/js/bundle.d3749f460563cd1b0884.js 46 | 47 | # Minified gzipped size 48 | $ gzip -c dist/js/*.js | wc -c 49 | 77748 50 | ``` 51 | 52 | ## Development 53 | 54 | For a deeper dive, see: [DEVELOPMENT](DEVELOPMENT.md) 55 | 56 | ### Dev Mode 57 | 58 | Install, setup. 59 | 60 | ```sh 61 | $ npm install # Install dependencies 62 | $ npm run install-dev # Install dev. environment (selenium, etc.). 63 | ``` 64 | 65 | Run the watchers, dev and source maps servers for the real production build: 66 | 67 | ```sh 68 | $ npm run prod 69 | ``` 70 | 71 | Run the watchers and the Webpack dev server: 72 | 73 | ```sh 74 | $ npm run dev 75 | ``` 76 | 77 | Run the watchers and the Webpack dev server w/ React hot loader: 78 | 79 | ```sh 80 | $ npm run hot 81 | ``` 82 | 83 | Ports various servers run on: 84 | 85 | * [`2992`](http://127.0.0.1:2992/): Webpack dev server for dev. server. 86 | * [`3000`](http://127.0.0.1:3000/): Development application server. 87 | * [`3001`](http://127.0.0.1:3001/): Sourcemaps static server / test (in-browser) server. 88 | * [`3010`](http://127.0.0.1:3010/): Webpack dev server for ephemeral client 89 | Karma tests run one-off with full build. 90 | * [`3020`](http://127.0.0.1:3020/): Ephemeral app server for REST server tests. 91 | Override via `TEST_REST_PORT` environment variable. 92 | * [`3030`](http://127.0.0.1:3030/): Ephemeral app server for functional tests. 93 | Override via `TEST_FUNC_PORT` environment variable. 94 | * [`3031`](http://127.0.0.1:3031/): Webpack dev server for ephemeral functional 95 | tests run one-off with full build. 96 | Override via `TEST_FUNC_WDS_PORT` environment variable. 97 | 98 | URLS to test things out: 99 | 100 | * [`127.0.0.1:3000/`](http://127.0.0.1:3000/): Server-side bootstrap, then JS. 101 | * [`127.0.0.1:3000/?__mode=noss`](http://127.0.0.1:3000/?__mode=noss): Pure JS. 102 | * [`127.0.0.1:3000/?__mode=nojs`](http://127.0.0.1:3000/?__mode=nojs): Pure 103 | server-side. Note that while some links may work (e.g. clicking on a note 104 | title in list), many things do not since there are absolutely no JS libraries. 105 | This is intended to just be a small demo of SEO / "crawlable" content. 106 | This mode is incompatible with the React hot loader mode because in hot mode 107 | JS is used to load CSS. If you want to run a development server while using 108 | `nojs`, use `npm run dev`. 109 | 110 | ### Bootstrapped Data 111 | 112 | As a development helper, we allow a querystring injection of data to bootstrap 113 | the application off of. Normally, you wouldn't allow users to add this, and 114 | instead would choose how to best bootstrap your app. 115 | 116 | * [`127.0.0.1:3000/?__bootstrap=camel:hello%20there`](http://127.0.0.1:3000/?__bootstrap=camel:hello%20there): 117 | Server-side data bootstrapped into the application + render. 118 | * [`127.0.0.1:3000/?__mode=noss&__bootstrap=camel:hello%20there`](http://127.0.0.1:3000/?__mode=noss&__bootstrap=camel:hello%20there): 119 | Pure client-render, but bootstrap the store off `types` and `values` and 120 | initiate async `fetch` to backend for data automatically. 121 | * [`127.0.0.1:3000/?__mode=nojs&__bootstrap=camel:hello%20there`](http://127.0.0.1:3000/?__mode=nojs&__bootstrap=camel:hello%20there): 122 | Pure server-side render with no JS. Should fully render the inputs and 123 | converted values in static HTML. 124 | 125 | ## Production 126 | 127 | Install, setup. 128 | 129 | ```sh 130 | $ npm install --production 131 | $ npm run build 132 | ``` 133 | 134 | Run the server. 135 | 136 | ```sh 137 | $ NODE_ENV=production node server/index.js 138 | ``` 139 | 140 | ## Contributing 141 | 142 | Please see [CONTRIBUTING](CONTRIBUTING.md) 143 | 144 | [trav]: https://travis-ci.org/ 145 | [trav_img]: https://api.travis-ci.org/FormidableLabs/converter-react.svg 146 | [trav_site]: https://travis-ci.org/FormidableLabs/converter-react 147 | [av]: https://ci.appveyor.com/ 148 | [av_img]: https://ci.appveyor.com/api/projects/status/31hevq3yixwib0xg?svg=true 149 | [av_site]: https://ci.appveyor.com/project/ryan-roemer/converter-react 150 | [cov]: https://coveralls.io 151 | [cov_img]: https://img.shields.io/coveralls/FormidableLabs/converter-react.svg 152 | [cov_site]: https://coveralls.io/r/FormidableLabs/converter-react 153 | 154 | [react]: http://facebook.github.io/react/ 155 | [cjs]: http://wiki.commonjs.org/wiki/CommonJS 156 | [webpack]: http://webpack.github.io/ 157 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Good template: https://github.com/gruntjs/grunt/blob/master/appveyor.yml 2 | environment: 3 | global: 4 | # PhantomJS fails currently. (ROWDY_SETTINGS="local.phantomjs") 5 | # https://github.com/FormidableLabs/converter-react/issues/34 6 | ROWDY_SETTINGS: "local.firefox" 7 | matrix: 8 | - nodejs_version: 0.10 9 | - nodejs_version: 0.12 10 | 11 | # Get the latest stable version of Node 0.STABLE.latest 12 | install: 13 | - ps: Install-Product node $env:nodejs_version 14 | # Install and use local, modern NPM 15 | - npm install npm@next 16 | - node_modules\.bin\npm install 17 | - node_modules\.bin\npm run install-dev 18 | 19 | build: off 20 | 21 | branches: 22 | only: 23 | - master 24 | 25 | test_script: 26 | # Build environment. 27 | - node --version 28 | - node_modules\.bin\npm --version 29 | - echo %ROWDY_SETTINGS% 30 | 31 | # Build and test. 32 | - node_modules\.bin\npm run build 33 | - node_modules\.bin\npm run check-ci-win 34 | 35 | matrix: 36 | fast_finish: true 37 | 38 | cache: 39 | - node_modules -> package.json # local npm modules 40 | -------------------------------------------------------------------------------- /client/actions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Actions: Convert 3 | */ 4 | import { fetchConversions as fetchConversionsApi } from "../utils/api"; 5 | 6 | export const CONVERSION_ERROR = "CONVERSION_ERROR"; 7 | export const FETCH_CONVERSIONS = "FETCH_CONVERSIONS"; 8 | export const SET_CONVERSION_TYPES = "SET_CONVERSION_TYPES"; 9 | export const SET_CONVERSION_VALUE = "SET_CONVERSION_VALUE"; 10 | export const UPDATE_CONVERSIONS = "UPDATE_CONVERSIONS"; 11 | 12 | export const updateConversions = (data) => { 13 | return { 14 | type: UPDATE_CONVERSIONS, 15 | data 16 | }; 17 | }; 18 | 19 | export const conversionError = (err) => { 20 | return { 21 | type: CONVERSION_ERROR, 22 | err 23 | }; 24 | }; 25 | 26 | export const fetchConversions = (types, value) => { 27 | return (dispatch) => { 28 | dispatch(() => ({type: FETCH_CONVERSIONS})); 29 | 30 | return fetchConversionsApi(types, value) 31 | .then((datas) => { 32 | dispatch(updateConversions(datas)); 33 | }) 34 | .catch((err) => { 35 | dispatch(conversionError(err)); 36 | }); 37 | }; 38 | }; 39 | 40 | export const setConversionTypes = (types) => { 41 | return { 42 | type: SET_CONVERSION_TYPES, 43 | types 44 | }; 45 | }; 46 | 47 | export const setConversionValue = (value) => { 48 | return { 49 | type: SET_CONVERSION_VALUE, 50 | value 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /client/app.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Client entry point. 3 | */ 4 | /*globals document:false, location:false */ 5 | import React from "react"; 6 | import ReactDOM from "react-dom"; 7 | import { Provider } from "react-redux"; 8 | 9 | import createStore from "./store/create-store"; 10 | import { fetchConversions } from "./actions/"; 11 | import { parseBootstrap } from "./utils/query"; 12 | 13 | import Page from "./containers/page"; 14 | 15 | const rootEl = document.querySelector(".js-content"); 16 | 17 | // Although our Flux store is not a singleton, from the point of view of the 18 | // client-side application, we instantiate a single instance here which the 19 | // entire app will share. (So the client app _has_ an effective singleton). 20 | let store = createStore(); 21 | 22 | // Render helpers -- may defer based on client-side actions. 23 | let deferRender = false; 24 | const render = () => { 25 | ReactDOM.render( 26 | 27 | 28 | , rootEl 29 | ); 30 | }; 31 | 32 | // Try server bootstrap _first_ because doesn't need a fetch. 33 | let serverBootstrap; 34 | const serverBootstrapEl = document.querySelector(".js-bootstrap"); 35 | if (serverBootstrapEl) { 36 | try { 37 | serverBootstrap = JSON.parse(serverBootstrapEl.innerHTML); 38 | store = createStore(serverBootstrap); 39 | /*eslint-disable no-empty*/ 40 | } catch (err) { /* Ignore error. */ } 41 | /*eslint-enable no-empty*/ 42 | } 43 | 44 | // Then try client bootstrap: Get types, value from URL, then _fetch_ data. 45 | if (!serverBootstrap) { 46 | const clientBootstrap = parseBootstrap(location.search); 47 | if (clientBootstrap) { 48 | // Defer render and do it after conversions are fetched. 49 | deferRender = true; 50 | 51 | store = createStore(clientBootstrap); 52 | store 53 | .dispatch(fetchConversions( 54 | clientBootstrap.conversions.types, 55 | clientBootstrap.conversions.value 56 | )) 57 | .then(render); 58 | } 59 | } 60 | 61 | // Render if not deferred. 62 | if (!deferRender) { 63 | render(); 64 | } 65 | -------------------------------------------------------------------------------- /client/components/convert.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert button. 3 | */ 4 | import React from "react"; 5 | import { connect } from "react-redux"; 6 | import Button from "react-bootstrap/lib/Button"; 7 | import { fetchConversions } from "../actions/"; 8 | 9 | class Convert extends React.Component { 10 | onClick(e) { 11 | e.preventDefault(); 12 | const store = this.props; 13 | store.dispatch(fetchConversions(store.types, store.value)); 14 | } 15 | 16 | render() { 17 | return ( 18 | 21 | ); 22 | } 23 | } 24 | 25 | export default connect((state) => ({ 26 | types: state.conversions.types, 27 | value: state.conversions.value 28 | }))(Convert); 29 | -------------------------------------------------------------------------------- /client/components/error-panel.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert output panel 3 | */ 4 | import React from "react"; 5 | import Panel from "react-bootstrap/lib/Panel"; 6 | 7 | export default class ErrorPanel extends React.Component { 8 | render() { 9 | return ( 10 | Conversion Error 14 | > 15 | {this.props.children} 16 | 17 | ); 18 | } 19 | } 20 | 21 | ErrorPanel.propTypes = { 22 | children: React.PropTypes.arrayOf(React.PropTypes.element) 23 | }; 24 | -------------------------------------------------------------------------------- /client/components/input.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert input. 3 | */ 4 | import React from "react"; 5 | import { connect } from "react-redux"; 6 | import FormControl from "react-bootstrap/lib/FormControl"; 7 | import { setConversionValue, fetchConversions } from "../actions/"; 8 | 9 | class UserInput extends React.Component { 10 | onChange(ev) { 11 | this.props.dispatch(setConversionValue(ev.target.value)); 12 | } 13 | 14 | onKeyDown(ev) { 15 | if (ev.which === 13 /* Enter key */) { 16 | ev.preventDefault(); 17 | const store = this.props; 18 | store.dispatch(fetchConversions(store.types, store.value)); 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 | 32 | ); 33 | } 34 | } 35 | 36 | UserInput.propTypes = { 37 | dispatch: React.PropTypes.func, 38 | value: React.PropTypes.string 39 | }; 40 | 41 | export default connect((state) => ({ 42 | types: state.conversions.types, 43 | value: state.conversions.value 44 | }))(UserInput); 45 | -------------------------------------------------------------------------------- /client/components/output-panel.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert output panel 3 | */ 4 | import React from "react"; 5 | import Panel from "react-bootstrap/lib/Panel"; 6 | 7 | export default class OutputPanel extends React.Component { 8 | render() { 9 | return ( 10 | {this.props.title} 13 | > 14 | {this.props.content} 15 | 16 | ); 17 | } 18 | } 19 | 20 | OutputPanel.propTypes = { 21 | content: React.PropTypes.string, 22 | title: React.PropTypes.string 23 | }; 24 | -------------------------------------------------------------------------------- /client/components/output.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert output. 3 | */ 4 | import React from "react"; 5 | import { connect } from "react-redux"; 6 | import OutputPanel from "./output-panel"; 7 | import ErrorPanel from "./error-panel"; 8 | 9 | class Output extends React.Component { 10 | render() { 11 | const content = this.props.conversionError ? 12 | {this.props.conversionError} : 13 | this.props.conversions.map((conv) => 14 | 15 | ); 16 | 17 | return ( 18 |
19 | {content} 20 |
21 | ); 22 | } 23 | } 24 | 25 | Output.propTypes = { 26 | conversionError: React.PropTypes.string, 27 | conversions: React.PropTypes.array 28 | }; 29 | 30 | export default connect((state) => ({ 31 | conversions: state.conversions.conversions, 32 | conversionError: state.conversions.conversionError 33 | }))(Output); 34 | -------------------------------------------------------------------------------- /client/components/types-title.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Conversion types title. 3 | */ 4 | import React from "react"; 5 | 6 | export default class Title extends React.Component { 7 | render() { 8 | return ( 9 | 10 | to  11 | {this.props.title} 12 |  ! 13 | 14 | ); 15 | } 16 | } 17 | 18 | Title.propTypes = { 19 | title: React.PropTypes.string 20 | }; 21 | -------------------------------------------------------------------------------- /client/components/types.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Conversion types. 3 | */ 4 | import React from "react"; 5 | import { connect } from "react-redux"; 6 | import DropdownButton from "react-bootstrap/lib/DropdownButton"; 7 | import MenuItem from "react-bootstrap/lib/MenuItem"; 8 | import { setConversionTypes } from "../actions/"; 9 | 10 | import Title from "./types-title"; 11 | 12 | import types from "../utils/types"; 13 | 14 | const noop = () => {}; 15 | 16 | class Types extends React.Component { 17 | setTypes(conversionTypes) { 18 | this.props.dispatch(setConversionTypes(conversionTypes)); 19 | } 20 | 21 | render() { 22 | const items = Object.keys(types.TYPES).map((type) => ( 23 | 25 | {types.getTitle(type)} 26 | 27 | )); 28 | 29 | return ( 30 | } 36 | > 37 | {items} 38 | 39 | 41 | {types.ALL_DESC} 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | Types.propTypes = { 49 | dispatch: React.PropTypes.func, 50 | types: React.PropTypes.string 51 | }; 52 | 53 | export default connect((state) => ({ 54 | types: state.conversions.types, 55 | value: state.conversions.value 56 | }))(Types); 57 | -------------------------------------------------------------------------------- /client/containers/page.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Container page. 3 | */ 4 | import React from "react"; 5 | import Jumbotron from "react-bootstrap/lib/Jumbotron"; 6 | import Form from "react-bootstrap/lib/Form"; 7 | import FormGroup from "react-bootstrap/lib/FormGroup"; 8 | 9 | import Convert from "../components/convert"; 10 | import Input from "../components/input"; 11 | import Types from "../components/types"; 12 | import Output from "../components/output"; 13 | import InputGroup from "react-bootstrap/lib/InputGroup"; 14 | 15 | import "bootstrap/dist/css/bootstrap.css"; 16 | import "bootstrap/dist/css/bootstrap-theme.css"; 17 | import "../styles/app.css"; 18 | 19 | class Page extends React.Component { 20 | render() { 21 | return ( 22 |
23 | 24 |

The Converter!

25 |

Camel, snake and dasherize to awesomeness!

26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default Page; 47 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import types from "../utils/types"; 4 | 5 | import { 6 | CONVERSION_ERROR, 7 | FETCH_CONVERSIONS, 8 | SET_CONVERSION_TYPES, 9 | SET_CONVERSION_VALUE, 10 | UPDATE_CONVERSIONS 11 | } from "../actions"; 12 | 13 | const conversions = (state = { 14 | conversionError: null, 15 | conversions: [], 16 | types: types.DEFAULT_TYPE, 17 | value: "" 18 | }, action) => { 19 | switch (action.type) { 20 | case CONVERSION_ERROR: 21 | return Object.assign({}, state, { 22 | conversionError: action.err.message || action.err.toString() 23 | }); 24 | case FETCH_CONVERSIONS: 25 | return Object.assign({}, state, { 26 | conversionError: null 27 | }); 28 | case SET_CONVERSION_TYPES: 29 | return Object.assign({}, state, { 30 | types: action.types 31 | }); 32 | case SET_CONVERSION_VALUE: 33 | return Object.assign({}, state, { 34 | value: action.value 35 | }); 36 | case UPDATE_CONVERSIONS: 37 | return Object.assign({}, state, { 38 | conversions: action.data 39 | }); 40 | default: 41 | return state; 42 | } 43 | }; 44 | 45 | const rootReducer = combineReducers({ 46 | conversions 47 | }); 48 | 49 | export default rootReducer; 50 | -------------------------------------------------------------------------------- /client/store/create-store.js: -------------------------------------------------------------------------------- 1 | import { createStore as reduxCreateStore, applyMiddleware } from "redux"; 2 | import thunkMiddleware from "redux-thunk"; 3 | import createLogger from "redux-logger"; 4 | import rootReducer from "../reducers"; 5 | 6 | const loggerMiddleware = createLogger(); 7 | 8 | const createStoreWithMiddleware = applyMiddleware( 9 | thunkMiddleware, 10 | loggerMiddleware 11 | )(reduxCreateStore); 12 | 13 | const createStore = (initialState) => { 14 | return createStoreWithMiddleware(rootReducer, initialState); 15 | }; 16 | 17 | export default createStore; 18 | -------------------------------------------------------------------------------- /client/styles/app.css: -------------------------------------------------------------------------------- 1 | .output-panel { 2 | margin-top: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /client/utils/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch data from rest API. 3 | */ 4 | import Promise from "bluebird"; 5 | import "isomorphic-fetch"; 6 | 7 | const api = { 8 | BASE_URL: "", 9 | 10 | // Statefully set the base port and host (for server-side). 11 | setBase: (host, port) => { 12 | if (host) { 13 | api.BASE_URL = "http://" + host; 14 | if (port) { 15 | api.BASE_URL = api.BASE_URL + ":" + port; 16 | } 17 | } 18 | }, 19 | 20 | // Invoke fetches for each of the different data types and return array. 21 | fetchConversions: (types, value) => 22 | Promise.all(types.split(",").map((type) => 23 | fetch(`${api.BASE_URL}/api/${type}?from=${encodeURIComponent(value)}`) 24 | .then((res) => { 25 | if (res.status >= 400) { 26 | throw new Error("Bad server response"); 27 | } 28 | return res.json(); 29 | }) 30 | .then((data) => ({ 31 | title: type, 32 | content: data.to 33 | })) 34 | )) 35 | }; 36 | 37 | export default api; 38 | -------------------------------------------------------------------------------- /client/utils/query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Querystring utilities. 3 | */ 4 | export default { 5 | // Parse querystring into bootstrap object. 6 | parseBootstrap: (querystring) => { 7 | const bootstrap = (querystring || "") 8 | .replace(/^\?/, "") 9 | .split("&") 10 | .map((part) => part.split("=")) 11 | .filter((pair) => pair[0] === "__bootstrap")[0]; 12 | 13 | if (!bootstrap) { 14 | return null; 15 | } 16 | 17 | const [types, value] = bootstrap[1].split(":"); 18 | return { 19 | conversions: { 20 | types, 21 | value: decodeURIComponent(value), 22 | conversions: [] 23 | } 24 | }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /client/utils/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enums / values for "types". 3 | */ 4 | // All the individual types. 5 | const types = { 6 | DEFAULT_TYPE: "camel", 7 | 8 | TYPES: { 9 | camel: "camel case", 10 | snake: "snake case", 11 | dash: "dasherized" 12 | }, 13 | 14 | /** 15 | * Get title from array of type keys. 16 | * 17 | * @param {String} type conversion type (e.g., "camel") 18 | * @returns {String} UI-friendly title 19 | */ 20 | getTitle: (type) => types.TYPES[type] || 21 | (type === types.ALL ? types.ALL_DESC : undefined) 22 | }; 23 | 24 | // Special case "all types". 25 | types.ALL = Object.keys(types.TYPES).join(","); 26 | types.ALL_DESC = "all the things"; 27 | 28 | export default types; 29 | -------------------------------------------------------------------------------- /heroku/doc/_tmpl/layout.jade: -------------------------------------------------------------------------------- 1 | doctype 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | meta(http-equiv="X-UA-Compatible", content="IE=edge,chrome=1") 6 | meta(name="viewport", content="width=device-width, initial-scale=1") 7 | meta(name="apple-mobile-web-app-capable", content="yes") 8 | title Converter (React + Flux) 9 | link(rel="stylesheet", href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css") 10 | link(rel="stylesheet", href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap-theme.min.css") 11 | link(rel="stylesheet", href="//cdnjs.cloudflare.com/ajax/libs/jasny-bootstrap/3.1.3/css/jasny-bootstrap.min.css") 12 | link(rel="stylesheet", href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/github.min.css") 13 | link(rel="stylesheet", href="/public/site.css") 14 | body 15 | //- Banner 16 | //- * forkme_right_gray_6d6d6d 17 | //- * forkme_right_white_ffffff 18 | //- * forkme_right_darkblue_121621 19 | a.hidden-xs( 20 | href="https://github.com/FormidableLabs/converter-react" 21 | style="position: absolute; top: 0; right: 0; border: 0;") 22 | img.banner( 23 | src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png" 24 | alt="Fork me on GitHub") 25 | 26 | .jumbotron.backing-gradient.text-center 27 | h1.js-page-title Converter App 28 | p.js-page-subtitle with React + Flux! 29 | 30 | .container-fluid 31 | block nav 32 | .nav-wrapper 33 | nav#nav.navmenu.navmenu-inverse.navmenu-fixed-left.offcanvas(role="navigation") 34 | a#home.navmenu-brand(href="/") Home 35 | ul.nav.navmenu-nav 36 | 37 | div.navbar.navbar-default.navbar-fixed-top 38 | button.navbar-toggle(type="button" 39 | data-toggle="offcanvas" data-target="#nav" data-canvas="body") 40 | span.icon-bar 41 | span.icon-bar 42 | span.icon-bar 43 | 44 | block content 45 | 46 | script(src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.min.js") 47 | script(src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js") 48 | script(src="//cdnjs.cloudflare.com/ajax/libs/jasny-bootstrap/3.1.3/js/jasny-bootstrap.min.js") 49 | script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js") 50 | script(src="/public/site.js") 51 | -------------------------------------------------------------------------------- /heroku/doc/contributing.jade: -------------------------------------------------------------------------------- 1 | extends _tmpl/layout 2 | 3 | block content 4 | include:md ../../CONTRIBUTING.md 5 | -------------------------------------------------------------------------------- /heroku/doc/development.jade: -------------------------------------------------------------------------------- 1 | extends _tmpl/layout 2 | 3 | block content 4 | include:md ../../DEVELOPMENT.md 5 | -------------------------------------------------------------------------------- /heroku/doc/index.jade: -------------------------------------------------------------------------------- 1 | extends _tmpl/layout 2 | 3 | block content 4 | include:md ../../README.md 5 | -------------------------------------------------------------------------------- /heroku/doc/public/site.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------ 2 | * Page 3 | * --------------------------------------------------------------- */ 4 | @media (min-width: 768px) { 5 | .container-fluid { 6 | padding-left: 75px; 7 | padding-right: 75px; 8 | } 9 | } 10 | 11 | /* ------------------------------------------------------------------ 12 | * Jumbotron Backgrounds 13 | * --------------------------------------------------------------- */ 14 | .backing-gradient { 15 | color: #fff; 16 | text-shadow: 0 5px 7px rgba(0,0,0,.8), 0 0 30px rgba(0,0,0,.375); 17 | background-color: #337ac7; 18 | background: -webkit-gradient(linear, left top, right top, from(#3F4757), to(#337ac7)); 19 | background: -webkit-linear-gradient(left, #3F4757, #337ac7); 20 | background: -moz-linear-gradient(left, #3F4757, #337ac7); 21 | background: -ms-linear-gradient(left, #3F4757, #337ac7); 22 | background: -o-linear-gradient(left, #3F4757, #337ac7); 23 | } 24 | 25 | /* ------------------------------------------------------------------ 26 | * Navbar 27 | * --------------------------------------------------------------- */ 28 | .navmenu { 29 | width: 200px; 30 | } 31 | 32 | .navbar-toggle { 33 | float: left; 34 | margin-left: 10px; 35 | background-color: #fff; 36 | } 37 | @media (max-width: 768px) { 38 | .navbar-toggle { 39 | opacity: 0.5; 40 | } 41 | } 42 | 43 | .navmenu-inverse { 44 | background-color: #3F4757; 45 | border-color: #080808; 46 | } 47 | 48 | li.nav-item > a { 49 | padding: 5px 15px; 50 | line-height: 1.2em; 51 | } 52 | .navmenu-inverse .navmenu-brand, 53 | .navmenu-inverse .navmenu-nav > li > a { 54 | color: #ccc; 55 | } 56 | .navmenu-fixed-left, 57 | .navbar-offcanvas.navmenu-fixed-left { 58 | border-right: 1px solid #888; 59 | } 60 | 61 | .nav-item-H2 { 62 | font-weight: bold; 63 | } 64 | 65 | .nav-item-H3 { 66 | padding-left: 10px 67 | } 68 | 69 | @media (min-width: 0) { 70 | .navbar-toggle { 71 | display: block; /* force showing the toggle */ 72 | } 73 | .navbar { 74 | right: auto; 75 | background: none; 76 | border: none; 77 | -webkit-box-shadow: none; 78 | -moz-box-shadow: none; 79 | box-shadow: none; 80 | 81 | } 82 | } -------------------------------------------------------------------------------- /heroku/doc/public/site.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | /*global $*/ 3 | // -------------------------------------------------------------------------- 4 | // UI Extras 5 | // -------------------------------------------------------------------------- 6 | // Populate offcanvas menu if Jasny detected. 7 | var $nav = $("#nav"); 8 | if ($nav.offcanvas) { 9 | var $navContent = $(".navmenu-nav"); 10 | 11 | // Convert headings to menu items. 12 | $("h2,h3").each(function () { 13 | var $heading = $(this); 14 | 15 | $("
  • ") 16 | .clone().appendTo($navContent) 17 | .addClass("nav-item nav-item-" + $heading.prop("tagName")) 18 | .find("> a") 19 | .attr("href", "#" + $heading.prop("id")) 20 | .text($heading.text()); 21 | }); 22 | 23 | // Close menu on any click. 24 | $("#nav, #home, li.nav-item > a").click(function () { 25 | $nav.offcanvas("hide"); 26 | }); 27 | 28 | } else { 29 | // Hide the nav wrapper if no offcanvas nav available. 30 | $(".nav-wrapper").hide(); 31 | } 32 | 33 | // Add highlighting. 34 | if (window.hljs) { 35 | $("pre code").each(function (i, block) { 36 | var cls = $(block).attr("class"); 37 | 38 | // Highlight all `lang-*` classed blocks. 39 | if (cls && cls.indexOf("lang") === 0) { 40 | window.hljs.highlightBlock(block); 41 | } 42 | }); 43 | } 44 | })(); 45 | -------------------------------------------------------------------------------- /heroku/scripts/cluster.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clustered server. 3 | * 4 | * See: https://github.com/doxout/recluster 5 | */ 6 | var path = require("path"); 7 | var recluster = require("recluster"); 8 | var cluster = recluster(path.join(__dirname, "server.js")); 9 | 10 | // Log worker deaths. 11 | cluster.on("exit", function (worker) { 12 | console.log("Worker " + worker.id + " died."); 13 | }); 14 | 15 | // Set up reload. 16 | process.on("SIGUSR2", function () { 17 | console.log("Got SIGUSR2, reloading cluster..."); 18 | cluster.reload(); 19 | }); 20 | 21 | // Start and warn log (so we can grep on starts). 22 | // Reload with `$ kill -s SIGUSR2 PID` (like the message says). 23 | console.log("Spawned cluster, kill -s SIGUSR2 " + process.pid + " to reload"); 24 | cluster.run(); 25 | -------------------------------------------------------------------------------- /heroku/scripts/install.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Install Heroku. 3 | */ 4 | // HACKAGE: Before _first require_, we add global modules in path (for `npm` 5 | // programmatic access). 6 | var delim = process.platform.indexOf("win") === 0 ? ";" : ":"; 7 | var globalMods = process.execPath + "/../../lib/node_modules"; 8 | process.env.NODE_PATH = (process.env.NODE_PATH || "") 9 | .split(delim) 10 | .filter(function (x) { return x; }) 11 | .concat([globalMods]) 12 | .join(delim); 13 | 14 | // Manually initialize paths. 15 | require("module").Module._initPaths(); 16 | 17 | // Normal requires 18 | var fs = require("fs"); 19 | var path = require("path"); 20 | var root = path.resolve(__dirname, "../.."); 21 | 22 | // First test that we are "in" a Heroku dyno. 23 | var isHeroku = !!process.env.DYNO; 24 | if (!isHeroku) { 25 | throw new Error("Should only call in Heroku environment"); 26 | } 27 | 28 | // Write out a procfile. 29 | fs.writeFileSync(path.join(root, "Procfile"), "web: node heroku/scripts/cluster.js"); 30 | 31 | // NPM install certain dev. dependencies for Heroku usage. 32 | var npm = require("npm"); 33 | var pkg = require("../../package.json"); 34 | var herokuDeps = [ 35 | "jade", 36 | "marked", 37 | "recluster" 38 | ].map(function (key) { 39 | return [key, pkg.devDependencies[key]].join("@"); 40 | }); 41 | 42 | // Install. 43 | npm.load(function (loadErr) { 44 | if (loadErr) { throw loadErr; } 45 | npm.commands.install(herokuDeps, function (installErr) { 46 | if (installErr) { throw installErr; } 47 | }); 48 | npm.on("log", function (msg) { 49 | console.log(msg); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /heroku/scripts/not-heroku.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exit code 0 if not Heroku, 1 otherwise. 3 | */ 4 | // Use `DYNO` as proxy for Heroku test. 5 | var isHeroku = !!process.env.DYNO; 6 | var exitCode = isHeroku ? 1 : 0; 7 | 8 | /*eslint-disable no-process-exit*/ 9 | process.exit(exitCode); 10 | /*eslint-enable no-process-exit*/ 11 | -------------------------------------------------------------------------------- /heroku/scripts/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Demo / live web server. 3 | * 4 | * This server showcases tests and documentation along with the webapp 5 | * and is _not_ what we are creating for the workshop. 6 | */ 7 | var express = require("express"); 8 | var app = require("../../server"); 9 | 10 | var marked = require("marked"); 11 | var renderer = new marked.Renderer(); 12 | 13 | // Serve the application. 14 | app.use("/public/", express.static("heroku/doc/public")); 15 | app.indexRoute("/app"); 16 | 17 | // Marked options and custom rendering. 18 | // Skip intro heading. 19 | renderer.heading = function (text, level) { 20 | if (text === "Converter - React" && level === 1) { return ""; } 21 | return marked.Renderer.prototype.heading.apply(this, arguments); 22 | }; 23 | 24 | // Convert `.md` internal links to full links via a map. 25 | var linkMap = { 26 | "127.0.0.1:3000": "converter-react.formidablelabs.com/app" 27 | }; 28 | renderer.link = function (href, title, text) { 29 | // Mutate the links for production. 30 | Object.keys(linkMap).forEach(function (key) { 31 | var regex = new RegExp(key); 32 | href = href.replace(regex, linkMap[key]); 33 | text = text.replace(regex, linkMap[key]); 34 | }); 35 | 36 | return marked.Renderer.prototype.link.apply(this, [href, title, text]); 37 | }; 38 | 39 | marked.setOptions({ 40 | gfm: true, 41 | tables: true, 42 | renderer: renderer 43 | }); 44 | 45 | // Serve docs as root. 46 | app.engine("jade", require('jade').__express); 47 | app.get("/", function (req, res) { 48 | res.render("../heroku/doc/index.jade"); 49 | }); 50 | app.get("/DEVELOPMENT.md", function (req, res) { 51 | res.render("../heroku/doc/development.jade"); 52 | }); 53 | app.get("/CONTRIBUTING.md", function (req, res) { 54 | res.render("../heroku/doc/contributing.jade"); 55 | }); 56 | 57 | // Start server. 58 | app.start(); 59 | -------------------------------------------------------------------------------- /karma.conf.coverage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* 3 | * Karma Configuration: "coverage" version. 4 | * 5 | * This configuration is the same as basic one-shot version, just with coverage. 6 | */ 7 | var webpackCovCfg = require("./webpack.config.coverage"); 8 | 9 | module.exports = function (config) { 10 | require("./karma.conf")(config); 11 | config.set({ 12 | reporters: ["spec", "coverage"], 13 | webpack: webpackCovCfg, 14 | coverageReporter: { 15 | reporters: [ 16 | { type: "json", file: "coverage.json" }, 17 | { type: "lcov" }, 18 | { type: "text-summary" } 19 | ], 20 | dir: "coverage/client" 21 | } 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /karma.conf.dev.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* 3 | * Karma Configuration: "dev" version. 4 | * 5 | * This configuration relies on a `webpack-dev-server` already running and 6 | * bundling `webpack.config.test.js` on port 3001. If this is not running, 7 | * then the alternate `karma.conf.js` file will _also_ run the webpack dev 8 | * server during the test run. 9 | */ 10 | module.exports = function (config) { 11 | config.set({ 12 | frameworks: ["mocha", "phantomjs-shim"], 13 | reporters: ["spec"], 14 | browsers: ["PhantomJS"], 15 | basePath: ".", // repository root. 16 | files: [ 17 | // Sinon has issues with webpack. Do global include. 18 | "node_modules/sinon/pkg/sinon.js", 19 | 20 | // Test bundle (must be created via `npm run dev|hot|server-test`) 21 | "http://127.0.0.1:3001/assets/main.js" 22 | ], 23 | port: 9999, 24 | singleRun: true, 25 | client: { 26 | mocha: { 27 | ui: "bdd" 28 | } 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* 3 | * Karma Configuration: "full" version. 4 | * 5 | * This configuration runs a temporary `webpack-dev-server` and builds 6 | * the test files one-off for just a single run. This is appropriate for a 7 | * CI environment or if you're not otherwise running `npm run dev|hot`. 8 | */ 9 | var webpackCfg = require("./webpack.config.test"); 10 | 11 | module.exports = function (config) { 12 | // Start with the "dev" (webpack-dev-server is already running) config 13 | // and add in the webpack stuff. 14 | require("./karma.conf.dev")(config); 15 | 16 | // Overrides. 17 | config.set({ 18 | preprocessors: { 19 | "test/client/main.js": ["webpack"] 20 | }, 21 | files: [ 22 | // Sinon has issues with webpack. Do global include. 23 | "node_modules/sinon/pkg/sinon.js", 24 | 25 | // Test bundle (created via local webpack-dev-server in this config). 26 | "test/client/main.js" 27 | ], 28 | webpack: webpackCfg, 29 | webpackServer: { 30 | port: 3010, // Choose a non-conflicting port. 31 | quiet: false, 32 | noInfo: true, 33 | stats: { 34 | assets: false, 35 | colors: true, 36 | version: false, 37 | hash: false, 38 | timings: false, 39 | chunks: false, 40 | chunkModules: false 41 | } 42 | } 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "converter-react", 3 | "version": "0.0.1", 4 | "description": "Converter application (React)", 5 | "scripts": { 6 | "lint-client": "eslint --ext .js,.jsx -c .eslintrc-client client templates", 7 | "lint-client-test": "eslint --ext .js,.jsx -c .eslintrc-client-test test/client", 8 | "lint-server": "eslint -c .eslintrc-server server", 9 | "lint-server-test": "eslint -c .eslintrc-server-test test/server test/func", 10 | "lint": "npm run lint-client && npm run lint-client-test && npm run lint-server && npm run lint-server-test", 11 | "test-client": "node node_modules/karma/bin/karma start karma.conf.js", 12 | "test-client-ci": "node node_modules/karma/bin/karma start --browsers PhantomJS,Firefox karma.conf.coverage.js", 13 | "test-client-ci-win": "node node_modules/karma/bin/karma start --browsers PhantomJS,IE karma.conf.js", 14 | "test-client-cov": "node node_modules/karma/bin/karma start karma.conf.coverage.js", 15 | "test-client-dev": "node node_modules/karma/bin/karma start karma.conf.dev.js", 16 | "test-server-rest": "mocha --opts test/server/mocha.opts test/server/rest", 17 | "test-server-rest-cov": "istanbul cover --config .istanbul.server-rest.yml _mocha -- --opts test/server/mocha.opts test/server/rest", 18 | "test-server-unit": "mocha --opts test/server/mocha.opts test/server/spec", 19 | "test-server-unit-cov": "istanbul cover --config .istanbul.server-unit.yml _mocha -- --opts test/server/mocha.opts test/server/spec", 20 | "test-server": "npm run test-server-unit && npm run test-server-rest", 21 | "test-server-cov": "npm run test-server-unit-cov && npm run test-server-rest-cov", 22 | "test-func": "mocha --opts test/func/mocha.opts test/func/spec", 23 | "test-func-cov": "istanbul cover --config .istanbul.func.yml _mocha -- --opts test/func/mocha.opts test/func/spec", 24 | "test-func-dev": "mocha --opts test/func/mocha.dev.opts test/func/spec", 25 | "test": "npm run test-client && npm run test-server && npm run test-func", 26 | "test-ci": "npm run test-client-ci && npm run test-server-cov && npm run test-func-cov", 27 | "test-ci-win": "npm run test-client-ci-win && npm run test-server && echo 'TODO(36) fix Appveyor test-func'", 28 | "test-cov": "npm run test-client-cov && npm run test-server-cov && npm run test-func-cov", 29 | "test-dev": "npm run test-client-dev && npm run test-server && npm run test-func-dev", 30 | "check": "npm run lint && npm run test", 31 | "check-ci": "npm run lint && npm run test-ci", 32 | "check-ci-win": "npm run lint && npm run test-ci-win", 33 | "check-cov": "npm run lint && npm run test-cov", 34 | "check-dev": "npm run lint && npm run test-dev", 35 | "start": "node server/index.js", 36 | "server": "nodemon --watch client --watch server --watch templates --ext js,jsx server/index.js", 37 | "server-dev": "nodemon --watch client --watch server --watch templates --ext js,jsx server/index-dev.js", 38 | "server-hot": "nodemon --watch client --watch server --watch templates --ext js,jsx server/index-hot.js", 39 | "server-wds-dev": "webpack-dev-server --config webpack.config.dev.js --progress --colors --port 2992", 40 | "server-wds-hot": "webpack-dev-server --config webpack.config.hot.js --hot --progress --colors --port 2992 --inline", 41 | "server-wds-test": "webpack-dev-server --port 3001 --config webpack.config.test.js --colors", 42 | "sources": "http-server -p 3001 .", 43 | "watch": "webpack --watch --colors", 44 | "prod": "builder concurrent watch server sources", 45 | "dev": "builder concurrent server-wds-test server-wds-dev server-dev", 46 | "hot": "builder concurrent server-wds-test server-wds-hot server-hot", 47 | "build": "webpack", 48 | "install-dev": "selenium-standalone install", 49 | "postinstall": "node heroku/scripts/not-heroku.js || (node heroku/scripts/install.js && npm run build)" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/FormidableLabs/converter-react.git" 54 | }, 55 | "keywords": [ 56 | "react", 57 | "webpack", 58 | "babel", 59 | "example" 60 | ], 61 | "author": "Ryan Roemer ", 62 | "license": "MIT", 63 | "bugs": { 64 | "url": "https://github.com/FormidableLabs/converter-react/issues" 65 | }, 66 | "homepage": "https://github.com/FormidableLabs/converter-react", 67 | "engines": { 68 | "node": "0.12.x", 69 | "npm": "2.1.x" 70 | }, 71 | "dependencies": { 72 | "babel": "^5.8.19", 73 | "babel-core": "^5.8.19", 74 | "babel-loader": "^5.3.2", 75 | "babel-runtime": "^5.8.19", 76 | "bluebird": "^2.9.34", 77 | "bootstrap": "^3.3.5", 78 | "clean-webpack-plugin": "^0.1.3", 79 | "compression": "^1.5.2", 80 | "css-loader": "^0.19.0", 81 | "es6-promise": "^3.0.2", 82 | "express": "^4.13.1", 83 | "extract-text-webpack-plugin": "^0.8.2", 84 | "file-loader": "^0.8.4", 85 | "isomorphic-fetch": "^2.1.1", 86 | "react": "^15.0.1", 87 | "react-bootstrap": "^0.29.4", 88 | "react-dom": "^15.0.2", 89 | "react-redux": "^3.0.1", 90 | "redux": "^3.0.2", 91 | "redux-logger": "^2.0.1", 92 | "redux-thunk": "^1.0.0", 93 | "style-loader": "^0.12.4", 94 | "url-loader": "^0.5.6", 95 | "webpack": "^1.12.2", 96 | "webpack-stats-plugin": "0.1.0" 97 | }, 98 | "devDependencies": { 99 | "babel-eslint": "^4.0.10", 100 | "builder": "^3.1.0", 101 | "chai": "^3.2.0", 102 | "coveralls": "^2.11.4", 103 | "eslint": "^1.2.1", 104 | "eslint-config-defaults": "^4.2.0", 105 | "eslint-plugin-filenames": "^0.1.2", 106 | "eslint-plugin-react": "^3.2.3", 107 | "guacamole": "^1.1.2", 108 | "http-server": "^0.8.0", 109 | "isparta-loader": "^0.2.0", 110 | "istanbul": "^0.3.18", 111 | "jade": "^1.11.0", 112 | "karma": "^0.13.9", 113 | "karma-chrome-launcher": "^0.2.0", 114 | "karma-coverage": "^0.5.0", 115 | "karma-firefox-launcher": "^0.1.6", 116 | "karma-ie-launcher": "^0.2.0", 117 | "karma-mocha": "^0.2.0", 118 | "karma-phantomjs-launcher": "^0.2.1", 119 | "karma-phantomjs-shim": "^1.1.1", 120 | "karma-safari-launcher": "^0.1.1", 121 | "karma-sauce-launcher": "^0.2.14", 122 | "karma-spec-reporter": "0.0.20", 123 | "karma-webpack": "^1.7.0", 124 | "lodash": "^3.10.1", 125 | "marked": "^0.3.4", 126 | "mocha": "^2.2.5", 127 | "nodemon": "^1.4.0", 128 | "phantomjs": "^1.9.18", 129 | "react-addons-test-utils": "^15.0.2", 130 | "react-hot-loader": "^1.2.8", 131 | "recluster": "^0.4.0", 132 | "rowdy": "^0.3.2", 133 | "saucelabs": "^0.1.1", 134 | "selenium-standalone": "^5.1.0", 135 | "sinon": "^1.16.1", 136 | "sinon-chai": "^2.8.0", 137 | "supertest": "^1.0.1", 138 | "webdriverio": "^3.1.0", 139 | "webpack-dev-server": "^1.10.1" 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /server/converter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Convert strings! 5 | * 6 | * Common behavior: 7 | * 8 | * - Strip leading / trailing spaces. 9 | * - Internal spaces are treated as a delimeter. 10 | * - Collapse multiple occurences of delimeter. 11 | */ 12 | /*eslint-disable func-style*/ 13 | 14 | /** 15 | * Camel case a string. 16 | * 17 | * myString -> myString 18 | * mySTring -> myString 19 | * my_string -> myString 20 | * my-string -> myString 21 | * 22 | * @param {String} val string to convert 23 | * @returns {String} camel-cased string 24 | */ 25 | function camel(val) { 26 | return (val || "") 27 | .replace(/^\s+|\s+$/g, "") 28 | .replace(/([A-Z])([A-Z]+)/g, function (m, first, second) { 29 | return first + second.toLowerCase(); 30 | }) 31 | .replace(/[-_ ]+(.)/g, function (m, first) { 32 | return first.toUpperCase(); 33 | }); 34 | } 35 | 36 | /** 37 | * Parse string into delimeter version. 38 | * 39 | * Works for snake and dashed cases. 40 | * 41 | * @param {String} val string to convert 42 | * @param {String} delim delimiter to case string with 43 | * @returns {String} cased string 44 | * @api private 45 | */ 46 | function _convert(val, delim) { 47 | return (val || "") 48 | .replace(/^\s+|\s+$/g, "") 49 | .replace(/([a-z])([A-Z])/g, function (m, first, second) { 50 | return first + delim + second; 51 | }) 52 | .split(/[-_ ]+/).join(delim) 53 | .toLowerCase(); 54 | } 55 | 56 | /** 57 | * Snake case a string. 58 | * 59 | * myString -> my_string 60 | * mySTring -> my_string 61 | * my_string -> my_string 62 | * my-string -> my_string 63 | * 64 | * @param {String} val string to convert 65 | * @returns {String} snake-cased string 66 | */ 67 | function snake(val) { 68 | return _convert(val, "_"); 69 | } 70 | 71 | /** 72 | * Dasherize a string. 73 | * 74 | * myString -> my-string 75 | * my_string -> my-string 76 | * my-string -> my-string 77 | * 78 | * @param {String} val string to convert 79 | * @returns {String} dasherized string 80 | */ 81 | function dash(val) { 82 | return _convert(val, "-"); 83 | } 84 | 85 | module.exports = { 86 | camel: camel, 87 | snake: snake, 88 | dash: dash 89 | }; 90 | -------------------------------------------------------------------------------- /server/index-dev.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Development server. 5 | */ 6 | // Set environment. 7 | process.env.WEBPACK_DEV = "true"; // Switch to dev webpack-dev-server 8 | 9 | // Proxy existing server. 10 | var app = module.exports = require("./index"); 11 | 12 | // Actually start server if script. 13 | /* istanbul ignore next */ 14 | if (require.main === module) { 15 | app.indexRoute(/^\/$/); 16 | app.start(); 17 | } 18 | -------------------------------------------------------------------------------- /server/index-hot.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Development server. 5 | */ 6 | // Set environment. 7 | process.env.WEBPACK_HOT = "true"; // Switch to dev webpack-dev-server 8 | 9 | // Proxy existing server. 10 | var app = module.exports = require("./index"); 11 | 12 | // Actually start server if script. 13 | /* istanbul ignore next */ 14 | if (require.main === module) { 15 | app.indexRoute(/^\/$/); 16 | app.start(); 17 | } 18 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Express web server. 5 | */ 6 | // Globals 7 | var HOST = process.env.HOST || "127.0.0.1"; 8 | var PORT = process.env.PORT || 3000; 9 | var RENDER_JS = true; 10 | var RENDER_SS = true; 11 | 12 | // Hooks / polyfills 13 | require("babel/register"); 14 | // Prevent node from attempting to require .css files on the server 15 | require.extensions[".css"] = function () { return null; }; 16 | 17 | var clientApi = require("../client/utils/api"); 18 | 19 | var path = require("path"); 20 | var express = require("express"); 21 | var compress = require("compression"); 22 | var mid = require("./middleware"); 23 | 24 | var app = module.exports = express(); 25 | var converter = require("./converter"); 26 | 27 | // ---------------------------------------------------------------------------- 28 | // Setup, Static Routes 29 | // ---------------------------------------------------------------------------- 30 | app.use(compress()); 31 | 32 | // Static libraries and application HTML page. 33 | app.use("/js", express.static(path.join(__dirname, "../dist/js"))); 34 | 35 | // ---------------------------------------------------------------------------- 36 | // REST API 37 | // ---------------------------------------------------------------------------- 38 | app.get("/api/camel", function (req, res) { 39 | var from = req.query.from || ""; 40 | res.json({ from: from, to: converter.camel(from) }); 41 | }); 42 | app.get("/api/snake", function (req, res) { 43 | var from = req.query.from || ""; 44 | res.json({ from: from, to: converter.snake(from) }); 45 | }); 46 | app.get("/api/dash", function (req, res) { 47 | var from = req.query.from || ""; 48 | res.json({ from: from, to: converter.dash(from) }); 49 | }); 50 | 51 | // ---------------------------------------------------------------------------- 52 | // Application. 53 | // ---------------------------------------------------------------------------- 54 | // Client-side imports 55 | var React = require("react"); 56 | var ReactDOM = require("react-dom/server"); 57 | var Provider = require("react-redux").Provider; 58 | var Page = require("../client/containers/page"); 59 | var createStore = require("../client/store/create-store"); 60 | 61 | // Server-side React 62 | var Index = React.createFactory(require("../templates/index")); 63 | // Have to manually hack in the doctype because not contained with single 64 | // element for full page. 65 | var renderIndex = function (component) { 66 | return "" + ReactDOM.renderToStaticMarkup(component); 67 | }; 68 | 69 | app.indexRoute = function (root) { 70 | // -------------------------------------------------------------------------- 71 | // Middleware choice! 72 | // -------------------------------------------------------------------------- 73 | // 74 | // We support two different flux bootstrap data/component middlewares, that 75 | // can be set like: 76 | // 77 | // var fluxMiddleware = mid.flux.fetch(Page); // Fetch manually 78 | // var fluxMiddleware = mid.flux.actions(Page); // Instance actions. 79 | // 80 | var fluxMiddleware = mid.flux.fetch(Page); // Fetch manually. 81 | 82 | app.use(root, [fluxMiddleware], function (req, res) { 83 | /*eslint max-statements:[2,25]*/ 84 | // JS Bundle sources. 85 | var WEBPACK_TEST_BUNDLE = process.env.WEBPACK_TEST_BUNDLE; // Switch to test webpack-dev-server 86 | var WEBPACK_DEV = process.env.WEBPACK_DEV === "true"; // Switch to dev webpack-dev-server 87 | var WEBPACK_HOT = process.env.WEBPACK_HOT === "true"; 88 | 89 | // Render JS? Server-side? Bootstrap? 90 | var mode = req.query.__mode; 91 | var renderJs = RENDER_JS && mode !== "nojs"; 92 | var renderSs = RENDER_SS && mode !== "noss"; 93 | 94 | // JS/CSS bundle rendering. 95 | var devBundleJsUrl = "http://127.0.0.1:2992/js/bundle.js"; 96 | var devBundleCssUrl = "http://127.0.0.1:2992/js/style.css"; 97 | var bundleJs; 98 | var bundleCss; 99 | 100 | if (WEBPACK_TEST_BUNDLE) { 101 | bundleJs = renderJs ? WEBPACK_TEST_BUNDLE : null; 102 | bundleCss = devBundleCssUrl; 103 | } else if (WEBPACK_HOT) { 104 | // In hot mode, there is no CSS file because styles are inlined in a