├── .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 |
27 | ));
28 |
29 | return (
30 | }
36 | >
37 | {items}
38 |
39 |
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 |
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