├── Procfile
├── .coveralls.yml
├── .jshintignore
├── configs
├── local.env.json
├── images
│ └── index.js
├── analytics
│ └── index.js
└── index.js
├── assets
├── google24e9e21ce1f6df19.html
├── images
│ ├── 3.jpg
│ ├── 4.jpg
│ ├── 5.jpg
│ ├── favicon.ico
│ ├── mstile-70x70.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-36x36.png
│ ├── android-chrome-48x48.png
│ ├── android-chrome-72x72.png
│ ├── android-chrome-96x96.png
│ ├── android-chrome-144x144.png
│ ├── android-chrome-192x192.png
│ ├── apple-touch-icon-57x57.png
│ ├── apple-touch-icon-60x60.png
│ ├── apple-touch-icon-72x72.png
│ ├── apple-touch-icon-76x76.png
│ ├── apple-touch-icon-114x114.png
│ ├── apple-touch-icon-120x120.png
│ ├── apple-touch-icon-144x144.png
│ ├── apple-touch-icon-152x152.png
│ ├── apple-touch-icon-180x180.png
│ ├── apple-touch-icon-precomposed.png
│ ├── browserconfig.xml
│ ├── logo.svg
│ └── manifest.json
├── fonts
│ ├── icomoon.eot
│ ├── icomoon.ttf
│ └── icomoon.woff
├── robots.txt
├── scripts
│ └── header.js
└── styles
│ ├── _fonts.scss
│ ├── index.scss
│ └── _icon-fonts.scss
├── .slugignore
├── .gitignore
├── components
├── footer
│ ├── index.js
│ ├── License.jsx
│ ├── ByLine.jsx
│ ├── SiteBullets.jsx
│ ├── Footer.jsx
│ ├── _styles.scss
│ └── LocalBusiness.jsx
├── header
│ ├── index.js
│ ├── Logo.jsx
│ ├── Header.jsx
│ ├── Ribbon.jsx
│ ├── Nav.jsx
│ └── _styles.scss
├── pages
│ ├── contact
│ │ ├── index.js
│ │ ├── _result.scss
│ │ ├── _styles.scss
│ │ ├── _anim.scss
│ │ ├── elements.js
│ │ ├── Input.jsx
│ │ ├── Nav.jsx
│ │ ├── Steps.jsx
│ │ ├── _steps.scss
│ │ ├── Result.jsx
│ │ ├── _nav.scss
│ │ └── _arrows.scss
│ ├── Spinner.jsx
│ ├── _styles.scss
│ ├── ContentPage.jsx
│ └── index.js
├── PageContainer.jsx
├── _app.scss
├── Background.jsx
├── sizeReporter.js
└── Application.jsx
├── server
├── workers
│ └── contact
│ │ └── bin
│ │ └── contact
├── utils.js
├── sitemap.js
├── robots.js
└── index.js
├── services
├── mail
│ ├── index.js
│ ├── mailer.js
│ └── queue.js
├── data
│ ├── markdown.js
│ ├── index.js
│ ├── fetch.js
│ └── cache.js
├── error.js
├── page.js
├── routes.js
└── contact.js
├── actions
├── interface.js
├── size.js
├── init.js
├── contact.js
├── routes.js
└── page.js
├── tests
├── mocks
│ ├── service-mail.js
│ ├── mailer.js
│ ├── queue.js
│ ├── fetch.js
│ ├── cache.js
│ ├── superagent.js
│ ├── service-data.js
│ └── amqplib.js
├── functional
│ ├── browsers.js
│ ├── main.js
│ ├── run-parallel.js
│ ├── basic-specs.js
│ └── sauce-travis.js
├── utils
│ ├── jscsFilter.js
│ ├── tests.js
│ ├── testdom.js
│ └── mocks.js
├── unit
│ ├── utils
│ │ └── codes.js
│ ├── services
│ │ ├── mail
│ │ │ ├── index.js
│ │ │ └── queue.js
│ │ ├── contact.js
│ │ ├── page.js
│ │ ├── routes.js
│ │ ├── data
│ │ │ ├── index.js
│ │ │ └── fetch.js
│ │ └── error.js
│ ├── actions
│ │ ├── size.js
│ │ ├── init.js
│ │ ├── page.js
│ │ ├── contact.js
│ │ └── routes.js
│ └── stores
│ │ ├── RouteStore.js
│ │ ├── ApplicationStore.js
│ │ └── ContentStore.js
├── fixtures
│ ├── models-response.js
│ ├── fluxible-routes.js
│ ├── routes-response.js
│ └── cache-resources.js
├── workers
│ └── contact
│ │ └── contact.js
└── generators
│ └── routes-models.js
├── utils
├── serverStub.js
├── codes.js
├── index.js
└── urls.js
├── polyfill.js
├── app.js
├── client.js
├── LICENSE.md
├── stores
├── RouteStore.js
├── ApplicationStore.js
├── ContactStore.js
└── ContentStore.js
├── .travis.yml
└── .jscsrc
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm start
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 |
--------------------------------------------------------------------------------
/.jshintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | reports
3 | dist
--------------------------------------------------------------------------------
/configs/local.env.json:
--------------------------------------------------------------------------------
1 | {
2 | "PORT": 3000,
3 | "NODE_ENV": "development"
4 | }
--------------------------------------------------------------------------------
/assets/google24e9e21ce1f6df19.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google24e9e21ce1f6df19.html
--------------------------------------------------------------------------------
/.slugignore:
--------------------------------------------------------------------------------
1 | /assets
2 | /reports
3 | /tests
4 | Gruntfile.js
5 | client.js
6 | README.md
7 | LICENSE.md
--------------------------------------------------------------------------------
/assets/images/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/3.jpg
--------------------------------------------------------------------------------
/assets/images/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/4.jpg
--------------------------------------------------------------------------------
/assets/images/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/5.jpg
--------------------------------------------------------------------------------
/assets/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/fonts/icomoon.eot
--------------------------------------------------------------------------------
/assets/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/assets/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/fonts/icomoon.woff
--------------------------------------------------------------------------------
/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/favicon.ico
--------------------------------------------------------------------------------
/assets/images/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/mstile-70x70.png
--------------------------------------------------------------------------------
/assets/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/favicon-16x16.png
--------------------------------------------------------------------------------
/assets/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/favicon-32x32.png
--------------------------------------------------------------------------------
/assets/images/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/favicon-96x96.png
--------------------------------------------------------------------------------
/assets/images/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/mstile-144x144.png
--------------------------------------------------------------------------------
/assets/images/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/mstile-150x150.png
--------------------------------------------------------------------------------
/assets/images/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/mstile-310x150.png
--------------------------------------------------------------------------------
/assets/images/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/mstile-310x310.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/assets/images/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/android-chrome-36x36.png
--------------------------------------------------------------------------------
/assets/images/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/android-chrome-48x48.png
--------------------------------------------------------------------------------
/assets/images/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/android-chrome-72x72.png
--------------------------------------------------------------------------------
/assets/images/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/android-chrome-96x96.png
--------------------------------------------------------------------------------
/assets/images/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/android-chrome-144x144.png
--------------------------------------------------------------------------------
/assets/images/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/android-chrome-192x192.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/assets/images/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/localnerve/flux-react-example/master/assets/images/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .sass-cache
2 | .validate.json
3 | dist
4 | reports
5 | node_modules
6 | npm-debug.log
7 | /tmp
8 | /configs/settings/assets.json
9 | *sublime*
10 | /webpack-stats*
11 |
--------------------------------------------------------------------------------
/assets/robots.txt:
--------------------------------------------------------------------------------
1 | # www.robotstxt.org/
2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
3 |
4 | User-agent: *
5 |
6 | ALLOWURLS
7 |
8 | Sitemap: SITEMAPURL
9 |
10 | User-agent: ia_archiver
11 | Disallow: /
12 |
--------------------------------------------------------------------------------
/components/footer/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | module.exports = require('./Footer.jsx');
8 |
--------------------------------------------------------------------------------
/components/header/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | module.exports = require('./Header.jsx');
8 |
--------------------------------------------------------------------------------
/components/pages/contact/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | module.exports = require('./Contact.jsx');
--------------------------------------------------------------------------------
/server/workers/contact/bin/contact:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * Blocks while consuming the outgoing mail queue.
4 | * Use CTL-C to end.
5 | *
6 | * Uses environment variables listed in configs/contact/index.js
7 | */
8 | require('../../../../services/mail').worker();
9 |
--------------------------------------------------------------------------------
/services/mail/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var queue = require('./queue');
8 |
9 | module.exports = {
10 | send: queue.sendMail,
11 | worker: queue.contactWorker
12 | };
13 |
--------------------------------------------------------------------------------
/actions/interface.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * The actions that are eligible to be referenced from the backend data service.
6 | */
7 | 'use strict';
8 |
9 | module.exports = {
10 | page: require('./page')
11 | };
12 |
--------------------------------------------------------------------------------
/tests/mocks/service-mail.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | module.exports = {
8 | send: function(params, callback) {
9 | if (params.emulateError) {
10 | return callback(new Error('mock'));
11 | }
12 |
13 | callback();
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/tests/mocks/mailer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | */
6 | 'use strict';
7 |
8 | function send (payload, done) {
9 | if (payload.emulateError) {
10 | return done(new Error('mailer'));
11 | }
12 | done(null, payload);
13 | }
14 |
15 | module.exports = {
16 | send: send
17 | };
--------------------------------------------------------------------------------
/tests/functional/browsers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | module.exports = {
8 | chrome: {
9 | browserName: 'chrome'
10 | },
11 | firefox: {
12 | browserName: 'firefox'
13 | },
14 | explorer: {
15 | browserName: 'internet explorer'
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/assets/images/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #00a300
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/mocks/queue.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | module.exports = {
8 | sendMail: function (input, callback) {
9 | if (input.emulateError) {
10 | return callback(new Error('mock'));
11 | }
12 | callback();
13 | },
14 |
15 | contactWorker: function () {
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/tests/mocks/fetch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | module.exports = {
8 | fetchOne: function (params, callback) {
9 | callback(null, 'fetch');
10 | },
11 | fetchMain: function (callback) {
12 | callback(null, 'fetch');
13 | },
14 | fetchAll: function (callback) {
15 | callback(null, 'fetch');
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/utils/serverStub.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Build stub for ReactDOMServer to keep it out of the client bundle.
6 | */
7 | 'use strict';
8 |
9 | var noop = require('lodash/noop');
10 |
11 | /**
12 | * ReactDOMServer dummy.
13 | */
14 | var ReactDOMServer = {
15 | renderToString: noop,
16 | renderToStaticMarkup: noop
17 | };
18 |
19 | module.exports = ReactDOMServer;
20 |
--------------------------------------------------------------------------------
/assets/scripts/header.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * All custom header javascript.
6 | * This is the source of the built asset, not the asset itself.
7 | */
8 | /* global window */
9 |
10 | require('fontfaceobserver/fontfaceobserver');
11 |
12 | new window.FontFaceObserver('Source Sans Pro', {})
13 | .check()
14 | .then(function() {
15 | window.document.documentElement.className += 'fonts-loaded';
16 | });
17 |
--------------------------------------------------------------------------------
/polyfill.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Apply global client-side polyfills.
6 | */
7 | /* global Promise, Object */
8 | 'use strict';
9 |
10 | if (!Promise) {
11 | require('es6-promise').polyfill();
12 | }
13 |
14 | if (!Object.assign) {
15 | Object.defineProperty(Object, 'assign', {
16 | enumerable: false,
17 | configurable: true,
18 | writable: true,
19 | value: require('object-assign')
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/components/PageContainer.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var sizeReporter = require('./sizeReporter');
9 |
10 | var PageContainer = React.createClass({
11 | render: function () {
12 | return (
13 |
14 | {this.props.children}
15 |
16 | );
17 | }
18 | });
19 |
20 | module.exports = sizeReporter(PageContainer, '.page', {
21 | reportWidth: true
22 | });
23 |
--------------------------------------------------------------------------------
/components/pages/Spinner.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var ReactSpinner = require('react-spinner');
9 |
10 | var Spinner = React.createClass({
11 | render: function () {
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 | });
19 |
20 | module.exports = Spinner;
21 |
--------------------------------------------------------------------------------
/utils/codes.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | /**
8 | * Contain a status code to a finite set.
9 | * For now, if the code is a 404 it remains 404, otherwise its 500.
10 | *
11 | * @param {Number} statusCode - The status code to conform.
12 | * @returns {Number} 404 or 500.
13 | */
14 | function conformErrorStatus (statusCode) {
15 | return statusCode !== 404 ? '500' : '404';
16 | }
17 |
18 | module.exports = {
19 | conformErrorStatus: conformErrorStatus
20 | };
21 |
--------------------------------------------------------------------------------
/tests/mocks/cache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var routesResponse = require('../fixtures/routes-response');
8 |
9 | module.exports = {
10 | get: function (resource) {
11 | var result = 'hello world'; // ref: mocks/superagent.js defaultResponse
12 |
13 | if (resource === 'routes') {
14 | return routesResponse;
15 | }
16 |
17 |
18 | if (resource === 'miss') {
19 | result = undefined;
20 | }
21 | return result;
22 | },
23 | put: function () {}
24 | };
25 |
--------------------------------------------------------------------------------
/tests/utils/jscsFilter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * HACK:
6 | * This is a jscs error filter to hold me over until I figure out how to disable jsDoc rules
7 | * only for the test code.
8 | */
9 | var debug = require('debug')('jscs:errorFilter');
10 |
11 | module.exports = function errorFilter (err) {
12 | debug('jscs err: ', err);
13 |
14 | var ignore =
15 | err.filename.indexOf('/tests/') !== -1 && err.rule.indexOf('jsDoc') !== -1;
16 |
17 | debug('ignore', ignore);
18 |
19 | return !ignore;
20 | };
21 |
--------------------------------------------------------------------------------
/services/data/markdown.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:Markdown');
8 |
9 | var Remarkable = require('remarkable');
10 | var remarkable = new Remarkable('full', {
11 | html: true,
12 | linkify: true
13 | });
14 |
15 | /**
16 | * Parse markdown to markup.
17 | *
18 | * @param {String} input - The markdown to parse.
19 | * @returns {String} The markup.
20 | */
21 | function markdown (input) {
22 | debug('parsing markdown');
23 |
24 | return remarkable.render(input);
25 | }
26 |
27 | module.exports = markdown;
28 |
--------------------------------------------------------------------------------
/components/pages/_styles.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // styles for all pages
5 |
6 | .page-content {
7 | @include grid-content;
8 | min-width: 20rem;
9 |
10 | // set a layout boundary
11 | // height: 22rem;
12 | // width: 100%;
13 | // let content dictate, get perf elsewhere
14 |
15 | @include breakpoint(medium) {
16 | padding: 0 2rem;
17 | min-height: 20rem;
18 | min-width: 26rem;
19 | }
20 |
21 | p {
22 | @include breakpoint(medium) {
23 | font-size: 1.2rem;
24 | }
25 | }
26 |
27 | a {
28 | text-decoration: underline;
29 | }
30 | }
31 |
32 | @import "contact/styles";
33 |
--------------------------------------------------------------------------------
/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/actions/size.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:SizeAction');
8 |
9 | /**
10 | * The size action.
11 | * Just dispatches the UPDATE_SIZE action with the given payload.
12 | *
13 | * @param {Object} context - The fluxible action context.
14 | * @param {Object} payload - The UPDATE_SIZE action payload.
15 | * @param {Function} done - The callback to execute on action completion.
16 | */
17 | function updateSize (context, payload, done) {
18 | debug('dispatching UPDATE_SIZE', payload);
19 | context.dispatch('UPDATE_SIZE', payload);
20 | done();
21 | }
22 |
23 | module.exports = updateSize;
24 |
--------------------------------------------------------------------------------
/components/pages/contact/_result.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // styles for the result component
5 |
6 | .contact-result {
7 | h3, p {
8 | margin-top: 0.5rem;
9 | }
10 | .failure label {
11 | font-size: inherit;
12 | }
13 | }
14 | .contact-result-contact {
15 | margin-bottom: 0;
16 | margin-left: 1.2rem;
17 |
18 | a {
19 | display: block;
20 | text-decoration: none;
21 | span {
22 | padding-left: 1rem;
23 | }
24 | .help-note {
25 | display: block;
26 | }
27 | }
28 | a:not(:last-child) {
29 | margin-bottom: 0.7rem;
30 | @media only screen and (min-height: 600px) {
31 | margin-bottom: 1.2rem;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/components/pages/contact/_styles.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // Styles for the Contact component
5 |
6 | // import styles for supportive components
7 | @import "steps";
8 | @import "anim";
9 | @import "result";
10 | @import "nav";
11 |
12 | .contact-form {
13 | label {
14 | display: block;
15 | padding-bottom: 0.2rem;
16 | font-size: larger;
17 | }
18 | input, textarea {
19 | color: #222;
20 | }
21 | .form-value-element {
22 | width: 100%;
23 | }
24 | }
25 | .contact-intro {
26 | height: 2.2rem;
27 |
28 | // Set the min width only for not skinny phones
29 | @media only screen and (min-width: 350px) {
30 | min-width: rem-calc(328px);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/components/header/Logo.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var NavLink = require('fluxible-router').NavLink;
9 |
10 | var Logo = React.createClass({
11 | propTypes: {
12 | site: React.PropTypes.object.isRequired
13 | },
14 |
15 | render: function () {
16 | return (
17 |
18 |
19 |
20 | {this.props.site.name}
21 |
22 |
23 | {this.props.site.tagLine}
24 |
25 |
26 |
27 | );
28 | }
29 | });
30 |
31 | module.exports = Logo;
32 |
--------------------------------------------------------------------------------
/components/footer/License.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 |
9 | var License = React.createClass({
10 | propTypes: {
11 | license: React.PropTypes.object.isRequired
12 | },
13 |
14 | render: function () {
15 | var statements = this.props.license.statement.split(
16 | this.props.license.type
17 | );
18 |
19 | return (
20 |
29 | );
30 | }
31 | });
32 |
33 | module.exports = License;
34 |
--------------------------------------------------------------------------------
/tests/functional/main.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global before, after, describe, afterEach */
6 | 'use strict';
7 |
8 | var fs = require('fs');
9 | var path = require('path');
10 | var test = require('./sauce-travis');
11 |
12 | describe(test.name + ' (' + test.caps + ')', function() {
13 | this.timeout(test.timeout);
14 |
15 | before(function(done) {
16 | test.beforeAll(done);
17 | });
18 |
19 | afterEach(function(done) {
20 | test.updateState(this);
21 | done();
22 | });
23 |
24 | after(function(done) {
25 | test.afterAll(done);
26 | });
27 |
28 | fs.readdirSync(__dirname).forEach(function(item) {
29 | var name = path.basename(item);
30 | if (name.indexOf('-specs') !== -1) {
31 | require('./' + name);
32 | }
33 | });
34 | });
--------------------------------------------------------------------------------
/tests/unit/utils/codes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | var conformErrorStatus = require('../../../utils').conformErrorStatus;
11 |
12 | describe('conformErrorStatus', function () {
13 | it('should conform error status 404 to \'404\'', function () {
14 | var status = conformErrorStatus(404);
15 | expect(status).to.equal('404');
16 | expect(status).to.not.equal(404);
17 | });
18 |
19 | it('should conform any other status to \'500\'', function () {
20 | [
21 | 0, 200, 300, 304, 400, 401, 403, '404', 410, 412, 499, 500, 501, 503
22 | ].forEach(function (status) {
23 | expect(conformErrorStatus(status)).to.equal('500');
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/assets/images/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My app",
3 | "icons": [
4 | {
5 | "src": "\/android-chrome-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/android-chrome-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/android-chrome-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/android-chrome-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/android-chrome-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/android-chrome-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/components/footer/ByLine.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 |
9 | var ByLine = React.createClass({
10 | propTypes: {
11 | author: React.PropTypes.object.isRequired
12 | },
13 |
14 | render: function () {
15 | var byLine = this.props.author.byLine.replace(
16 | ' '+this.props.author.name, ''
17 | );
18 |
19 | return (
20 |
29 | );
30 | }
31 | });
32 |
33 | module.exports = ByLine;
34 |
--------------------------------------------------------------------------------
/components/footer/SiteBullets.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 |
9 | var SiteBullets = React.createClass({
10 | propTypes: {
11 | items: React.PropTypes.array.isRequired
12 | },
13 |
14 | render: function () {
15 | var items = this.props.items.map(function (item, index, arr) {
16 | var bullet = index < (arr.length - 1) ?
17 | • : ;
18 |
19 | return (
20 | {item}{bullet}
21 | );
22 | });
23 |
24 | return (
25 |
26 |
27 | {items}
28 |
29 |
30 | );
31 | }
32 | });
33 |
34 | module.exports = SiteBullets;
35 |
--------------------------------------------------------------------------------
/actions/init.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:InitAction');
8 |
9 | /**
10 | * Perform the init action.
11 | * The init action is intended for perparing the app state on the server.
12 | * Extensible - Many different properties can be passed to the app on this action.
13 | * Stores that listen to this action check for properties they are interested in.
14 | *
15 | * @param {Object} context - The fluxible action context.
16 | * @param {Object} payload - The INIT_APP action payload.
17 | * @param {Function} done - The callback to execute on completion.
18 | */
19 | function init (context, payload, done) {
20 | debug('dispatching INIT_APP', payload);
21 | context.dispatch('INIT_APP', payload);
22 | done();
23 | }
24 |
25 | module.exports = init;
26 |
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var FluxibleRouteTransformer = require('./FluxibleRouteTransformer');
8 | var buildImageUrl = require('./imageServiceUrls');
9 | var codes = require('./codes');
10 |
11 | /**
12 | * Factory to create a FluxibleRouteTransformer object.
13 | *
14 | * @param {Object} options - Options to control the object creation.
15 | * @param {Object} options.actions - The actions available for use in route transformations, and thus in the backend.
16 | */
17 | function createFluxibleRouteTransformer (options) {
18 | options = options || {};
19 | return new FluxibleRouteTransformer(options.actions);
20 | }
21 |
22 | module.exports = {
23 | createFluxibleRouteTransformer: createFluxibleRouteTransformer,
24 | buildImageUrl: buildImageUrl,
25 | conformErrorStatus: codes.conformErrorStatus
26 | };
27 |
--------------------------------------------------------------------------------
/components/footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var ByLine = require('./ByLine.jsx');
9 | var SiteBullets = require('./SiteBullets.jsx');
10 | var License = require('./License.jsx');
11 | var LocalBusiness = require('./LocalBusiness.jsx');
12 |
13 | var Footer = React.createClass({
14 | propTypes: {
15 | models: React.PropTypes.object.isRequired
16 | },
17 |
18 | render: function () {
19 | return (
20 |
26 | );
27 | }
28 | });
29 |
30 | module.exports = Footer;
31 |
--------------------------------------------------------------------------------
/components/pages/contact/_anim.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 |
5 | // manually keep in sync with animTimeout in Contact.jsx
6 | $transition-duration: 0.5s;
7 |
8 | .contact-anim-container {
9 | transform: TranslateZ(0);
10 |
11 | padding: 10px; // give the spread some room to show
12 |
13 | /*
14 | * Add layout bounderizer
15 | */
16 | height: 4.5rem;
17 | width: 100%;
18 | overflow: hidden;
19 | &.final {
20 | height: auto;
21 | overflow: visible;
22 | }
23 | }
24 |
25 | .contact-anim {
26 | transition: background-color $transition-duration,
27 | box-shadow $transition-duration,
28 | color $transition-duration;
29 | }
30 | .contact-anim-prev-enter, .contact-anim-next-enter {
31 | background-color: rgba($app-accent-light-bgcolor, 0.87);
32 | box-shadow: 0px 0px 10px 4px $app-accent-light-bgcolor;
33 | color: $app-primary-dark-color;
34 | }
35 |
--------------------------------------------------------------------------------
/components/pages/ContentPage.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var Spinner = require('./Spinner.jsx');
9 |
10 | var ContentPage = React.createClass({
11 | render: function () {
12 | var content = this.renderContent();
13 |
14 | return (
15 |
16 | {content}
17 |
18 | );
19 | },
20 | shouldComponentUpdate: function (nextProps) {
21 | return this.props.content !== nextProps.content;
22 | },
23 | renderContent: function () {
24 | if (this.props.spinner) {
25 | return (
26 |
27 | );
28 | } else {
29 | return (
30 |
31 |
32 | );
33 | }
34 | }
35 | });
36 |
37 | module.exports = ContentPage;
38 |
--------------------------------------------------------------------------------
/server/utils.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global Promise */
6 | 'use strict';
7 |
8 | /**
9 | * Utility to promisify a Node function
10 | *
11 | * @param {Function} nodeFunc - The node function to Promisify.
12 | */
13 | function nodeCall (nodeFunc /* args... */) {
14 | var nodeArgs = Array.prototype.slice.call(arguments, 1);
15 |
16 | return new Promise(function (resolve, reject) {
17 | /**
18 | * Resolve a node callback
19 | */
20 | function nodeResolver (err, value) {
21 | if (err) {
22 | reject(err);
23 | } else if (arguments.length > 2) {
24 | resolve.apply(resolve, Array.prototype.slice.call(arguments, 1));
25 | } else {
26 | resolve(value);
27 | }
28 | }
29 |
30 | nodeArgs.push(nodeResolver);
31 | nodeFunc.apply(nodeFunc, nodeArgs);
32 | });
33 | }
34 |
35 | module.exports = {
36 | nodeCall: nodeCall
37 | };
38 |
--------------------------------------------------------------------------------
/tests/unit/services/mail/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global before, after, describe, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var mocks = require('../../../utils/mocks');
10 |
11 | describe('mail/index', function () {
12 | var mail;
13 |
14 | before(function () {
15 | mocks.mail.begin();
16 | mail = require('../../../../services/mail/index');
17 | });
18 |
19 | after(function () {
20 | mocks.mail.end();
21 | });
22 |
23 | it('should send mail without error', function (done) {
24 | mail.send({
25 | name: 'tom',
26 | email: 'tom@heaven.org',
27 | message: 'thinking of you'
28 | }, function(err) {
29 | done(err);
30 | });
31 | });
32 |
33 | it('should expose a worker method', function () {
34 | expect(mail.worker).to.be.a('function');
35 | expect(mail).to.respondTo('worker');
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/assets/styles/_fonts.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | //
5 | // font styles
6 | // -------------------------
7 |
8 | @font-face {
9 | font-family: "Source Sans Pro";
10 | src: local("Source Sans Pro"), local("SourceSansPro-Regular"),
11 | url("http://fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2") format("woff2"),
12 | url("http://fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff") format("woff"),
13 | url("http://fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlEY6Fu39Tt9XkmtSosaMoEA.ttf") format("truetype");
14 | /* unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; */
15 | font-weight: 400;
16 | font-style: normal;
17 | }
18 |
19 | .fonts-loaded body {
20 | font-family: "Source Sans Pro", sans-serif;
21 | }
--------------------------------------------------------------------------------
/services/error.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Conform errors to Yahoo fetchr requirements for client reporting.
6 | */
7 | 'use strict';
8 |
9 | /**
10 | * Conform an error to Yahoo Fetchr requirements.
11 | *
12 | * @param {String | Object | Error} error - The error to conform, can be null.
13 | * @param {Number} [statusCode] - An optional statusCode to use to override
14 | * or define specific statusCode.
15 | * @returns {Falsy | Error | decorated} A Fetchr conformed error.
16 | */
17 | function decorateFetchrError (error, statusCode) {
18 | if (error) {
19 | error = typeof error === 'object' ? error : new Error(error.toString());
20 |
21 | error.statusCode = error.statusCode || error.status || statusCode || 400;
22 |
23 | error.output = {
24 | message: error.message,
25 | full: error.toString()
26 | };
27 | }
28 |
29 | return error;
30 | }
31 |
32 | module.exports = decorateFetchrError;
33 |
--------------------------------------------------------------------------------
/services/data/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:Data');
8 | var cache = require('./cache');
9 | var fetchLib = require('./fetch');
10 |
11 | /**
12 | * Get a resource from cache.
13 | * If not found, get a resource from FRED
14 | *
15 | * @param {Object} params - The parameters controlling fetch.
16 | * @param {String} params.resource - The name of the resource to fetch.
17 | * @param {Function} callback - The callback to execute on completion.
18 | */
19 | function fetch (params, callback) {
20 | debug('fetching resource "'+params.resource+'"');
21 |
22 | var resource = cache.get(params.resource);
23 |
24 | if (resource) {
25 | debug('cache hit');
26 | return callback(null, resource);
27 | }
28 |
29 | fetchLib.fetchOne(params, callback);
30 | }
31 |
32 | module.exports = {
33 | fetch: fetch,
34 | initialize: fetchLib.fetchMain,
35 | update: fetchLib.fetchAll
36 | };
37 |
--------------------------------------------------------------------------------
/components/header/Header.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var Ribbon = require('./Ribbon.jsx');
9 | var Logo = require('./Logo.jsx');
10 | var Nav = require('./Nav.jsx');
11 |
12 | var Header = React.createClass({
13 | propTypes: {
14 | selected: React.PropTypes.string.isRequired,
15 | links: React.PropTypes.array.isRequired,
16 | models: React.PropTypes.object.isRequired
17 | },
18 |
19 | render: function () {
20 | return (
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | });
34 |
35 | module.exports = Header;
36 |
--------------------------------------------------------------------------------
/tests/utils/tests.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Specific tests for reuse
6 | */
7 | 'use strict';
8 |
9 | function testTransform (expect, actual, expected) {
10 | Object.keys(expected).forEach(function (key) {
11 | expect(actual[key].page).to.eql(expected[key].page);
12 | expect(actual[key].path).to.eql(expected[key].path);
13 | expect(actual[key].method).to.eql(expected[key].method);
14 | expect(actual[key].label).to.eql(expected[key].label);
15 |
16 | var expectedActionContents = /\{([^\}]+)\}/.exec(''+expected[key].action)[1].replace(/\s+/g, '');
17 |
18 | expect(actual[key].action).to.be.a('function');
19 | expect(actual[key].action.length).to.eql(expected[key].action.length);
20 |
21 | // just compare function contents with no whitespace to cover instrumented code case.
22 | expect((''+actual[key].action).replace(/\s+/g, '')).to.contain(expectedActionContents);
23 | });
24 | }
25 |
26 | module.exports = {
27 | testTransform: testTransform
28 | };
29 |
--------------------------------------------------------------------------------
/assets/styles/index.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // style main
5 |
6 | @charset "UTF-8";
7 |
8 | $app-alert-bgcolor: #E53935;
9 | $app-primary-bgcolor: #43A047;
10 | $app-accent-light-bgcolor: #FFAB00;
11 | $app-accent-dark-bgcolor: #1B5E20;
12 | $app-accent-dark-shadow: rgba($app-accent-dark-bgcolor, 0.7);
13 | $app-primary-light-color: rgba(255, 255, 255, 0.87);
14 | $app-primary-dark-color: rgba(0, 0, 0, 0.87);
15 |
16 | @import
17 | "vendor",
18 | "icon-fonts",
19 | "fonts",
20 | "react-spinner";
21 |
22 | $height-constrained-phone: "only screen and (orientation: portrait) and (max-height: 480px)";
23 |
24 | @mixin vertical-block($align: center, $size: expand) {
25 | @include grid-block($size, vertical, false, $align);
26 | @content;
27 | }
28 |
29 | .grid-container-center {
30 | @include grid-container;
31 | }
32 |
33 | .grid-row-spaced {
34 | @include grid-block(expand, horizontal, false, spaced);
35 | }
36 |
37 | .hide {
38 | display: none !important;
39 | }
40 |
41 | @import "app";
42 |
--------------------------------------------------------------------------------
/tests/unit/services/contact.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, before, after, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var mocks = require('../../utils/mocks');
10 |
11 | describe('contact service', function() {
12 | var contact;
13 |
14 | before(function() {
15 | mocks.serviceMail.begin();
16 | contact = require('../../../services/contact');
17 | });
18 |
19 | after(function() {
20 | mocks.serviceMail.end();
21 | });
22 |
23 | describe('object', function() {
24 | it('should have name and create members', function() {
25 | expect(contact.name).to.be.a('string');
26 | expect(contact.create).to.be.a('function');
27 | });
28 | });
29 |
30 | describe('create', function() {
31 | it('should return a valid response', function(done) {
32 | contact.create(null, null, {}, null, null, function(err) {
33 | if (err) {
34 | done(err);
35 | }
36 | done();
37 | });
38 | });
39 | });
40 | });
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Assemble the Fluxible app.
6 | */
7 | 'use strict';
8 |
9 | var debug = require('debug')('Example:App');
10 | var FluxibleApp = require('fluxible');
11 | var fetchrPlugin = require('fluxible-plugin-fetchr');
12 | var ApplicationStore = require('./stores/ApplicationStore');
13 | var ContentStore = require('./stores/ContentStore');
14 | var ContactStore = require('./stores/ContactStore');
15 | var BackgroundStore = require('./stores/BackgroundStore');
16 | var RouteStore = require('./stores/RouteStore');
17 |
18 | debug('Creating FluxibleApp');
19 | var app = new FluxibleApp({
20 | component: require('./components/Application.jsx')
21 | });
22 |
23 | debug('Adding Plugins');
24 | app.plug(fetchrPlugin({ xhrPath: '/_api' }));
25 |
26 | debug('Registering Stores');
27 | app.registerStore(ApplicationStore);
28 | app.registerStore(ContentStore);
29 | app.registerStore(ContactStore);
30 | app.registerStore(BackgroundStore);
31 | app.registerStore(RouteStore);
32 |
33 | module.exports = app;
34 |
--------------------------------------------------------------------------------
/services/page.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * A Yahoo fetchr service definition for a page request
6 | */
7 | 'use strict';
8 |
9 | var data = require('./data');
10 | var error = require('./error');
11 |
12 | module.exports = {
13 | name: 'page',
14 |
15 | /**
16 | * The read CRUD method definition.
17 | * Just directs work. Params are per Yahoo fetchr.
18 | *
19 | * @param {Object} req - Not used.
20 | * @param {String} resource - Not used.
21 | * @param {Object} params - The data fetch parameters.
22 | * @param {Object} config - Not used.
23 | * @param {Function} callback - The callback to execute on completion.
24 | */
25 | read: function (req, resource, params, config, callback) {
26 | return data.fetch(params, function (err, data) {
27 | callback(error(err), data);
28 | });
29 | }
30 |
31 | // create: function(req, resource, params, body, config, callback) {},
32 | // update: function(resource, params, body, config, callback) {},
33 | // delete: function(resource, params, config, callback) {}
34 | };
35 |
--------------------------------------------------------------------------------
/services/routes.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * A Yahoo fetchr service definition for a routes request
6 | */
7 | 'use strict';
8 |
9 | var data = require('./data');
10 | var error = require('./error');
11 |
12 | module.exports = {
13 | name: 'routes',
14 |
15 | /**
16 | * The read CRUD method definition.
17 | * Directs work and mediates the response. Params are per Yahoo fetchr.
18 | *
19 | * @param {Object} req - Not used.
20 | * @param {String} resource - Not used.
21 | * @param {Object} params - The data fetch parameters.
22 | * @param {Object} config - Not used.
23 | * @param {Function} callback - The callback to execute on completion.
24 | */
25 | read: function (req, resource, params, config, callback) {
26 | return data.fetch(params, function (err, res) {
27 | callback(error(err), res ? res.content : null);
28 | });
29 | }
30 |
31 | // create: function(req, resource, params, body, config, callback) {},
32 | // update: function(resource, params, body, config, callback) {},
33 | // delete: function(resource, params, config, callback) {}
34 | };
35 |
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * The client-side entry point.
6 | */
7 | /* global document, window, DEBUG */
8 | 'use strict';
9 |
10 | // Apply global polyfills
11 | require('./polyfill');
12 |
13 | var debugLib = require('debug');
14 | var debug = debugLib('Example:Client');
15 | var ReactDOM = require('react-dom');
16 | var createElementWithContext = require('fluxible-addons-react').createElementWithContext;
17 |
18 | if (DEBUG) {
19 | window.React = require('react'); // for chrome dev tool support
20 | debugLib.enable('*'); // show debug trail
21 | }
22 |
23 | var app = require('./app');
24 | var dehydratedState = window.App; // sent from the server
25 |
26 | debug('rehydrating app');
27 | app.rehydrate(dehydratedState, function (err, context) {
28 | if (err) {
29 | throw err;
30 | }
31 |
32 | if (DEBUG) {
33 | window.context = context;
34 | }
35 |
36 | debug('rendering app');
37 | ReactDOM.render(
38 | createElementWithContext(context, {
39 | analytics: dehydratedState.analytics
40 | }),
41 | document.getElementById('application')
42 | );
43 | });
44 |
--------------------------------------------------------------------------------
/components/header/Ribbon.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var NavLink = require('fluxible-router').NavLink;
9 |
10 | var Ribbon = React.createClass({
11 | propTypes: {
12 | social: React.PropTypes.object.isRequired,
13 | business: React.PropTypes.object.isRequired
14 | },
15 |
16 | render: function () {
17 | var uriTel = 'tel:+1-' + this.props.business.telephone;
18 |
19 | return (
20 |
36 | );
37 | }
38 | });
39 |
40 | module.exports = Ribbon;
41 |
--------------------------------------------------------------------------------
/services/contact.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * A Yahoo fetchr service definition for contact creation.
6 | */
7 | 'use strict';
8 |
9 | var mail = require('./mail');
10 | var error = require('./error');
11 |
12 | module.exports = {
13 | name: 'contact',
14 |
15 | /**
16 | * The create CRUD method definition.
17 | * Just directs work. Params are per Yahoo fetchr.
18 | *
19 | * @param {Object} req - Not used.
20 | * @param {String} resource - Not used.
21 | * @param {Object} params - The collected contact fields to send.
22 | * @param {Object} body - Not used.
23 | * @param {Object} config - Not used.
24 | * @param {Function} callback - The callback to execute on completion.
25 | */
26 | create: function (req, resource, params, body, config, callback) {
27 | return mail.send(params, function (err, data) {
28 | callback(error(err), data);
29 | });
30 | }
31 |
32 | // read: function(req, resource, params, config, callback) {},
33 | // update: function(resource, params, body, config, callback) {},
34 | // delete: function(resource, params, config, callback) {}
35 | };
36 |
--------------------------------------------------------------------------------
/tests/fixtures/models-response.js:
--------------------------------------------------------------------------------
1 | /** This is a generated file **/
2 | /**
3 | NODE_ENV = development
4 | FRED_URL = https://api.github.com/repos/localnerve/flux-react-example-data/contents/resources.json?ref=development
5 | **/
6 | module.exports = JSON.parse(JSON.stringify(
7 | {"LocalBusiness":{"legalName":"LocalNerve, LLC","alternateName":"LocalNerve","url":"http://localnerve.com","telephone":"207-370-8005","email":"alex@localnerve.com","address":{"streetAddress":"PO BOX 95","addressRegion":"ME","addressLocality":"Windham","addressCountry":"USA","postalCode":"04062","postOfficeBoxNumber":"95"}},"SiteInfo":{"site":{"name":"Contactor","tagLine":"A Fluxible, Reactive reference app with a good prognosis.","bullets":["Fluxible","React","Data Driven"]},"license":{"type":"BSD","url":"https://github.com/localnerve/flux-react-example/blob/master/LICENSE.md","statement":"All code licensed under LocalNerve BSD License."},"developer":{"name":"LocalNerve","byLine":"Developed by LocalNerve","url":"http://localnerve.com"},"social":{"github":"https://github.com/localnerve/flux-react-example","twitter":"https://twitter.com/localnerve","facebook":"https://facebook.com/localnerve","linkedin":"https://www.linkedin.com/in/alexpaulgrant","googleplus":"https://plus.google.com/118303375063449115817/"}}}
8 | ));
--------------------------------------------------------------------------------
/tests/unit/actions/size.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | var createMockActionContext = require('fluxible/utils').createMockActionContext;
11 | var sizeAction = require('../../../actions/size');
12 | var BackgroundStore = require('../../../stores/BackgroundStore');
13 |
14 | describe('size action', function () {
15 | var context, params = {
16 | width: 1,
17 | height: 2,
18 | top: 3,
19 | add: false
20 | };
21 |
22 | // create the action context wired to BackgroundStore
23 | beforeEach(function () {
24 | context = createMockActionContext({
25 | stores: [ BackgroundStore ]
26 | });
27 | });
28 |
29 | it('should update the background store', function (done) {
30 | // TODO: add listener to store to get the whole story
31 | context.executeAction(sizeAction, params, function (err) {
32 | if (err) {
33 | return done(err);
34 | }
35 |
36 | var store = context.getStore(BackgroundStore);
37 | expect(store.getTop()).to.equal(params.top);
38 |
39 | done();
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/components/header/Nav.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var NavLink = require('fluxible-router').NavLink;
9 | var sizeReporter = require('../sizeReporter');
10 | var cx = require('classnames');
11 |
12 | var Nav = React.createClass({
13 | propTypes: {
14 | selected: React.PropTypes.string.isRequired,
15 | links: React.PropTypes.array.isRequired
16 | },
17 |
18 | render: function () {
19 | var selected = this.props.selected,
20 | links = this.props.links,
21 | linkHTML = links.map(function (link) {
22 | return (
23 |
29 | {link.label}
30 |
31 | );
32 | });
33 | return (
34 |
37 | );
38 | }
39 | });
40 |
41 | module.exports = sizeReporter(Nav, '.navigation', {
42 | reportTop: true
43 | });
44 |
--------------------------------------------------------------------------------
/services/mail/mailer.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * TODO: add sanitizer
6 | */
7 | 'use strict';
8 |
9 | var mailer = require('nodemailer');
10 | var contact = require('../../configs').create().contact;
11 |
12 | /**
13 | * Send mail to a well-known mail service.
14 | * Uses configs/contact configuration object for mail settings.
15 | *
16 | * @param {Object} payload - The mail payload
17 | * @param {String} payload.name - The replyTo name
18 | * @param {String} payload.email - The replyTo email address
19 | * @param {String} payload.message - The mail message body
20 | * @param {Function} done - The callback to execute on completion.
21 | */
22 | function send (payload, done) {
23 | var transport = mailer.createTransport({
24 | service: contact.mail.service(),
25 | auth: {
26 | user: contact.mail.username(),
27 | pass: contact.mail.password()
28 | }
29 | });
30 |
31 | transport.sendMail({
32 | from: contact.mail.from(),
33 | to: contact.mail.to(),
34 | replyTo: payload.name + ' <' + payload.email + '>',
35 | subject: contact.mail.subject,
36 | text: payload.message
37 | }, done);
38 | }
39 |
40 | module.exports = {
41 | send: send
42 | };
43 |
--------------------------------------------------------------------------------
/tests/unit/services/page.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, before, after, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var mocks = require('../../utils/mocks');
10 |
11 | describe('page service', function () {
12 | var page;
13 |
14 | before(function () {
15 | mocks.serviceData.begin();
16 | page = require('../../../services/page');
17 | });
18 |
19 | after(function () {
20 | mocks.serviceData.end();
21 | });
22 |
23 | describe('object', function () {
24 | it('should have name and read members', function () {
25 | expect(page.name).to.be.a('string');
26 | expect(page.read).to.be.a('function');
27 | });
28 | });
29 |
30 | describe('read', function () {
31 | it('should return a valid response', function (done) {
32 | page.read(null, null, { resource: 'home' }, null, function (err, data) {
33 | if (err) {
34 | done(err);
35 | }
36 | expect(data).to.be.an('object');
37 | expect(data).to.have.property('models')
38 | .that.is.an('object');
39 | expect(data).to.have.property('content')
40 | .that.is.a('string');
41 | done();
42 | });
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/tests/unit/services/routes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, before, after, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | var mocks = require('../../utils/mocks');
11 | var routesResponse = require('../../fixtures/routes-response');
12 |
13 | describe('routes service', function () {
14 | var routes;
15 |
16 | before(function () {
17 | mocks.serviceData.begin();
18 | routes = require('../../../services/routes');
19 | });
20 |
21 | after(function () {
22 | mocks.serviceData.end();
23 | });
24 |
25 | describe('object', function () {
26 | it('should have name and read members', function () {
27 | expect(routes.name).to.be.a('string');
28 | expect(routes.read).to.be.a('function');
29 | });
30 | });
31 |
32 | describe('read', function () {
33 | it('should return a valid response', function (done) {
34 | routes.read(null, null, { resource: 'routes' }, null, function (err, data) {
35 | if (err) {
36 | done(err);
37 | }
38 | expect(data).to.be.an('object');
39 | expect(JSON.stringify(routesResponse.home)).to.equal(JSON.stringify(data.home));
40 | done();
41 | });
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/tests/unit/actions/init.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | var createMockActionContext = require('fluxible/utils').createMockActionContext;
11 | var initAction = require('../../../actions/init');
12 | var BackgroundStore = require('../../../stores/BackgroundStore');
13 |
14 | describe('init action', function () {
15 | var context, params = {
16 | backgrounds: {
17 | serviceUrl: 'http://lorempixel.com',
18 | backgrounds: ['1', '2']
19 | }
20 | };
21 |
22 | // create the action context wired to BackgroundStore
23 | beforeEach(function () {
24 | context = createMockActionContext({
25 | stores: [ BackgroundStore ]
26 | });
27 | });
28 |
29 | it('should update the background store', function (done) {
30 | context.executeAction(initAction, params, function (err) {
31 | if (err) {
32 | return done(err);
33 | }
34 |
35 | var store = context.getStore(BackgroundStore);
36 |
37 | expect(store.getImageServiceUrl()).to.equal(params.backgrounds.serviceUrl);
38 | expect(Object.keys(store.backgroundUrls)).to.have.length(2);
39 |
40 | done();
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/components/pages/contact/elements.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var Input = require('./Input.jsx');
9 | var Result = require('./Result.jsx');
10 | var merge = require('lodash/merge');
11 |
12 | var classes = {
13 | name: Input,
14 | email: Input,
15 | message: Input,
16 | result: Result
17 | };
18 |
19 | var inputProps = {
20 | name: {
21 | inputElement: 'input',
22 | inputType: 'text',
23 | inputId: 'name-input'
24 | },
25 | email: {
26 | inputElement: 'input',
27 | inputType: 'email',
28 | inputId: 'email-input'
29 | },
30 | message: {
31 | inputElement: 'textarea',
32 | inputId: 'message-input'
33 | },
34 | result: {}
35 | };
36 |
37 | module.exports = {
38 | /**
39 | * Create a Contact Element
40 | *
41 | * @param {String} component - The name of the component to create.
42 | * @param {Object} props - The props to create the component with.
43 | * @returns {Object} A React Element for the given contact component name and props.
44 | */
45 | createElement: function (component, props) {
46 | return React.createElement(
47 | classes[component],
48 | merge(props, inputProps[component])
49 | );
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/tests/mocks/superagent.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var routesResponse = require('../fixtures/routes-response');
8 | var config = require('../../configs').create().data;
9 |
10 | // setup some canned responses
11 | var defaultResponse = 'aGVsbG8gd29ybGQK'; // base64 encoded 'hello world'
12 | var responses = {};
13 | // base64 encode routes-response fixture as response for FRED.url
14 | responses[config.FRED.url()] =
15 | new Buffer(JSON.stringify(routesResponse)).toString(config.FRED.contentEncoding());
16 |
17 | function SuperAgent () {
18 | }
19 |
20 | SuperAgent.prototype = {
21 | get: function (url) {
22 | this.url = url;
23 | return this;
24 | },
25 | set: function () {
26 | return this;
27 | },
28 | end: function (cb) {
29 | var body;
30 |
31 | if (!this.noData) {
32 | body = {
33 | content: responses[this.url] || defaultResponse
34 | };
35 | }
36 |
37 | cb(this.emulateError ? new Error('mock error') : null, { body: body });
38 | },
39 | /***
40 | * Mock control only
41 | */
42 | setEmulateError: function (value) {
43 | this.emulateError = value;
44 | },
45 | setNoData: function (value) {
46 | this.noData = value;
47 | }
48 | };
49 |
50 | module.exports = new SuperAgent();
51 |
--------------------------------------------------------------------------------
/tests/utils/testdom.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Start/stop jsdom environment
6 | */
7 | /* global global, document */
8 | 'use strict';
9 |
10 | var jsdom = require('jsdom').jsdom;
11 |
12 | /**
13 | * Shim document, window, and navigator with jsdom if not defined.
14 | * Init document with markup if specified.
15 | * Add globals if specified.
16 | */
17 | function start (markup, addGlobals) {
18 | if (typeof document !== 'undefined') {
19 | return;
20 | }
21 |
22 | var globalKeys = [];
23 |
24 | global.document = jsdom(markup || '');
25 | global.window = document.defaultView;
26 | global.navigator = global.window.navigator;
27 |
28 | if (addGlobals) {
29 | Object.keys(addGlobals).forEach(function (key) {
30 | global.window[key] = addGlobals[key];
31 | globalKeys.push(key);
32 | });
33 | }
34 |
35 | return globalKeys;
36 | }
37 |
38 | /**
39 | * Remove globals
40 | */
41 | function stop (globalKeys) {
42 | if (globalKeys) {
43 | globalKeys.forEach(function (key) {
44 | delete global.window[key];
45 | });
46 | }
47 |
48 | delete global.document;
49 | delete global.window;
50 | delete global.navigator;
51 | }
52 |
53 | module.exports = {
54 | start: start,
55 | stop: stop
56 | };
57 |
--------------------------------------------------------------------------------
/assets/styles/_icon-fonts.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | //
5 | // icon font styles
6 | // -------------------------
7 |
8 | @font-face {
9 | font-family: "icomoon";
10 | src: font-url("icomoon.eot?1xh0ll");
11 | src: font-url("icomoon.eot?#iefix1xh0ll") format("embedded-opentype"),
12 | inline-font-files("icomoon.woff", "woff"),
13 | font-url("icomoon.ttf?1xh0ll") format("truetype"),
14 | font-url("icomoon.svg?1xh0ll#icomoon") format("svg");
15 | font-weight: normal;
16 | font-style: normal;
17 | }
18 |
19 | [class^="icon-"], [class*=" icon-"] {
20 | font-family: "icomoon";
21 | speak: none;
22 | font-style: normal;
23 | font-weight: normal;
24 | font-variant: normal;
25 | text-transform: none;
26 | line-height: 1;
27 |
28 | -webkit-font-smoothing: antialiased;
29 | -moz-osx-font-smoothing: grayscale;
30 | }
31 |
32 | .icon-copy:before {
33 | content: "\e92c";
34 | }
35 | .icon-phone:before {
36 | content: "\e942";
37 | }
38 |
39 | .icon-envelop:before {
40 | content: "\e945";
41 | }
42 |
43 | .icon-google-plus:before {
44 | content: "\ea88";
45 | }
46 |
47 | .icon-facebook:before {
48 | content: "\ea8c";
49 | }
50 |
51 | .icon-twitter:before {
52 | content: "\ea91";
53 | }
54 |
55 | .icon-github4:before {
56 | content: "\eab4";
57 | }
58 |
59 | .icon-linkedin2:before {
60 | content: "\eac9";
61 | }
62 |
--------------------------------------------------------------------------------
/configs/images/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Environment specific configuration for images.
6 | *
7 | * Environment variables:
8 | * IMAGE_SERVICE_URL - A string that denotes an image service, default lorempixel.
9 | * CLOUD_NAME - A string that denotes a cloud name for use in an image service.
10 | */
11 | 'use strict';
12 |
13 | /**
14 | * Get the IMAGE_SERVICE_URL configuration value.
15 | * Defaults to FIRESIZE_URL or lorempixel if FIRESIZE_URL is not defined.
16 | * Note: To use Cloudinary set IMAGE_SERVICE_URL to 'http://res.cloudinary.com'
17 | *
18 | * @returns {String} The IMAGE_SERVICE_URL configuration value.
19 | */
20 | function IMAGE_SERVICE_URL () {
21 | return process.env.IMAGE_SERVICE_URL || process.env.FIRESIZE_URL || 'http://lorempixel.com';
22 | }
23 |
24 | /**
25 | * Get the CLOUD_NAME configuration value.
26 | * This is used in Cloudinary to identify the account.
27 | *
28 | * @returns {String} The CLOUD_NAME configuration value.
29 | */
30 | function CLOUD_NAME () {
31 | return process.env.CLOUD_NAME;
32 | }
33 |
34 | /**
35 | * Make the images configuration object.
36 | *
37 | * @returns the images configuration object.
38 | */
39 | function makeConfig () {
40 | return {
41 | service: {
42 | url: IMAGE_SERVICE_URL,
43 | cloudName: CLOUD_NAME
44 | }
45 | };
46 | }
47 |
48 | module.exports = makeConfig;
49 |
--------------------------------------------------------------------------------
/tests/functional/run-parallel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * A node command line program to run mocha in parallel per browser spec.
6 | * Relies on global mocha package
7 | */
8 | 'use strict';
9 |
10 | var exec = require('child_process').exec;
11 | var Q = require('q');
12 | var browserSpecs = require('./browsers');
13 |
14 | var mochaArgs = process.argv[2];
15 | var baseUrl = process.argv[3];
16 |
17 | var browsers = Object.keys(browserSpecs);
18 |
19 | // context specific log
20 | function log(config, data) {
21 | config = (config + ' ').slice(0, 10);
22 | ('' + data).split(/(\r?\n)/g).forEach(function(line) {
23 | if (line.replace(/\033\[[0-9;]*m/g,'').trim().length >0) {
24 | console.log(config + ': ' + line.trimRight() );
25 | }
26 | });
27 | }
28 |
29 | // Run a mocha test for a given browser
30 | function runMocha(browser, baseUrl, done) {
31 | var env = JSON.parse(JSON.stringify(process.env));
32 | env.TEST_BROWSER = browser;
33 | env.TEST_BASEURL = baseUrl;
34 |
35 | var mocha = exec('mocha ' + mochaArgs, {
36 | env: env
37 | }, done);
38 |
39 | mocha.stdout.on('data', log.bind(null, browser));
40 | mocha.stderr.on('data', log.bind(null, browser));
41 | }
42 |
43 | Q.all(browsers.map(function(browser) {
44 | return Q.nfcall(runMocha, browser, baseUrl);
45 | })).then(function() {
46 | console.log('ALL TESTS SUCCESSFUL');
47 | })
48 | .done();
--------------------------------------------------------------------------------
/components/pages/contact/Input.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 |
9 | var ContactInput = React.createClass({
10 | propTypes: {
11 | fieldValue: React.PropTypes.string,
12 | setInputReference: React.PropTypes.func.isRequired,
13 | label: React.PropTypes.object.isRequired,
14 | inputElement: React.PropTypes.string.isRequired,
15 | inputType: React.PropTypes.string,
16 | inputId: React.PropTypes.string.isRequired,
17 | focus: React.PropTypes.bool.isRequired
18 | },
19 |
20 | render: function () {
21 | var inputElement = React.createElement(this.props.inputElement, {
22 | type: this.props.inputType,
23 | id: this.props.inputId,
24 | name: this.props.inputId,
25 | key: this.props.inputId,
26 | title: this.props.label.help,
27 | placeholder: this.props.label.help,
28 | ref: this.props.setInputReference,
29 | className: 'form-value-element',
30 | autoFocus: this.props.focus,
31 | required: true,
32 | 'aria-required': true,
33 | defaultValue: this.props.fieldValue
34 | });
35 | return (
36 |
37 |
40 | {inputElement}
41 |
42 | );
43 | }
44 | });
45 |
46 | module.exports = ContactInput;
47 |
--------------------------------------------------------------------------------
/components/_app.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // Components styles
5 | //
6 | .app-frame {
7 | @include grid-frame(vertical);
8 | // If viewport height less than 728 AND landscape, float the footer
9 | // under the content. Otherwise height is 100vh, and footer sticks to viewport
10 | // bottom.
11 | @media only screen and (orientation: landscape) and (max-height: 727px) {
12 | height: auto;
13 | }
14 | }
15 | .app-block {
16 | @include vertical-block(left);
17 | }
18 |
19 | .app-bg {
20 | position: absolute;
21 | // assigned in js
22 | // top: 0;
23 | right:0;
24 | bottom: 0;
25 | left: 0;
26 | width: 100%;
27 | // assigned in js
28 | // height: 100%;
29 |
30 | background-color: transparent;
31 | background-repeat: no-repeat;
32 |
33 | // assigned in js
34 | // opacity
35 |
36 | transition: opacity 0.4s ease;
37 | }
38 |
39 | .page {
40 | @include vertical-block(spaced);
41 | }
42 |
43 | .swipe-container {
44 | // Allow vertical content scrolling for longer content/constrained height
45 | overflow-y: auto !important;
46 | }
47 |
48 | a, a:visited {
49 | color: $app-primary-light-color;
50 | text-decoration: none;
51 | outline: 0;
52 | }
53 | a:hover {
54 | color: darken($app-primary-light-color, 10%);
55 | }
56 |
57 | %header-footer-bg {
58 | background: $app-primary-bgcolor;
59 | }
60 |
61 | @import "header/styles";
62 | @import "pages/styles";
63 | @import "footer/styles";
64 |
--------------------------------------------------------------------------------
/tests/unit/services/data/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global before, after, describe, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var mocks = require('../../../utils/mocks');
10 |
11 | describe('data/index', function () {
12 | var data, cache;
13 |
14 | before(function () {
15 | mocks.fetch.begin();
16 | data = require('../../../../services/data');
17 | cache = require('./cache');
18 | });
19 |
20 | after(function () {
21 | mocks.fetch.end();
22 | });
23 |
24 | describe('fetch', function () {
25 | it('should pull from cache if exists', function (done) {
26 | data.fetch({}, function (err, res) {
27 | if (err) {
28 | done(err);
29 | }
30 |
31 | expect(res).to.equal(cache.get());
32 | done();
33 | });
34 | });
35 |
36 | it('should fetch if not in cache', function (done) {
37 | data.fetch({ resource: 'miss' }, function (err, res) {
38 | if (err) {
39 | done(err);
40 | }
41 |
42 | expect(res).to.equal('fetch');
43 | done();
44 | });
45 | });
46 | });
47 |
48 | describe('initialize', function () {
49 | it('should initialize', function (done) {
50 | data.initialize(done);
51 | });
52 | });
53 |
54 | describe('update', function () {
55 | it('should update', function (done) {
56 | data.update(done);
57 | });
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 |
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 |
13 | * Neither the name of the LocalNerve, LLC nor the
14 | names of its contributors may be used to endorse or promote products
15 | derived from this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 | DISCLAIMED. IN NO EVENT SHALL LocalNerve, LLC BE LIABLE FOR ANY
21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/tests/unit/services/error.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, before, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | describe('error decorator', function () {
11 | var error;
12 |
13 | before(function() {
14 | error = require('../../../services/error');
15 | });
16 |
17 | it('should pass through falsy error', function () {
18 | expect(error(false)).to.be.false;
19 | expect(error()).to.be.undefined;
20 | expect(error(null)).to.be.null;
21 | expect(error('')).to.be.a('string').that.is.empty;
22 | expect(error(0)).to.equal(0);
23 | expect(isNaN(error(NaN))).to.be.true;
24 | });
25 |
26 | it('should convert string to Error', function () {
27 | expect(error('this is an error')).to.be.an.instanceof(Error);
28 | });
29 |
30 | it('should decorate an object with Fetchr requirements', function () {
31 | var decoratedErr = error({});
32 |
33 | expect(decoratedErr).to.have.property('statusCode');
34 | expect(decoratedErr).to.have.property('output');
35 | });
36 |
37 | it('should decorate an Error with Fetchr requirements', function () {
38 | var err = new Error('this is an error');
39 | var decoratedErr = error(err);
40 |
41 | expect(decoratedErr).to.have.property('statusCode');
42 | expect(decoratedErr).to.have.property('output');
43 | expect(decoratedErr.output).to.have.property('message');
44 | expect(decoratedErr.output.message).to.equal(err.message);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/stores/RouteStore.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Extend the fluxible route store so routes can have action functions.
6 | * Just de/rehydate the functions.
7 | */
8 | 'use strict';
9 |
10 | var FluxibleRouteStore = require('fluxible-router').RouteStore;
11 | var inherits = require('inherits');
12 | var transformer = require('../utils').createFluxibleRouteTransformer({
13 | actions: require('../actions/interface')
14 | });
15 |
16 | /**
17 | * Creates a RouteStore.
18 | *
19 | * @class
20 | */
21 | function RouteStore () {
22 | FluxibleRouteStore.apply(this, arguments);
23 | }
24 |
25 | inherits(RouteStore, FluxibleRouteStore);
26 |
27 | RouteStore.storeName = FluxibleRouteStore.storeName;
28 | RouteStore.handlers = FluxibleRouteStore.handlers;
29 |
30 | /**
31 | * Dehydrates this object to state.
32 | * Transforms routes to json.
33 | *
34 | * @returns {Object} The RouteStore represented as state.
35 | */
36 | RouteStore.prototype.dehydrate = function dehydrate () {
37 | var state = FluxibleRouteStore.prototype.dehydrate.apply(this, arguments);
38 | state.routes = transformer.fluxibleToJson(state.routes);
39 | return state;
40 | };
41 |
42 | /**
43 | * Rehydrates this object from state.
44 | * Creates routes from json using transformer.
45 | */
46 | RouteStore.prototype.rehydrate = function rehydrate (state) {
47 | state.routes = transformer.jsonToFluxible(state.routes);
48 | return FluxibleRouteStore.prototype.rehydrate.apply(this, arguments);
49 | };
50 |
51 | module.exports = RouteStore;
52 |
--------------------------------------------------------------------------------
/actions/contact.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:ContactAction');
8 |
9 | /**
10 | * Perform the contact service request.
11 | *
12 | * @param {Object} context - The fluxible action context.
13 | * @param {Object} fields - The contact fields.
14 | * @param {Function} done - The callback to execute on completion.
15 | */
16 | function serviceRequest (context, fields, done) {
17 | context.service.create('contact', fields, {}, {}, function(err) {
18 | if (err) {
19 | debug('dispatching CREATE_CONTACT_FAILURE');
20 | context.dispatch('CREATE_CONTACT_FAILURE', fields);
21 | return done();
22 | }
23 |
24 | debug('dispatching CREATE_CONTACT_SUCCESS');
25 | context.dispatch('CREATE_CONTACT_SUCCESS', fields);
26 | done();
27 | });
28 | }
29 |
30 | /**
31 | * Perform the contact action.
32 | *
33 | * @param {Object} context - The fluxible context.
34 | * @param {Object} payload - The action payload.
35 | * @param {Object} payload.fields - The contact fields.
36 | * @param {Boolean} payload.complete - Flag indicating contact field gathering is complete.
37 | * @param {Function} done - The callback to execute on completion.
38 | */
39 | function contact (context, payload, done) {
40 | debug('dispatching UPDATE_CONTACT_FIELDS', payload.fields);
41 | context.dispatch('UPDATE_CONTACT_FIELDS', payload.fields);
42 |
43 | if (!payload.complete) {
44 | return done();
45 | }
46 |
47 | serviceRequest(context, payload.fields, done);
48 | }
49 |
50 | module.exports = contact;
51 |
--------------------------------------------------------------------------------
/configs/analytics/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Environment variables can override the following:
6 | * ANALYTICS_ID - The analytics id used in the trackingTemplate.
7 | */
8 | /*jshint multistr: true */
9 | 'use strict';
10 |
11 | var uaID = {
12 | development: 'UA-XXXXXXXX-D',
13 | production: 'UA-31065754-3'
14 | };
15 |
16 | var uaRef = 'ga';
17 |
18 | var trackingTemplate = '(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){ \
19 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), \
20 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) \
21 | })(window,document,"script","//www.google-analytics.com/analytics.js","__UAREF__"); \
22 | __UAREF__("create", "__UAID__", "auto"); \
23 | __UAREF__("send", "pageview");';
24 |
25 | /**
26 | * Get the analytics id.
27 | *
28 | * @param {String} env - The node environment
29 | * @access private
30 | * @returns {String} The analytics id for use in the trackingTemplate.
31 | */
32 | function UAID (env) {
33 | return process.env.ANALYTICS_ID || uaID[env];
34 | }
35 |
36 | /**
37 | * Make the analytics configuration object.
38 | *
39 | * @param {Object} nconf - The nconfig object
40 | * @returns {Object} The analytics configuration object.
41 | */
42 | function makeConfig (nconf) {
43 | var env = nconf.get('NODE_ENV');
44 |
45 | return {
46 | snippet: trackingTemplate
47 | .replace(/__UAID__/g, UAID(env))
48 | .replace(/__UAREF__/g, uaRef),
49 | globalRef: uaRef
50 | };
51 | }
52 |
53 | module.exports = makeConfig;
54 |
--------------------------------------------------------------------------------
/components/footer/_styles.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | .app-footer {
5 | @include vertical-block(left) {
6 | max-height: 8rem;
7 | min-height: 8rem;
8 | @media #{$height-constrained-phone} {
9 | max-height: 6rem;
10 | min-height: 6rem;
11 | }
12 | }
13 |
14 | @extend %header-footer-bg;
15 |
16 | box-shadow: inset 0 8px 10px -7px $app-accent-dark-shadow;
17 | /*
18 | span {
19 | @extend %header-footer-text;
20 | }
21 | */
22 | }
23 |
24 | .footer-line {
25 | align-items: center;
26 | padding: 0 1rem;
27 | overflow-y: hidden;
28 |
29 | .contact-text {
30 | font-size: 85%;
31 | padding-right: 1em;
32 |
33 | @media #{$height-constrained-phone} {
34 | font-size: 80%;
35 | }
36 | }
37 | .contact-links {
38 | a {
39 | display: block;
40 | padding: 0.25em 0;
41 | text-align: right;
42 | }
43 | }
44 | }
45 |
46 | .contact-line {
47 | @include grid-container;
48 |
49 | margin: 0;
50 | @include breakpoint(medium) {
51 | margin: 0 auto;
52 | min-width: 40rem;
53 | // hack for IE, ref: #41
54 | width: 0;
55 | }
56 | }
57 |
58 | .att-line {
59 | padding-top: 0.25rem;
60 |
61 | span {
62 | line-height: 1.2;
63 | font-size: 1.2rem;
64 | font-weight: bold;
65 |
66 | @media #{$height-constrained-phone} {
67 | font-size: inherit;
68 | }
69 | }
70 | }
71 |
72 | .license {
73 | font-size: 75%;
74 | }
75 |
76 | .by-line {
77 | padding-bottom: 0.25rem;
78 |
79 | @media #{$height-constrained-phone} {
80 | font-size: 75%;
81 | overflow-y: visible;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tests/fixtures/fluxible-routes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * NOTE: Only used in transformer tests.
6 | * Used in transformer tests = Can't use transformer to generate from fixture.
7 | *
8 | * Could partially generate from backend, but must supply action closures manually.
9 | */
10 | 'use strict';
11 |
12 | var actions = require('../../actions/interface');
13 |
14 | var params = {
15 | resource: 'test',
16 | key: '/path/to/test',
17 | pageTitle: 'A Test Title'
18 | };
19 |
20 | var action = actions.page;
21 |
22 | // This code is symbolicly compared to method in fluxibleRouteTransformer
23 | function makeAction () {
24 | var copyParams = JSON.parse(JSON.stringify(params));
25 | return function dynAction (context, payload, done) {
26 | context.executeAction(action, copyParams, done);
27 | };
28 | }
29 |
30 | module.exports = {
31 | home: {
32 | path: '/',
33 | method: 'get',
34 | page: 'home',
35 | label: 'Home',
36 | component: 'ContentPage',
37 | order: 0,
38 | priority: 1,
39 | background: '3',
40 | mainNav: true,
41 | action: makeAction()
42 | },
43 | about: {
44 | path: '/about',
45 | method: 'get',
46 | page: 'about',
47 | label: 'About',
48 | component: 'ContentPage',
49 | mainNav: true,
50 | background: '4',
51 | order: 1,
52 | priority: 1,
53 | action: makeAction()
54 | },
55 | contact: {
56 | path: '/contact',
57 | method: 'get',
58 | page: 'contact',
59 | label: 'Contact',
60 | component: 'Contact',
61 | mainNav: true,
62 | background: '5',
63 | order: 2,
64 | priority: 1,
65 | action: makeAction()
66 | }
67 | };
68 |
--------------------------------------------------------------------------------
/utils/urls.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | /**
8 | * Given a url, extract the hostname part.
9 | *
10 | * @param {String} url - The url from which to pull the hostname.
11 | * @returns {String} The hostname from the given url.
12 | */
13 | function getHostname (url) {
14 | return url.replace(/^[^\/]*\/\/([^\/\?#\:]+).*$/,
15 | function (all, hostname) {
16 | return hostname;
17 | });
18 | }
19 |
20 | /**
21 | * Given a url, extract the last path segment.
22 | * Last path segment is the file name, or file, or any last part of the path
23 | * before ?|#|$
24 | *
25 | * @param {String} url - The url from which to pull the last path segment.
26 | * Can be absolute or relative url, path, file, or path and qs/hash
27 | * @returns {String} The last path segment
28 | */
29 | function getLastPathSegment (url) {
30 | var matches = /(?:\/{1}|^)([\w\-\.]+)\/?(?=\?|#|$)/.exec(url);
31 | return matches && matches[1] || '';
32 | }
33 |
34 | /**
35 | * The significant hostname is the last hostname token before the TLD.
36 | * http://subdom.significant-hostname.com/someotherstuff
37 | *
38 | * @param {String} url - The url from which to pull the significant hostname.
39 | * @returns The second to last hostname token between dots for a given url.
40 | */
41 | function getSignificantHostname (url) {
42 | var hostname = getHostname(url);
43 | var names = hostname.split('.');
44 | var significantIndex = names.length < 2 ? 0 : names.length - 2;
45 | return names[significantIndex];
46 | }
47 |
48 | module.exports = {
49 | getHostname: getHostname,
50 | getSignificantHostname: getSignificantHostname,
51 | getLastPathSegment: getLastPathSegment
52 | };
53 |
--------------------------------------------------------------------------------
/tests/mocks/service-data.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * TODO: change to use fixture (to be created) with content responses.
6 | * Specifically needed for content: contact.json.
7 | */
8 | 'use strict';
9 |
10 | var routesResponse = require('../fixtures/routes-response');
11 | var modelsResponse = require('../fixtures/models-response');
12 |
13 | module.exports = {
14 | createContent: function (input) {
15 | var content = typeof input === 'string' ? ''+input+'
'
16 | : input;
17 |
18 | return {
19 | models: modelsResponse,
20 | content: content
21 | };
22 | },
23 | fetch: function (params, callback) {
24 | var result;
25 |
26 | if (params.emulateError) {
27 | return callback(new Error('mock'));
28 | }
29 |
30 | if (params.noData) {
31 | return callback();
32 | }
33 |
34 | switch (params.resource) {
35 | case 'routes':
36 | result = callback(null, {
37 | models: undefined,
38 | content: JSON.parse(JSON.stringify(routesResponse))
39 | });
40 | break;
41 |
42 | case 'about':
43 | result = callback(null, this.createContent('About'));
44 | break;
45 |
46 | case 'contact':
47 | result = callback(null, this.createContent('Contact'));
48 | break;
49 |
50 | case 'home':
51 | result = callback(null, this.createContent('Home'));
52 | break;
53 |
54 | default:
55 | throw new Error('service-data test mock received unexpected resource request');
56 | }
57 |
58 | return result;
59 | },
60 |
61 | initialize: function (callback) {
62 | callback();
63 | },
64 |
65 | update: function (callback) {
66 | callback();
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/tests/unit/stores/RouteStore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var RouteStore = require('../../../stores/RouteStore');
10 | var routesResponseFixture = require('../../fixtures/routes-response');
11 | var helperTests = require('../../utils/tests');
12 | var transformer = require('../../../utils').createFluxibleRouteTransformer({
13 | actions: require('../../../actions/interface')
14 | });
15 |
16 | describe('Route store', function () {
17 | var routesResponse, storeInstance;
18 |
19 | beforeEach(function () {
20 | storeInstance = new RouteStore();
21 | });
22 |
23 | it('should instantiate correctly', function () {
24 | expect(storeInstance).to.be.an('object');
25 | expect(storeInstance._handleReceiveRoutes).to.be.a('function');
26 | expect(storeInstance.dehydrate).to.be.a('function');
27 | expect(storeInstance.rehydrate).to.be.a('function');
28 | });
29 |
30 | describe('with routes', function () {
31 | beforeEach(function () {
32 | // clone the routes-response fixture data
33 | routesResponse = JSON.parse(JSON.stringify(routesResponseFixture));
34 | });
35 |
36 | it('should dehydrate routes to json', function () {
37 | storeInstance._handleReceiveRoutes(transformer.jsonToFluxible(routesResponse));
38 | var state = storeInstance.dehydrate();
39 | expect(state.routes).to.eql(routesResponse);
40 | });
41 |
42 | it('should rehydrate to fluxible routes', function () {
43 | storeInstance.rehydrate({ routes: routesResponse });
44 |
45 | helperTests.testTransform(
46 | expect, storeInstance._routes, transformer.jsonToFluxible(routesResponse)
47 | );
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '6'
4 | env:
5 | global:
6 | - CXX=g++-4.8
7 | - DEPLOY_URL=$(if test $TRAVIS_BRANCH = master -o $TRAVIS_BRANCH = stage; then echo "http://flux-react-example.herokuapp.com"; elif test $TRAVIS_BRANCH = test; then echo "https://flux-react-example-test.herokuapp.com"; else echo "."; fi)
8 | - secure: NcG3pl3uhwuhfni18QpywTHZ2pZ1dxTCSpSDNlfkdYAKKjKrocWh5vuf+HmZzT4r0XuHb4cN8/cvXTn5J4+e+NtcEP7QkN1YCbqU0towoU/KvDZEr0cW8nanbQctUEc9Fe1X9WJd/f5k0T2oXxJ5DAqnLmSJH+t8I5qijM1pQ7k=
9 | - secure: JmtKHcbatR/ImIJ7Hv+2sJinVFreQLR8Af83fgf9M/N+iBAFrwjhXGAxKAHkdAmn33kOG5W6FMZxrQr74+2gzOv1YxTcCxJq2EumjdjUhv0qQ0NAA1kVeVV4EYlBn8o5OPtY9Z3DyltZDlH62eMu1fsU2C+hDlNcKlHQhtDWIR8=
10 | - secure: HkrVzqt1o1YN2kic9VWI3Uquwdjgar8DDfHFMc5Vr09xo5naokxRU0vntQa77fHmvUloCLPB01v0vHDqP/ORRWFgGAgGZFDq71Pubp+qWgwXibFbAmNdES0vzPZhDV62gNO4AOxZlU84QNp15XtkxdpIqu00WmoqNgPtbKF5hCE=
11 | addons:
12 | sauce_connect: true
13 | apt:
14 | sources:
15 | - ubuntu-toolchain-r-test
16 | packages:
17 | - g++-4.8
18 | before_install:
19 | - echo DEPLOY_URL=$DEPLOY_URL
20 | - sudo apt-get update
21 | - sudo apt-get install rubygems
22 | - gem install compass
23 | before_script:
24 | - npm install -g grunt-cli@0.1.13
25 | after_success:
26 | - cat reports/coverage/lcov.info | ./node_modules/.bin/coveralls
27 | before_deploy:
28 | - npm run build
29 | - rm -rf node_modules/
30 | deploy:
31 | provider: heroku
32 | api_key:
33 | secure: LnMaWuov0LkD3H+MCmBSaIg90YBKH0Bs+1EhRzaGcU0setO+MVsfYyBQ/UCRgg9g/BvCYW+nocjajeZ+/04GbOXEitAG7gIWSdPrApf2RJHzOs/xzS2tiVnlcFXbloDIv4ChjZ0KH4254VI3vjmMcXYC3ZswJIE38dkMbVAUCnY=
34 | app:
35 | stage: flux-react-example
36 | master: flux-react-example
37 | test: flux-react-example-test
38 | skip_cleanup: true
39 | on:
40 | repo: localnerve/flux-react-example
41 | node: 6
42 | after_deploy:
43 | - npm install
44 | - npm run functest -- $DEPLOY_URL
45 | - grunt perfbudget:mobile
46 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "excludeFiles": [
3 | "./node_modules/**",
4 | "./dist/**",
5 | "./reports/**",
6 | "./tests/fixtures/**",
7 | "./tmp/**"
8 | ],
9 |
10 | "errorFilter": "./tests/utils/jscsFilter.js",
11 |
12 | "maxErrors": 100,
13 |
14 | "disallowMixedSpacesAndTabs": true,
15 | "disallowNewlineBeforeBlockStatements": true,
16 | "disallowPaddingNewlinesInBlocks": true,
17 | "disallowSpaceAfterObjectKeys": true,
18 | "disallowTrailingComma": true,
19 |
20 | "maximumLineLength": {
21 | "value": 120,
22 | "allowComments": true,
23 | "allowUrlComments": true,
24 | "allowRegex": true
25 | },
26 |
27 | "requireBlocksOnNewline": true,
28 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties",
29 | "requireCapitalizedConstructors": true,
30 | "requireCommaBeforeLineBreak": true,
31 | "requireSpaceAfterKeywords": [
32 | "if",
33 | "else",
34 | "for",
35 | "while",
36 | "do",
37 | "switch",
38 | "return",
39 | "try",
40 | "catch"
41 | ],
42 | "requireSpaceAfterLineComment": true,
43 | "requireSpaceBeforeBlockStatements": true,
44 | "requireSpacesInConditionalExpression": {
45 | "afterTest": true,
46 | "beforeConsequent": true,
47 | "afterConsequent": true,
48 | "beforeAlternate": true
49 | },
50 | "requireSpacesInAnonymousFunctionExpression": {
51 | "beforeOpeningCurlyBrace": true
52 | },
53 | "requireSpacesInFunctionExpression": {
54 | "beforeOpeningCurlyBrace": true
55 | },
56 | "requireSpacesInNamedFunctionExpression": {
57 | "beforeOpeningCurlyBrace": true
58 | },
59 |
60 | "safeContextKeyword": ["self"],
61 |
62 | "validateIndentation": 2,
63 |
64 | "jsDoc": {
65 | "checkAnnotations": "jsdoc3",
66 | "enforceExistence": "exceptExports",
67 | "checkParamNames": true,
68 | "checkRedundantParams": true,
69 | "requireParamTypes": true
70 | },
71 |
72 | "validateQuoteMarks": { "mark": "'", "escape": true }
73 | }
74 |
--------------------------------------------------------------------------------
/components/pages/contact/Nav.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var cx = require('classnames');
9 |
10 | var ContactNav = React.createClass({
11 | propTypes: {
12 | stepCurrent: React.PropTypes.number.isRequired,
13 | stepFinal: React.PropTypes.number.isRequired,
14 | onPrevious: React.PropTypes.func.isRequired,
15 | nav: React.PropTypes.object.isRequired
16 | },
17 |
18 | shouldComponentUpdate: function (nextProps) {
19 | return nextProps.stepCurrent !== this.props.stepCurrent;
20 | },
21 |
22 | render: function () {
23 | var last = this.props.stepCurrent === this.props.stepFinal;
24 | var nav = last ? [] : this.renderContactNav();
25 |
26 | return (
27 |
31 | {nav}
32 |
33 | );
34 | },
35 |
36 | renderContactNav: function () {
37 | var complete = this.props.stepCurrent === this.props.stepFinal - 1;
38 | var nextText = complete ? this.props.nav.next.last :
39 | this.props.nav.next.text;
40 |
41 | return [
42 | ,
50 |
57 | ];
58 | }
59 | });
60 |
61 | module.exports = ContactNav;
62 |
--------------------------------------------------------------------------------
/tests/unit/stores/ApplicationStore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var ApplicationStore = require('../../../stores/ApplicationStore');
10 |
11 | describe('application store', function () {
12 | var storeInstance;
13 | var defaultName = 'default';
14 | var page = { title: 'Fluxible Rocks' };
15 | var payload = {
16 | page: {
17 | defaultPageName: defaultName
18 | }
19 | };
20 |
21 | beforeEach(function () {
22 | storeInstance = new ApplicationStore();
23 | });
24 |
25 | it('should instantiate correctly', function () {
26 | expect(storeInstance).to.be.an('object');
27 | expect(storeInstance.defaultPageName).to.equal('');
28 | expect(storeInstance.currentPageTitle).to.equal('');
29 | });
30 |
31 | it('should update page title', function () {
32 | storeInstance.updatePageTitle(page);
33 | expect(storeInstance.getCurrentPageTitle()).to.equal(page.title);
34 | });
35 |
36 | it('should update default page name', function () {
37 | storeInstance.initApplication(payload);
38 | expect(storeInstance.getDefaultPageName()).to.equal(defaultName);
39 | });
40 |
41 | it('should dehydrate', function () {
42 | storeInstance.initApplication(payload);
43 | storeInstance.updatePageTitle(page);
44 |
45 | var state = storeInstance.dehydrate();
46 |
47 | expect(state.defaultPageName).to.equal(defaultName);
48 | expect(state.pageTitle).to.equal(page.title);
49 | });
50 |
51 | it('should rehydrate', function () {
52 | var state = {
53 | defaultPageName: defaultName,
54 | pageTitle: page.title
55 | };
56 |
57 | storeInstance.rehydrate(state);
58 |
59 | expect(storeInstance.getDefaultPageName()).to.equal(defaultName);
60 | expect(storeInstance.getCurrentPageTitle()).to.equal(page.title);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/tests/workers/contact/contact.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
4 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
5 | *
6 | * Manual test harness for mail and queue service exercise.
7 | *
8 | * PREREQUISITES:
9 | * Must have environment setup for mail and queue settings, auth.
10 | * Must have the services implied by those settings setup and operational.
11 | * (Must have AMQP queue running and pointed to by contact.queue.url, etc.)
12 | *
13 | * Plunks a message onto the queue then starts the worker to consume it.
14 | * Kills the worker after some constant time elapsed.
15 | *
16 | * Must manually verify mail and queue status is as expected.
17 | */
18 | /* global console, process */
19 |
20 | var path = require('path');
21 | var spawn = require('child_process').spawn;
22 |
23 | var mail = require('../../../services/mail');
24 | var contact = require('../../../configs').create().contact;
25 |
26 | var workerProcess = '../../../server/workers/contact/bin/contact';
27 | var workTime = 10000;
28 |
29 | if (!contact.mail.username() || !contact.mail.password()) {
30 | console.error('mail service credentials missing. Check environment.');
31 | console.error('mail config');
32 | console.error('service = ' + contact.mail.service());
33 | console.error('to = ' + contact.mail.to());
34 | console.error('from = ' + contact.mail.from());
35 | console.error('username = ' + contact.mail.username());
36 | console.error('password = ' + contact.mail.password());
37 | process.exit();
38 | }
39 |
40 | mail.send({
41 | name: 'Manual Test',
42 | email: 'manual@test.local',
43 | message: 'This is a test message from the manual test harness.'
44 | }, function (err) {
45 | if (err) {
46 | throw err;
47 | }
48 |
49 | var cp = spawn(path.resolve(workerProcess));
50 |
51 | cp.on('close', function () {
52 | console.log(workerProcess + ' complete');
53 | process.exit();
54 | });
55 |
56 | setTimeout(function () {
57 | cp.kill('SIGINT');
58 | }, workTime);
59 | });
60 |
--------------------------------------------------------------------------------
/actions/routes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:RoutesAction');
8 | var createFluxibleRouteTransformer = require('../utils').createFluxibleRouteTransformer;
9 |
10 | /**
11 | * The routes action.
12 | * This action is only executed on the server in this example to get
13 | * primary application routes into app state. However, this action could be
14 | * reused to retrieve or populate additional in-app routes later.
15 | * If routes are not passed in, it retrieves payload.resource from the 'routes' service.
16 | *
17 | * @param {Object} context - The fluxible action context.
18 | * @param {Object} payload - The action payload.
19 | * @param {Function} [payload.transform] - An optional custom route transformer.
20 | * @param {Object} [payload.routes] - Optional routes to add to the app without a service request.
21 | * @param {String} payload.resource - The name of the routes resource to retrieve with a service request.
22 | */
23 | function routes (context, payload, done) {
24 | var transformer = (typeof payload.transform === 'function' ?
25 | payload.transform : createFluxibleRouteTransformer({
26 | actions: require('./interface')
27 | }).jsonToFluxible);
28 |
29 | if (payload.routes) {
30 | var fluxibleRoutes = payload.routes;
31 |
32 | if (payload.transform) {
33 | debug('transforming routes');
34 |
35 | fluxibleRoutes = transformer(payload.routes);
36 | }
37 |
38 | context.dispatch('RECEIVE_ROUTES', fluxibleRoutes);
39 | return done();
40 | }
41 |
42 | debug('Routes request start');
43 | context.service.read('routes', payload, {}, function (err, routes) {
44 | debug('Routes request complete');
45 |
46 | if (err) {
47 | return done(err);
48 | }
49 |
50 | var fluxibleRoutes = transformer(routes);
51 | context.dispatch('RECEIVE_ROUTES', fluxibleRoutes);
52 | done(null, fluxibleRoutes);
53 | });
54 | }
55 |
56 | module.exports = routes;
57 |
--------------------------------------------------------------------------------
/components/footer/LocalBusiness.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 |
9 | var LocalBusiness = React.createClass({
10 | propTypes: {
11 | business: React.PropTypes.object.isRequired
12 | },
13 |
14 | render: function () {
15 | var uriMailTo = 'mailto:' + this.props.business.email;
16 | var uriTel = 'tel:+1-' + this.props.business.telephone;
17 |
18 | return (
19 |
23 |
24 |
25 | {this.props.business.legalName}
26 |
27 |
28 |
29 | {this.props.business.address.streetAddress}
30 |
31 |
32 |
33 | {this.props.business.address.addressLocality}
34 |
35 | ,
36 |
37 | {this.props.business.address.addressRegion}
38 |
39 |
40 |
41 | {this.props.business.address.postalCode}
42 |
43 |
44 |
45 |
46 |
58 |
59 | );
60 | }
61 | });
62 |
63 | module.exports = LocalBusiness;
64 |
--------------------------------------------------------------------------------
/tests/fixtures/routes-response.js:
--------------------------------------------------------------------------------
1 | /** This is a generated file **/
2 | /**
3 | NODE_ENV = development
4 | FRED_URL = https://api.github.com/repos/localnerve/flux-react-example-data/contents/resources.json?ref=development
5 | **/
6 | module.exports = JSON.parse(JSON.stringify(
7 | {"404":{"path":"/404","method":"get","page":"404","label":"Not Found","component":"ContentPage","mainNav":false,"background":"","order":0,"priority":0,"action":{"name":"page","params":{"resource":"404","url":"https://api.github.com/repos/localnerve/flux-react-example-data/contents/pages/404.md","format":"markdown","models":["LocalBusiness","SiteInfo"],"pageTitle":"Page Not Found"}}},"500":{"path":"/500","method":"get","page":"500","label":"Error","component":"ContentPage","mainNav":false,"background":"","order":0,"priority":0,"action":{"name":"page","params":{"resource":"500","url":"https://api.github.com/repos/localnerve/flux-react-example-data/contents/pages/500.md","format":"markdown","models":["LocalBusiness","SiteInfo"],"pageTitle":"Application Error"}}},"home":{"path":"/","method":"get","page":"home","label":"Home","component":"ContentPage","mainNav":true,"background":"3.jpg","order":0,"priority":1,"action":{"name":"page","params":{"resource":"home","url":"https://api.github.com/repos/localnerve/flux-react-example-data/contents/pages/home.md","format":"markdown","models":["LocalBusiness","SiteInfo"],"pageTitle":"An Example Isomorphic Application"}}},"about":{"path":"/about","method":"get","page":"about","label":"About","component":"ContentPage","mainNav":true,"background":"4.jpg","order":1,"priority":1,"action":{"name":"page","params":{"resource":"about","url":"https://api.github.com/repos/localnerve/flux-react-example-data/contents/pages/about.md","format":"markdown","models":["LocalBusiness","SiteInfo"],"pageTitle":"About"}}},"contact":{"path":"/contact","method":"get","page":"contact","label":"Contact","component":"Contact","mainNav":true,"background":"5.jpg","order":2,"priority":1,"action":{"name":"page","params":{"resource":"contact","url":"https://api.github.com/repos/localnerve/flux-react-example-data/contents/pages/contact.json","format":"json","models":["LocalBusiness","SiteInfo"],"pageTitle":"Contact"}}}}
8 | ));
--------------------------------------------------------------------------------
/components/pages/contact/Steps.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var cx = require('classnames');
9 | var noop = require('lodash/noop');
10 |
11 | var ContactSteps = React.createClass({
12 | propTypes: {
13 | steps: React.PropTypes.array.isRequired,
14 | stepCurrent: React.PropTypes.number.isRequired,
15 | stepFinal: React.PropTypes.number.isRequired,
16 | failure: React.PropTypes.bool.isRequired,
17 | resultMessage: React.PropTypes.string,
18 | retry: React.PropTypes.func.isRequired
19 | },
20 |
21 | shouldComponentUpdate: function (nextProps) {
22 | return nextProps.stepCurrent !== this.props.stepCurrent ||
23 | nextProps.failure !== this.props.failure;
24 | },
25 |
26 | render: function () {
27 | var contactSteps = this.renderContactSteps();
28 | return (
29 |
32 | );
33 | },
34 |
35 | renderContactSteps: function () {
36 | if (this.props.stepCurrent === this.props.stepFinal) {
37 | return (
38 |
42 | {this.props.resultMessage}
43 |
44 | );
45 | } else {
46 | return this.props.steps
47 | .sort(function (a, b) {
48 | return a.step - b.step;
49 | })
50 | .map(function (input) {
51 | var classNames = cx({
52 | complete: input.step < this.props.stepCurrent,
53 | current: input.step === this.props.stepCurrent,
54 | incomplete: input.step > this.props.stepCurrent,
55 | hide: input.step === this.props.stepFinal
56 | });
57 | return (
58 |
59 | {input.name}
60 |
61 | );
62 | }, this);
63 | }
64 | }
65 | });
66 |
67 | module.exports = ContactSteps;
68 |
--------------------------------------------------------------------------------
/configs/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * The server-side main config loader.
6 | *
7 | */
8 | 'use strict';
9 |
10 | var fs = require('fs');
11 | var path = require('path');
12 | var nconf = require('nconf');
13 |
14 | var localEnv = 'local.env.json';
15 |
16 | // Files to exclude when building the configuration objects.
17 | var exclude = [ 'index.js', localEnv ];
18 |
19 | /**
20 | * Loads modules in this directory.
21 | * Creates an object keyed by modules names found in this directory.
22 | *
23 | * @param {Object} nconf - The nconfig object.
24 | * @returns {Object} An object that contains all the configuration objects
25 | * keyed by module name. Configuration objects are formed by the
26 | * module export functions of modules found in this directory.
27 | */
28 | function configs (nconf) {
29 | var result = {};
30 | fs.readdirSync(__dirname).forEach(function (item) {
31 | var name = path.basename(item);
32 | if (exclude.indexOf(name) === -1) {
33 | result[name] = require('./' + name)(nconf);
34 | }
35 | });
36 | return result;
37 | }
38 |
39 | /**
40 | * Create a new configuration object, applying any overrides.
41 | *
42 | * Creates and tears off a new configuration object formed by the precedence:
43 | * 1. Overrides
44 | * 2. The process environment
45 | * 3. The local environment file
46 | * 4. Configuration object modules found in this directory
47 | *
48 | * @param {Object} overrides - highest priority configuration overrides.
49 | * @returns {Object} An object containing a copy of the full configuration.
50 | */
51 | function create (overrides) {
52 | nconf
53 | .overrides(overrides || {})
54 | .env()
55 | .file({ file: path.join(__dirname, localEnv) })
56 | .defaults(configs(nconf));
57 |
58 | var config = nconf.get();
59 |
60 | // Remove all the items that pass the filter
61 | Object.keys(config).filter(function (key) {
62 | return /^(?:npm)?_/.test(key);
63 | }).forEach(function (key) {
64 | delete config[key];
65 | });
66 |
67 | return config;
68 | }
69 |
70 | module.exports = {
71 | create: create
72 | };
73 |
--------------------------------------------------------------------------------
/server/sitemap.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Handle sitemap request.
6 | *
7 | * Reminder: There is a compression minimum threshold below which no compression
8 | * occurs.
9 | */
10 | 'use strict';
11 |
12 | var debug = require('debug')('sitemap');
13 | var urlLib = require('url');
14 | var sitemapLib = require('sitemap-xml');
15 |
16 | var baseDir = '..';
17 |
18 | var serviceData = require(baseDir + '/services/data');
19 | var config = require(baseDir + '/configs').create({
20 | baseDir: baseDir
21 | });
22 | var settings = config.settings;
23 | var utils = require('./utils');
24 |
25 | /**
26 | * Handle requests for sitemap.xml.
27 | *
28 | * @param {Object} req - The request object, not used.
29 | * @param {Object} res - The response object.
30 | * @param {Object} next - The next object.
31 | */
32 | function sitemap (req, res, next) {
33 | debug('Read routes');
34 |
35 | utils.nodeCall(serviceData.fetch, {
36 | resource: config.data.FRED.mainResource
37 | })
38 | .then(function (result) {
39 | var routes = result.content,
40 | ssl = settings.web.ssl || settings.web.sslRemote,
41 | stream = sitemapLib();
42 |
43 | res.header('Content-Type', 'text/xml');
44 | stream.pipe(res);
45 |
46 | Object.keys(routes)
47 | .filter(function (key) {
48 | return routes[key].mainNav;
49 | })
50 | .forEach(function (key) {
51 | stream.write({
52 | loc: urlLib.format({
53 | protocol: ssl ? 'https' : 'http',
54 | hostname: settings.web.appHostname,
55 | pathname: routes[key].path
56 | }),
57 | priority: routes[key].siteMeta ?
58 | routes[key].siteMeta.priority : 1.0,
59 | changefreq: routes[key].siteMeta ?
60 | routes[key].siteMeta.changefreq : 'monthly'
61 | });
62 | });
63 |
64 | stream.end();
65 | })
66 | .catch(function (err) {
67 | debug('Request failed: ', err);
68 | err.status = err.statusCode = (err.statusCode || err.status || 500);
69 | next(err);
70 | });
71 | }
72 |
73 | module.exports = sitemap;
74 |
--------------------------------------------------------------------------------
/tests/generators/routes-models.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Fetch main resource and write routes and models fixture files.
6 | * Run as npm script
7 | */
8 | /*jshint multistr: true */
9 | 'use strict';
10 |
11 | var debug = require('debug')('FixtureGenerator:Routes-Models');
12 | var fs = require('fs');
13 | var fetch = require('../../services/data/fetch');
14 | var cache = require('../../services/data/cache');
15 | var config = require('../../configs').create();
16 |
17 | var replacement = 'DATA';
18 |
19 | var template = '/** This is a generated file **/\n\
20 | /**\n\
21 | NODE_ENV = ' + (process.env.NODE_ENV || 'development') + '\n\
22 | FRED_URL = ' + config.data.FRED.url() + '\n\
23 | **/\n\
24 | module.exports = JSON.parse(JSON.stringify(\n' + replacement + '\n));'
25 | ;
26 |
27 | function run (output, done) {
28 | // Get main resource - includes routes, models
29 | // Fetch uses the environment to target backend versions (using branches)
30 | // The target environment is set in the calling grunt task
31 | fetch.fetchMain(function (err, routes) {
32 | if (err) {
33 | debug('main resource fetch failed');
34 | return done(err);
35 | }
36 |
37 | // Prepare routes file output
38 | var contents = template.replace(replacement, JSON.stringify(
39 | routes.content
40 | ));
41 |
42 | fs.writeFile(output.routes, contents, function (err) {
43 | if (err) {
44 | debug('write of routes response failed');
45 | return done(err);
46 | }
47 |
48 | debug('successfully wrote routes response file '+ output.routes);
49 |
50 | // Prepare models file output - models cached by main resource fetch
51 | contents = template.replace(replacement, JSON.stringify(
52 | cache.get('models').content
53 | )
54 | );
55 |
56 | fs.writeFile(output.models, contents, function (err) {
57 | if (err) {
58 | debug('write of models response failed');
59 | return done(err);
60 | }
61 |
62 | debug('successfully wrote models response file '+ output.models);
63 | done();
64 | });
65 | });
66 | });
67 | }
68 |
69 | module.exports = run;
70 |
--------------------------------------------------------------------------------
/services/mail/queue.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:Mail:Queue');
8 | var contact = require('../../configs').create().contact;
9 | var amqp = require('amqplib');
10 | var mailer = require('./mailer');
11 |
12 | /**
13 | * Add a mail payload to the outgoing mail queue.
14 | *
15 | * @param {Object} input - The contact mail payload.
16 | * @param {Function} callback - The callback to execute on completion.
17 | */
18 | function sendMail (input, callback) {
19 | var open = amqp.connect(contact.queue.url());
20 |
21 | open.then(function (conn) {
22 | debug('AMQP connection open');
23 |
24 | return conn.createChannel().then(function (ch) {
25 | debug('AMQP channel created');
26 |
27 | var q = contact.queue.name();
28 |
29 | return ch.assertQueue(q).then(function () {
30 | ch.sendToQueue(q, new Buffer(JSON.stringify(input)));
31 | debug('AMQP message sent', input);
32 | });
33 | });
34 | })
35 | .then(callback, function (err) {
36 | debug('AMQP message failure', err);
37 | callback(err);
38 | });
39 | }
40 |
41 | /**
42 | * This is the main proc of the contact worker process.
43 | * This blocks consuming the outgoing mail queue and sends mail
44 | * when a message is received. SIGINT will disrupt the process.
45 | * If the send fails, nacks it back onto the queue.
46 | */
47 | function contactWorker () {
48 | amqp.connect(contact.queue.url()).then(function (conn) {
49 | process.once('SIGINT', function () {
50 | conn.close();
51 | });
52 |
53 | return conn.createChannel().then(function (ch) {
54 | var q = contact.queue.name();
55 |
56 | return ch.assertQueue(q).then(function () {
57 | ch.consume(q, function (msg) {
58 | if (msg !== null) {
59 | mailer.send(JSON.parse(msg.content.toString()), function(err) {
60 | if (err) {
61 | debug('mailer failed to send ', msg);
62 | return ch.nack(msg);
63 | }
64 | debug('mailer successfully sent ', msg);
65 | ch.ack(msg);
66 | });
67 | }
68 | });
69 | });
70 | });
71 | }).then(null, console.warn);
72 | }
73 |
74 | module.exports = {
75 | sendMail: sendMail,
76 | contactWorker: contactWorker
77 | };
78 |
--------------------------------------------------------------------------------
/stores/ApplicationStore.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 | var createStore = require('fluxible/addons').createStore;
7 |
8 | var ApplicationStore = createStore({
9 | storeName: 'ApplicationStore',
10 |
11 | handlers: {
12 | 'INIT_APP': 'initApplication',
13 | 'UPDATE_PAGE_TITLE': 'updatePageTitle'
14 | },
15 |
16 | /**
17 | * Set inital store state.
18 | */
19 | initialize: function () {
20 | this.currentPageTitle = '';
21 | this.defaultPageName = '';
22 | },
23 |
24 | /**
25 | * INIT_APP handler.
26 | * Initialize application data from payload.page.
27 | *
28 | * @param {Object} payload - The INIT_APP action payload.
29 | * @param {Object} payload.page - Application data the ApplicationStore is interested in.
30 | * @param {String} payload.page.defaultPageName - The default page name.
31 | */
32 | initApplication: function (payload) {
33 | var init = payload.page;
34 | if (init) {
35 | this.defaultPageName = init.defaultPageName;
36 | this.emitChange();
37 | }
38 | },
39 |
40 | /**
41 | * UPDATE_PAGE_TITLE handler.
42 | * Update the application page title.
43 | *
44 | * @param {Object} page - The UPDATE_PAGE_TITLE action payload.
45 | * @param {String} page.title - The new page title.
46 | */
47 | updatePageTitle: function (page) {
48 | this.currentPageTitle = page.title;
49 | this.emitChange();
50 | },
51 |
52 | /**
53 | * @returns {String} The default page name for the application.
54 | */
55 | getDefaultPageName: function () {
56 | return this.defaultPageName;
57 | },
58 |
59 | /**
60 | * @returns {String} The current page title for the application.
61 | */
62 | getCurrentPageTitle: function () {
63 | return this.currentPageTitle;
64 | },
65 |
66 | /**
67 | * @returns {Object} The ApplicationStore state.
68 | */
69 | dehydrate: function () {
70 | return {
71 | pageTitle: this.currentPageTitle,
72 | defaultPageName: this.defaultPageName
73 | };
74 | },
75 |
76 | /**
77 | * Hydrate the ApplicationStore from the given state.
78 | *
79 | * @param {Object} state - The new ApplicationStore state.
80 | */
81 | rehydrate: function (state) {
82 | this.currentPageTitle = state.pageTitle;
83 | this.defaultPageName = state.defaultPageName;
84 | }
85 | });
86 |
87 | module.exports = ApplicationStore;
88 |
--------------------------------------------------------------------------------
/stores/ContactStore.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 | var createStore = require('fluxible/addons').createStore;
7 |
8 | var ContactStore = createStore({
9 | storeName: 'ContactStore',
10 |
11 | handlers: {
12 | 'UPDATE_CONTACT_FIELDS': 'updateContactFields',
13 | 'CREATE_CONTACT_SUCCESS': 'clearContactFields',
14 | 'CREATE_CONTACT_FAILURE': 'setContactFailure'
15 | },
16 |
17 | /**
18 | * Set ContactStore initial state.
19 | */
20 | initialize: function () {
21 | this.name = '';
22 | this.email = '';
23 | this.message = '';
24 | this.failure = false;
25 | },
26 |
27 | /**
28 | * UPDATE_CONTACT_FIELDS action handler.
29 | *
30 | * @param {Object} fields - The contact fields.
31 | */
32 | updateContactFields: function (fields) {
33 | this.name = fields.name || '';
34 | this.email = fields.email || '';
35 | this.message = fields.message || '';
36 | this.emitChange();
37 | },
38 |
39 | /**
40 | * CREATE_CONTACT_SUCCESS action handler.
41 | */
42 | clearContactFields: function () {
43 | this.initialize();
44 | this.emitChange();
45 | },
46 |
47 | /**
48 | * CREATE_CONTACT_FAILURE action handler.
49 | */
50 | setContactFailure: function () {
51 | this.failure = true;
52 | this.emitChange();
53 | },
54 |
55 | /**
56 | * @returns {Boolean} true if contact failed, false otherwise.
57 | */
58 | getContactFailure: function () {
59 | return this.failure;
60 | },
61 |
62 | /**
63 | * @returns {Object} Contact field object with name, email, and message.
64 | */
65 | getContactFields: function () {
66 | return {
67 | name: this.name,
68 | email: this.email,
69 | message: this.message
70 | };
71 | },
72 |
73 | /**
74 | * Reduce this store to state.
75 | *
76 | * @returns {Object} This store as serializable state.
77 | */
78 | dehydrate: function () {
79 | var state = this.getContactFields();
80 | state.failure = this.failure;
81 | return state;
82 | },
83 |
84 | /**
85 | * Hydrate this store from state.
86 | *
87 | * @param {Object} state - The new ContactStore state.
88 | */
89 | rehydrate: function (state) {
90 | this.name = state.name;
91 | this.email = state.email;
92 | this.message = state.message;
93 | this.failure = state.failure;
94 | }
95 | });
96 |
97 | module.exports = ContactStore;
98 |
--------------------------------------------------------------------------------
/actions/page.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:PageAction');
8 |
9 | /**
10 | * The compound action dispatch associated with each page action.
11 | *
12 | * @param {Object} context - The fluxible action context.
13 | * @param {String} resource - The content resource name.
14 | * @param {String} title - The page title.
15 | * @param {Object} data - The content data.
16 | */
17 | function dispatchActions (context, resource, title, data) {
18 | context.dispatch('RECEIVE_PAGE_CONTENT', {
19 | resource: resource,
20 | data: data
21 | });
22 |
23 | context.dispatch('UPDATE_PAGE_TITLE', {
24 | title: title
25 | });
26 | }
27 |
28 | /**
29 | * Perform the service request for the page action.
30 | *
31 | * @param {Object} context - The fluxible action context.
32 | * @param {Object} payload - The action payload.
33 | * @param {String} payload.resource - The name of the content resource.
34 | * @param {String} payload.pageTitle - The page title.
35 | * @param {Function} done - The callback to execute on request completion.
36 | */
37 | function serviceRequest (context, payload, done) {
38 | debug('Page service request start');
39 |
40 | context.service.read('page', payload, {}, function (err, data) {
41 | debug('Page service request complete');
42 |
43 | if (err) {
44 | return done(err);
45 | }
46 |
47 | if (!data) {
48 | var noData = new Error('Page not found');
49 | noData.statusCode = 404;
50 | return done(noData);
51 | }
52 |
53 | dispatchActions(context, payload.resource, payload.pageTitle, data);
54 |
55 | return done();
56 | });
57 | }
58 |
59 | /**
60 | * The page action.
61 | *
62 | * @param {Object} context - The fluxible action context.
63 | * @param {Object} payload - The action payload.
64 | * @param {String} payload.resource - The name of the content resource.
65 | * @param {String} payload.pageTitle - The page title.
66 | * @param {Function} done - The callback to execute on action completion.
67 | */
68 | function page (context, payload, done) {
69 | var data = context.getStore('ContentStore').get(payload.resource);
70 |
71 | if (data) {
72 | debug('Found '+payload.resource+' in cache');
73 | dispatchActions(context, payload.resource, payload.pageTitle, data);
74 | return done();
75 | }
76 |
77 | serviceRequest(context, payload, done);
78 | }
79 |
80 | module.exports = page;
81 |
--------------------------------------------------------------------------------
/components/pages/contact/_steps.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // Styles for the contact steps
5 | @import "arrows";
6 |
7 | // between 0 and 1
8 | $step-size-scale: 0.75;
9 |
10 | $step-size: $rem-base * $step-size-scale;
11 | @function step-size-unscale($size) {
12 | @return $rem-base;
13 | }
14 |
15 | $step-tighten-size: 10px;
16 |
17 | $step-complete-bgcolor: $app-accent-dark-bgcolor;
18 | $step-complete-color: $app-primary-light-color;
19 | $step-incomplete-bgcolor: $app-accent-dark-bgcolor;
20 | $step-incomplete-color: $app-primary-light-color;
21 | $step-current-bgcolor: $app-primary-bgcolor;
22 | $step-current-color: $app-primary-light-color;
23 |
24 |
25 | @mixin step-text($bgcolor, $color, $fatten: false) {
26 | @include arrow-text($step-size, step-size-unscale($step-size), $bgcolor, $color, $fatten);
27 | }
28 |
29 | @mixin step-arrows($color, $fatten: false) {
30 | @include arrow-decorate($step-size, step-size-unscale($step-size), $color, right, $fatten);
31 | }
32 |
33 | .contact-steps {
34 | @extend .grid-row-spaced;
35 | font-size: $step-size;
36 |
37 | list-style-type: none;
38 | user-select: none;
39 | padding: 0;
40 |
41 | @include breakpoint(medium) {
42 | font-size: step-size-unscale($step-size);
43 | }
44 |
45 | li {
46 | display: flex;
47 | justify-content: space-between;
48 | align-items: center;
49 |
50 | width: 100%;
51 | text-align: center;
52 |
53 | &.result-message {
54 | background: $app-primary-bgcolor;
55 | color: $app-primary-light-color;
56 | padding: 0.5rem;
57 | justify-content: center;
58 |
59 | font-weight: bold;
60 | line-height: 1.2;
61 | font-size: 1.2rem;
62 |
63 | &.failure {
64 | background: $app-alert-bgcolor;
65 | }
66 | }
67 | }
68 | li:not(:first-child) {
69 | margin-left: -$step-tighten-size;
70 | &.current {
71 | margin-left: -($step-tighten-size + $arrow-fatten-size);
72 | }
73 | }
74 |
75 | .complete {
76 | @include step-arrows($step-complete-bgcolor);
77 | span {
78 | @include step-text($step-complete-bgcolor, $step-complete-color);
79 | }
80 | }
81 | .current {
82 | @include step-arrows($step-current-bgcolor, true);
83 | span {
84 | @include step-text($step-current-bgcolor, $step-current-color, true);
85 | }
86 | }
87 | .incomplete {
88 | @include step-arrows($step-incomplete-bgcolor);
89 | span {
90 | @include step-text($step-incomplete-bgcolor, $step-incomplete-color);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/server/robots.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Handle robots request.
6 | * Dynamically create allowed urls from mainNav routes.
7 | * Reads a robots.txt template and replaces SITEMAPURL and ALLOWURLS.
8 | *
9 | * Reminder: There is a compression minimum threshold below which no compression
10 | * occurs.
11 | */
12 | /* global Promise */
13 | 'use strict';
14 |
15 | var debug = require('debug')('robots');
16 | var fs = require('fs');
17 | var urlLib = require('url');
18 |
19 | var baseDir = '..';
20 |
21 | var serviceData = require(baseDir + '/services/data');
22 | var config = require(baseDir + '/configs').create({
23 | baseDir: baseDir
24 | });
25 | var settings = config.settings;
26 | var utils = require('./utils');
27 |
28 | /**
29 | * Handle requests for robots.txt.
30 | *
31 | * @param {Object} req - The request object, not used.
32 | * @param {Object} res - The response object.
33 | * @param {Object} next - The next object.
34 | */
35 | function robots (req, res, next) {
36 | debug('Read routes and robots template ', settings.dist.robotsTemplate);
37 |
38 | Promise.all([
39 | utils.nodeCall(serviceData.fetch, {
40 | resource: config.data.FRED.mainResource
41 | }),
42 |
43 | utils.nodeCall(fs.readFile, settings.dist.robotsTemplate, {
44 | encoding: 'utf8'
45 | })
46 | ])
47 | .then(function (results) {
48 | var robotsContent,
49 | robotsTemplate = results[1],
50 | routes = results[0].content;
51 |
52 | debug('Got template', robotsTemplate);
53 |
54 | robotsContent = robotsTemplate
55 | .replace(/(SITEMAPURL)/i, function () {
56 | var ssl = settings.web.sslRemote || settings.web.ssl;
57 |
58 | return urlLib.format({
59 | protocol: ssl ? 'https' : 'http',
60 | hostname: settings.web.appHostname,
61 | pathname: settings.web.sitemap
62 | });
63 | })
64 | .replace(/(ALLOWURLS)/i, function () {
65 | return Object.keys(routes)
66 | .filter(function (key) {
67 | return routes[key].mainNav;
68 | })
69 | .map(function (key) {
70 | return 'Allow: ' + routes[key].path;
71 | })
72 | .join('\n');
73 | });
74 |
75 | res.header('Content-Type', 'text/plain');
76 | res.send(robotsContent);
77 | })
78 | .catch(function (err) {
79 | debug('Request failed: ', err);
80 | err.status = err.statusCode = (err.statusCode || err.status || 500);
81 | next(err);
82 | });
83 | }
84 |
85 | module.exports = robots;
86 |
--------------------------------------------------------------------------------
/tests/functional/basic-specs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global it */
6 | 'use strict';
7 |
8 | var test = require('./sauce-travis');
9 |
10 | var timeoutLink = 150;
11 |
12 | it('should get home page and navigate to others', function(done) {
13 | test.state.browser
14 | .get(test.baseUrl)
15 | .title()
16 | .should.eventually.include('Example')
17 | .elementByTagName('h2')
18 | .text()
19 | .should.eventually.include('Welcome')
20 | .elementByLinkText('About')
21 | .click()
22 | .waitForElementByCss('#page1 h2', timeoutLink)
23 | .text()
24 | .should.eventually.include('About')
25 | .title()
26 | .should.eventually.include('About')
27 | .elementByLinkText('Contact')
28 | .click()
29 | .waitForElementByCss('#page2 h2', timeoutLink)
30 | .text()
31 | .should.eventually.include('Contact')
32 | .title()
33 | .should.eventually.include('Contact')
34 | .nodeify(done);
35 | });
36 |
37 | it('should get about page and navigate to others', function(done) {
38 | test.state.browser
39 | .get(test.baseUrl+'/about')
40 | .title()
41 | .should.eventually.include('About')
42 | .elementByTagName('h2')
43 | .text()
44 | .should.eventually.include('About')
45 | .elementByLinkText('Home')
46 | .click()
47 | .waitForElementByCss('#page0 h2', timeoutLink)
48 | .text()
49 | .should.eventually.include('Welcome')
50 | .title()
51 | .should.eventually.include('Example')
52 | .elementByLinkText('Contact')
53 | .click()
54 | .waitForElementByCss('#page2 h2', timeoutLink)
55 | .text()
56 | .should.eventually.include('Contact')
57 | .title()
58 | .should.eventually.include('Contact')
59 | .nodeify(done);
60 | });
61 |
62 | it('should get contact page and navigate to others', function(done) {
63 | test.state.browser
64 | .get(test.baseUrl+'/contact')
65 | .title()
66 | .should.eventually.include('Contact')
67 | .elementByTagName('h2')
68 | .text()
69 | .should.eventually.include('Contact')
70 | .elementByLinkText('Home')
71 | .click()
72 | .waitForElementByCss('#page0 h2', timeoutLink)
73 | .text()
74 | .should.eventually.include('Welcome')
75 | .title()
76 | .should.eventually.include('Example')
77 | .elementByLinkText('About')
78 | .click()
79 | .waitForElementByCss('#page1 h2', timeoutLink)
80 | .text()
81 | .should.eventually.include('About')
82 | .title()
83 | .should.eventually.include('About')
84 | .nodeify(done);
85 | });
86 |
--------------------------------------------------------------------------------
/components/pages/contact/Result.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var cx = require('classnames');
9 |
10 | var ContactResult = React.createClass({
11 | propTypes: {
12 | failure: React.PropTypes.bool.isRequired,
13 | failedMessage: React.PropTypes.string.isRequired,
14 | label: React.PropTypes.object.isRequired,
15 | message: React.PropTypes.object.isRequired,
16 | business: React.PropTypes.object.isRequired
17 | },
18 |
19 | shouldComponentUpdate: function (nextProps) {
20 | return nextProps.failure !== this.props.failure;
21 | },
22 |
23 | render: function () {
24 | var links = this.renderLinks();
25 |
26 | return (
27 |
28 |
31 | {this.props.label.success.text}
32 |
33 |
34 | {this.props.message[this.props.failure ? 'failure' : 'success'].text}
35 |
36 |
37 | {links}
38 |
39 |
40 | );
41 | },
42 |
43 | renderLinks: function () {
44 | var uriMailTo = this.encodeURIMailTo();
45 | var uriTel = 'tel:+1-' + this.props.business.telephone;
46 |
47 | if (!this.props.failure) {
48 | return [
49 |
50 |
51 | {this.props.business.email}
52 |
53 | ,
54 |
55 |
56 | {this.props.business.telephone}
57 |
58 |
59 | ];
60 | } else {
61 | return [
62 |
63 |
64 | {this.props.message.failure.email}
65 |
66 |
67 | {this.props.message.failure.emailHelp}
68 |
69 | ,
70 |
71 |
72 | {this.props.message.failure.call}
73 |
74 |
75 | ];
76 | }
77 | },
78 |
79 | encodeURIMailTo: function () {
80 | var subject = encodeURIComponent(this.props.business.alternateName + ' contact email');
81 | var body = this.props.failure ? encodeURIComponent(this.props.failedMessage) : '';
82 |
83 | return "mailto:" + this.props.business.email + '?subject=' + subject + '&body=' + body;
84 | }
85 | });
86 |
87 | module.exports = ContactResult;
88 |
--------------------------------------------------------------------------------
/components/pages/contact/_nav.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // styles for the nav component
5 | @import "arrows";
6 |
7 | @mixin button-span() {
8 | display: block;
9 | float: left;
10 | padding: $rem-base/2 !important;
11 | min-width: 0;
12 | }
13 |
14 | // $button-submit-bgcolor: $app-accent-dark-bgcolor;
15 | // $button-submit-color: $app-primary-light-color;
16 | $button-submit-bgcolor: $app-accent-light-bgcolor;
17 | $button-submit-color: $app-primary-dark-color;
18 |
19 | // use if you need a different color scheme for .last
20 | // $button-submit-last-bgcolor: $app-accent-light-bgcolor;
21 | // $button-submit-last-color: $app-primary-dark-color;
22 |
23 | $button-prev-bgcolor: $app-primary-bgcolor;
24 | $button-prev-color: $app-primary-light-color;
25 |
26 | .form-navigation {
27 | @extend .grid-row-spaced;
28 | margin: 1em 0;
29 |
30 | // This hack stops the chrome justify-content: space-around bug (issue #34)
31 | transform: TranslateZ(0);
32 |
33 | button {
34 | display: inline-block;
35 | border: 0;
36 | background: transparent;
37 | }
38 | button[type=submit] {
39 | &::before {
40 | @include pre-triangle($rem-base, $rem-base, $button-submit-bgcolor, left);
41 | display: block;
42 | float: left;
43 | }
44 | &::after {
45 | @include post-triangle($rem-base, $rem-base, $button-submit-bgcolor, left);
46 | display: block;
47 | float: left;
48 | }
49 | span {
50 | @include arrow-text(
51 | $rem-base, $rem-base, $button-submit-bgcolor, $button-submit-color
52 | );
53 | @include button-span;
54 | }
55 | /*
56 | &.last {
57 | &::before {
58 | @include pre-triangle($rem-base, $rem-base, $button-submit-last-bgcolor, left);
59 | display: block;
60 | float: left;
61 | }
62 | &::after {
63 | @include post-triangle($rem-base, $rem-base, $button-submit-last-bgcolor, left);
64 | display: block;
65 | float: left;
66 | }
67 | span {
68 | @include arrow-text(
69 | $rem-base, $rem-base, $button-submit-last-bgcolor, $button-submit-last-color
70 | );
71 | @include button-span;
72 | }
73 | }
74 | */
75 | }
76 | button[type=button] {
77 | &::before {
78 | @include post-triangle($rem-base, $rem-base, $button-prev-bgcolor, right);
79 | display: block;
80 | float: left;
81 | }
82 | &::after {
83 | @include pre-triangle($rem-base, $rem-base, $button-prev-bgcolor, right);
84 | display: block;
85 | float: left;
86 | }
87 | span {
88 | @include arrow-text(
89 | $rem-base, $rem-base, $button-prev-bgcolor, $button-prev-color
90 | );
91 | @include button-span;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/components/header/_styles.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // header styles
5 | // =========================
6 |
7 | .app-header {
8 | @include grid-block(shrink, vertical);
9 | }
10 | .app-header-bg {
11 | @extend %header-footer-bg;
12 | }
13 |
14 | // ribbon styles
15 | // -------------------------
16 | .ribbon {
17 | margin: 0.25rem 0;
18 | padding-top: 0.2rem;
19 | overflow-y: hidden;
20 | @include breakpoint(medium) {
21 | margin: 1rem 0;
22 | padding-top: 0;
23 | }
24 | }
25 | .ribbon span {
26 | font-size: 1.6rem;
27 | @include breakpoint(medium) {
28 | font-size: 2rem;
29 | }
30 | }
31 |
32 | // logo styles
33 | // -------------------------
34 | .logo {
35 | @include vertical-block(center, shrink) {
36 | overflow-y: visible;
37 | overflow-x: visible;
38 | align-items: center;
39 | }
40 | margin: 0 2rem 0.8rem;
41 |
42 | @include breakpoint(medium) {
43 | margin: 0 2rem 1.5rem;
44 | }
45 | @media #{$height-constrained-phone} {
46 | margin: 0 1.2rem 0.5rem;
47 | }
48 | }
49 |
50 | .logo h1 {
51 | padding: 0.3em 2.2em 1em 0em;
52 | margin: 0;
53 |
54 | font-size: 1.8em;
55 |
56 | // png fallback
57 | background: image-url("logo.png") no-repeat 100% 50% / 35%;
58 | background: none, inline-image("logo.svg", "image/svg+xml") no-repeat 100% 80%;
59 |
60 | @include breakpoint(medium) {
61 | font-size: 3em;
62 | }
63 | @media #{$height-constrained-phone} {
64 | font-size: 1.4em;
65 | }
66 | }
67 |
68 | .logo .tagline {
69 | @include grid-content;
70 | overflow-y: visible;
71 |
72 | padding-left: 0;
73 | position: relative;
74 | margin-top: 1.2 * $rem-base * $base-line-height * -1;
75 |
76 | max-width: 80%;
77 | font-size: 85%;
78 | @include breakpoint(medium) {
79 | font-size: 100%;
80 | margin-top: 2 * $rem-base * $base-line-height * -1;
81 | }
82 | @media #{$height-constrained-phone} {
83 | margin-top: $rem-base * $base-line-height * -1;
84 | }
85 | }
86 |
87 | // nav styles
88 | // -----------------------------
89 | .navigation {
90 | list-style: none;
91 | padding: 0;
92 | margin: 0;
93 | }
94 | .navigation-link {
95 | line-height: 1.4;
96 | font-size: 1.4rem;
97 | font-weight: bold;
98 | padding: 0 1rem;
99 | flex-grow: 1;
100 | text-align: center;
101 |
102 | background: $app-primary-bgcolor;
103 | box-shadow: inset 0 -8px 6px -7px $app-accent-dark-shadow;
104 | z-index: 1;
105 |
106 | @include breakpoint(medium) {
107 | font-size: 1.8rem;
108 | }
109 |
110 | &.selected {
111 | background: transparent;
112 | box-shadow: 0 7px 6px 4px $app-accent-dark-shadow;
113 | z-index: 2;
114 | }
115 |
116 | a {
117 | display: block;
118 | width: 100%;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/tests/unit/stores/ContentStore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var ContentStore = require('../../../stores/ContentStore');
10 |
11 | describe('content store', function () {
12 | var storeInstance;
13 | var content1 = {
14 | resource: 'home',
15 | data: {
16 | models: 'model',
17 | content: 'home
'
18 | }
19 | };
20 |
21 | beforeEach(function () {
22 | storeInstance = new ContentStore();
23 | });
24 |
25 | it('should instantiate correctly', function () {
26 | expect(storeInstance).to.be.an('object');
27 | expect(storeInstance.currentResource).to.equal('');
28 | expect(storeInstance.defaultResource).to.equal('');
29 | expect(storeInstance.contents).to.be.empty;
30 | });
31 |
32 | it('should init content', function () {
33 | var defaultName = 'default';
34 | var payload = {
35 | page: {
36 | defaultPageName: defaultName
37 | }
38 | };
39 |
40 | storeInstance.initContent(payload);
41 | expect(storeInstance.defaultResource).to.equal(defaultName);
42 | });
43 |
44 | it('should receive page content', function () {
45 | storeInstance.receivePageContent(content1);
46 | expect(Object.keys(storeInstance.contents).length).to.equal(1);
47 | });
48 |
49 | it('should reject malformed page content', function () {
50 | storeInstance.receivePageContent({ foo: 'bar' });
51 | expect(Object.keys(storeInstance.contents).length).to.equal(0);
52 | });
53 |
54 | it('should get content by resource', function () {
55 | storeInstance.receivePageContent(content1);
56 | expect(storeInstance.get(content1.resource)).to.eql(content1.data);
57 | });
58 |
59 | it('should get the current content', function () {
60 | storeInstance.receivePageContent(content1);
61 | expect(storeInstance.getCurrentPageContent()).to.eql(content1.data.content);
62 | });
63 |
64 | it('should get the current models', function () {
65 | storeInstance.receivePageContent(content1);
66 | expect(storeInstance.getCurrentPageModels()).to.eql(content1.data.models);
67 | });
68 |
69 | it('should dehydrate', function () {
70 | storeInstance.receivePageContent(content1);
71 | var state = storeInstance.dehydrate();
72 |
73 | expect(state.resource).to.equal(content1.resource);
74 | expect(Object.keys(state.contents).length).to.equal(1);
75 | expect(state.contents[state.resource]).to.eql(content1.data);
76 | });
77 |
78 | it('should rehydrate', function () {
79 | var state = {
80 | resource: content1.resource,
81 | contents: { home: content1.data }
82 | };
83 |
84 | storeInstance.rehydrate(state);
85 |
86 | expect(storeInstance.currentResource).to.equal(state.resource);
87 | expect(Object.keys(storeInstance.contents).length).to.equal(1);
88 | expect(storeInstance.get(state.resource)).to.eql(content1.data);
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/components/pages/contact/_arrows.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 |
4 | $arrow-fatten-size: 2;
5 |
6 | @mixin pre-triangle($size, $medium-size, $color, $direction: left, $fatten: false) {
7 | content: "";
8 | display: inline-block;
9 | width: 0;
10 | height: 0;
11 |
12 | @if $fatten {
13 | border: inset ($size + $arrow-fatten-size);
14 | } @else {
15 | border: inset $size;
16 | }
17 |
18 | border-style: solid;
19 |
20 | @if ($direction == left) {
21 | border-color: $color $color $color transparent;
22 | margin-right: -($size * 0.75);
23 | }
24 | @if ($direction == right) {
25 | border-color: $color transparent $color $color;
26 | margin-left: -($size * 0.75);
27 | }
28 |
29 | @include breakpoint(medium) {
30 | @if $fatten {
31 | border: inset ($medium-size + $arrow-fatten-size);
32 | } @else {
33 | border: inset $medium-size;
34 | }
35 |
36 | border-style: solid;
37 | @if ($direction == left) {
38 | border-color: $color $color $color transparent;
39 | margin-right: -($medium-size * 0.75);
40 | }
41 | @if ($direction == right) {
42 | border-color: $color transparent $color $color;
43 | margin-left: -($medium-size * 0.75);
44 | }
45 | }
46 | }
47 |
48 | @mixin clip-triangle($direction) {
49 | @if ($direction == left) {
50 | border-right-width: 0;
51 | }
52 | @if ($direction == right) {
53 | border-left-width: 0;
54 | }
55 | }
56 |
57 | @mixin post-triangle($size, $medium-size, $color, $direction: left, $fatten: false) {
58 | display: inline-block;
59 |
60 | @if $fatten {
61 | $size: $size + $arrow-fatten-size;
62 | }
63 |
64 | @include css-triangle($size, $color, $direction);
65 | @include clip-triangle($direction);
66 | @include breakpoint(medium) {
67 | @if $fatten {
68 | $medium-size: $medium-size + $arrow-fatten-size;
69 | }
70 |
71 | @include css-triangle($medium-size, $color, $direction);
72 | @include clip-triangle($direction);
73 | }
74 | }
75 |
76 | @mixin arrow-text($size, $medium-size, $bgcolor, $color, $fatten: false) {
77 | position: relative;
78 | z-index: 1;
79 | @if $fatten {
80 | padding: (($size/2) + $arrow-fatten-size) 0;
81 | } @else {
82 | padding: ($size/2) 0;
83 | }
84 | background: $bgcolor;
85 | color: $color;
86 | flex-grow: 1;
87 | min-width: 55%;
88 | font-weight: bold;
89 |
90 | @include breakpoint(medium) {
91 | @if $fatten {
92 | padding: (($medium-size/2) + $arrow-fatten-size) 0;
93 | } @else {
94 | padding: ($medium-size/2) 0;
95 | }
96 | }
97 | }
98 |
99 | @mixin arrow-decorate($size, $medium-size, $color, $direction: right, $fatten: false) {
100 | @if ($direction == right) {
101 | &::before {
102 | @include pre-triangle($size, $medium-size, $color, left, $fatten);
103 | }
104 | &::after {
105 | @include post-triangle($size, $medium-size, $color, left, $fatten);
106 | }
107 | }
108 | @if ($direction == left) {
109 | &::before {
110 | @include post-triangle($size, $medium-size, $color, right, $fatten);
111 | }
112 | &::after {
113 | @include pre-triangle($size, $medium-size, $color, right, $fatten);
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/services/data/fetch.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global Promise */
6 | 'use strict';
7 |
8 | var request = require('superagent');
9 | var debug = require('debug')('Example:Data:Fetch');
10 |
11 | var cache = require('./cache');
12 | var config = require('../../configs').create().data;
13 |
14 | /**
15 | * Get a single resource from FRED and cache it.
16 | *
17 | * @param {Object} params - The parameters controlling fetch.
18 | * @param {String} params.resource - The name of the resource to fetch, the key for the fetched data.
19 | * @param {String} [params.url] - The url of the resource to fetch. If omitted, defaults to the FRED url.
20 | * @param {Function} callback - The callback to execute on completion.
21 | */
22 | function fetchOne (params, callback) {
23 | debug('fetching resource "'+params.resource+'"');
24 |
25 | // A manifest request has no url specified
26 | if (!params.url) {
27 | params.url = config.FRED.url();
28 | } else {
29 | params.url = config.FRED.branchify(params.url);
30 | }
31 |
32 | request.get(params.url)
33 | .set('User-Agent', 'superagent')
34 | .set('Accept', config.FRED.mediaType())
35 | .end(function(err, res) {
36 | if (err) {
37 | debug('GET failed for ' + params.url + ': ' + err);
38 | return callback(err);
39 | }
40 |
41 | var content = res.body && res.body.content;
42 |
43 | if (content) {
44 | debug('Content successfully retrieved for', params.url);
45 | // > github api v3 content is base64 encoded
46 | cache.put(params, new Buffer(content, config.FRED.contentEncoding()).toString());
47 | return callback(null, cache.get(params.resource));
48 | }
49 |
50 | debug('Content not found for', params.url, res.body);
51 | return callback(new Error('Content not found for '+params.url));
52 | });
53 | }
54 |
55 | /**
56 | * Get the main resource from FRED.
57 | * Populates/updates the routes and models (all top-level resources).
58 | *
59 | * @param {Function} callback - The callback to execute on completion.
60 | */
61 | function fetchMain (callback) {
62 | fetchOne({
63 | resource: config.FRED.mainResource
64 | }, callback);
65 | }
66 |
67 | /**
68 | * Get all resources from FRED and cache them.
69 | * Call to update or populate the entire data cache.
70 | * Returns an array of each routes' content.
71 | *
72 | * @param {Function} callback - The callback to execute on completion.
73 | */
74 | function fetchAll (callback) {
75 | fetchMain(function (err, routes) {
76 | if (err) {
77 | debug('fetchAll failed to get routes: '+err);
78 | return callback(err);
79 | }
80 |
81 | Promise.all(
82 | Object.keys(routes).map(function (route) {
83 | return new Promise(function (resolve, reject) {
84 | fetchOne(routes[route].action.params, function (err, res) {
85 | if (err) {
86 | return reject(err);
87 | }
88 | return resolve(res);
89 | });
90 | });
91 | })
92 | )
93 | .then(function (result) {
94 | callback(null, result);
95 | }, callback);
96 | });
97 | }
98 |
99 | module.exports = {
100 | fetchMain: fetchMain,
101 | fetchOne: fetchOne,
102 | fetchAll: fetchAll
103 | };
104 |
--------------------------------------------------------------------------------
/tests/fixtures/cache-resources.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * A fixture to supply data for cache testing
6 | */
7 | 'use strict';
8 |
9 | var markupData = 'Sunshine and Rainbows
';
10 | var markdownData = '## Unicorns and Rainbows';
11 | var jsonData = JSON.stringify({
12 | test: {
13 | testPropTest: 'testValueTest'
14 | }
15 | });
16 | var validModels = JSON.stringify({
17 | models: {
18 | ValidModel1: {
19 | testProp: 'value'
20 | },
21 | ValidModel2: {
22 | testPropAnother: 'anotherValue'
23 | }
24 | }
25 | });
26 | var validModelRef = [ 'ValidModel1' ];
27 | var validMultiModelRef = [ 'ValidModel1', 'ValidModel2' ];
28 | var invalidModelRef = [ 'InvalidModel' ];
29 |
30 | var i = 0;
31 | function makeResId () {
32 | return 'res' + (i++);
33 | }
34 |
35 | function makeResource (params) {
36 | var testRes = {};
37 |
38 | if (params.hasId) {
39 | testRes.resource = params.idValue || makeResId();
40 | }
41 | if (params.hasFormat) {
42 | testRes.format = params.formatValue;
43 | }
44 | if (params.hasModels) {
45 | testRes.models = params.modelsValue;
46 | }
47 | if (params.hasData) {
48 | testRes.data = params.dataValue;
49 | }
50 |
51 | return testRes;
52 | }
53 |
54 | function makeMarkupResource (hasModels, modelsValue) {
55 | return makeResource({
56 | hasId: true,
57 | idValue: null,
58 | hasFormat: true,
59 | formatValue: 'markup',
60 | hasModels: hasModels,
61 | modelsValue: modelsValue,
62 | hasData: true,
63 | dataValue: markupData
64 | });
65 | }
66 |
67 | module.exports = {
68 | markupData: markupData,
69 | markdownData: markdownData,
70 | jsonData: JSON.parse(jsonData),
71 | validModels: JSON.parse(validModels),
72 | models: makeResource({
73 | hasId: true,
74 | idValue: 'models',
75 | hasFormat: true,
76 | formatValue: 'json',
77 | hasModels: false,
78 | modelsValue: null,
79 | hasData: true,
80 | dataValue: validModels
81 | }),
82 | nothing: makeResource({
83 | hasId: false,
84 | idValue: null,
85 | hasFormat: false,
86 | formatValue: null,
87 | hasModels: false,
88 | modelsValue: null,
89 | hasData: false,
90 | dataValue: null
91 | }),
92 | noFormat: makeResource({
93 | hasId: true,
94 | idValue: 'test',
95 | hasFormat: false,
96 | formatValue: null,
97 | hasModels: false,
98 | modelsValue: null,
99 | hasData: true,
100 | dataValue: jsonData
101 | }),
102 | badFormat: makeResource({
103 | hasId: true,
104 | idValue: 'test',
105 | hasFormat: true,
106 | formatValue: 'bad',
107 | hasModels: false,
108 | modelsValue: null,
109 | hasData: true,
110 | dataValue: jsonData
111 | }),
112 | noData: makeResource({
113 | hasId: true,
114 | idValue: null,
115 | hasFormat: true,
116 | formatValue: undefined,
117 | hasModels: false,
118 | modelsValue: null,
119 | hasData: false,
120 | dataValue: null
121 | }),
122 | markup: {
123 | validNone: makeMarkupResource(false, null),
124 | validSingle: makeMarkupResource(true, validModelRef),
125 | validMulti: makeMarkupResource(true, validMultiModelRef),
126 | invalid: makeMarkupResource(true, invalidModelRef)
127 | }
128 | };
129 |
--------------------------------------------------------------------------------
/tests/unit/actions/page.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | var createMockActionContext = require('fluxible/utils').createMockActionContext;
11 | var MockService = require('fluxible-plugin-fetchr/utils/MockServiceManager');
12 | var ApplicationStore = require('../../../stores/ApplicationStore');
13 | var ContentStore = require('../../../stores/ContentStore');
14 | var pageAction = require('../../../actions/page');
15 | var serviceData = require('../../mocks/service-data');
16 |
17 | describe('page action', function () {
18 | var calledService;
19 | var context;
20 | var params = {
21 | resource: 'home',
22 | pageTitle: 'happy time home page'
23 | };
24 |
25 | beforeEach(function () {
26 | calledService = 0;
27 | context = createMockActionContext({
28 | stores: [ApplicationStore, ContentStore]
29 | });
30 | context.service = new MockService();
31 | context.service.setService('page', function (method, params, config, callback) {
32 | calledService++;
33 | serviceData.fetch(params, callback);
34 | });
35 | });
36 |
37 | it('should update the ApplicationStore', function (done) {
38 | context.executeAction(pageAction, params, function (err) {
39 | if (err) {
40 | return done(err);
41 | }
42 |
43 | var title = context.getStore(ApplicationStore).getCurrentPageTitle();
44 |
45 | expect(calledService).to.equal(1);
46 | expect(title).to.be.a('string').and.not.be.empty;
47 | done();
48 | });
49 | });
50 |
51 | it('should update the ContentStore', function (done) {
52 | context.executeAction(pageAction, params, function (err) {
53 | if (err) {
54 | return done(err);
55 | }
56 |
57 | var content = context.getStore(ContentStore).getCurrentPageContent();
58 |
59 | expect(calledService).to.equal(1);
60 | expect(content).to.be.a('string').and.not.be.empty;
61 | done();
62 | });
63 | });
64 |
65 | it('should use the ContentStore before making a service call',
66 | function (done) {
67 | var contentStore = context.getStore(ContentStore);
68 |
69 | // make sure content for params.resource is there
70 | if (!contentStore.get(params.resource)) {
71 | contentStore.contents[params.resource] =
72 | serviceData.createContent(params.resource);
73 | }
74 |
75 | context.executeAction(pageAction, params, function (err) {
76 | if (err) {
77 | return done(err);
78 | }
79 |
80 | expect(calledService).to.equal(0);
81 | done();
82 | });
83 | });
84 |
85 | it('should fail as expected', function (done) {
86 | context.executeAction(pageAction, {
87 | emulateError: true
88 | }, function (err) {
89 | if (err) {
90 | return done();
91 | }
92 |
93 | done(new Error('should have received an error'));
94 | });
95 | });
96 |
97 | it('should fail as expected with no data', function (done) {
98 | context.executeAction(pageAction, {
99 | noData: true
100 | }, function (err) {
101 | if (err) {
102 | return done();
103 | }
104 |
105 | done(new Error('should have received an error'));
106 | });
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/tests/mocks/amqplib.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * A limited mock for amqplib
6 | */
7 | 'use strict';
8 |
9 | var Q = require('q');
10 |
11 | function Channel (chanError, msg, ack, nack) {
12 | this.chanError = chanError;
13 | this.msg = msg;
14 | this.cbAck = ack;
15 | this.cbNack = nack;
16 | this.error = new Error('channel');
17 | }
18 | Channel.prototype = {
19 | // Return promise, reject if chanError is true.
20 | assertQueue: function () {
21 | var deferred = Q.defer();
22 |
23 | if (this.chanError) {
24 | deferred.reject(this.error);
25 | } else {
26 | deferred.resolve();
27 | }
28 |
29 | return deferred.promise;
30 | },
31 | sendToQueue: function () {
32 | },
33 | consume: function (q, cb) {
34 | cb(this.msg);
35 | },
36 | nack: function (msg) {
37 | this.cbNack(msg);
38 | },
39 | ack: function (msg) {
40 | this.cbAck(msg);
41 | }
42 | };
43 |
44 | function Connection (connError, chanError, msg, ack, nack) {
45 | this.connError = connError;
46 | this.chanError = chanError;
47 | this.msg = msg;
48 | this.cbAck = ack;
49 | this.cbNack = nack;
50 | this.error = new Error('connection');
51 | }
52 | Connection.prototype = {
53 | // Return promise that receives a Channel on resolve
54 | // or reject if connError is true.
55 | createChannel: function () {
56 | var deferred = Q.defer();
57 |
58 | if (this.connError) {
59 | deferred.reject(this.error);
60 | } else {
61 | deferred.resolve(new Channel(
62 | this.chanError, this.msg, this.cbAck, this.cbNack
63 | ));
64 | }
65 |
66 | return deferred.promise;
67 | }
68 | };
69 |
70 | var errorProfile = {
71 | connect: false,
72 | connection: false,
73 | channel: false
74 | };
75 |
76 | var consumerMessage = {
77 | content: {
78 | toString: function() {
79 | return JSON.stringify({});
80 | }
81 | }
82 | };
83 |
84 | var consumerAck = function () {
85 | };
86 |
87 | var consumerNack = function () {
88 | };
89 |
90 | // Return promise that receives a connection on resolve
91 | // or reject if emulateError is true.
92 | function connect (input) {
93 | var deferred = Q.defer();
94 |
95 | if (errorProfile.connect) {
96 | deferred.reject(new Error('connect'));
97 | } else {
98 | deferred.resolve(new Connection(
99 | errorProfile.connection, errorProfile.channel,
100 | consumerMessage, consumerAck, consumerNack
101 | ));
102 | }
103 |
104 | return deferred.promise;
105 | }
106 |
107 | function setErrors (profile) {
108 | errorProfile.connect = profile.connect;
109 | errorProfile.connection = profile.connection;
110 | errorProfile.channel = profile.channel;
111 | }
112 |
113 | function setConsumerMessage (msg) {
114 | consumerMessage.content = new Buffer(JSON.stringify(msg));
115 | }
116 |
117 | function getConsumerMessage(msg) {
118 | return JSON.parse(msg.content.toString());
119 | }
120 |
121 | function setConsumerAck (cb) {
122 | consumerAck = cb;
123 | }
124 |
125 | function setConsumerNack (cb) {
126 | consumerNack = cb;
127 | }
128 |
129 | module.exports = {
130 | connect: connect,
131 | setErrors: setErrors,
132 | setConsumerMessage: setConsumerMessage,
133 | getConsumerMessage: getConsumerMessage,
134 | setConsumerAck: setConsumerAck,
135 | setConsumerNack: setConsumerNack
136 | };
--------------------------------------------------------------------------------
/tests/functional/sauce-travis.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Setup remote tests on SauceLabs from Travis.
6 | *
7 | * Parameters read from env:
8 | * TEST_BASEURL - The base url for requests from SauceLabs. Must be accessible to SauceLabs.
9 | * TEST_BROWSER - The key to browsers.js to find the SauceLabs platform/browser spec for this test.
10 | * TRAVIS - Boolean to indicate running on Travis
11 | * TRAVIS_BUILD_NUMBER - The SauceLabs required build number from Travis
12 | * TRAVIS_BRANCH - The branch being tested on Travis
13 | * SAUCE_USERNAME - The SauceLabs account username
14 | * SAUCE_ACCESS_KEY - The SauceLabs access key
15 | * VERBOSE - Boolean to indicate verbose test output
16 | */
17 | 'use strict';
18 |
19 | var wd = require('wd');
20 | require('colors');
21 | var chai = require('chai');
22 | var chaiAsPromised = require('chai-as-promised');
23 | var browserSpecs = require('./browsers');
24 |
25 | var testName = 'Basic test';
26 | var baseUrl = process.env.TEST_BASEURL;
27 | var timeout = 60000;
28 | var state = {
29 | allPassed: true
30 | };
31 |
32 | chai.use(chaiAsPromised);
33 | chai.should();
34 | chaiAsPromised.transferPromiseness = wd.transferPromiseness;
35 |
36 | wd.configureHttp({
37 | timeout: timeout,
38 | retryDelay: 15000,
39 | retries: 5
40 | });
41 |
42 | // Define the SauceLabs test
43 | var browserKey = process.env.TEST_BROWSER;
44 | var test = browserSpecs[browserKey];
45 | test.name = testName + ' with ' + browserKey;
46 | test.tags = ['flux-react-example'];
47 | if (process.env.TRAVIS) {
48 | test.tags = test.tags.concat('travis', process.env.TRAVIS_BRANCH);
49 | test.build = process.env.TRAVIS_BUILD_NUMBER;
50 | }
51 |
52 | // Build caps report string for this test
53 | var sauceCaps = {
54 | browserName: 1,
55 | platform: 1,
56 | version: 1,
57 | deviceName: 1,
58 | 'device-orientation': 1
59 | };
60 | var caps = Object.keys(test).reduce(function(prev, curr) {
61 | if (sauceCaps[curr] && test[curr]) {
62 | prev = prev + (prev ? ', ' : '') + test[curr];
63 | }
64 | return prev;
65 | }, '');
66 |
67 | /**
68 | * The one-time setup to perform before all tests
69 | */
70 | function beforeAll(done) {
71 | var username = process.env.SAUCE_USERNAME;
72 | var accessKey = process.env.SAUCE_ACCESS_KEY;
73 | state.browser = wd.promiseChainRemote('ondemand.saucelabs.com', 80, username, accessKey);
74 | if (process.env.VERBOSE) {
75 | // optional logging
76 | state.browser.on('status', function(info) {
77 | console.log(info.cyan);
78 | });
79 | state.browser.on('command', function(meth, path, data) {
80 | console.log(' > ' + meth.yellow, path.grey, data || '');
81 | });
82 | }
83 | state.browser
84 | .init(test)
85 | .nodeify(done);
86 | }
87 |
88 | /**
89 | * The one-time teardown to perform after all tests
90 | */
91 | function afterAll(done) {
92 | state.browser
93 | .quit()
94 | .sauceJobStatus(state.allPassed)
95 | .nodeify(done);
96 | }
97 |
98 | function updateState(mocha) {
99 | state.allPassed = state.allPassed && (mocha.currentTest.state === 'passed');
100 | }
101 |
102 | module.exports = {
103 | name: testName,
104 | caps: caps,
105 | timeout: timeout,
106 | baseUrl: baseUrl,
107 | state: state,
108 | beforeAll: beforeAll,
109 | afterAll: afterAll,
110 | updateState: updateState
111 | };
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Creates an Express application, the middleware stack,
6 | * registers the app services, initializes the data layer, and binds to a port.
7 | */
8 | 'use strict';
9 |
10 | // some environments run the app from a different directory
11 | process.chdir(__dirname);
12 | var baseDir = '..';
13 |
14 | require('node-jsx').install({ extension: '.jsx' });
15 |
16 | var express = require('express');
17 | var favicon = require('serve-favicon');
18 | var compress = require('compression');
19 | var logger = require('morgan');
20 | var bodyParser = require('body-parser');
21 | var cookieParser = require('cookie-parser');
22 | var csrf = require('csurf');
23 | var errorHandler = require('express-error-handler');
24 | var rewrite = require('connect-modrewrite');
25 |
26 | var config = require(baseDir + '/configs').create({
27 | baseDir: baseDir
28 | });
29 | var fluxibleApp = require(baseDir + '/app');
30 | var main = require('./main');
31 | var robots = require('./robots');
32 | var sitemap = require('./sitemap');
33 |
34 | var data = require(baseDir + '/services/data');
35 |
36 | var settings = config.settings;
37 | var protocol = require(settings.web.ssl ? 'https' : 'http');
38 |
39 | var rewriteRules = [
40 | // rewrite root image requests to settings.web.images
41 | '^/([^\\/]+\\.(?:png|jpg|jpeg|webp|ico|svg|gif)(?:\\?.*)?$) ' +
42 | settings.web.images + '/$1 [NC L]',
43 | // alias home to root
44 | '^/home/?$ / [L]',
45 | // forbid 404 and 500 direct requests
46 | '^/(?:404|500)/?$ [F L]'
47 | ];
48 |
49 | var app = express();
50 | var server = protocol.createServer(app);
51 |
52 | app.use(favicon(settings.dist.favicon));
53 | app.use(logger(settings.loggerFormat));
54 | app.use(compress());
55 | app.use(errorHandler.maintenance());
56 | app.use(rewrite(rewriteRules));
57 | app.use(settings.web.baseDir, express.static(
58 | settings.dist.baseDir, { maxAge: settings.web.assetAge }
59 | ));
60 | app.use(cookieParser({ httpOnly: true, secure: settings.web.ssl }));
61 | app.use(bodyParser.json());
62 | app.use(csrf({ cookie: true }));
63 |
64 | // Access fetchr plugin instance, register services, and setup middleware
65 | var fetchrPlugin = fluxibleApp.getPlugin('FetchrPlugin');
66 | fetchrPlugin.registerService(require(baseDir + '/services/routes'));
67 | fetchrPlugin.registerService(require(baseDir + '/services/page'));
68 | fetchrPlugin.registerService(require(baseDir + '/services/contact'));
69 | app.use(fetchrPlugin.getXhrPath(), fetchrPlugin.getMiddleware());
70 |
71 | // Handle robots.txt
72 | app.get(settings.web.robots, robots);
73 |
74 | // Handle sitemap.xml
75 | app.get(settings.web.sitemap, sitemap);
76 |
77 | // Every other request gets the app bootstrap
78 | app.use(main(fluxibleApp));
79 |
80 | app.use(errorHandler({
81 | server: server,
82 | static: {
83 | // This 'hard' 500 will cause a restart.
84 | // Actually covers all 500s except for 503 via errorHandler.
85 | // The PM in charge should be configured to notify dev on restarts.
86 | '500': settings.dist.five00,
87 | // The notice for maintenance mode.
88 | '503': settings.dist.five03
89 | }
90 | }));
91 |
92 | // Initialize the data layer and start the server.
93 | data.initialize(function (err) {
94 | if (err) {
95 | throw err;
96 | }
97 | server.listen(config.PORT, function() {
98 | console.log('Listening on port ' + config.PORT);
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/tests/unit/services/mail/queue.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global before, after, describe, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var mocks = require('../../../utils/mocks');
10 |
11 | describe('mail/queue', function () {
12 | var queue, amqplib,
13 | fields = {
14 | name: 'testuser',
15 | email: 'test@test.com',
16 | message: 'this is a message'
17 | };
18 |
19 | before(function () {
20 | mocks.queue.begin();
21 | queue = require('../../../../services/mail/queue');
22 | amqplib = require('amqplib');
23 | });
24 |
25 | after(function () {
26 | mocks.queue.end();
27 | });
28 |
29 | describe('sendMail', function () {
30 | // test sendMail
31 |
32 | it('should handle connect error', function (done) {
33 | var errorType = 'connect';
34 | amqplib.setErrors({
35 | connect: true
36 | });
37 |
38 | queue.sendMail(fields, function (err) {
39 | expect(err).to.be.an.instanceof(Error);
40 | expect(err.message).to.equal(errorType);
41 | done();
42 | });
43 | });
44 |
45 | it('should handle a connection error', function (done) {
46 | var errorType = 'connection';
47 | amqplib.setErrors({
48 | connection: true
49 | });
50 |
51 | queue.sendMail(fields, function (err) {
52 | expect(err).to.be.an.instanceof(Error);
53 | expect(err.message).to.equal(errorType);
54 | done();
55 | });
56 | });
57 |
58 | it('should handle a channel error', function (done) {
59 | var errorType = 'channel';
60 | amqplib.setErrors({
61 | channel: true
62 | });
63 |
64 | queue.sendMail(fields, function (err) {
65 | expect(err).to.be.an.instanceof(Error);
66 | expect(err.message).to.equal(errorType);
67 | done();
68 | });
69 | });
70 |
71 | it('should work with no errors', function (done) {
72 | amqplib.setErrors({});
73 |
74 | queue.sendMail(fields, function (err) {
75 | done(err);
76 | });
77 | });
78 | });
79 |
80 | describe('contactWorker', function () {
81 | // test contactWorker
82 |
83 | it('should ack messages', function (done) {
84 | amqplib.setErrors({});
85 |
86 | amqplib.setConsumerMessage(fields);
87 |
88 | amqplib.setConsumerAck(function (msg) {
89 | var result = amqplib.getConsumerMessage(msg);
90 | expect(result).to.eql(fields);
91 | done();
92 | });
93 | amqplib.setConsumerNack(function (msg) {
94 | done(new Error('Nack should not have been called'));
95 | });
96 |
97 | queue.contactWorker();
98 | });
99 |
100 | it('should nack messages', function (done) {
101 | amqplib.setErrors({});
102 |
103 | var payload = JSON.parse(JSON.stringify(fields));
104 | payload.emulateError = true;
105 |
106 | amqplib.setConsumerMessage(payload);
107 |
108 | amqplib.setConsumerAck(function (msg) {
109 | done(new Error('Ack should not have been called'));
110 | });
111 | amqplib.setConsumerNack(function (msg) {
112 | var result = amqplib.getConsumerMessage(msg);
113 | delete result.emulateError;
114 | expect(result).to.eql(fields);
115 | done();
116 | });
117 |
118 | queue.contactWorker();
119 | });
120 | });
121 | });
--------------------------------------------------------------------------------
/tests/utils/mocks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * Mockery and mock manager
6 | */
7 | 'use strict';
8 |
9 | var mockery = require('mockery');
10 | var debug = require('debug')('Test:Mocks');
11 | var serviceData = require('../mocks/service-data');
12 | var serviceMail = require('../mocks/service-mail');
13 | var superAgent = require('../mocks/superagent');
14 | var amqplib = require('../mocks/amqplib');
15 | var cache = require('../mocks/cache');
16 | var fetch = require('../mocks/fetch');
17 | var queue = require('../mocks/queue');
18 | var mailer = require('../mocks/mailer');
19 |
20 | function mockModuleBegin (mocks) {
21 | mocks.forEach(function (mock) {
22 | debug('registering mock "' + mock.pattern + '"');
23 | mockery.registerMock(mock.pattern, mock.module);
24 | });
25 |
26 | mockery.enable({
27 | useCleanCache: true,
28 | warnOnUnregistered: false
29 | });
30 | }
31 |
32 | function mockModuleEnd (mocks) {
33 | mockery.disable();
34 |
35 | mocks.forEach(function (mock) {
36 | mockery.deregisterMock(mock.pattern);
37 | });
38 | }
39 |
40 | function mockServiceDataBegin () {
41 | mockModuleBegin([{
42 | pattern: './data',
43 | module: serviceData
44 | }]);
45 | }
46 |
47 | function mockServiceDataEnd () {
48 | mockModuleEnd([{
49 | pattern: './data'
50 | }]);
51 | }
52 |
53 | function mockServiceMailBegin () {
54 | mockModuleBegin([{
55 | pattern: './mail',
56 | module: serviceMail
57 | }]);
58 | }
59 |
60 | function mockServiceMailEnd () {
61 | mockModuleEnd([{
62 | pattern: './mail'
63 | }]);
64 | }
65 |
66 | function mockSuperAgentBegin () {
67 | mockModuleBegin([{
68 | pattern: 'superagent',
69 | module: superAgent
70 | }, {
71 | pattern: './cache',
72 | module: cache
73 | }]);
74 | }
75 |
76 | function mockSuperAgentEnd () {
77 | mockModuleEnd([{
78 | pattern: 'superagent'
79 | }, {
80 | pattern: './cache'
81 | }]);
82 | }
83 |
84 | function mockFetchBegin () {
85 | mockModuleBegin([{
86 | pattern: './fetch',
87 | module: fetch
88 | }, {
89 | pattern: './cache',
90 | module: cache
91 | }]);
92 | }
93 |
94 | function mockFetchEnd () {
95 | mockModuleEnd([{
96 | pattern: './fetch'
97 | }, {
98 | pattern: './cache'
99 | }]);
100 | }
101 |
102 | function mockMailBegin () {
103 | mockModuleBegin([{
104 | pattern: './queue',
105 | module: queue
106 | }]);
107 | }
108 |
109 | function mockMailEnd () {
110 | mockModuleEnd([{
111 | pattern: './queue'
112 | }]);
113 | }
114 |
115 | function mockQueueBegin () {
116 | mockModuleBegin([{
117 | pattern: 'amqplib',
118 | module: amqplib
119 | }, {
120 | pattern: './mailer',
121 | module: mailer
122 | }]);
123 | }
124 |
125 | function mockQueueEnd () {
126 | mockModuleEnd([{
127 | pattern: 'amqplib'
128 | }, {
129 | pattern: './mailer'
130 | }]);
131 | }
132 |
133 | module.exports = {
134 | serviceData: {
135 | begin: mockServiceDataBegin,
136 | end: mockServiceDataEnd
137 | },
138 | superAgent: {
139 | begin: mockSuperAgentBegin,
140 | end: mockSuperAgentEnd
141 | },
142 | fetch: {
143 | begin: mockFetchBegin,
144 | end: mockFetchEnd
145 | },
146 | serviceMail: {
147 | begin: mockServiceMailBegin,
148 | end: mockServiceMailEnd
149 | },
150 | mail: {
151 | begin: mockMailBegin,
152 | end: mockMailEnd
153 | },
154 | queue: {
155 | begin: mockQueueBegin,
156 | end: mockQueueEnd
157 | }
158 | };
159 |
--------------------------------------------------------------------------------
/components/Background.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global document */
6 | 'use strict';
7 |
8 | var React = require('react');
9 |
10 | var Background = React.createClass({
11 | contextTypes: {
12 | getStore: React.PropTypes.func.isRequired,
13 | executeAction: React.PropTypes.func.isRequired
14 | },
15 |
16 | propTypes: {
17 | prefetch: React.PropTypes.bool
18 | },
19 |
20 | getInitialState: function () {
21 | return {
22 | top: 0,
23 | height: 0,
24 | loaded: false
25 | };
26 | },
27 |
28 | render: function () {
29 | var gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5))';
30 | // Setting 'image' initially to 'none' seems to help IE 11, but it still can't
31 | // change images properly - you always get the same image. #41
32 | // Tried all prop combos incl bg shorthand. closest was image render w/gradient,
33 | // but won't change raster image - css does reflect, but browser won't render.
34 | // Leaving in gradient only state for IE. Best of all bad options for IE.
35 | var image = gradient;
36 |
37 | if (this.state.loaded) {
38 | image = gradient + ', url(' + this.state.src + ')';
39 | } else {
40 | if (this.state.prevSrc) {
41 | image = gradient + ', url(' + this.state.prevSrc + ')';
42 | }
43 | }
44 |
45 | return (
46 |
58 | );
59 | },
60 |
61 | componentDidMount: function () {
62 | this.context.getStore('BackgroundStore').addChangeListener(this.onChange);
63 | },
64 |
65 | componentWillUnmount: function () {
66 | this.context.getStore('BackgroundStore').removeChangeListener(this.onChange);
67 | },
68 |
69 | getStateFromStore: function () {
70 | var store = this.context.getStore('BackgroundStore');
71 | return {
72 | src: store.getCurrentBackgroundUrl(),
73 | top: store.getTop(),
74 | height: store.getHeight()
75 | };
76 | },
77 |
78 | fetchImage: function (fetchOnly) {
79 | var self = this;
80 | var img = document.createElement('img');
81 |
82 | if (!fetchOnly) {
83 | img.onload = function () {
84 | setTimeout(function () {
85 | if (typeof document !== 'undefined') {
86 | self.setState({
87 | loaded: true
88 | });
89 | }
90 | }, 200);
91 | };
92 | }
93 |
94 | img.src = fetchOnly || this.state.src;
95 | },
96 |
97 | prefetchImages: function () {
98 | if (this.props.prefetch && !this.prefetched) {
99 | this.context.getStore('BackgroundStore')
100 | .getNotCurrentBackgroundUrls()
101 | .forEach(function (notCurrentUrl) {
102 | this.fetchImage(notCurrentUrl);
103 | }, this);
104 |
105 | this.prefetched = true;
106 | }
107 | },
108 |
109 | onChange: function () {
110 | var state = this.getStateFromStore();
111 | state.prevSrc = this.state.src;
112 | state.loaded = false;
113 |
114 | this.setState(state);
115 | this.fetchImage();
116 |
117 | this.prefetchImages();
118 | }
119 | });
120 |
121 | module.exports = Background;
122 |
--------------------------------------------------------------------------------
/stores/ContentStore.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 | var createStore = require('fluxible/addons').createStore;
7 |
8 | var ContentStore = createStore({
9 | storeName: 'ContentStore',
10 |
11 | handlers: {
12 | 'INIT_APP': 'initContent',
13 | 'RECEIVE_PAGE_CONTENT': 'receivePageContent'
14 | },
15 |
16 | /**
17 | * Set ContentStore initial state.
18 | */
19 | initialize: function () {
20 | this.contents = {};
21 | this.currentResource = '';
22 | this.defaultResource = '';
23 | },
24 |
25 | /**
26 | * INIT_APP handler.
27 | *
28 | * @param {Object} payload - INIT_APP action payload.
29 | * @param {Object} payload.page - Content data this Store is intereseted in.
30 | * @param {String} payload.page.defaultPageName - The default resource.
31 | */
32 | initContent: function (payload) {
33 | var init = payload.page;
34 | if (init) {
35 | this.defaultResource = init.defaultPageName;
36 | this.emitChange();
37 | }
38 | },
39 |
40 | /**
41 | * RECEIVE_PAGE_CONTENT handler.
42 | *
43 | * @param {Object} page - RECEIVE_PAGE_CONTENT action payload.
44 | * @param {String} page.resource - The current resource and the key to its data.
45 | * @param {Object} page.data - The page data, containing models and content.
46 | */
47 | receivePageContent: function (page) {
48 | if (!page || !page.hasOwnProperty('resource')) {
49 | return;
50 | }
51 |
52 | this.currentResource = page.resource;
53 | this.contents[page.resource] = page.data;
54 | this.emitChange();
55 | },
56 |
57 | /**
58 | * Get content and models for the given arbitrary resource.
59 | *
60 | * @param {String} resource - The resource to get (The key).
61 | * @returns {Object} Content and models for the given resource, or undefined if not found.
62 | */
63 | get: function (resource) {
64 | return this.contents[resource];
65 | },
66 |
67 | /**
68 | * Get the page content for the current resource.
69 | * If the current resource is not defined, use the defaultResource.
70 | *
71 | * @returns {Object|String} the current page content or null if not found.
72 | */
73 | getCurrentPageContent: function () {
74 | var resource = this.get(this.currentResource || this.defaultResource);
75 | if (resource) {
76 | return resource.content;
77 | }
78 | return null;
79 | },
80 |
81 | /**
82 | * Get the page models for the current resource.
83 | * If the current resource is not defined, use the deafultResource.
84 | *
85 | * @returns {Object} the current page models or null if not found.
86 | */
87 | getCurrentPageModels: function () {
88 | var resource = this.get(this.currentResource || this.defaultResource);
89 | if (resource) {
90 | return resource.models;
91 | }
92 | return null;
93 | },
94 |
95 | /**
96 | * Reduce this store to state.
97 | *
98 | * @returns {Object} this store as state.
99 | */
100 | dehydrate: function () {
101 | return {
102 | resource: this.currentResource,
103 | defaultResource: this.defaultResource,
104 | contents: this.contents
105 | };
106 | },
107 |
108 | /**
109 | * Hydrate this store from state.
110 | *
111 | * @param {Object} state - The new ContentStore state.
112 | */
113 | rehydrate: function (state) {
114 | this.currentResource = state.resource;
115 | this.defaultResource = state.defaultResource;
116 | this.contents = state.contents;
117 | }
118 | });
119 |
120 | module.exports = ContentStore;
121 |
--------------------------------------------------------------------------------
/components/pages/index.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var React = require('react');
8 | var ContentPage = require('./ContentPage.jsx');
9 | var Contact = require('./contact');
10 | var conformErrorStatus = require('../../utils').conformErrorStatus;
11 | var merge = require('lodash/merge');
12 |
13 | /***
14 | * Component to class map
15 | */
16 | var pageTypes = {
17 | ContentPage: ContentPage,
18 | Contact: Contact
19 | };
20 |
21 | /**
22 | * Exchange a component string for a React class.
23 | *
24 | * @param {String} component - The name of a page component.
25 | * @returns {Object} A React class named by the component parameter.
26 | */
27 | function getClass (component) {
28 | return pageTypes[component];
29 | }
30 |
31 | /**
32 | * Form a props object given content and models.
33 | *
34 | * @param {String|Object} content - page html or json content.
35 | * @param {Object} models - Json containing models for the page.
36 | * @returns {Object} If content is undefined returns a spinner property,
37 | * otherwise returns an object with content and models.
38 | */
39 | function getProps (content, models) {
40 | var props;
41 |
42 | if (content) {
43 | props = Object.prototype.toString.call(content) === '[object Object]' ?
44 | merge(content, { models: models }) : {
45 | models: models,
46 | content: content
47 | };
48 | } else {
49 | props = {
50 | spinner: true
51 | };
52 | }
53 |
54 | return props;
55 | }
56 |
57 | /**
58 | * Return the main navigable pages for the app as an ordered array.
59 | * These are routes that have mainNav === 'true'.
60 | * If there is an error, the page at the ordinal will be the required error page.
61 | *
62 | * @param {Object} error - A fluxible routes navigationError.
63 | * @param {Number} error.statusCode - The error status code.
64 | * @param {Object} pages - A routes object from RouteStore.
65 | * @param {Number} ordinal - A zero based order for the current page in the routes Object.
66 | * @returns {Array} An ordered array of the main navigable pages of the application.
67 | */
68 | function getMainNavPages (error, pages, ordinal) {
69 | var mainPages = Object.keys(pages)
70 | .filter(function (page) {
71 | return pages[page].mainNav;
72 | })
73 | .sort(function (a, b) {
74 | return pages[a].order - pages[b].order;
75 | })
76 | .map(function (page) {
77 | return pages[page];
78 | });
79 |
80 | if (error) {
81 | mainPages[ordinal] = pages[conformErrorStatus(error.statusCode)];
82 | }
83 |
84 | return mainPages;
85 | }
86 |
87 | /**
88 | * Create React elements for the given navigable pages.
89 | * Unfortunately, the key and id have to always be the same for each slot for swipe.
90 | *
91 | * @param {Array} navPages - An ordered array of the main navigable pages.
92 | * @param {Object} contentStore - A reference to the ContentStore.
93 | * @returns {Array} Array of React Elements, one for each navPage.
94 | */
95 | function createElements (navPages, contentStore) {
96 | var count = 0, key = 'page';
97 |
98 | return navPages.map(function (np) {
99 | var data = contentStore.get(np.page) || {};
100 |
101 | return React.createElement('div', {
102 | key: key + count,
103 | id: key + count++
104 | }, React.createElement(
105 | getClass(np.component),
106 | getProps(data.content, data.models)
107 | )
108 | );
109 | });
110 | }
111 |
112 | module.exports = {
113 | createElements: createElements,
114 | getMainNavPages: getMainNavPages
115 | };
116 |
--------------------------------------------------------------------------------
/tests/unit/services/data/fetch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global before, beforeEach, after, describe, it */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 | var mocks = require('../../../utils/mocks');
10 |
11 | var config = require('../../../../configs').create().data;
12 |
13 | describe('data/fetch', function () {
14 | var fetch, cache, request;
15 |
16 | before(function () {
17 | mocks.superAgent.begin();
18 | fetch = require('../../../../services/data/fetch');
19 | cache = require('./cache');
20 | request = require('superagent');
21 | });
22 |
23 | after(function () {
24 | mocks.superAgent.end();
25 | });
26 |
27 | beforeEach(function () {
28 | request.setEmulateError(false);
29 | request.setNoData(false);
30 | });
31 |
32 | describe('fetchOne', function () {
33 | it('should fetch the FRED if no url supplied', function (done) {
34 | fetch.fetchOne({ resource: 'test' }, function (err, res) {
35 | if (err) {
36 | return done(err);
37 | }
38 |
39 | expect(res).to.equal(cache.get());
40 | expect(request.url).to.equal(config.FRED.url());
41 |
42 | done();
43 | });
44 | });
45 |
46 | it('should fetch the supplied url', function (done) {
47 | var supplied = '123456789';
48 | fetch.fetchOne({ resource: 'test', url: supplied }, function (err, res) {
49 | if (err) {
50 | return done(err);
51 | }
52 |
53 | expect(res).to.equal(cache.get());
54 | expect(request.url).to.contain(supplied);
55 |
56 | done();
57 | });
58 | });
59 |
60 | it('should fail if no data', function (done) {
61 | request.setNoData(true);
62 |
63 | fetch.fetchOne({ resource: 'test' }, function (err, res) {
64 | if (err) {
65 | return done();
66 | }
67 |
68 | done(new Error('Expected error'));
69 | });
70 | });
71 |
72 | it('should fail if network fails', function (done) {
73 | request.setEmulateError(true);
74 |
75 | fetch.fetchOne({ resource: 'test' }, function (err, res) {
76 | if (err) {
77 | return done();
78 | }
79 |
80 | done(new Error('Expected error'));
81 | });
82 | });
83 | });
84 |
85 | describe('fetchMain', function () {
86 | it('should fetch the main resource', function (done) {
87 | fetch.fetchMain(function (err, res) {
88 | if (err) {
89 | return done(err);
90 | }
91 |
92 | expect(res).to.equal(cache.get('routes'));
93 | expect(request.url).to.equal(config.FRED.url());
94 |
95 | done();
96 | });
97 | });
98 | });
99 |
100 | describe('fetchAll', function () {
101 | it('should fetch all resources', function (done) {
102 | fetch.fetchAll(function (err, res) {
103 | if (err) {
104 | return done(err);
105 | }
106 |
107 | var routes = cache.get('routes');
108 |
109 | // It should return content for each route
110 | expect(Object.keys(routes).length).to.equal(res.length);
111 | if (res.length > 0) {
112 | // And they should be the default response
113 | expect(res[0]).to.equal(cache.get());
114 | }
115 |
116 | done();
117 | });
118 | });
119 |
120 | it('should fail if network fails', function (done) {
121 | request.setEmulateError(true);
122 |
123 | fetch.fetchAll(function (err, res) {
124 | if (err) {
125 | return done();
126 | }
127 |
128 | done(new Error('Expected error'));
129 | });
130 | });
131 | });
132 | });
133 |
--------------------------------------------------------------------------------
/components/sizeReporter.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | *
5 | * A higher order component to report a rendered client size.
6 | *
7 | * In this example, this is used to report size in the retrieval of the background
8 | * image to avoid extra bandwidth and expensive image scaling on a limited device.
9 | * Everytime you invalidate an area with a scaled image, you incur the scaling cost.
10 | */
11 | /* global document, window */
12 | 'use strict';
13 |
14 | var React = require('react');
15 | var sizeAction = require('../actions/size');
16 |
17 | /***
18 | * reporters and callCounts.
19 | *
20 | * A count of reporters is accumulated each time the factory below is called to
21 | * create a size reporter.
22 | * When all the reporters finish reporting (and initially),
23 | * callCount % reporters === 0.
24 | * This is used to determine if dimensions should be accumulated (add === true).
25 | * They should be accumulated when reporters have not all reported.
26 | *
27 | * Unscoped, these assume there is only one thing in the app to report size about.
28 | * Not so scalable/repeatable/reusable this way.
29 | * If the app had multiple items to report size on, this component would need
30 | * a key to distinguish a scope for reporters and their callCounts.
31 | * This key would also be of interest to the appropriate store.
32 | */
33 | var reporters = 0;
34 | var callCount = 0;
35 |
36 | /**
37 | * Factory to create a high order React component to report size changes.
38 | *
39 | * @param {Object} Component - The React class the size reporter wraps.
40 | * @param {String} selector - The selector used to find the DOM element to report on.
41 | * @param {Object} options - Options to control what gets reported.
42 | * @returns {Object} A SizeReporter React class that renders the given `Component`,
43 | * reporting on DOM element found at `selector`.
44 | */
45 | function reportRenderedSize (Component, selector, options) {
46 | options = options || {};
47 | reporters++;
48 |
49 | var SizeReporter = React.createClass({
50 | contextTypes: {
51 | executeAction: React.PropTypes.func.isRequired
52 | },
53 |
54 | /**
55 | * Render the wrapped component with props.
56 | */
57 | render: function () {
58 | return React.createElement(Component, this.props);
59 | },
60 |
61 | /**
62 | * Set up the resize event listener and kick off the first report.
63 | */
64 | componentDidMount: function () {
65 | window.addEventListener('resize', this.reportSize);
66 | // reportSize is bound to this by React, so this is safe.
67 | setTimeout(this.reportSize, 0);
68 | },
69 |
70 | /**
71 | * Remove the resize event listener.
72 | */
73 | componentWillUnmount: function () {
74 | window.removeEventListener('resize', this.reportSize);
75 | },
76 |
77 | /**
78 | * Report the size of the DOM element found at `selector`.
79 | */
80 | reportSize: function () {
81 | var el = document.querySelector(selector);
82 |
83 | var width;
84 | if (options.reportWidth) {
85 | width = el ? el.clientWidth : null;
86 | }
87 |
88 | var top;
89 | if (options.reportTop) {
90 | var rect = el ? el.getBoundingClientRect() : { top: 0 };
91 | top = rect.top + window.pageYOffset - document.documentElement.clientTop;
92 | }
93 |
94 | this.context.executeAction(sizeAction, {
95 | height: el ? el.clientHeight : null,
96 | width: width,
97 | top: top,
98 | add: callCount % reporters !== 0
99 | });
100 | callCount++;
101 | }
102 | });
103 |
104 | return SizeReporter;
105 | }
106 |
107 | module.exports = reportRenderedSize;
108 |
--------------------------------------------------------------------------------
/tests/unit/actions/contact.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | var createMockActionContext = require('fluxible/utils').createMockActionContext;
11 | var MockService = require('fluxible-plugin-fetchr/utils/MockServiceManager');
12 | var ContactStore = require('../../../stores/ContactStore');
13 | var serviceMail = require('../../mocks/service-mail');
14 | var contactAction = require('../../../actions/contact');
15 |
16 | describe('contact action', function () {
17 | var context;
18 |
19 | var fields = {
20 | name: 'alex',
21 | email: 'alex@test.domain',
22 | message: 'the truth about seafood is it\'s people'
23 | };
24 |
25 | function getContactData () {
26 | var store = context.getStore(ContactStore);
27 | return {
28 | fields: store.getContactFields(),
29 | failure: store.getContactFailure()
30 | };
31 | }
32 |
33 | function populateStore (callback) {
34 | context.executeAction(contactAction, { fields: fields }, callback);
35 | }
36 |
37 | beforeEach(function () {
38 | context = createMockActionContext({
39 | stores: [ ContactStore ]
40 | });
41 | context.service = new MockService();
42 | context.service.setService('contact', function (method, params, body, config, callback) {
43 | serviceMail.send(params, callback);
44 | });
45 | });
46 |
47 | it('should update the ContactStore with one field', function (done) {
48 | var partialFields = {
49 | email: fields.email
50 | };
51 |
52 | context.executeAction(contactAction, { fields: partialFields }, function (err) {
53 | if (err) {
54 | return done(err);
55 | }
56 |
57 | var data = getContactData();
58 |
59 | expect(data.fields.name).to.equal('');
60 | expect(data.fields.email).to.deep.equal(partialFields.email);
61 | expect(data.fields.message).to.equal('');
62 | expect(data.failure).to.be.false;
63 |
64 | done();
65 | });
66 | });
67 |
68 | it('should update the ContactStore with all fields', function (done) {
69 | populateStore(function (err) {
70 | if (err) {
71 | return done(err);
72 | }
73 |
74 | var data = getContactData();
75 |
76 | expect(data.fields).to.deep.equal(fields);
77 | expect(data.failure).to.be.false;
78 |
79 | done();
80 | });
81 | });
82 |
83 | it('should send and clear the ContactStore when complete, success', function (done) {
84 | context.executeAction(contactAction, { fields: fields, complete: true }, function (err) {
85 | if (err) {
86 | return done(err);
87 | }
88 |
89 | var data = getContactData();
90 |
91 | expect(data.fields.name).to.equal('');
92 | expect(data.fields.email).to.equal('');
93 | expect(data.fields.message).to.equal('');
94 | expect(data.failure).to.be.false;
95 |
96 | done();
97 | });
98 | });
99 |
100 | it('should update the ContactStore and send when complete, failure', function (done) {
101 | populateStore(function (err) {
102 | if (err) {
103 | return done(err);
104 | }
105 |
106 | var mockFields = JSON.parse(JSON.stringify(fields));
107 | mockFields.emulateError = true;
108 |
109 | context.executeAction(contactAction, { fields: mockFields, complete: true }, function (err) {
110 | if (err) {
111 | return done(err);
112 | }
113 |
114 | var data = getContactData();
115 |
116 | expect(data.fields).to.deep.equal(fields);
117 | expect(data.failure).to.be.true;
118 |
119 | done();
120 | });
121 | });
122 | });
123 | });
124 |
--------------------------------------------------------------------------------
/tests/unit/actions/routes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global describe, it, beforeEach */
6 | 'use strict';
7 |
8 | var expect = require('chai').expect;
9 |
10 | var createMockActionContext = require('fluxible/utils').createMockActionContext;
11 | var MockService = require('fluxible-plugin-fetchr/utils/MockServiceManager');
12 |
13 | var RouteStore = require('../../../stores/RouteStore');
14 | var routes = require('../../../actions/routes');
15 | var routesResponse = require('../../fixtures/routes-response');
16 | var transformer = require('../../../utils').createFluxibleRouteTransformer({
17 | actions: require('../../../actions/interface')
18 | });
19 | var testUtils = require('../../utils/tests');
20 |
21 | describe('routes action', function () {
22 | var context;
23 | var response;
24 | var testPage = 'home';
25 |
26 | function checkTestPage() {
27 | var pages = context.getStore(RouteStore).getRoutes();
28 |
29 | expect(pages).to.be.an('object');
30 | expect(pages).to.not.be.empty;
31 | expect(pages[testPage]).to.be.an('object');
32 | }
33 |
34 | // create the action context wired to RouteStore
35 | beforeEach(function () {
36 | context = createMockActionContext({
37 | stores: [RouteStore]
38 | });
39 | });
40 |
41 | describe('with routes payload', function () {
42 | var params = {
43 | routes: null
44 | };
45 |
46 | // clone the response fixture, set it to a fluxible state.
47 | beforeEach(function () {
48 | response = transformer.jsonToFluxible(
49 | JSON.parse(JSON.stringify(routesResponse))
50 | );
51 | params.routes = response;
52 | });
53 |
54 | it('should update the RouteStore', function (done) {
55 | context.executeAction(routes, params, function (err) {
56 | if (err) {
57 | return done(err);
58 | }
59 |
60 | checkTestPage();
61 | done();
62 | });
63 | });
64 |
65 | it('should use a custom transformer if supplied', function (done) {
66 | var custom;
67 |
68 | params.transform = function (input) {
69 | custom = input;
70 | return custom;
71 | };
72 |
73 | context.executeAction(routes, params, function (err) {
74 | if (err) {
75 | done(err);
76 | }
77 |
78 | expect(custom).to.be.an('object');
79 | checkTestPage();
80 | done();
81 | });
82 | });
83 | });
84 |
85 | describe('without routes payload', function () {
86 | var fluxibleRoutesFixture;
87 |
88 | // Setup the context.service
89 | // clone the response fixture, set it to a wire state.
90 | beforeEach(function () {
91 | response = JSON.parse(JSON.stringify(routesResponse));
92 | fluxibleRoutesFixture = transformer.jsonToFluxible(response);
93 |
94 | context.service = new MockService();
95 | context.service.setService('routes', function (method, params, config, callback) {
96 | if (params.emulateError) {
97 | return callback(new Error('mock'));
98 | }
99 | callback(null, response);
100 | });
101 | });
102 |
103 | it('should update the ApplicationStore', function (done) {
104 | context.executeAction(routes, {}, function (err, fluxibleRoutes) {
105 | if (err) {
106 | return done(err);
107 | }
108 |
109 | testUtils.testTransform(expect, fluxibleRoutes, fluxibleRoutesFixture);
110 | checkTestPage();
111 | done();
112 | });
113 | });
114 |
115 | it('should throw an error if the service does', function (done) {
116 | context.executeAction(routes, { emulateError: true }, function (err) {
117 | expect(err).to.be.instanceof(Error);
118 | done();
119 | });
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/services/data/cache.js:
--------------------------------------------------------------------------------
1 | /***
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:Data:Cache');
8 | var markdown = require('./markdown');
9 |
10 | // FIXME:
11 | // Cache storage should not be in this process
12 | // in a real application (issue #9)
13 | var cache = {};
14 |
15 | /**
16 | * Write a data entity to the cache.
17 | *
18 | * @param {Object} params - data entity parameters.
19 | * @param {String} params.resource - The key for the data entity: Its resource name.
20 | * @param {Object} params.models - The models associated with the data for the current key.
21 | * @param {Object|String} data - The html or json data for the resource.
22 | */
23 | function writeToCache (params, data) {
24 | var obj = {
25 | models: params.models,
26 | content: data
27 | };
28 |
29 | cache[params.resource] = obj;
30 |
31 | debug(
32 | 'wrote cache[' + params.resource + ']',
33 | require('util').inspect(obj, { depth: null })
34 | );
35 | }
36 |
37 | /***
38 | * Format the data by its type and write it to the cache.
39 | */
40 | var formatToCache = {
41 | /**
42 | * Markup is a pass-thru format. Write directly to cache.
43 | */
44 | markup: function (params, data) {
45 | writeToCache(params, data);
46 | },
47 | /**
48 | * Format Mardown to markup then write to cache.
49 | */
50 | markdown: function (params, data) {
51 | writeToCache(params, markdown(data));
52 | },
53 | /**
54 | * For Json data, write each top-level key as a separate resource to the cache.
55 | * Data formatted as parsed javascript.
56 | */
57 | json: function (params, data) {
58 | var obj = JSON.parse(data);
59 | Object.keys(obj).forEach(function(key) {
60 | writeToCache({
61 | resource: key,
62 | models: params.models
63 | }, obj[key]);
64 | });
65 | }
66 | };
67 |
68 | /**
69 | * Mediate models as appropriate after resource was read from cache.
70 | *
71 | * @param {Object} cached - The object as read from cache.
72 | * @returns {Object} Cached object with its models expanded into objects.
73 | */
74 | function readFromCache (cached) {
75 | var result = {
76 | models: cached.models,
77 | content: cached.content
78 | };
79 |
80 | if (cached.models) {
81 | // Expand a resource's model references to the model data
82 | result.models = cached.models.reduce(function(prev, curr) {
83 | prev[curr] = cache.models.content[curr];
84 | return prev;
85 | }, {});
86 | }
87 |
88 | debug(
89 | 'read from cache',
90 | require('util').inspect(result, { depth: null })
91 | );
92 |
93 | return result;
94 | }
95 |
96 | module.exports = {
97 | /**
98 | * Read from cache.
99 | *
100 | * @param {String} resource - The resource name to lookup.
101 | * @returns {Object} The mediated cached object, or undefined if not found.
102 | */
103 | get: function (resource) {
104 | var result;
105 | var cached = cache[resource];
106 |
107 | if (cached) {
108 | result = readFromCache(cached);
109 | }
110 |
111 | return result;
112 | },
113 |
114 | /**
115 | * Write to cache.
116 | *
117 | * @param {Object} params - The data accompanying the main payload.
118 | * @param {String} params.resource - The key: The resource name.
119 | * @param {String} params.format - The format of the main payload.
120 | * @param {Object} params.models - Models associated with the main payload.
121 | * @param {Object|String} data - The main payload. The content.
122 | */
123 | put: function (params, data) {
124 | debug(
125 | 'putting data into cache',
126 | 'resource: '+params.resource,
127 | 'format: '+params.format,
128 | 'models: '+params.models,
129 | 'data: '+data
130 | );
131 |
132 | if (!params || !params.resource || !data) {
133 | throw new Error('Invalid arguments to cache put');
134 | }
135 |
136 | formatToCache[params.format || 'json'](params, data);
137 | }
138 | };
139 |
--------------------------------------------------------------------------------
/components/Application.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | /* global window, document */
6 | 'use strict';
7 |
8 | var debug = require('debug')('Example:Application.jsx');
9 | var React = require('react');
10 | var connectToStores = require('fluxible-addons-react/connectToStores');
11 | var provideContext = require('fluxible-addons-react/provideContext');
12 | var handleHistory = require('fluxible-router').handleHistory;
13 | var navigateAction = require('fluxible-router').navigateAction;
14 | var ReactSwipe = require('react-swipe');
15 |
16 | var pages = require('./pages');
17 | var Header = require('./header');
18 | var Footer = require('./footer');
19 | var Background = require('./Background.jsx');
20 | var PageContainer = require('./PageContainer.jsx');
21 |
22 | var Application = React.createClass({
23 | contextTypes: {
24 | getStore: React.PropTypes.func.isRequired,
25 | executeAction: React.PropTypes.func.isRequired
26 | },
27 |
28 | handleSwipe: function (index) {
29 | var pages = this.props.pages;
30 | if (pages[this.props.pageName].order !== index) {
31 | var nextPageName = Object.keys(pages).filter(function (page) {
32 | return pages[page].order === index && pages[page].mainNav;
33 | })[0];
34 |
35 | this.context.executeAction(navigateAction, {
36 | name: nextPageName,
37 | url: pages[nextPageName].path
38 | });
39 | }
40 | },
41 |
42 | render: function () {
43 | debug('pageName', this.props.pageName);
44 | debug('pages', this.props.pages);
45 | debug('navigateError', this.props.currentNavigateError);
46 |
47 | var routeOrdinal = this.props.pages[this.props.pageName].order;
48 |
49 | var navPages = pages.getMainNavPages(
50 | this.props.currentNavigateError,
51 | this.props.pages,
52 | routeOrdinal
53 | );
54 |
55 | var pageElements = pages.createElements(
56 | navPages, this.context.getStore('ContentStore')
57 | );
58 |
59 | return (
60 |
61 |
62 |
67 |
68 |
73 | {pageElements}
74 |
75 |
76 |
77 |
78 | );
79 | },
80 |
81 | shouldComponentUpdate: function (nextProps) {
82 | return nextProps.navigateComplete && this.props.navigateComplete;
83 | },
84 |
85 | componentDidUpdate: function () {
86 | document.title = this.props.pageTitle;
87 |
88 | var analytics = window[this.props.analytics];
89 | if (analytics) {
90 | analytics('set', {
91 | page: this.props.pages[this.props.pageName].path,
92 | title: this.props.pageTitle
93 | });
94 | analytics('send', 'pageview');
95 | }
96 | }
97 | });
98 |
99 | Application = connectToStores(
100 | Application, ['ApplicationStore', 'ContentStore', 'RouteStore'],
101 | function (context) {
102 | var routeStore = context.getStore('RouteStore'),
103 | appStore = context.getStore('ApplicationStore'),
104 | currentRoute = routeStore.getCurrentRoute(),
105 | pageName = (currentRoute && currentRoute.page) ||
106 | appStore.getDefaultPageName();
107 |
108 | return {
109 | navigateComplete: routeStore.isNavigateComplete(),
110 | pageName: pageName,
111 | pageTitle: appStore.getCurrentPageTitle(),
112 | pageModels: context.getStore('ContentStore').getCurrentPageModels(),
113 | pages: routeStore.getRoutes()
114 | };
115 | });
116 |
117 | Application = handleHistory(Application, {
118 | enableScroll: false
119 | });
120 |
121 | Application = provideContext(Application);
122 |
123 | module.exports = Application;
124 |
--------------------------------------------------------------------------------