├── 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 | 9 | 10 | 11 | 12 | image/svg+xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 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 |
21 | 22 | {statements[0]} 23 | 24 | {this.props.license.type} 25 | 26 | {statements[1]} 27 | 28 |
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 |
21 | 22 | {byLine}  23 | 24 | {this.props.author.name} 25 | 26 |  © {(new Date()).getFullYear()} 27 | 28 |
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 |
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 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
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 |
    47 | 48 | 49 | {this.props.business.email} 50 | 51 | 52 | 53 | 54 | {this.props.business.telephone} 55 | 56 | 57 |
    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 |
    51 |
    56 |
    57 |
    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 |
    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 | --------------------------------------------------------------------------------