├── Procfile ├── .coveralls.yml ├── .jshintignore ├── configs ├── local.env.json ├── push │ └── index.js ├── images │ └── index.js ├── analytics │ └── index.js ├── index.js └── settings │ └── utils.js ├── assets ├── google24e9e21ce1f6df19.html ├── images │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── icon-128x128.png │ ├── mstile-70x70.png │ ├── apple-touch-icon.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.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 │ ├── android-chrome-256x256.png │ ├── android-chrome-512x512.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 │ └── logo.svg ├── fonts │ ├── icomoon.eot │ ├── icomoon.ttf │ └── icomoon.woff ├── robots.txt ├── browserconfig.xml ├── styles │ ├── settings.scss │ ├── _mixins.scss │ ├── index.scss │ ├── _vars.scss │ ├── _fonts.scss │ └── _icon-fonts.scss ├── manifest.webapp ├── manifest.json └── scripts │ ├── header.js │ └── sw │ ├── assets.js │ ├── activate.js │ ├── utils │ └── debug.js │ └── init │ └── update.js ├── .slugignore ├── .gitignore ├── components ├── footer │ ├── index.js │ ├── License.jsx │ ├── ByLine.jsx │ ├── SiteBullets.jsx │ ├── Footer.jsx │ ├── _styles.scss │ └── LocalBusiness.jsx ├── header │ ├── index.js │ ├── ModalLink.jsx │ ├── Logo.jsx │ ├── Header.jsx │ ├── Nav.jsx │ └── Ribbon.jsx ├── pages │ ├── contact │ │ ├── index.js │ │ ├── _result.scss │ │ ├── _styles.scss │ │ ├── _anim.scss │ │ ├── elements.js │ │ ├── Input.jsx │ │ ├── Nav.jsx │ │ ├── Steps.jsx │ │ └── _steps.scss │ ├── settings │ │ ├── index.js │ │ ├── _switch.scss │ │ ├── _styles.scss │ │ ├── Topics.jsx │ │ ├── Switch.jsx │ │ └── _topics.scss │ ├── Spinner.jsx │ ├── _styles.scss │ └── ContentPage.jsx ├── _react-modal.scss ├── _notification.scss ├── PageContainer.jsx └── _app.scss ├── server ├── workers │ └── contact │ │ └── bin │ │ └── contact ├── utils.js └── sitemap.js ├── tests ├── mocks │ ├── blob.js │ ├── remarkable.js │ ├── service-mail.js │ ├── mailer.js │ ├── queue.js │ ├── service-subs.js │ ├── actionInterface.js │ ├── fetch.js │ ├── sw-sync-push.js │ ├── sw-data.js │ ├── sw-utils-db.js │ ├── response.js │ ├── request.js │ ├── cache.js │ ├── superagent.js │ ├── subscription.js │ ├── worker.js │ ├── sw-utils-idb-treo.js │ ├── sw-caches.js │ └── service-data.js ├── functional │ ├── browsers.js │ ├── main.js │ └── run-parallel.js ├── utils │ ├── jscsFilter.js │ ├── tests.js │ ├── settings.js │ └── testdom.js ├── unit │ ├── services │ │ ├── data │ │ │ ├── markdown.js │ │ │ ├── utils.js │ │ │ └── index.js │ │ ├── mail │ │ │ └── index.js │ │ ├── contact.js │ │ ├── page.js │ │ ├── routes.js │ │ ├── error.js │ │ └── subscription.js │ ├── utils │ │ ├── codes.js │ │ ├── push.js │ │ ├── syncable.js │ │ └── splits.js │ ├── actions │ │ ├── size.js │ │ └── init.js │ ├── stores │ │ ├── RouteStore.js │ │ └── ApplicationStore.js │ ├── sw │ │ └── utils │ │ │ └── idb.js │ └── configs │ │ └── settings │ │ └── utils.js ├── fixtures │ ├── models-response.js │ ├── fluxible-routes.js │ └── routes-response.js ├── workers │ └── contact │ │ └── contact.js └── generators │ └── routes-models.js ├── services ├── mail │ ├── index.js │ ├── mailer.js │ └── queue.js ├── data │ ├── markdown.js │ ├── utils.js │ └── index.js ├── error.js ├── page.js ├── routes.js └── contact.js ├── utils ├── react │ └── reactDOMServer.js ├── codes.js ├── index.js ├── push.js ├── urls.js └── property.js ├── grunt └── tasks │ ├── contrib-clean.js │ ├── contrib-jshint.js │ ├── contrib-watch.js │ ├── svg2png.js │ ├── contrib-cssmin.js │ ├── svgmin.js │ ├── contrib-copy.js │ ├── contrib-imagemin.js │ ├── autoprefixer.js │ ├── nodemon.js │ ├── perfbudget.js │ ├── header.js │ ├── ccss.js │ ├── contrib-compass.js │ ├── concurrent.js │ └── fixtures.js ├── polyfill.js ├── actions ├── interface.js ├── size.js ├── init.js ├── contact.js ├── routes.js └── page.js ├── client.js ├── app.js ├── LICENSE.md ├── stores ├── RouteStore.js ├── ApplicationStore.js └── ContactStore.js ├── Gruntfile.js └── .jscsrc /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | reports 3 | dist 4 | tmp 5 | -------------------------------------------------------------------------------- /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-sw/master/assets/images/3.jpg -------------------------------------------------------------------------------- /assets/images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/4.jpg -------------------------------------------------------------------------------- /assets/images/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/5.jpg -------------------------------------------------------------------------------- /assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/favicon.ico -------------------------------------------------------------------------------- /assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /assets/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/favicon-96x96.png -------------------------------------------------------------------------------- /assets/images/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/icon-128x128.png -------------------------------------------------------------------------------- /assets/images/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/mstile-70x70.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/images/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/mstile-144x144.png -------------------------------------------------------------------------------- /assets/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/mstile-150x150.png -------------------------------------------------------------------------------- /assets/images/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/mstile-310x150.png -------------------------------------------------------------------------------- /assets/images/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/mstile-310x310.png -------------------------------------------------------------------------------- /assets/images/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-36x36.png -------------------------------------------------------------------------------- /assets/images/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-48x48.png -------------------------------------------------------------------------------- /assets/images/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-72x72.png -------------------------------------------------------------------------------- /assets/images/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-96x96.png -------------------------------------------------------------------------------- /assets/images/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-144x144.png -------------------------------------------------------------------------------- /assets/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/images/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-256x256.png -------------------------------------------------------------------------------- /assets/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/flux-react-example-sw/master/assets/images/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | .validate.json 3 | dist 4 | reports 5 | tmp 6 | node_modules 7 | npm-debug.log 8 | /configs/settings/assets.json 9 | /assets/scripts/sw/precache.js 10 | /assets/scripts/sw/data.js 11 | *sublime* 12 | .DS_Store 13 | /webpack-stats* 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/pages/settings/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('./Settings.jsx'); 8 | -------------------------------------------------------------------------------- /tests/mocks/blob.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 | function Blob (content, options) { 8 | this.content = content; 9 | } 10 | 11 | module.exports = Blob; 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/mocks/remarkable.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 | function Remarkable () { 8 | } 9 | 10 | Remarkable.prototype.render = function (input) { 11 | return '

'+input+'

'; 12 | }; 13 | 14 | module.exports = Remarkable; 15 | -------------------------------------------------------------------------------- /components/_react-modal.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 | // React-Modal styles 6 | // 7 | .ReactModal__Overlay { 8 | z-index: $app-max-zindex; 9 | } 10 | .ReactModal__Content { 11 | color: $app-accent-dark-bgcolor; 12 | background: $app-primary-light-color; 13 | } 14 | -------------------------------------------------------------------------------- /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 | /* 15 | , explorer: { 16 | browserName: 'internet explorer' 17 | } 18 | */ 19 | }; 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #43A047 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/styles/settings.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 settings component. 5 | // This chunk relies on index.scss being parsed first. 6 | // 7 | 8 | // don't include foundation global in this chunk. 9 | $include-foundation-global: false; 10 | 11 | @import "vars", 12 | "vendor", 13 | "mixins", 14 | "pages/settings/styles"; 15 | -------------------------------------------------------------------------------- /assets/styles/_mixins.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 global mixins 5 | // 6 | @mixin close-button() { 7 | .close { 8 | @extend %close-button; 9 | &::after { 10 | content: '✖' 11 | } 12 | } 13 | } 14 | 15 | @mixin vertical-block($align: center, $size: expand) { 16 | @include grid-block($size, vertical, false, $align); 17 | @content; 18 | } 19 | -------------------------------------------------------------------------------- /components/pages/settings/_switch.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 | // Switch styles 6 | // 7 | 8 | .switch { 9 | @include switch; 10 | @include switch-layout(rem-calc(60), rem-calc(38)); 11 | vertical-align: middle; 12 | } 13 | .switch-label { 14 | display: inline-block; 15 | padding-left: 0.5rem; 16 | } 17 | .switch-notice { 18 | margin: 0.4rem; 19 | text-align: left; 20 | } 21 | -------------------------------------------------------------------------------- /utils/react/reactDOMServer.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 | * Legacy, React 0.14.x 7 | */ 8 | 'use strict'; 9 | 10 | var noop = require('lodash/noop'); 11 | 12 | /** 13 | * ReactDOMServer dummy. 14 | */ 15 | var ReactDOMServer = { 16 | renderToString: noop, 17 | renderToStaticMarkup: noop 18 | }; 19 | 20 | module.exports = ReactDOMServer; 21 | -------------------------------------------------------------------------------- /grunt/tasks/contrib-clean.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 | * grunt-contrib-clean grunt config. 6 | * Requires nconfig task to be run first. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('clean', { 12 | before: [ 13 | '<%= project.dist.baseDir %>', 14 | '<%= project.src.assetsJson %>' 15 | ] 16 | }); 17 | 18 | grunt.loadNpmTasks('grunt-contrib-clean'); 19 | }; 20 | -------------------------------------------------------------------------------- /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 all 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 | -------------------------------------------------------------------------------- /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 | * This interface can be (and is) augmented dynamically as the backend defines 8 | * lazy loaded actions (and components, etc) it is interested in using. 9 | * @see utils/splits.js 10 | * @see actions/modal.js 11 | */ 12 | 'use strict'; 13 | 14 | module.exports = { 15 | page: require('./page') 16 | }; 17 | -------------------------------------------------------------------------------- /tests/mocks/service-subs.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 | create: function (subscriptionId, endpoint, callback) { 9 | callback(); 10 | }, 11 | read: function (subscriptionId, callback) { 12 | callback(); 13 | }, 14 | update: function (subscriptionId, topics, endpoint, newId, callback) { 15 | callback(); 16 | }, 17 | delete: function (subscriptionId, callback) { 18 | callback(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /tests/mocks/actionInterface.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 | function mockAction (context, payload, done) { 9 | var error; 10 | 11 | if (payload.emulateError) { 12 | error = new Error('mock'); 13 | } 14 | 15 | if (done) { 16 | return done(error); 17 | } 18 | 19 | return error ? Promise.reject() : Promise.resolve(); 20 | } 21 | 22 | module.exports = { 23 | page: mockAction, 24 | settings: mockAction 25 | }; 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /grunt/tasks/contrib-jshint.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 | * grunt-contrib-jshint grunt config. 6 | */ 7 | 'use strict'; 8 | 9 | module.exports = function (grunt) { 10 | grunt.config('jshint', { 11 | options: { 12 | jshintrc: true 13 | }, 14 | all: { 15 | src: [ 16 | '*.js', 17 | '{configs,utils,actions,components,services,stores,tests}/**/*.js' 18 | ] 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-contrib-jshint'); 23 | }; 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /grunt/tasks/contrib-watch.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 | * grunt-contrib-watch grunt config. 6 | * Requires the nconfig task to be run first. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('watch', { 12 | // autoprefixer 13 | ap: { 14 | options: { 15 | spawn: false 16 | }, 17 | files: '<%= project.dist.styles %>/*.css', 18 | tasks: ['autoprefixer'] 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-contrib-watch'); 23 | }; 24 | -------------------------------------------------------------------------------- /grunt/tasks/svg2png.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 | * grunt-svg2png grunt config. 6 | * Generates png fallbacks. 7 | * Requires nconfig task to be run first. 8 | */ 9 | 'use strict'; 10 | 11 | module.exports = function (grunt) { 12 | grunt.config('svg2png', { 13 | all: { 14 | files: [{ 15 | cwd: '<%= project.src.images %>/', 16 | src: ['**/*.svg'], 17 | dest: '<%= project.dist.images %>/' 18 | }] 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-svg2png'); 23 | }; 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /grunt/tasks/contrib-cssmin.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 | * grunt-contrib-cssmin grunt config. 6 | * Requires nconfig task to be run first. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('cssmin', { 12 | prod: { 13 | files: [{ 14 | expand: true, 15 | cwd: '<%= project.dist.styles %>', 16 | src: '*.css', 17 | dest: '<%= project.dist.styles %>' 18 | }] 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 23 | }; 24 | -------------------------------------------------------------------------------- /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 | $include-foundation-global: true; 9 | 10 | @import 11 | "vars", 12 | "vendor/normalize", 13 | "vendor", 14 | "mixins", 15 | "icon-fonts", 16 | "fonts", 17 | "react-spinner"; 18 | 19 | .grid-container-center { 20 | @include grid-container; 21 | } 22 | 23 | .grid-row-spaced { 24 | @include grid-block(expand, horizontal, false, spaced); 25 | } 26 | 27 | .hide { 28 | display: none !important; 29 | } 30 | 31 | @import "app"; 32 | -------------------------------------------------------------------------------- /grunt/tasks/svgmin.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 | * grunt-svgmin grunt config. 6 | * Minifies svgs. 7 | * Requires nconfig task to be run first. 8 | */ 9 | 'use strict'; 10 | 11 | module.exports = function (grunt) { 12 | grunt.config('svgmin', { 13 | options: { 14 | }, 15 | all: { 16 | files: [{ 17 | expand: true, 18 | cwd: '<%= project.src.images %>/', 19 | src: ['**/*.svg'], 20 | dest: '<%= project.dist.images %>/' 21 | }] 22 | } 23 | }); 24 | 25 | grunt.loadNpmTasks('grunt-svgmin'); 26 | }; 27 | -------------------------------------------------------------------------------- /grunt/tasks/contrib-copy.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 | * grunt-contrib-copy grunt config. 6 | * Requires nconfig task to be run first. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('copy', { 12 | assets: { 13 | files: [{ 14 | expand: true, 15 | cwd: '<%= project.src.assets %>', 16 | src: ['**', '!**/styles/**', '!images/*.svg', '!scripts/**'], 17 | dest: '<%= project.dist.baseDir %>/' 18 | }] 19 | } 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-contrib-copy'); 23 | }; 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /grunt/tasks/contrib-imagemin.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 | * grunt-contrib-imagemin grunt config. 6 | * Used to optimize raster images. 7 | * Requires nconfig task to be run first. 8 | */ 9 | 'use strict'; 10 | 11 | module.exports = function (grunt) { 12 | grunt.config('imagemin', { 13 | all: { 14 | files: [{ 15 | expand: true, 16 | cwd: '<%= project.src.images %>/', 17 | src: ['**/*.{jpg,jpeg,png}'], 18 | dest: '<%= project.dist.images %>/' 19 | }] 20 | } 21 | }); 22 | 23 | grunt.loadNpmTasks('grunt-contrib-imagemin'); 24 | }; 25 | -------------------------------------------------------------------------------- /assets/styles/_vars.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 | // application global vars 5 | 6 | // colors 7 | $app-alert-bgcolor: #E53935; 8 | $app-primary-bgcolor: #43A047; 9 | $app-accent-light-bgcolor: #FFAB00; 10 | $app-accent-dark-bgcolor: #1B5E20; 11 | $app-accent-dark-shadow: rgba($app-accent-dark-bgcolor, 0.7); 12 | $app-primary-light-color: rgba(255, 255, 255, 0.87); 13 | $app-primary-dark-color: rgba(0, 0, 0, 0.87); 14 | 15 | $app-max-zindex: 2; 16 | 17 | // media query to detect height contstrained phones 18 | $height-constrained-phone: "only screen and (orientation: portrait) and (max-height: 480px)"; 19 | -------------------------------------------------------------------------------- /components/_notification.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 | // Notification styles 6 | // 7 | .notification { 8 | @include notification; 9 | display: flex; 10 | position: absolute; 11 | 12 | @include notification-layout(middle, bottom); 13 | @include notification-style($app-accent-light-bgcolor, $app-primary-dark-color); 14 | 15 | opacity: 0; 16 | transition: opacity 0.4s ease; 17 | 18 | &.is-active { 19 | // display: 'flex' already defined 20 | opacity: 1; 21 | } 22 | 23 | @include close-button; 24 | } 25 | 26 | .notification-content { 27 | font-weight: bold; 28 | flex: 1; 29 | } 30 | -------------------------------------------------------------------------------- /grunt/tasks/autoprefixer.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 | * grunt-autoprefixer grunt config. 6 | * Requires nconfig task to be run first. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('autoprefixer', { 12 | options: { 13 | browsers: ['last 2 versions', '> 2% in US'] 14 | }, 15 | all: { 16 | files: [{ 17 | expand: true, 18 | cwd: '<%= project.dist.styles %>', 19 | src: '*.css', 20 | dest: '<%= project.dist.styles %>' 21 | }] 22 | } 23 | }); 24 | 25 | grunt.loadNpmTasks('grunt-autoprefixer'); 26 | }; 27 | -------------------------------------------------------------------------------- /grunt/tasks/nodemon.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 | * grunt-nodemon grunt config. 6 | * Relies on nconfig task. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('nodemon', { 12 | options: { 13 | ignore: ['node_modules/**', '<%= project.distbase %>/**'], 14 | ext: 'js,jsx' 15 | }, 16 | app: { 17 | script: './<%= pkg.main %>' 18 | }, 19 | debug: { 20 | options: { 21 | nodeArgs: ['--debug-brk'] 22 | }, 23 | script: './<%= pkg.main %>' 24 | } 25 | }); 26 | 27 | grunt.loadNpmTasks('grunt-nodemon'); 28 | }; 29 | -------------------------------------------------------------------------------- /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 | var Notification = require('./Notification.jsx'); 10 | 11 | var PageContainer = React.createClass({ 12 | render: function () { 13 | return ( 14 |
15 | {this.props.children} 16 | 17 |
18 | ); 19 | } 20 | }); 21 | 22 | module.exports = sizeReporter(PageContainer, '.page', { 23 | reportWidth: true, 24 | reportHeight: true, 25 | cover: { 26 | height: 10 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | image/svg+xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/unit/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 | /* global before, after, describe, it */ 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | var mocks = require('../../../mocks'); 10 | 11 | describe('markdown', function () { 12 | var markdown; 13 | 14 | before(function () { 15 | mocks.remarkable.begin(); 16 | markdown = require('../../../../services/data/markdown'); 17 | }); 18 | 19 | after(function () { 20 | mocks.remarkable.end(); 21 | }); 22 | 23 | it('should return some markup', function () { 24 | expect(markdown('Hello')).to.contain(' 21 | {this.props.children} 22 | 23 | ); 24 | }, 25 | 26 | clickHandler: function () { 27 | this.context.executeAction(modalAction, this.props.data); 28 | } 29 | }); 30 | 31 | module.exports = ModalLink; 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/mocks/sw-sync-push.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 | * Mock idb for sw/sync/push 6 | */ 7 | /* global Promise */ 8 | 'use strict'; 9 | 10 | module.exports = { 11 | synchronize: function synchronizePushSubscription (subscriptionId) { 12 | var error = this.error; 13 | var value = typeof this.mockValue === 'undefined' ? false : this.mockValue; 14 | 15 | return new Promise(function (resolve, reject) { 16 | if (error) { 17 | return reject(new Error('mock error')); 18 | } 19 | 20 | return resolve(value); 21 | }); 22 | }, 23 | setEmulateError: function (error) { 24 | this.error = error; 25 | }, 26 | setValue: function (value) { 27 | this.mockValue = value; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tests/mocks/sw-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 | * A fixture to supply sw-data. 6 | * TODO: add to automatically generated test fixtures. 7 | */ 8 | 'use strict'; 9 | 10 | var hostnames = [ 11 | 'fonts.gstatic.com', 12 | 'cdn.google.com' 13 | ]; 14 | 15 | module.exports = { 16 | debug: false, 17 | cacheId: 'flux-react-example-sw/0.12.2', 18 | assets: [ 19 | '//'+ hostnames[1] +'/somepath/to/some/resource', 20 | '//'+ hostnames[0] +'/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2', 21 | '//'+ hostnames[0] +'/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff', 22 | '//'+ hostnames[0] +'/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlEY6Fu39Tt9XkmtSosaMoEA.ttf' 23 | ] 24 | }; 25 | -------------------------------------------------------------------------------- /grunt/tasks/perfbudget.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 | * grunt-perfbudget grunt config. 6 | */ 7 | 'use strict'; 8 | 9 | module.exports = function (grunt) { 10 | grunt.config('perfbudget', { 11 | options: { 12 | url: process.env.DEPLOY_URL, 13 | key: process.env.WPT_API_KEY, 14 | location: 'Dulles:Chrome', 15 | repeatView: false, 16 | timeout: 300 17 | }, 18 | mobile: { 19 | options: { 20 | connectivity: '3G', 21 | emulateMobile: true, 22 | runs: 3, 23 | budget: { 24 | // 3000 nominal + (2 * 300) ssl negotiation 25 | SpeedIndex: 3600 26 | } 27 | } 28 | } 29 | }); 30 | 31 | grunt.loadNpmTasks('grunt-perfbudget'); 32 | }; 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 codes = require('./codes'); 9 | 10 | /** 11 | * Factory to create a FluxibleRouteTransformer object. 12 | * 13 | * @param {Object} options - Options to control the object creation. 14 | * @param {Object} options.actions - The actions available for use in route transformations, and thus in the backend. 15 | */ 16 | function createFluxibleRouteTransformer (options) { 17 | options = options || {}; 18 | return new FluxibleRouteTransformer(options.actions); 19 | } 20 | 21 | module.exports = { 22 | createFluxibleRouteTransformer: createFluxibleRouteTransformer, 23 | conformErrorStatus: codes.conformErrorStatus 24 | }; 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/mocks/sw-utils-db.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 | * Mock idb for sw/utils/db 6 | */ 7 | /* global Promise */ 8 | 'use strict'; 9 | 10 | module.exports = { 11 | stores: { 12 | init: 'init', 13 | requests: 'requests' 14 | }, 15 | emulateError: function (error) { 16 | this.error = error; 17 | }, 18 | setValue: function (value) { 19 | this.mockValue = value; 20 | }, 21 | get: function (storeName, keyName) { 22 | var testValue; 23 | 24 | if (!this.error) { 25 | testValue = this.mockValue || 'test value'; 26 | } 27 | 28 | return Promise.resolve(testValue); 29 | }, 30 | put: function (storeName, keyName, value) { 31 | if (this.error) { 32 | return Promise.reject(new Error('mock error')); 33 | } 34 | 35 | return Promise.resolve(); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /services/data/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 | 'use strict'; 6 | 7 | /** 8 | * Find the first [object Object] with key that matches value. 9 | * 10 | * @param {String} key - The property name. 11 | * @param {String} value - The property value. 12 | * @param {Object} obj - The object to search. 13 | * @returns {Object} the object that contains key===value. Otherwise undefined. 14 | */ 15 | function objContains (key, value, obj) { 16 | if (obj[key] === value) { 17 | return obj; 18 | } 19 | 20 | var found; 21 | 22 | Object.keys(obj).some(function (k) { 23 | if (Object.prototype.toString.call(obj[k]) === '[object Object]') { 24 | found = objContains(key, value, obj[k]); 25 | return !!found; 26 | } 27 | }); 28 | 29 | return found; 30 | } 31 | 32 | module.exports = { 33 | objContains: objContains 34 | }; 35 | -------------------------------------------------------------------------------- /configs/push/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 push notification service. 6 | * 7 | * Environment variables: 8 | * PUSH_API_KEY - An api key for messaging service. 9 | */ 10 | 'use strict'; 11 | 12 | /** 13 | * Get a PUSH_API_KEY configuration value. Use to authenticate as a sender to 14 | * a cloud messaging service. Defaults to GCM_API_URL. 15 | * 16 | * @returns {String} The PUSH_API_KEY configuration value. 17 | */ 18 | function PUSH_API_KEY () { 19 | return process.env.PUSH_API_KEY || process.env.GCM_API_KEY || undefined; 20 | } 21 | 22 | /** 23 | * Make the images configuration object. 24 | * 25 | * @returns the images configuration object. 26 | */ 27 | function makeConfig () { 28 | return { 29 | service: { 30 | apiKey: PUSH_API_KEY 31 | } 32 | }; 33 | } 34 | 35 | module.exports = makeConfig; 36 | -------------------------------------------------------------------------------- /grunt/tasks/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 | * Custom compile header script task. 6 | * Compiles the header script in a task series or standalone. 7 | * Relies on nconfig, webpack. 8 | */ 9 | 'use strict'; 10 | 11 | module.exports = function (grunt) { 12 | /** 13 | * Custom task to build the header script, standalone (w/o the dev task). 14 | * For now, just uses webpack, but that makes it unnecessarily bigger. 15 | * Syntax: header:dev | header:prod 16 | * 17 | * @access public 18 | */ 19 | grunt.registerTask('header', 'Build the header script', function() { 20 | var isProd = this.args.shift() === 'prod'; 21 | var tasks; 22 | if (isProd) { 23 | tasks = ['nconfig:prod', 'webpack:headerProd']; 24 | } 25 | else { 26 | tasks = ['nconfig:dev', 'webpack:headerDev']; 27 | } 28 | grunt.task.run(tasks); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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("//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2") format("woff2"), 12 | url("//fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff") format("woff"), 13 | url("//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 | } 22 | -------------------------------------------------------------------------------- /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('../../../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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /utils/push.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 | * Consistent method for returning a subscription id for a pushSubscription. 9 | * 10 | * @param {Object} subscription - The pushSubscription object. 11 | * @returns {String} The subscription id, null if no subscription supplied. 12 | */ 13 | function getSubscriptionId (subscription) { 14 | if (!subscription) { 15 | return null; 16 | } 17 | 18 | var subscriptionId = null; 19 | 20 | if (subscription.endpoint) { 21 | var endpointSections = subscription.endpoint.split('/'); 22 | subscriptionId = endpointSections[endpointSections.length - 1]; 23 | } 24 | 25 | if (!subscriptionId && typeof subscription.getKey === 'function') { 26 | // This should be unique enough to act like an id for purpose. 27 | subscriptionId = subscription.getKey(); 28 | } 29 | 30 | return subscriptionId; 31 | } 32 | 33 | module.exports = { 34 | getSubscriptionId: getSubscriptionId 35 | }; 36 | -------------------------------------------------------------------------------- /assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en-US", 3 | "short_name": "LocalNerve Example App", 4 | "name": "flux-react-example-sw", 5 | "icons": [ 6 | { 7 | "src": "/public/images/android-chrome-36x36.png", 8 | "sizes": "36x36", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/public/images/android-chrome-48x48.png", 13 | "sizes": "48x48", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/public/images/android-chrome-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/public/images/android-chrome-96x96.png", 23 | "sizes": "96x96", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "/public/images/android-chrome-144x144.png", 28 | "sizes": "144x144", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "/public/images/android-chrome-192x192.png", 33 | "sizes": "192x192", 34 | "type": "image/png" 35 | } 36 | ], 37 | "start_url": "/?homescreen=1", 38 | "display": "standalone", 39 | "orientation": "portrait", 40 | "background_color": "#43A047", 41 | "theme_color": "#1B5E20", 42 | "gcm_sender_id": "54583389178" 43 | } 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/utils/settings.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 and helpers for settings 6 | */ 7 | 'use strict'; 8 | 9 | function getSettingsFields (context, SettingsStore) { 10 | var settingsStore = context.getStore(SettingsStore); 11 | return { 12 | hasServiceWorker: settingsStore.getHasServiceWorker(), 13 | hasPushMessaging: settingsStore.getHasPushMessaging(), 14 | hasPermissions: settingsStore.getHasPermissions(), 15 | hasNotifications: settingsStore.getHasNotifications(), 16 | pushBlocked: settingsStore.getPushBlocked(), 17 | syncBlocked: settingsStore.getSyncBlocked(), 18 | pushSubscription: settingsStore.getPushSubscription(), 19 | pushSubscriptionError: settingsStore.getPushSubscriptionError(), 20 | pushTopics: settingsStore.getPushTopics(), 21 | pushTopicsError: settingsStore.getPushTopicsError(), 22 | transition: settingsStore.getTransition() 23 | }; 24 | } 25 | 26 | module.exports = { 27 | getSettingsFields: getSettingsFields 28 | }; 29 | -------------------------------------------------------------------------------- /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('../../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 | }); 41 | -------------------------------------------------------------------------------- /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 | 28 | 29 |
30 |
32 | ); 33 | } 34 | }); 35 | 36 | module.exports = Header; 37 | -------------------------------------------------------------------------------- /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 | * This is an entry module, so there are no global concerns here. 8 | */ 9 | /* global document, window */ 10 | 11 | // -------------------------------------------------- 12 | // Fontface observer to quickly load fonts. 13 | // Relies on Promise polyfill. 14 | // 15 | 16 | require('fontfaceobserver/fontfaceobserver'); 17 | 18 | new window.FontFaceObserver('Source Sans Pro', {}) 19 | .check() 20 | .then(function() { 21 | window.document.documentElement.className += 'fonts-loaded'; 22 | }) 23 | .catch(function (error) { 24 | console.error('font failed to load: ', error); 25 | }); 26 | 27 | // -------------------------------------------------- 28 | // Load non-critical stylesheets 29 | // 30 | var i, loadCss = require('fg-loadcss').loadCSS, 31 | cssHrefs = document.querySelectorAll('meta[content$=".css"]'); 32 | 33 | for (i = 0; i < cssHrefs.length; i++) { 34 | loadCss(cssHrefs[i].content); 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/mocks/response.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 simple mock for Fetch API Response, purposed for this test suite. 6 | */ 7 | /* global Promise */ 8 | 'use strict'; 9 | 10 | function Response (body, init) { 11 | init = init || {}; 12 | 13 | var status = parseInt(init.status, 10); 14 | 15 | this.bodyUsed = false; 16 | this.status = status >= 0 ? status : 200; 17 | this.statusText = init.statusText || 'OK'; 18 | this.headers = init.headers; 19 | this.ok = status >= 200 && status <= 299; 20 | this._body = body; 21 | } 22 | 23 | Response.prototype = { 24 | json: function json () { 25 | this.bodyUsed = true; 26 | return Promise.resolve(this._body); 27 | }, 28 | text: function text () { 29 | this.bodyUsed = true; 30 | return Promise.resolve(JSON.stringify(this._body)); 31 | }, 32 | clone: function clone () { 33 | return new Response(this._body, { 34 | status: this.status, 35 | statusText: this.statusText, 36 | headers: this.headers 37 | }); 38 | } 39 | }; 40 | 41 | module.exports = Response; 42 | -------------------------------------------------------------------------------- /components/pages/settings/_styles.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 | // Settings styles 6 | // 7 | 8 | @import "topics"; 9 | @import "switch"; 10 | 11 | .settings { 12 | min-height: rem-calc(350); 13 | 14 | @include close-button; 15 | 16 | .control-section { 17 | margin-top: 1rem; 18 | 19 | .push-demo { 20 | button { 21 | @extend %button; 22 | @include button-size(large, true); 23 | @include button-style( 24 | $app-primary-bgcolor, 25 | $app-accent-dark-bgcolor, 26 | $app-primary-light-color, 27 | solid 28 | ); 29 | border-radius: 0.25rem 30 | } 31 | button:disabled { 32 | background: #ccc; 33 | } 34 | } 35 | } 36 | 37 | // divider for sections 38 | .control-section:not(:last-child) { 39 | &::after { 40 | content: ''; 41 | display: block; 42 | margin: 1rem 0; 43 | border-top: 1px solid $app-primary-bgcolor; 44 | border-bottom: 1px solid $app-accent-dark-bgcolor; 45 | height: 2px; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | var ModalStore = require('./stores/ModalStore'); 18 | 19 | debug('Creating FluxibleApp'); 20 | var app = new FluxibleApp({ 21 | component: require('./components/Application.jsx') 22 | }); 23 | 24 | debug('Adding Plugins'); 25 | app.plug(fetchrPlugin({ xhrPath: '/_api' })); 26 | 27 | debug('Registering Stores'); 28 | app.registerStore(ApplicationStore); 29 | app.registerStore(ContentStore); 30 | app.registerStore(ContactStore); 31 | app.registerStore(BackgroundStore); 32 | app.registerStore(RouteStore); 33 | app.registerStore(ModalStore); 34 | 35 | module.exports = app; 36 | -------------------------------------------------------------------------------- /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/mocks/request.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 simple mock of the Fetch API Request, purposed for this test suite. 6 | */ 7 | /* global Promise */ 8 | 'use strict'; 9 | 10 | function Request (url, options) { 11 | options = options || {}; 12 | 13 | var body = options.body && options.body.content || options.body; 14 | 15 | if (Object.prototype.toString.call(body) === '[object Array]') { 16 | body = body[0]; 17 | } 18 | 19 | this.url = url; 20 | this.method = options.method; 21 | this._body = body; 22 | this.mode = options.mode; 23 | this.headers = options.headers; 24 | this.bodyUsed = false; 25 | this.credentials = options.credentials; 26 | } 27 | 28 | Request.prototype = { 29 | json: function json () { 30 | this.bodyUsed = true; 31 | return Promise.resolve(this._body); 32 | }, 33 | clone: function clone () { 34 | return new Request(this.url, { 35 | method: this.method, 36 | mode: this.mode, 37 | headers: this.headers, 38 | body: this._body, 39 | credentials: this.credentials 40 | }); 41 | } 42 | }; 43 | 44 | module.exports = Request; 45 | -------------------------------------------------------------------------------- /grunt/tasks/ccss.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 | * Custom compile css task. 6 | * Compiles the scss in a task series or standalone. 7 | * Relies on nconfig, concurrent, svgmin, svg2png, compass, and autoprefixer. 8 | */ 9 | /* global global */ 10 | 'use strict'; 11 | 12 | module.exports = function (grunt) { 13 | /** 14 | * scss compile custom task 15 | * Sets the env config if req'd, runs required css build tasks, compiles, then runs post processing. 16 | * Used only for standalone css builds outside of the main dev task. 17 | * Syntax: ccss:prod | ccss:dev 18 | * 19 | * @access public 20 | */ 21 | grunt.registerTask('ccss', 'Compile scss', function () { 22 | var isProd = this.args.shift() === 'prod'; 23 | var tasks = global._nconfig ? [] : ['nconfig:'+(isProd ? 'prod' : 'dev')]; 24 | 25 | tasks = tasks.concat([ 26 | 'svg2png', 'svgmin', 'compass:'+(isProd ? 'prod' : 'dev'), 'autoprefixer' 27 | ]); 28 | if (!isProd) { 29 | tasks = tasks.concat(['concurrent:css']); 30 | } 31 | 32 | grunt.task.run(isProd ? tasks.concat(['cssmin:prod']) : tasks); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /components/pages/settings/Topics.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 Topics = React.createClass({ 10 | propTypes: { 11 | topics: React.PropTypes.array.isRequired, 12 | disabled: React.PropTypes.bool.isRequired, 13 | onChange: React.PropTypes.func.isRequired 14 | }, 15 | 16 | render: function () { 17 | var topics = this.props.topics.map(function (topic) { 18 | return ( 19 |
  • 20 |
    21 | 25 | 26 |
    27 |
    28 | {topic.label} 29 |
    30 |
  • 31 | ); 32 | }, this); 33 | 34 | return ( 35 | 38 | ); 39 | } 40 | }); 41 | 42 | module.exports = Topics; 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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?hx4sgq"); 11 | src: font-url("icomoon.eot?hx4sgq#iefix") format("embedded-opentype"), 12 | inline-font-files("icomoon.woff", "woff"), 13 | font-url("icomoon.ttf?hx4sgq") format("truetype"), 14 | font-url("icomoon.svg?hx4sgq#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-phone:before { 33 | content: "\e900"; 34 | } 35 | .icon-envelop:before { 36 | content: "\e901"; 37 | } 38 | .icon-cog:before { 39 | content: "\e600"; 40 | } 41 | .icon-twitter:before { 42 | content: "\e902"; 43 | } 44 | .icon-github4:before { 45 | content: "\e903"; 46 | } 47 | .icon-linkedin2:before { 48 | content: "\e904"; 49 | } 50 | -------------------------------------------------------------------------------- /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 | var calledFind; 10 | var calledGet; 11 | var calledPut; 12 | 13 | module.exports = { 14 | mockReset: function () { 15 | calledFind = calledGet = calledPut = 0; 16 | delete this.findValue; 17 | }, 18 | mockCounts: function () { 19 | return { 20 | find: calledFind, 21 | get: calledGet, 22 | put: calledPut 23 | }; 24 | }, 25 | 26 | find: function (resource) { 27 | calledFind++; 28 | return this.findValue; 29 | }, 30 | get: function (resource) { 31 | calledGet++; 32 | var result = 'hello world'; // ref: mocks/superagent.js defaultResponse 33 | 34 | if (resource === 'routes') { 35 | delete this.findValue; 36 | return routesResponse; 37 | } 38 | 39 | if (resource === 'find') { 40 | this.findValue = result; 41 | result = undefined; 42 | } 43 | 44 | if (resource === 'miss') { 45 | delete this.findValue; 46 | result = undefined; 47 | } 48 | 49 | return result; 50 | }, 51 | put: function () { 52 | calledPut++; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /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/data/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 before, describe, it */ 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | 10 | describe('data/utils', function () { 11 | var utils; 12 | 13 | before('utils', function () { 14 | utils = require('../../../../services/data/utils'); 15 | }); 16 | 17 | describe('objContains', function () { 18 | var testKey = 'testKey'; 19 | var testValue = 'testValue'; 20 | var test = { 21 | some: 'string' 22 | }; 23 | var object = { 24 | other: { 25 | test: { 26 | some: 'string' 27 | } 28 | }, 29 | test: {} 30 | }; 31 | 32 | before('objContains', function () { 33 | test[testKey] = testValue; 34 | object.test = Object.assign(object.test, test); 35 | }); 36 | 37 | it('should retrieve test object', function () { 38 | var result = utils.objContains(testKey, testValue, object); 39 | expect(result).to.eql(test); 40 | }); 41 | 42 | it('should returned undefined if not found', function () { 43 | var result = utils.objContains('nope', 'nothing', object); 44 | expect(result).to.be.undefined; 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /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('../../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('../../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 | -------------------------------------------------------------------------------- /components/pages/settings/Switch.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 Switch = React.createClass({ 10 | propTypes: { 11 | inputId: React.PropTypes.string.isRequired, 12 | checked: React.PropTypes.bool.isRequired, 13 | disabled: React.PropTypes.bool.isRequired, 14 | onChange: React.PropTypes.func.isRequired, 15 | label: React.PropTypes.string.isRequired, 16 | notice: React.PropTypes.string 17 | }, 18 | 19 | render: function () { 20 | return ( 21 |
    22 |
    23 | 27 | 28 |
    29 |
    30 | {this.props.label} 31 |
    32 |
    35 | {this.props.notice} 36 |
    37 |
    38 | ); 39 | } 40 | }); 41 | 42 | module.exports = Switch; 43 | -------------------------------------------------------------------------------- /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: 'https://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 | -------------------------------------------------------------------------------- /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 | reportHeight: true, 44 | cover: { 45 | top: 5, 46 | height: 10 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/utils/push.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 push = require('../../../utils/push'); 11 | 12 | describe('push', function () { 13 | it('should expose getSubscriptionId', function () { 14 | expect(push).to.respondTo('getSubscriptionId'); 15 | }); 16 | 17 | describe('getSubscriptionId', function () { 18 | var subId = '1234', 19 | subId2 = '5678', 20 | endpoint = 'https://endpoint/'+subId2; 21 | 22 | it('should return null if falsy subscription supplied', function () { 23 | expect(push.getSubscriptionId()).to.be.null; 24 | }); 25 | 26 | it('should return null if no endpoint or getKey', function () { 27 | expect(push.getSubscriptionId({})).to.be.null; 28 | }); 29 | 30 | it('should return subscriptionId from endpoint', function () { 31 | expect(push.getSubscriptionId({ 32 | endpoint: endpoint 33 | })).to.equal(subId2); 34 | }); 35 | 36 | it('should use getKey if no endpoint', function () { 37 | expect(push.getSubscriptionId({ 38 | getKey: function () { 39 | return subId; 40 | } 41 | })).to.equal(subId); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /grunt/tasks/contrib-compass.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 | * grunt-contrib-compass grunt config. 6 | * Requires nconfig task to be run first. 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('compass', { 12 | options: { 13 | sassDir: '<%= project.src.styles %>', 14 | imagesDir: '<%= project.dist.images %>', 15 | httpImagesPath: '<%= project.web.images %>', 16 | fontsDir: '<%= project.dist.fonts %>', 17 | httpFontsPath: '<%= project.web.fonts %>', 18 | cssDir: '<%= project.dist.styles %>', 19 | httpPath: '/', 20 | importPath: [ 21 | '<%= project.vendor.css %>', 22 | '<%= project.src.components %>', 23 | 'node_modules/react-spinner' 24 | ], 25 | environment: 'development', 26 | 27 | httpGeneratedImagesPath: '<%= project.web.images %>' 28 | }, 29 | dev: { 30 | options: { 31 | watch: false 32 | } 33 | }, 34 | watch: { 35 | options: { 36 | watch: true 37 | } 38 | }, 39 | prod: { 40 | options: { 41 | outputStyle: 'compressed', 42 | noLineComments: true, 43 | environment: 'production' 44 | } 45 | } 46 | }); 47 | 48 | grunt.loadNpmTasks('grunt-contrib-compass'); 49 | }; 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | var ModalLink = require('./ModalLink.jsx'); 10 | 11 | var Ribbon = React.createClass({ 12 | propTypes: { 13 | social: React.PropTypes.object.isRequired, 14 | business: React.PropTypes.object.isRequired, 15 | settings: React.PropTypes.object.isRequired 16 | }, 17 | 18 | render: function () { 19 | var uriTel = 'tel:+1-' + this.props.business.telephone; 20 | 21 | return ( 22 |
    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
    39 | ); 40 | } 41 | }); 42 | 43 | module.exports = Ribbon; 44 | -------------------------------------------------------------------------------- /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 {String} [params.url] - The url of the resource to fetch. 18 | * Not required if expected in cache. 19 | * @param {Function} callback - The callback to execute on completion. 20 | */ 21 | function fetch (params, callback) { 22 | debug('fetching resource "'+ params.resource +'"'); 23 | 24 | var resource = cache.get(params.resource); 25 | 26 | if (resource) { 27 | debug('cache hit'); 28 | return callback(null, resource); 29 | } 30 | 31 | // If a cache hit was required, see if we already have a fetchable spec. 32 | if (!fetchLib.isManifestRequest(params) && !params.url) { 33 | var spec = cache.find(params.resource); 34 | if (spec) { 35 | params = spec; 36 | } 37 | } 38 | 39 | fetchLib.fetchOne(params, callback); 40 | } 41 | 42 | module.exports = { 43 | fetch: fetch, 44 | initialize: fetchLib.fetchMain, 45 | update: fetchLib.fetchAll 46 | }; 47 | -------------------------------------------------------------------------------- /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-sw-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-sw/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-sw","twitter":"https://twitter.com/localnerve","facebook":"https://facebook.com/localnerve","linkedin":"https://www.linkedin.com/in/alexpaulgrant","googleplus":"https://plus.google.com/118303375063449115817/"}},"Settings":{"component":"Settings","resource":"settings","url":"https://api.github.com/repos/localnerve/flux-react-example-sw-data/contents/pages/settings.json","format":"json","action":{"name":"settings","params":{}},"models":["LocalBusiness","SiteInfo","Settings"]}} 8 | )); -------------------------------------------------------------------------------- /tests/mocks/subscription.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 | * Mock responses for the subscription service. 6 | */ 7 | 'use strict'; 8 | 9 | var allTopics = [{ 10 | label: 'Alerts', 11 | tag: 'push-alerts-tag' 12 | }, { 13 | label: 'Upcoming Events', 14 | tag: 'push-upcoming-events-tag' 15 | }]; 16 | 17 | var updateTopic = [{ 18 | label: 'Alerts', 19 | tag: 'push-alerts-tag', 20 | subscribe: true 21 | }]; 22 | 23 | function mockError (params) { 24 | var err; 25 | if (params.emulateError) { 26 | err = new Error('mock service error'); 27 | } 28 | return err; 29 | } 30 | 31 | function unsubscribe (params, config, callback) { 32 | var err = mockError(params); 33 | callback(err); 34 | } 35 | 36 | module.exports = { 37 | updateTopic: updateTopic, 38 | topics: allTopics, 39 | read: function read (params, config, callback) { 40 | var err = mockError(params); 41 | callback(err, allTopics); 42 | }, 43 | create: function create (params, body, config, callback) { 44 | var err = mockError(params); 45 | callback(err, allTopics); 46 | }, 47 | update: function update (params, body, config, callback) { 48 | var err = mockError(params); 49 | // just send update back 50 | callback(err, body.topics); 51 | }, 52 | // This is 'del' because of fluxible-plugin-fetchr/utils/MockServiceManager 53 | del: unsubscribe, 54 | delete: unsubscribe 55 | }; 56 | -------------------------------------------------------------------------------- /assets/scripts/sw/assets.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 | * Precaching and route installs for non-project (cdn) assets. 6 | * The 'data' module is generated by the build. 7 | */ 8 | 'use strict'; 9 | 10 | var toolbox = require('sw-toolbox'); 11 | var data = require('./data'); 12 | var urlm = require('../../../utils/urls'); 13 | 14 | /** 15 | * Install route GET handlers for cdn requests and precache assets. 16 | * 17 | * Route handlers for CDN requests are installed everytime as a side effect 18 | * of setting up precaching. However, precaching is only carried out as a result 19 | * of an 'install' event (not everytime). 20 | * 21 | * @see sw-toolbox 22 | */ 23 | function setupAssetRequests () { 24 | var next, hostname; 25 | 26 | toolbox.precache( 27 | data.assets 28 | .sort() 29 | .map(function (asset) { 30 | next = urlm.getHostname(asset); 31 | 32 | if (hostname !== next) { 33 | hostname = next; 34 | // New hostname, so install GET handler for that host 35 | toolbox.router.get('*', toolbox.networkFirst, { 36 | origin: hostname, 37 | // any/all CDNs get 3 seconds max 38 | networkTimeoutSeconds: 3 39 | }); 40 | } 41 | 42 | // Precache the asset in 'install' 43 | return asset; 44 | }) 45 | ); 46 | } 47 | 48 | module.exports = { 49 | setupAssetRequests: setupAssetRequests 50 | }; 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/_app.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 | // Components styles 6 | // 7 | .app-frame { 8 | @include grid-frame(vertical); 9 | // If viewport height less than 728 AND landscape, float the footer 10 | // under the content. Otherwise height is 100vh, and footer sticks to viewport 11 | // bottom. 12 | @media only screen and (orientation: landscape) and (max-height: 727px) { 13 | height: auto; 14 | } 15 | } 16 | .app-block { 17 | @include vertical-block(left); 18 | } 19 | 20 | .app-bg { 21 | position: absolute; 22 | // assigned in js 23 | // top: 0; 24 | right:0; 25 | bottom: 0; 26 | left: 0; 27 | width: 100%; 28 | // assigned in js 29 | // height: 100%; 30 | 31 | background-color: transparent; 32 | background-repeat: no-repeat; 33 | 34 | // assigned in js 35 | // opacity 36 | 37 | transition: opacity 0.4s ease; 38 | } 39 | 40 | .page { 41 | @include vertical-block(spaced); 42 | } 43 | 44 | .swipe-container { 45 | // Allow vertical content scrolling for longer content/constrained height 46 | overflow-y: auto !important; 47 | } 48 | 49 | a, a:visited { 50 | color: $app-primary-light-color; 51 | text-decoration: none; 52 | outline: 0; 53 | } 54 | a:hover { 55 | color: darken($app-primary-light-color, 10%); 56 | } 57 | 58 | %header-footer-bg { 59 | background: $app-primary-bgcolor; 60 | } 61 | 62 | @import "header/styles"; 63 | @import "pages/styles"; 64 | @import "footer/styles"; 65 | @import "react-modal"; 66 | @import "notification"; 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/pages/settings/_topics.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 | // Topics styles 6 | // 7 | 8 | .topics-list { 9 | list-style: none; 10 | 11 | .topic-box > input { 12 | position: absolute; 13 | left: -9999px; 14 | outline: none; 15 | } 16 | .topic-box { 17 | display: inline-block; 18 | position: relative; 19 | vertical-align: middle; 20 | width: 2rem; 21 | height: 2rem; 22 | margin: 0.5rem auto; 23 | 24 | input[type="checkbox"]:checked + label:after { 25 | opacity: 1; 26 | } 27 | 28 | input[type="checkbox"]:disabled + label, 29 | input[type="checkbox"]:disabled + label:after { 30 | background: #ccc; 31 | } 32 | 33 | label { 34 | cursor: pointer; 35 | position: absolute; 36 | width: 2rem; 37 | height: 2rem; 38 | top: 0; 39 | border-radius: 0.25rem; 40 | background: $app-primary-bgcolor; 41 | 42 | &::after { 43 | position: absolute; 44 | content: ''; 45 | opacity: 0; 46 | width: 0.9rem; 47 | height: 0.5rem; 48 | background: transparent; 49 | top: 0.6rem; 50 | left: 0.6rem; 51 | border: 3px solid #fcfff4; 52 | border-top: none; 53 | border-right: none; 54 | transform: rotate(-45deg); 55 | } 56 | &:hover::after { 57 | opacity: 0.3; 58 | } 59 | } 60 | } 61 | .topic-label { 62 | display: inline-block; 63 | padding-left: 0.5rem; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 lorempixel if IMAGE_SERVICE_URL is not defined. 16 | * Lorempixel does not maintain an SSL certificate properly. 17 | * image service must be SSL for use with this app (except development). 18 | * Note: To use Cloudinary, set IMAGE_SERVICE_URL to 'https://res.cloudinary.com' 19 | * AND set CLOUD_NAME appropriately. 20 | * 21 | * @returns {String} The IMAGE_SERVICE_URL configuration value. 22 | */ 23 | function IMAGE_SERVICE_URL () { 24 | return process.env.IMAGE_SERVICE_URL || 'http://lorempixel.com'; 25 | } 26 | 27 | /** 28 | * Get the CLOUD_NAME configuration value. 29 | * This is used in Cloudinary to identify the account. 30 | * 31 | * @returns {String} The CLOUD_NAME configuration value. 32 | */ 33 | function CLOUD_NAME () { 34 | return process.env.CLOUD_NAME; 35 | } 36 | 37 | /** 38 | * Make the images configuration object. 39 | * 40 | * @returns the images configuration object. 41 | */ 42 | function makeConfig () { 43 | return { 44 | service: { 45 | url: IMAGE_SERVICE_URL, 46 | cloudName: CLOUD_NAME 47 | } 48 | }; 49 | } 50 | 51 | module.exports = makeConfig; 52 | -------------------------------------------------------------------------------- /Gruntfile.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 path = require('path'); 8 | 9 | /** 10 | * The grunt function export 11 | */ 12 | module.exports = function (grunt) { 13 | grunt.initConfig({ 14 | baseDir: __dirname, 15 | pkg: grunt.file.readJSON('package.json') 16 | }); 17 | 18 | grunt.loadTasks(path.join(__dirname, 'grunt/tasks')); 19 | 20 | // npm script interface 21 | grunt.registerTask('default', 'dev'); 22 | grunt.registerTask('dev', ['nconfig:dev', 'clean:before', 'copy', 'jshint', 'concurrent:dev']); 23 | grunt.registerTask('debug', ['nconfig:dev', 'clean:before', 'copy', 'jshint', 'concurrent:debug']); 24 | grunt.registerTask('prod', ['nconfig:prod', 'clean:before', 'copy', 'jshint', 'imagemin', 'concurrent:prod']); 25 | grunt.registerTask('perf', ['nconfig:prod', 'clean:before', 'copy', 'jshint', 'imagemin', 'concurrent:perf']); 26 | grunt.registerTask('build', [ 27 | 'nconfig:prod', 'clean:before', 'copy', 'imagemin', 'ccss:prod', 'webpack:headerProd', 'webpack:prod', 28 | 'service-worker:prod' 29 | ]); 30 | 31 | // Also commonly used: 32 | // 1. fixtures:dev | fixtures:prod - generate/update test fixtures from backend 33 | // 2. jshint 34 | // 3. dumpconfig:dev | dumpconfig:prod - dump nconfig configuration 35 | // 4. ccss:dev | ccss:prod - css compile subtasks, dev starts watch 36 | // 5. header:dev | header:prod - standalone header script compile 37 | // 6. service-worker:dev | service-worker:prod - standalone service worker generation 38 | }; 39 | -------------------------------------------------------------------------------- /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-XXXXXXXX-P' 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 | * https://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/unit/utils/syncable.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, describe, it */ 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | 10 | describe('syncable', function () { 11 | var syncable; 12 | 13 | before(function () { 14 | syncable = require('../../../utils/syncable'); 15 | }); 16 | 17 | it('should expose expected operations', function () { 18 | expect(syncable).to.respondTo('push'); 19 | expect(syncable).to.respondTo('contact'); 20 | expect(syncable).to.have.property('ops').that.is.an('object') 21 | .that.is.not.empty; 22 | expect(syncable).to.have.property('types').that.is.an('object') 23 | .that.is.not.empty; 24 | expect(syncable).to.have.property('propertyName').that.is.a('string') 25 | .that.is.not.empty; 26 | }); 27 | 28 | describe('push', function () { 29 | it('should do nothing for a bad input', function () { 30 | expect(syncable.push(null)).to.be.null; 31 | }); 32 | 33 | it('should create a fallback property for push', function () { 34 | var test = {}; 35 | var result = syncable.push(test); 36 | 37 | expect(result._fallback).to.have.property('type'); 38 | expect(result._fallback.type).to.equal('push'); 39 | }); 40 | }); 41 | 42 | describe('contact', function () { 43 | it('should do nothing for a bad input', function () { 44 | expect(syncable.contact(null)).to.be.null; 45 | }); 46 | 47 | it('should create a fallback property for contact', function () { 48 | var test = {}; 49 | var result = syncable.contact(test); 50 | 51 | expect(result._fallback).to.have.property('type'); 52 | expect(result._fallback.type).to.equal('contact'); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /assets/scripts/sw/activate.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 | * Install activate message handler for this code's concerns. 6 | * 7 | * TODO: maintain IndexedDB too. 8 | * This is tricky because: 9 | * 1. sw/index uses idb immediately to setup routes on startup. 10 | * 2. activate will occur after startup. 11 | */ 12 | /* global self, Promise, caches */ 13 | 'use strict'; 14 | 15 | var toolbox = require('sw-toolbox'); 16 | var cacheId = require('./data').cacheId; 17 | var debug = require('./utils/debug')('activate'); 18 | 19 | /** 20 | * Remove any previous cache that might have been under this code's governance. 21 | * Relies on how cacheName is constructed in index.js 22 | * 23 | * Previous caches are identified using the following: 24 | * 1. starts with cacheId 25 | * 2. contains the current scope. 26 | * 3. does not end with the 'inactive$$$'. 27 | * 4. is not exactly the current sw-toolbox cacheName 28 | */ 29 | self.addEventListener('activate', function (event) { 30 | debug('activate event fired, scope: ', toolbox.options.scope); 31 | 32 | if (!toolbox.options.scope) { 33 | return debug('Unable to determine cache scope, no action taken'); 34 | } 35 | 36 | event.waitUntil( 37 | caches.keys().then(function (cacheNames) { 38 | return Promise.all( 39 | cacheNames.map(function (cacheName) { 40 | if (cacheName.indexOf(cacheId) === 0 && 41 | cacheName.indexOf(toolbox.options.scope) > -1 && 42 | !/inactive\${3}$/i.test(cacheName) && 43 | cacheName !== toolbox.options.cache.name 44 | ) { 45 | debug('deleting old cache ', cacheName); 46 | return caches.delete(cacheName); 47 | } 48 | }) 49 | ); 50 | }) 51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | var syncable = require('../utils/syncable'); 9 | 10 | /** 11 | * Perform the contact service request. 12 | * 13 | * @param {Object} context - The fluxible action context. 14 | * @param {Object} fields - The contact fields. 15 | * @param {String} fields.email - The contact replyTo email address. 16 | * @param {Function} done - The callback to execute on completion. 17 | */ 18 | function serviceRequest (context, fields, done) { 19 | context.service.create( 20 | 'contact', 21 | syncable.contact(fields, fields.email), {}, {}, 22 | function (err) { 23 | if (err) { 24 | debug('dispatching CREATE_CONTACT_FAILURE'); 25 | context.dispatch('CREATE_CONTACT_FAILURE', fields); 26 | return done(); 27 | } 28 | 29 | debug('dispatching CREATE_CONTACT_SUCCESS'); 30 | context.dispatch('CREATE_CONTACT_SUCCESS', fields); 31 | done(); 32 | } 33 | ); 34 | } 35 | 36 | /** 37 | * Perform the contact action. 38 | * 39 | * @param {Object} context - The fluxible context. 40 | * @param {Object} payload - The action payload. 41 | * @param {Object} payload.fields - The contact fields. 42 | * @param {Boolean} payload.complete - Flag indicating contact field gathering is complete. 43 | * @param {Function} done - The callback to execute on completion. 44 | */ 45 | function contact (context, payload, done) { 46 | debug('dispatching UPDATE_CONTACT_FIELDS', payload.fields); 47 | context.dispatch('UPDATE_CONTACT_FIELDS', payload.fields); 48 | 49 | if (!payload.complete) { 50 | return done(); 51 | } 52 | 53 | serviceRequest(context, payload.fields, done); 54 | } 55 | 56 | module.exports = contact; 57 | -------------------------------------------------------------------------------- /grunt/tasks/concurrent.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 | * grunt-concurrent grunt config. 6 | * Relies on several grunt tasks (see serial task sequences below). 7 | */ 8 | 'use strict'; 9 | 10 | module.exports = function (grunt) { 11 | grunt.config('concurrent', { 12 | options: { 13 | logConcurrentOutput: true 14 | }, 15 | css: ['_cc-watch-ap', '_cc-watch-compass'], 16 | dev: ['_cc-compass-dev', '_cc-nodemon-dev', '_cc-webpack-dev'], 17 | debug: ['_cc-compass-dev', '_cc-nodemon-debug', '_cc-webpack-dev'], 18 | prod: ['_cc-compass-prod', '_cc-nodemon-prod', '_cc-webpack-prod'], 19 | perf: ['_cc-compass-prod', '_cc-nodemon-prod', '_cc-webpack-perf'] 20 | }); 21 | 22 | grunt.loadNpmTasks('grunt-concurrent'); 23 | 24 | // Serial task sequences for concurrent, each sequence an external grunt process 25 | grunt.registerTask('_cc-watch-compass', ['nconfig:dev', 'compass:watch']); 26 | grunt.registerTask('_cc-watch-ap', ['nconfig:dev', 'watch:ap']); 27 | grunt.registerTask('_cc-compass-dev', ['nconfig:dev', 'ccss:dev']); 28 | grunt.registerTask('_cc-compass-prod', ['nconfig:prod', 'ccss:prod']); 29 | grunt.registerTask('_cc-nodemon-dev', ['nconfig:dev', 'nodemon:app']); 30 | grunt.registerTask('_cc-nodemon-debug', ['nconfig:dev', 'nodemon:debug']); 31 | grunt.registerTask('_cc-nodemon-prod', ['nconfig:prod', 'nodemon:app']); 32 | grunt.registerTask('_cc-webpack-dev', [ 33 | 'nconfig:dev', 'webpack:headerDev', 'service-worker:dev', 'webpack:dev' 34 | ]); 35 | grunt.registerTask('_cc-webpack-prod', [ 36 | 'nconfig:prod', 'webpack:headerProd', 'webpack:prod', 'service-worker:prod' 37 | ]); 38 | grunt.registerTask('_cc-webpack-perf', [ 39 | 'nconfig:prod', 'webpack:headerProd', 'webpack:perf', 'service-worker:perf' 40 | ]); 41 | }; 42 | -------------------------------------------------------------------------------- /tests/mocks/worker.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 handledMessageChannel = 'messageChannel'; 8 | var handledWorker = 'worker'; 9 | 10 | /** 11 | * Mock worker container 12 | * 13 | * @param {Object} options - The mock worker options 14 | * @param {Boolean} options.simulateError - true to simulate an error 15 | * @param {Object} options.messageChannel - a Message Channel 16 | */ 17 | function Worker (options) { 18 | this.simulateError = options.simulateError; 19 | this.messageChannel = options.messageChannel; 20 | if (options.onmessage) { 21 | this.onmessage = null; 22 | } 23 | } 24 | 25 | Worker.prototype = { 26 | /** 27 | * postMessage just calls the onMessage handler. 28 | * If a MessageChannel is supplied, use that. 29 | */ 30 | postMessage: function () { 31 | var reply = { 32 | data: { 33 | error: this.simulateError, 34 | message: 'pong' 35 | } 36 | }; 37 | 38 | if (this.messageChannel) { 39 | reply.data.handled = handledMessageChannel; 40 | this.messageChannel.port1.onmessage(reply); 41 | } else if (this.onmessage) { 42 | reply.data.handled = handledWorker; 43 | this.onmessage(reply); 44 | } else { 45 | throw new Error('Worker mock cannot reply'); 46 | } 47 | } 48 | }; 49 | 50 | /** 51 | * Factory for mock workers. 52 | * 53 | * @param {Object} [options] - The options for operations. 54 | * @param {Boolean} [options.simulateError] - Simulate an error. 55 | * @return A mock worker object. 56 | */ 57 | function createWorker (options) { 58 | return new Worker(options || {}); 59 | } 60 | 61 | module.exports = { 62 | createWorker: createWorker, 63 | handled: { 64 | messageChannel: handledMessageChannel, 65 | worker: handledWorker 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /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/utils/splits.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, describe, it */ 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | 10 | var createMockActionContext = require('fluxible/utils').createMockActionContext; 11 | var splits = require('../../../utils/splits'); 12 | 13 | describe('splits', function () { 14 | it('should expose settings split', function () { 15 | expect(splits).to.respondTo('settings'); 16 | }); 17 | 18 | describe('settings', function () { 19 | var context, 20 | payload = { 21 | action: { 22 | name: 'settings' 23 | }, 24 | component: 'settings' 25 | }, 26 | action = function (context, payload, done) { 27 | expect(context).to.respondTo('dispatch'); 28 | expect(context).to.respondTo('getStore'); 29 | expect(context).to.respondTo('executeAction'); 30 | if (payload.emulateError) { 31 | return done(new Error('mock')); 32 | } 33 | return done(); 34 | }; 35 | 36 | before(function () { 37 | context = createMockActionContext(); 38 | }); 39 | 40 | it('should resolve successfully', function (done) { 41 | splits.settings(context, payload, action).then(function () { 42 | done(); 43 | }).catch(function (error) { 44 | done(error); 45 | }); 46 | }); 47 | 48 | it('should reject as expected', function (done) { 49 | payload.emulateError = true; 50 | 51 | function complete (error) { 52 | delete payload.emulateError; 53 | done(error); 54 | } 55 | 56 | splits.settings(context, payload, action).then(function () { 57 | complete(new Error('should have thrown an error')); 58 | }).catch(function (error) { 59 | complete(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/unit/sw/utils/idb.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 after, afterEach, before, describe, it */ 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | var mocks = require('../../../mocks'); 10 | 11 | describe('sw/utils/idb', function () { 12 | var treoMock, idb; 13 | 14 | before('setup sw/utils/idb', function () { 15 | mocks.swUtilsIdbTreo.begin(); 16 | 17 | idb = require('../../../../assets/scripts/sw/utils/idb'); 18 | treoMock = require('treo'); 19 | treoMock.setValue('some value'); 20 | }); 21 | 22 | after(function () { 23 | mocks.swUtilsIdbTreo.end(); 24 | }); 25 | 26 | it('should export expected things', function () { 27 | expect(idb.stores).to.be.an('object').that.is.not.empty; 28 | expect(idb).to.respondTo('all'); 29 | expect(idb).to.respondTo('batch'); 30 | expect(idb).to.respondTo('del'); 31 | expect(idb).to.respondTo('get'); 32 | expect(idb).to.respondTo('put'); 33 | }); 34 | 35 | describe('method', function () { 36 | var method = 'get'; 37 | var storeName; 38 | 39 | before(function () { 40 | storeName = Object.keys(idb.stores)[0]; 41 | }); 42 | 43 | afterEach(function () { 44 | expect(treoMock.status.getCloseCount()).to.equal(1); 45 | }); 46 | 47 | it('should execute successfully', function (done) { 48 | idb[method](storeName).then(function (value) { 49 | expect(value).to.be.a('string').that.is.not.empty; 50 | done(); 51 | }); 52 | }); 53 | 54 | it('should fail successfully', function (done) { 55 | idb[method](storeName, 'emulateError').then(function () { 56 | done(new Error('expected failure')); 57 | }) 58 | .catch(function (error) { 59 | expect(error.message).to.be.an('string').that.is.not.empty; 60 | done(); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /assets/scripts/sw/utils/debug.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 | * Overrides load and save for visionmedia/debug, using IndexedDB. 6 | */ 7 | 'use strict'; 8 | 9 | var idb = require('./idb'); 10 | var debugLib = require('debug'); 11 | // debugLib.storage is undefined, but made irrelevant by this module. 12 | // debugLib.load has run and failed silently in the visionmedia/debug module. 13 | 14 | var key = 'debug'; 15 | 16 | /** 17 | * Override debugLib load. 18 | * 19 | * @returns {Promise} Resolves to undefined when complete. 20 | */ 21 | debugLib.load = function workerDebugLoad () { 22 | return idb.get(idb.stores.state, key); 23 | }; 24 | 25 | /*jshint unused:true, eqnull:true */ 26 | /** 27 | * Override debugLib save. 28 | * 29 | * @returns {Promise} Resolves to undefined when complete. 30 | */ 31 | debugLib.save = function workerDebugSave (namespaces) { 32 | if (namespaces == null) { 33 | return idb.del(idb.stores.state, key) 34 | .catch(function () { 35 | // silent failure 36 | }); 37 | } 38 | 39 | return idb.put(idb.stores.state, key, namespaces) 40 | .catch(function (error) { 41 | console.error('debug failed to save namespace', error); 42 | }); 43 | }; 44 | 45 | /*** 46 | * Echo the functionality of debugLib. 47 | * On module load, enable from storage. 48 | */ 49 | debugLib.load().then(function (namespaces) { 50 | debugLib.enable(namespaces); 51 | }); 52 | 53 | /** 54 | * Wrap the main debugLib function 55 | * 56 | * @param {String} namespace 57 | * @returns {Function} The debug function for chaining. 58 | */ 59 | function debugWrapper (namespace) { 60 | return debugLib('sw:'+namespace); 61 | } 62 | 63 | // Mixin all debugLib props and methods 64 | for (var item in debugLib) { 65 | if (debugLib.hasOwnProperty(item)) { 66 | debugWrapper[item] = debugLib[item]; 67 | } 68 | } 69 | 70 | module.exports = debugWrapper; 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": [ 3 | "./node_modules/**", 4 | "./dist/**", 5 | "./reports/**", 6 | "./tests/fixtures/**", 7 | "./assets/scripts/sw/precache.js", 8 | "./assets/scripts/sw/data.js", 9 | "./tmp/**" 10 | ], 11 | 12 | "errorFilter": "./tests/utils/jscsFilter.js", 13 | 14 | "maxErrors": 100, 15 | 16 | "disallowMixedSpacesAndTabs": true, 17 | "disallowNewlineBeforeBlockStatements": true, 18 | "disallowPaddingNewlinesInBlocks": true, 19 | "disallowSpaceAfterObjectKeys": true, 20 | "disallowTrailingComma": true, 21 | 22 | "maximumLineLength": { 23 | "value": 120, 24 | "allowComments": true, 25 | "allowUrlComments": true, 26 | "allowRegex": true 27 | }, 28 | 29 | "requireBlocksOnNewline": true, 30 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 31 | "requireCapitalizedConstructors": true, 32 | "requireCommaBeforeLineBreak": true, 33 | "requireSpaceAfterKeywords": [ 34 | "if", 35 | "else", 36 | "for", 37 | "while", 38 | "do", 39 | "switch", 40 | "return", 41 | "try", 42 | "catch" 43 | ], 44 | "requireSpaceAfterLineComment": true, 45 | "requireSpaceBeforeBlockStatements": true, 46 | "requireSpacesInConditionalExpression": { 47 | "afterTest": true, 48 | "beforeConsequent": true, 49 | "afterConsequent": true, 50 | "beforeAlternate": true 51 | }, 52 | "requireSpacesInAnonymousFunctionExpression": { 53 | "beforeOpeningCurlyBrace": true 54 | }, 55 | "requireSpacesInFunctionExpression": { 56 | "beforeOpeningCurlyBrace": true 57 | }, 58 | "requireSpacesInNamedFunctionExpression": { 59 | "beforeOpeningCurlyBrace": true 60 | }, 61 | 62 | "safeContextKeyword": ["self"], 63 | 64 | "validateIndentation": 2, 65 | 66 | "jsDoc": { 67 | "checkAnnotations": "jsdoc3", 68 | "enforceExistence": "exceptExports", 69 | "checkParamNames": true, 70 | "checkRedundantParams": true, 71 | "requireParamTypes": true 72 | }, 73 | 74 | "validateQuoteMarks": { "mark": "'", "escape": true } 75 | } 76 | -------------------------------------------------------------------------------- /assets/scripts/sw/init/update.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 | * Handles IndexedDB updates. 6 | * Only updates init IDBObjectStore if it gets new/first data. 7 | */ 8 | 'use strict'; 9 | 10 | var stores = require('./stores'); 11 | var apis = require('../utils/db').init({ key: 'apis' }); 12 | var timestamp = require('../utils/db').init({ key: 'timestamp' }); 13 | var debug = require('../utils/debug')('init.update'); 14 | 15 | /** 16 | * Update the IndexedDB init IDBObjectStore if appropriate. 17 | * 18 | * @param {Object} payload - Initial payload 19 | * @param {Object} payload.stores - The flux stores for the app. 20 | * @param {Object} payload.apis - The api information for the app. 21 | * @param {Number} payload.timestamp - The timestamp of the app state. 22 | * @return {Boolean} A promise that resolves to boolean indicating if init 23 | * got new data and should run. 24 | */ 25 | module.exports = function update (payload) { 26 | debug('Running update'); 27 | 28 | return timestamp.read().then(function (currentTs) { 29 | // If the incoming timestamp is newer, it's on. 30 | return payload.timestamp && currentTs < payload.timestamp; 31 | }, function () { 32 | // No existing timestamp found, so brand new - it's on! 33 | return true; 34 | }).then(function (shouldUpdate) { 35 | if (shouldUpdate) { 36 | // Update the init.timestamp 37 | return timestamp.update(payload.timestamp) 38 | .then(function () { 39 | // Update init.stores 40 | return stores.updateInitStores(payload.stores); 41 | }) 42 | .then(function () { 43 | // Update init.apis 44 | return apis.update(payload.apis).then(function () { 45 | return true; 46 | }); 47 | }).catch(function (error) { 48 | debug('Failed to update', error); 49 | throw error; // rethrow 50 | }); 51 | } else { 52 | return false; 53 | } 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /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/unit/services/subscription.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('../../mocks'); 10 | 11 | describe('subscription service', function () { 12 | var subscription; 13 | 14 | before(function () { 15 | mocks.serviceSubscription.begin(); 16 | subscription = require('../../../services/subscription'); 17 | }); 18 | 19 | after(function () { 20 | mocks.serviceSubscription.end(); 21 | }); 22 | 23 | function subCall (method, done) { 24 | var args = []; 25 | if (method === 'read' || method === 'delete') { 26 | args.push( 27 | null, null, {}, null 28 | ); 29 | } else { 30 | args.push( 31 | null, null, {}, {}, null 32 | ); 33 | } 34 | args.push(function (err) { 35 | if (err) { 36 | return done(err); 37 | } 38 | done(); 39 | }); 40 | subscription[method].apply(subscription, args); 41 | } 42 | 43 | describe('object', function () { 44 | it('should have name and create members', function () { 45 | expect(subscription.name).to.be.a('string'); 46 | expect(subscription.create).to.be.a('function'); 47 | expect(subscription.read).to.be.a('function'); 48 | expect(subscription.update).to.be.a('function'); 49 | expect(subscription.delete).to.be.a('function'); 50 | }); 51 | }); 52 | 53 | describe('create', function () { 54 | it('should return a valid response', function (done) { 55 | subCall('create', done); 56 | }); 57 | }); 58 | 59 | describe('read', function () { 60 | it('should return a valid response', function (done) { 61 | subCall('read', done); 62 | }); 63 | }); 64 | 65 | describe('update', function () { 66 | it('should return a valid response', function (done) { 67 | subCall('update', done); 68 | }); 69 | }); 70 | 71 | describe('delete', function () { 72 | it('should return a valid response', function (done) { 73 | subCall('delete', done); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /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-sw-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-sw-data/contents/pages/404.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"],"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-sw-data/contents/pages/500.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"],"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-sw-data/contents/pages/home.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"],"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-sw-data/contents/pages/about.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"],"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-sw-data/contents/pages/contact.json","format":"json","models":["LocalBusiness","SiteInfo","Settings"],"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/mocks/sw-utils-idb-treo.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 | * Mock treo for sw/utils/idb 6 | */ 7 | 'use strict'; 8 | 9 | var closeCount = 0; 10 | var mockValue; 11 | var mockReporter; 12 | 13 | function TreoStoreMock () {} 14 | ['all', 'batch', 'del', 'get', 'put'].forEach(function (method) { 15 | TreoStoreMock.prototype[method] = function () { 16 | var lastTwoArgs = Array.prototype.slice.call(arguments, -2), 17 | cb = lastTwoArgs[1] || lastTwoArgs[0], 18 | test = lastTwoArgs[0]; 19 | 20 | // error emulation is a little specific/funky for this 21 | if (test === 'emulateError') { 22 | return cb(new Error('mock error')); 23 | } 24 | 25 | if (mockReporter) { 26 | mockReporter.apply(mockReporter, 27 | [method].concat(Array.prototype.slice.call(arguments))); 28 | } 29 | 30 | // allow purposed falsy flow, default is undefined for mock value. 31 | if (typeof mockValue === 'undefined') { 32 | return cb(null, 'mock value'); 33 | } 34 | 35 | cb(null, mockValue); 36 | }; 37 | }); 38 | 39 | function TreoDBMock () { 40 | closeCount = 0; 41 | } 42 | TreoDBMock.prototype = { 43 | close: function () { 44 | closeCount++; 45 | }, 46 | store: function () { 47 | return new TreoStoreMock(); 48 | } 49 | }; 50 | 51 | function TreoMock () { 52 | return new TreoDBMock(); 53 | } 54 | TreoMock.schema = function treoMockSchema () { 55 | return { 56 | version: function () { 57 | return { 58 | addStore: function () {} 59 | }; 60 | } 61 | }; 62 | }; 63 | 64 | /*** 65 | * Mock only methods and properties 66 | */ 67 | TreoMock.status = { 68 | getCloseCount: function () { 69 | return closeCount; 70 | } 71 | }; 72 | TreoMock.setValue = function (value) { 73 | mockValue = value; 74 | }; 75 | TreoMock.getValue = function () { 76 | return mockValue; 77 | }; 78 | TreoMock.setReporter = function (reporter) { 79 | mockReporter = reporter; 80 | }; 81 | // Make it possible to wrap/chain reporters 82 | TreoMock.getReporter = function () { 83 | return mockReporter; 84 | }; 85 | 86 | module.exports = TreoMock; 87 | -------------------------------------------------------------------------------- /tests/mocks/sw-caches.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 simple mock for service worker CacheStorage API. 6 | */ 7 | /* global Promise */ 8 | 'use strict'; 9 | 10 | var Response = require('./response'); 11 | 12 | function Cache (options) { 13 | this.options = options || {}; 14 | this.storage = Object.create(null); 15 | } 16 | Cache.prototype = { 17 | match: function match (req) { 18 | var res; 19 | 20 | var urlString = typeof req === 'string' ? req : req.url; 21 | res = this.storage[urlString]; 22 | 23 | if (!res && this.options.default) { 24 | res = new Response({ 25 | test: 'hello' 26 | }, { 27 | status: 200 28 | }); 29 | } 30 | 31 | return Promise.resolve(res); 32 | }, 33 | put: function put (req, res) { 34 | var urlString = typeof req === 'string' ? req : req.url; 35 | this.storage[urlString] = res; 36 | return Promise.resolve(); 37 | } 38 | }; 39 | 40 | /** 41 | * A limited, simple mock of CacheStorage. 42 | * 43 | * @param {Object} [options] - behavioral options 44 | * If not supplied a default behavior is supplied. 45 | * @param {Boolean} [options.openFail] - open should fail. 46 | * @param {Object} [options.cacheNames] - A map of supported named caches. 47 | * If not supplied, a new one is created. 48 | * @param {Boolean} [options.cache.default] - if true, then return a default 49 | * successful response. If not supplied, cache returns undefined (not found). 50 | */ 51 | function CacheStorage (options) { 52 | this.options = options || {}; 53 | } 54 | CacheStorage.prototype = { 55 | open: function open (cacheName) { 56 | var cache, cacheNames = this.options.cacheNames; 57 | 58 | if (cacheNames) { 59 | cache = cacheNames[cacheName]; 60 | } else { 61 | cache = new Cache(this.options.cache); 62 | } 63 | 64 | return this.options.openFail ? Promise.reject(new Error('mock error')) : 65 | Promise.resolve(cache); 66 | } 67 | }; 68 | 69 | module.exports = { 70 | create: function createCacheStorage (options) { 71 | return new CacheStorage(options); 72 | }, 73 | Cache: Cache 74 | }; 75 | -------------------------------------------------------------------------------- /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 | * Really needed for settings.json and 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 | case 'settings': 55 | result = callback(null, this.createContent({ 56 | pushNotifications: { 57 | topics: [{ 58 | label: 'Alerts', 59 | tag: 'push-alerts-tag' 60 | }, { 61 | label: 'Upcoming Events', 62 | tag: 'push-upcoming-events-tag' 63 | }] 64 | } 65 | })); 66 | break; 67 | 68 | default: 69 | throw new Error('service-data test mock recieved unexpected resource request'); 70 | } 71 | 72 | return result; 73 | }, 74 | 75 | initialize: function (callback) { 76 | callback(); 77 | }, 78 | 79 | update: function (callback) { 80 | callback(); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /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, beforeEach, describe, it */ 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | var mocks = require('../../../mocks'); 10 | 11 | describe('data/index', function () { 12 | var data, cache, fetchLib; 13 | 14 | before(function () { 15 | mocks.fetch.begin(); 16 | data = require('../../../../services/data'); 17 | cache = require('./cache'); 18 | fetchLib = require('./fetch'); 19 | }); 20 | 21 | after(function () { 22 | mocks.fetch.end(); 23 | }); 24 | 25 | beforeEach(function () { 26 | cache.mockReset(); 27 | fetchLib.mockReset(); 28 | }); 29 | 30 | describe('fetch', function () { 31 | it('should pull from cache if exists', function (done) { 32 | data.fetch({}, function (err, res) { 33 | if (err) { 34 | done(err); 35 | } 36 | 37 | expect(res).to.equal(cache.get()); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('should fetch if not in cache', function (done) { 43 | data.fetch({ resource: 'miss' }, function (err, res) { 44 | if (err) { 45 | done(err); 46 | } 47 | 48 | expect(res).to.equal('fetch'); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('should fetch using find spec if not in cache', function (done) { 54 | data.fetch({ resource: 'find' }, function (err, res) { 55 | if (err) { 56 | done(err); 57 | } 58 | 59 | var callCounts = cache.mockCounts(); 60 | var params = fetchLib.mockParams(); 61 | 62 | expect(callCounts.get).to.equal(1); 63 | expect(callCounts.find).to.equal(1); 64 | expect(params).to.equal(cache.find()); 65 | expect(res).to.equal('fetch'); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | 71 | describe('initialize', function () { 72 | it('should initialize', function (done) { 73 | data.initialize(done); 74 | }); 75 | }); 76 | 77 | describe('update', function () { 78 | it('should update', function (done) { 79 | data.update(done); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /grunt/tasks/fixtures.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 | * Custom fixtures task and grunt config. 6 | * Generates the test fixtures from the backend. Run as needed. 7 | * Relies on nconfig task. 8 | */ 9 | 'use strict'; 10 | 11 | module.exports = function (grunt) { 12 | grunt.config('fixtures', { 13 | options: { 14 | generators: '<%= baseDir %>/tests/generators', 15 | 'routes-models.js': { 16 | output: { 17 | routes: 'tests/fixtures/routes-response.js', 18 | models: 'tests/fixtures/models-response.js' 19 | } 20 | } 21 | } 22 | }); 23 | 24 | /** 25 | * _runFixtureGenerators custom task 26 | * Runs the fixture generators using backend data services. 27 | * Backend data sources selected by environment - 28 | * Must be run after nconfig 29 | * This private task is only run by 'fixtures' task. 30 | * 31 | * @access private 32 | */ 33 | grunt.registerTask('_runFixtureGenerators', 'Subtask to generate test fixtures', function () { 34 | var fs = require('fs'); 35 | var path = require('path'); 36 | var generator, options = this.options(); 37 | 38 | var async = this.async(); 39 | 40 | fs.readdirSync(options.generators).forEach(function (item) { 41 | generator = path.join(options.generators, item); 42 | grunt.log.writeln( 43 | 'Executing '+generator + '(' + 44 | require('util').inspect(options[item].output) + ', callback)' 45 | ); 46 | require(generator)(options[item].output, async); 47 | }); 48 | }); 49 | 50 | /** 51 | * fixtures custom task 52 | * Runs nconfig and _runFixtureGenerators in order. 53 | * Syntax: fixtures:dev | fixtures:prod 54 | * 55 | * @access public 56 | */ 57 | grunt.registerTask('fixtures', 'Generate test fixtures', function () { 58 | var isProd = this.args.shift() === 'prod'; 59 | var options = { 60 | options: this.options() 61 | }; 62 | 63 | var tasks = [ 64 | 'nconfig:'+(isProd ? 'prod' : 'dev'), 65 | '_runFixtureGenerators' 66 | ]; 67 | 68 | // Pass along the options to subtasks 69 | grunt.config.set('_runFixtureGenerators', options); 70 | 71 | grunt.task.run(tasks); 72 | }); 73 | }; 74 | -------------------------------------------------------------------------------- /configs/settings/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 | 'use strict'; 6 | 7 | var path = require('path'); 8 | var toString = Object.prototype.toString; 9 | 10 | /** 11 | * Prepends a path to string properties of an object or array. 12 | * Returns a new object or array result. 13 | * If a property value is not a 'string' or null, it is passed along by reference. 14 | * If a property value is an 'object', recurse. 15 | * 16 | * @param {Object|Array} fromObj - Collection whose String properties are to have paths prepended to them. 17 | * @param {String} prePath - The path to prepend. 18 | * @returns {Object} A fromObject copy with the given path prepended to the String values. 19 | */ 20 | function prependPath (fromObj, prePath) { 21 | var conversion = toString.call(fromObj) === '[object Array]' ? { 22 | from: fromObj, 23 | to: [], 24 | /** 25 | * Get the value from an array 26 | */ 27 | getValue: function (val, index) { 28 | return fromObj[index]; 29 | }, 30 | /** 31 | * Set the value to an array 32 | */ 33 | setValue: function (obj, val, index, newValue) { 34 | obj[index] = newValue; 35 | } 36 | } : { 37 | from: Object.keys(fromObj), 38 | to: {}, 39 | /** 40 | * Get the value from an Object 41 | */ 42 | getValue: function (val) { 43 | return fromObj[val]; 44 | }, 45 | /** 46 | * Set the value to an Object 47 | */ 48 | setValue: function (obj, val, index, newValue) { 49 | obj[val] = newValue; 50 | } 51 | }; 52 | 53 | return conversion.from.reduce(function (obj, val, index) { 54 | var fromValue = conversion.getValue(val, index); 55 | if (typeof fromValue === 'string') { 56 | // prepend the prePath to fromValue 57 | conversion.setValue(obj, val, index, path.join(prePath, fromValue)); 58 | } else if (fromValue && typeof fromValue === 'object') { 59 | // go again 60 | conversion.setValue(obj, val, index, prependPath(fromValue, prePath)); 61 | } else { 62 | // pass thru 63 | conversion.setValue(obj, val, index, fromValue); 64 | } 65 | return obj; 66 | }, conversion.to); 67 | } 68 | 69 | module.exports = { 70 | prependPathToObject: prependPath 71 | }; 72 | -------------------------------------------------------------------------------- /utils/property.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 | * Property helpers 6 | */ 7 | 'use strict'; 8 | 9 | var toString = Object.prototype.toString; 10 | 11 | /** 12 | * Detect object or array object class 13 | * 14 | * @param {Object} thing - something to test 15 | * @returns {Boolean} true if it is an Object or Array. 16 | */ 17 | function isObjectOrArray (thing) { 18 | return toString.call(thing) === '[object Object]' || 19 | toString.call(thing) === '[object Array]'; 20 | } 21 | 22 | /** 23 | * Recursively find a property on an Object object and return the value. 24 | * Follows objects and arrays on the search. 25 | * Finds and returns the value of the first matching property found by name. 26 | * 27 | * @param {String} propertyName - The property name to search for. 28 | * @param {Object|Array} input - The object to search. 29 | * @param {Boolean} [remove] - True to also remove the property if found. 30 | * @returns {Object} The value of the property found or undefined. 31 | */ 32 | function find (propertyName, input, remove) { 33 | var property; 34 | 35 | // bail if not something we search or invalid 36 | if (!isObjectOrArray(input) || !propertyName) { 37 | return property; 38 | } 39 | 40 | // found 41 | if (propertyName in input) { 42 | var result = input[propertyName]; 43 | if (remove) { 44 | delete input[propertyName]; 45 | } 46 | return result; 47 | } 48 | 49 | // setup to search objects and arrays 50 | var search = toString.call(input) === '[object Array]' ? { 51 | collection: input, 52 | /** 53 | * pass thru getter for array 54 | */ 55 | get: function (i) { 56 | return i; 57 | } 58 | } : { 59 | collection: Object.keys(input), 60 | /** 61 | * key getter for Object 62 | */ 63 | get: function (i) { 64 | return input[i]; 65 | } 66 | }; 67 | 68 | // find first 69 | search.collection.some(function (item) { 70 | var found; 71 | if (isObjectOrArray(search.get(item))) { 72 | found = find(propertyName, search.get(item), remove); 73 | if (!property) { 74 | property = found; 75 | } 76 | } 77 | return !!property; 78 | }); 79 | 80 | return property; 81 | } 82 | 83 | module.exports = { 84 | find: find 85 | }; 86 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/unit/configs/settings/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 describe, it, before */ 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var expect = require('chai').expect; 10 | var utils = require('../../../../configs/settings/utils'); 11 | 12 | describe('settings/utils', function () { 13 | describe('prependPathToObject', function () { 14 | var strToken = 'astring', 15 | lastToken = 'dobeedo', 16 | prePath = 'dobee/'+lastToken; 17 | 18 | var testObj, testTerminalCount, terminals = { 19 | str: strToken, 20 | num: 10, 21 | bool: false, 22 | nope: null 23 | }; 24 | 25 | before(function () { 26 | // The number of times terminals appears below 27 | testTerminalCount = 6; 28 | 29 | testObj = JSON.parse(JSON.stringify(terminals)); 30 | testObj.obj = _.assign(JSON.parse(JSON.stringify(terminals)), { 31 | arr: _.values(terminals) 32 | }, { 33 | obj: { 34 | arr: [terminals, terminals] 35 | } 36 | }); 37 | testObj.arr = _.values(terminals); 38 | }); 39 | 40 | function collectStrings (obj, strings) { 41 | if (typeof obj === 'string') { 42 | strings.push(obj); 43 | } else if (Object.prototype.toString.call(obj) === '[object Array]') { 44 | obj.forEach(function (o) { 45 | if (typeof o === 'string') { 46 | strings.push(o); 47 | } else if ( typeof o === 'object') { 48 | collectStrings(o, strings); 49 | } 50 | }); 51 | } else if (Object.prototype.toString.call(obj) === '[object Object]') { 52 | Object.keys(obj).forEach(function (key) { 53 | if (typeof obj[key] === 'string') { 54 | strings.push(obj[key]); 55 | } else if (typeof obj[key] === 'object') { 56 | collectStrings(obj[key], strings); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | it('should prepend all occurences of string with valid path', function () { 63 | var result = utils.prependPathToObject(testObj, prePath); 64 | 65 | var strings = []; 66 | collectStrings(result, strings); 67 | 68 | expect(strings).length.to.be(testTerminalCount); 69 | strings.forEach(function (aString) { 70 | expect(aString).to.contain(strToken); 71 | expect(aString).to.contain(prePath); 72 | expect(aString).to.contain(lastToken+'/'); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /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', data); 42 | 43 | if (err) { 44 | return done(err); 45 | } 46 | 47 | if (!data) { 48 | debug('no data found', payload.resource); 49 | 50 | var noData = new Error('Page not found'); 51 | noData.statusCode = 404; 52 | return done(noData); 53 | } 54 | 55 | dispatchActions(context, payload.resource, payload.pageTitle, data); 56 | 57 | return done(); 58 | }); 59 | } 60 | 61 | /** 62 | * The page action. 63 | * 64 | * @param {Object} context - The fluxible action context. 65 | * @param {Object} payload - The action payload. 66 | * @param {String} payload.resource - The name of the content resource. 67 | * @param {String} payload.pageTitle - The page title. 68 | * @param {Function} done - The callback to execute on action completion. 69 | */ 70 | function page (context, payload, done) { 71 | var data = context.getStore('ContentStore').get(payload.resource); 72 | 73 | if (data) { 74 | debug('Found '+payload.resource+' in cache'); 75 | dispatchActions(context, payload.resource, payload.pageTitle, data); 76 | return done(); 77 | } 78 | 79 | serviceRequest(context, payload, done); 80 | } 81 | 82 | module.exports = page; 83 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------