├── 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 |
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('').and.contain('Hello');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/actions/size.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var debug = require('debug')('Example:SizeAction');
8 |
9 | /**
10 | * The size action.
11 | * Just dispatches the UPDATE_SIZE action with the given payload.
12 | *
13 | * @param {Object} context - The fluxible action context.
14 | * @param {Object} payload - The UPDATE_SIZE action payload.
15 | * @param {Function} done - The callback to execute on action completion.
16 | */
17 | function updateSize (context, payload, done) {
18 | debug('dispatching UPDATE_SIZE', payload);
19 | context.dispatch('UPDATE_SIZE', payload);
20 | done();
21 | }
22 |
23 | module.exports = updateSize;
24 |
--------------------------------------------------------------------------------
/tests/mocks/fetch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
4 | */
5 | 'use strict';
6 |
7 | var fetchOneParams;
8 |
9 | module.exports = {
10 | mockReset: function () {
11 | fetchOneParams = undefined;
12 | },
13 | mockParams: function () {
14 | return fetchOneParams;
15 | },
16 |
17 | fetchOne: function (params, callback) {
18 | fetchOneParams = params;
19 | callback(null, 'fetch');
20 | },
21 | fetchMain: function (callback) {
22 | callback(null, 'fetch');
23 | },
24 | fetchAll: function (callback) {
25 | callback(null, 'fetch');
26 | },
27 | isManifestRequest: function (params) {
28 | return params.resource === 'routes';
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/components/pages/contact/_result.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // styles for the result component
5 |
6 | .contact-result {
7 | h3, p {
8 | margin-top: 0.5rem;
9 | }
10 | .failure label {
11 | font-size: inherit;
12 | }
13 | }
14 | .contact-result-contact {
15 | margin-bottom: 0;
16 | margin-left: 1.2rem;
17 |
18 | a {
19 | display: block;
20 | text-decoration: none;
21 | span {
22 | padding-left: 1rem;
23 | }
24 | .help-note {
25 | display: block;
26 | }
27 | }
28 | a:not(:last-child) {
29 | margin-bottom: 0.7rem;
30 | @media only screen and (min-height: 600px) {
31 | margin-bottom: 1.2rem;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/components/pages/contact/_styles.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // Styles for the Contact component
5 |
6 | // import styles for supportive components
7 | @import "steps";
8 | @import "anim";
9 | @import "result";
10 | @import "nav";
11 |
12 | .contact-form {
13 | label {
14 | display: block;
15 | padding-bottom: 0.2rem;
16 | font-size: larger;
17 | }
18 | input, textarea {
19 | color: #222;
20 | }
21 | .form-value-element {
22 | width: 100%;
23 | }
24 | }
25 | .contact-intro {
26 | height: 2.2rem;
27 |
28 | // Set the min width only for not skinny phones
29 | @media only screen and (min-width: 350px) {
30 | min-width: rem-calc(328px);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/assets/manifest.webapp:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "name": "LocalNerve Example App",
4 | "launch_path": "/",
5 | "description": "An isomorphic, data driven flux/react/service-worker example",
6 | "icons": {
7 | "128": "/public/images/icon-128x128.png",
8 | "16": "/public/images/favicon-16x16.png",
9 | "32": "/public/images/favicon-32x32.png",
10 | "48": "/public/images/android-chrome-48x48.png"
11 | },
12 | "developer": {
13 | "name": "localnerve",
14 | "url": "https://github.com/localnerve"
15 | },
16 | "installs_allowed_from": [
17 | "*"
18 | ],
19 | "default_locale": "en",
20 | "permissions": {
21 | },
22 | "locales": {
23 | "en": {
24 | "name": "LocalNerve Example App",
25 | "description": "An isomorphic, data driven flux/react/service-worker example."
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/components/pages/_styles.scss:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015, 2016 Alex Grant (@localnerve), LocalNerve LLC
2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
3 | //
4 | // styles for all pages
5 |
6 | .page-content {
7 | @include grid-content;
8 | min-width: 20rem;
9 |
10 | // set a layout boundary
11 | // height: 22rem;
12 | // width: 100%;
13 | // let content dictate, get perf elsewhere
14 |
15 | @include breakpoint(medium) {
16 | padding: 0 2rem;
17 | min-height: 20rem;
18 | min-width: 26rem;
19 | }
20 |
21 | p {
22 | @include breakpoint(medium) {
23 | font-size: 1.2rem;
24 | }
25 | }
26 |
27 | a {
28 | text-decoration: underline;
29 | }
30 | }
31 |
32 | @import "contact/styles";
33 | // This was split into another bundle.
34 | // @import "settings/styles";
35 |
--------------------------------------------------------------------------------
/components/header/ModalLink.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 modalAction = require('../../actions/modal').openModal;
9 |
10 | var ModalLink = React.createClass({
11 | propTypes: {
12 | data: React.PropTypes.object.isRequired
13 | },
14 | contextTypes: {
15 | executeAction: React.PropTypes.func.isRequired
16 | },
17 |
18 | render: function () {
19 | return (
20 |
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 |
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 |
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 |
31 |
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 |
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 |
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 |
--------------------------------------------------------------------------------