├── Procfile ├── assets ├── fonts │ └── .gitkeep ├── google24e9e21ce1f6df19.html ├── images │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── icon-128x128.png │ ├── icon-source.xcf │ ├── mstile-70x70.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── apple-touch-icon.png │ ├── android-chrome-36x36.png │ ├── android-chrome-48x48.png │ ├── android-chrome-72x72.png │ ├── android-chrome-96x96.png │ ├── android-chrome-144x144.png │ ├── android-chrome-192x192.png │ ├── 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 │ └── safari-pinned-tab.svg ├── robots.txt ├── browserconfig.xml ├── manifest.webapp └── manifest.json ├── src ├── node_modules │ ├── configs │ │ ├── local.env.json │ │ ├── push │ │ │ └── index.js │ │ ├── images │ │ │ └── index.js │ │ ├── analytics │ │ │ └── index.js │ │ └── index.js │ └── utils │ │ ├── polyfills │ │ ├── object-assign.js │ │ └── es6-promise.js │ │ ├── react │ │ └── reactDOMServer.js │ │ ├── codes.js │ │ ├── index.js │ │ ├── node.js │ │ ├── push.js │ │ └── urls.js ├── tests │ ├── unit │ │ ├── aaa-config.js │ │ ├── services │ │ │ ├── mail │ │ │ │ └── index.js │ │ │ ├── contact.js │ │ │ ├── data │ │ │ │ ├── utils.js │ │ │ │ ├── markdown.js │ │ │ │ └── index.js │ │ │ ├── page.js │ │ │ ├── routes.js │ │ │ ├── error.js │ │ │ └── subscription.js │ │ ├── utils │ │ │ ├── codes.js │ │ │ ├── push.js │ │ │ ├── syncable.js │ │ │ └── splits.js │ │ ├── actions │ │ │ ├── size.js │ │ │ └── init.js │ │ ├── sw │ │ │ └── utils │ │ │ │ └── idb.js │ │ └── stores │ │ │ └── RouteStore.js │ ├── node_modules │ │ └── test │ │ │ ├── fixtures │ │ │ ├── index.js │ │ │ ├── models-response.js │ │ │ ├── 404-response.js │ │ │ ├── 500-response.js │ │ │ ├── fluxible-routes.js │ │ │ ├── home-response.js │ │ │ ├── settings-response.js │ │ │ ├── about-response.js │ │ │ └── routes-response.js │ │ │ ├── mocks │ │ │ ├── blob.js │ │ │ ├── service-mail.js │ │ │ ├── mailer.js │ │ │ ├── queue.js │ │ │ ├── remarkHtml.js │ │ │ ├── messagechannel.js │ │ │ ├── service-subs.js │ │ │ ├── remark.js │ │ │ ├── fetch.js │ │ │ ├── actionInterface.js │ │ │ ├── sw-data.js │ │ │ ├── sw-utils-db.js │ │ │ ├── sw-sync-push.js │ │ │ ├── response.js │ │ │ ├── request.js │ │ │ ├── cache.js │ │ │ ├── subscription.js │ │ │ ├── requestLib.js │ │ │ ├── sw-init.js │ │ │ ├── worker.js │ │ │ └── sw-utils-idb-treo.js │ │ │ └── utils │ │ │ ├── settings.js │ │ │ └── tests.js │ ├── functional │ │ ├── browsers.js │ │ ├── main.js │ │ └── run-parallel.js │ └── workers │ │ └── contact │ │ └── contact.js ├── application │ ├── components │ │ ├── footer │ │ │ ├── index.js │ │ │ ├── Footer.jsx │ │ │ ├── License.jsx │ │ │ ├── SiteBullets.jsx │ │ │ ├── ByLine.jsx │ │ │ ├── _styles.scss │ │ │ └── LocalBusiness.jsx │ │ ├── header │ │ │ ├── index.js │ │ │ ├── _styles.scss │ │ │ ├── _ribbon.scss │ │ │ ├── Logo.jsx │ │ │ ├── _nav.scss │ │ │ ├── ModalLink.jsx │ │ │ ├── Header.jsx │ │ │ ├── _logo.scss │ │ │ └── Nav.jsx │ │ ├── pages │ │ │ ├── contact │ │ │ │ ├── index.js │ │ │ │ ├── _result.scss │ │ │ │ ├── _styles.scss │ │ │ │ ├── _anim.scss │ │ │ │ ├── elements.js │ │ │ │ ├── Input.jsx │ │ │ │ ├── Nav.jsx │ │ │ │ └── Steps.jsx │ │ │ ├── settings │ │ │ │ ├── index.js │ │ │ │ ├── _switch.scss │ │ │ │ ├── _styles.scss │ │ │ │ ├── Topics.jsx │ │ │ │ ├── Switch.jsx │ │ │ │ └── _topics.scss │ │ │ ├── _spinner.scss │ │ │ ├── Spinner.jsx │ │ │ ├── _styles.scss │ │ │ └── ContentPage.jsx │ │ ├── _react-modal.scss │ │ ├── _notification.scss │ │ ├── PageContainer.jsx │ │ └── _app.scss │ ├── server │ │ ├── services │ │ │ ├── mail │ │ │ │ ├── index.js │ │ │ │ └── mailer.js │ │ │ ├── data │ │ │ │ ├── utils.js │ │ │ │ ├── markdown.js │ │ │ │ └── index.js │ │ │ ├── page.js │ │ │ ├── routes.js │ │ │ ├── contact.js │ │ │ └── error.js │ │ ├── workers │ │ │ └── contact │ │ │ │ └── bin │ │ │ │ └── contact │ │ ├── statics.js │ │ ├── rewrites.js │ │ └── sitemap.js │ ├── client │ │ ├── styles │ │ │ ├── settings.scss │ │ │ ├── _mixins.scss │ │ │ ├── inline.scss │ │ │ ├── _vars.scss │ │ │ └── _fonts.scss │ │ ├── index.js │ │ └── sw │ │ │ ├── assets.js │ │ │ ├── activate.js │ │ │ └── init │ │ │ └── update.js │ ├── actions │ │ ├── size.js │ │ ├── init.js │ │ ├── interface.js │ │ ├── contact.js │ │ ├── page.js │ │ └── routes.js │ ├── app.js │ └── stores │ │ └── RouteStore.js └── build │ ├── webpack │ ├── utils │ │ └── mode.js │ ├── plugins │ │ └── uglify.js │ ├── inline.js │ ├── swReg.js │ ├── index.js │ └── swMain.js │ ├── clean.js │ ├── symlink.js │ ├── prep.js │ ├── copy.js │ ├── perfbudget.js │ ├── imagemin.js │ ├── fixtures.js │ └── nodemon.js ├── .slugignore ├── .eslintignore ├── .browserslistrc ├── gulpfile.babel.js ├── start.js ├── babel.config.js ├── .eslintrc.json ├── .gitignore ├── .github └── workflows │ ├── verify.yml │ └── deploy.yml ├── prestart.js └── LICENSE.md /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /assets/fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/google24e9e21ce1f6df19.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google24e9e21ce1f6df19.html -------------------------------------------------------------------------------- /src/node_modules/configs/local.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "PORT": 3000, 3 | "NODE_ENV": "development" 4 | } 5 | -------------------------------------------------------------------------------- /assets/images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/3.jpg -------------------------------------------------------------------------------- /assets/images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/4.jpg -------------------------------------------------------------------------------- /assets/images/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/5.jpg -------------------------------------------------------------------------------- /assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/favicon.ico -------------------------------------------------------------------------------- /.slugignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | .vscode 3 | /reports 4 | .jshint* 5 | start.js 6 | webpack*.json 7 | README.md 8 | LICENSE.md 9 | -------------------------------------------------------------------------------- /assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /assets/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/favicon-96x96.png -------------------------------------------------------------------------------- /assets/images/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/icon-128x128.png -------------------------------------------------------------------------------- /assets/images/icon-source.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/icon-source.xcf -------------------------------------------------------------------------------- /assets/images/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/mstile-70x70.png -------------------------------------------------------------------------------- /assets/images/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/mstile-144x144.png -------------------------------------------------------------------------------- /assets/images/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/mstile-150x150.png -------------------------------------------------------------------------------- /assets/images/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/mstile-310x150.png -------------------------------------------------------------------------------- /assets/images/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/mstile-310x310.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/images/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-36x36.png -------------------------------------------------------------------------------- /assets/images/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-48x48.png -------------------------------------------------------------------------------- /assets/images/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-72x72.png -------------------------------------------------------------------------------- /assets/images/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-96x96.png -------------------------------------------------------------------------------- /assets/images/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-144x144.png -------------------------------------------------------------------------------- /assets/images/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/images/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-256x256.png -------------------------------------------------------------------------------- /assets/images/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /assets/images/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/react-pwa-reference/HEAD/assets/images/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | /node_modules 3 | reports 4 | tmp 5 | output 6 | src/application/client/sw/precache.js 7 | src/tests/node_modules/test/fixtures 8 | src/node_modules/application 9 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # support matrix 2 | # do not add/mix 'node' in here or it will break support for client bundle dynamic import chunks in webpack 5 3 | 4 | last 1 version 5 | > 1% in US 6 | not dead -------------------------------------------------------------------------------- /src/tests/unit/aaa-config.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Root configuration for tests 3 | * Add root level repeating hooks here (before, beforeEach, etc) 4 | */ 5 | /* global */ 6 | 7 | global.__TEST__ = true; 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/application/components/footer/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | export { default } from './Footer'; 6 | -------------------------------------------------------------------------------- /src/application/components/header/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | export { default } from './Header'; 6 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file 4 | * for terms. 5 | */ 6 | export const dummy = 'dummy'; 7 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | export { default } from './Contact'; 6 | -------------------------------------------------------------------------------- /src/application/components/pages/settings/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | export { default } from './Settings'; 6 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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) { 8 | this.content = content; 9 | } 10 | 11 | module.exports = Blob; 12 | -------------------------------------------------------------------------------- /src/node_modules/utils/polyfills/object-assign.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock for older modules that need static object-assign polyfill. 3 | * (Promise is polyfilled prior to app load) 4 | * 5 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 6 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 7 | */ 8 | module.exports = Object.assign; 9 | -------------------------------------------------------------------------------- /src/application/server/services/mail/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import queue from './queue'; 6 | 7 | export const send = queue.sendMail; 8 | export const worker = queue.contactWorker; 9 | 10 | export default { 11 | send, 12 | worker 13 | }; 14 | -------------------------------------------------------------------------------- /src/application/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('@babel/register')({ 9 | presets: [ 10 | '@babel/env' 11 | ], 12 | ignore: [] 13 | }); 14 | require('application/server/services/mail').worker(); 15 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import gulp from 'gulp'; 6 | import taskFactories from './src/build'; 7 | 8 | // Register the project tasks 9 | Object.keys(taskFactories).forEach((factoryName) => { 10 | gulp.task(factoryName, taskFactories[factoryName]()); 11 | }); 12 | -------------------------------------------------------------------------------- /src/application/components/_react-modal.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/service-mail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/components/pages/_spinner.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 3 | // 4 | // custom styles for the spinner 5 | 6 | .react-spinner-custom.react-spinner { 7 | width: 128px; 8 | height: 128px; 9 | } 10 | 11 | .react-spinner-custom .react-spinner_bar { 12 | background-color: $app-primary-bgcolor; 13 | } 14 | -------------------------------------------------------------------------------- /src/node_modules/utils/polyfills/es6-promise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock for older components that statically require es6-promise. 3 | * (Promise is polyfilled prior to app load). 4 | * 5 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 6 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 7 | */ 8 | /* global Promise */ 9 | module.exports = { 10 | Promise: Promise, 11 | polyfill: function () {} 12 | } 13 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/mailer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | }; 18 | -------------------------------------------------------------------------------- /src/tests/functional/browsers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/components/header/_styles.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 3 | // 4 | // header styles 5 | // ========================= 6 | 7 | .app-header { 8 | @include grid-block(shrink, vertical); 9 | } 10 | .app-header-bg { 11 | @extend %header-footer-bg; 12 | } 13 | 14 | @import "ribbon"; 15 | @import "logo"; 16 | @import "nav"; 17 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * The development startup entry point. 6 | */ 7 | const basePath = require('path').basename(__dirname); 8 | 9 | require('@babel/register')({ 10 | ignore: [ 11 | new RegExp(`${basePath}/node_modules/.*`) 12 | ] 13 | }); 14 | 15 | module.exports = require('./src/application/server'); 16 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/build/webpack/utils/mode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert build type to webpack 4 mode. 3 | * 4 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 5 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 6 | */ 7 | 8 | /** 9 | * Convert the build type into a webpack mode. 10 | * 11 | * @param {String} type - One of ['dev', 'prod', 'perf']. 12 | */ 13 | export default function makeMode (type) { 14 | return type === 'dev' ? 'development' : 'production'; 15 | } -------------------------------------------------------------------------------- /src/node_modules/utils/react/reactDOMServer.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import noop from 'lodash/noop'; 9 | 10 | /** 11 | * ReactDOMServer dummy. 12 | */ 13 | export default { 14 | renderToString: noop, 15 | renderToStaticMarkup: noop 16 | }; 17 | -------------------------------------------------------------------------------- /src/application/client/styles/settings.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 inline.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 | -------------------------------------------------------------------------------- /src/application/client/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/components/pages/settings/_switch.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /assets/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #6a1b9a 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/remarkHtml.js: -------------------------------------------------------------------------------- 1 | /** 2 | * remark-html dummy mock. 3 | * 4 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 5 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 6 | */ 7 | class RemarkHtml { 8 | constructor () { 9 | this._merr = false; 10 | } 11 | set mockError (value) { 12 | this._merr = value; 13 | } 14 | get mockError () { 15 | return this._merr; 16 | } 17 | } 18 | 19 | const remarkHtmlObj = new RemarkHtml(); 20 | 21 | module.exports = remarkHtmlObj; -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'lodash' 4 | ], 5 | presets: [ 6 | '@babel/preset-env', 7 | '@babel/preset-react' 8 | ], 9 | ignore: [ 10 | 'assets/**', 11 | 'dist/**', 12 | 'output/**', 13 | 'reports/**', 14 | 'tmp/**' 15 | ], 16 | env: { 17 | cover: { 18 | plugins: [ 19 | [ 20 | 'istanbul', { 21 | exclude: [ 22 | '!**/node_modules/**', 23 | '**/tests/**' 24 | ] 25 | } 26 | ] 27 | ] 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/node_modules/utils/codes.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | 6 | /** 7 | * Contain a status code to a finite set. 8 | * For now, if the code is a 404 it remains 404, otherwise its 500. 9 | * 10 | * @param {Number} statusCode - The status code to conform. 11 | * @returns {Number} 404 or 500. 12 | */ 13 | export function conformErrorStatus (statusCode) { 14 | return statusCode !== 404 ? '500' : '404'; 15 | } 16 | 17 | export default { 18 | conformErrorStatus 19 | }; 20 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/messagechannel.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 MessageChannel, purposed for this test suite. 6 | */ 7 | 'use strict'; 8 | 9 | function MessageChannel () { 10 | var port1 = {}; 11 | 12 | this.port1 = port1; 13 | this.port2 = { 14 | mockRespond: function (event) { 15 | if (port1.onmessage) { 16 | port1.onmessage(event); 17 | } 18 | } 19 | }; 20 | } 21 | 22 | module.exports = MessageChannel; 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parser": "babel-eslint", 6 | "plugins": [ 7 | "react" 8 | ], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:react/recommended" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | } 17 | }, 18 | "rules": { 19 | "indent": [2, 2, { 20 | "SwitchCase": 1, 21 | "MemberExpression": 1 22 | }], 23 | "quotes": [2, "single"], 24 | "dot-notation": [2, {"allowKeywords": true}] 25 | }, 26 | "settings": { 27 | "react": { 28 | "version": "detect" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/service-subs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/client/styles/inline.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 | "fonts"; 16 | 17 | .grid-container-center { 18 | @include grid-container; 19 | } 20 | 21 | .grid-row-spaced { 22 | @include grid-block(expand, horizontal, false, spaced); 23 | } 24 | 25 | .hide { 26 | display: none !important; 27 | } 28 | 29 | @import "app"; 30 | -------------------------------------------------------------------------------- /src/build/webpack/plugins/uglify.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * Kept to allow custom minification options. Historically, was UglifyJS (thus name). 6 | */ 7 | 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | 10 | /** 11 | * Create the webpack es6 compression with custom options. 12 | */ 13 | export default function uglifyPluginFactory () { 14 | return new TerserPlugin({ 15 | terserOptions: { 16 | compress: { 17 | warnings: false 18 | }, 19 | output: { 20 | comments: false 21 | } 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Dependency directory 12 | node_modules/* 13 | 14 | # Optional npm cache directory 15 | .npm 16 | 17 | # Optional nvm version resource 18 | .nvmrc 19 | 20 | # Optional REPL history 21 | .node_repl_history 22 | 23 | # macosx ignores 24 | .DS_Store 25 | 26 | # Optional vscode junk 27 | .vscode 28 | 29 | # Project Boilerplate ignores 30 | .jshint* 31 | tmp 32 | dist/ 33 | reports/ 34 | output/ 35 | .nyc_output/ 36 | /src/node_modules/application 37 | /src/node_modules/configs/settings/*.json 38 | /src/application/client/sw/precache.js 39 | /src/application/client/sw/node_modules/sw/data.json 40 | /webpack-stats* 41 | -------------------------------------------------------------------------------- /src/build/clean.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import del from 'del'; 6 | 7 | /** 8 | * Factory for the clean task. 9 | * Cleanup build output and other miscellaneous generated src files. 10 | * 11 | * @param {Object} settings - The project settings. 12 | * @returns {Function} The clean task. 13 | */ 14 | export default function cleanTaskFactory (settings) { 15 | return function clean () { 16 | return del([ 17 | settings.dist.baseDir, 18 | settings.src.assetsJson, 19 | settings.src.serviceWorker.precache, 20 | settings.src.serviceWorker.data 21 | ]); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/application/client/styles/_vars.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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: #9C27B0; 9 | $app-accent-light-bgcolor: #EA80FC; 10 | $app-accent-dark-bgcolor: #6A1B9A; 11 | 12 | $app-accent-dark-shadow: rgba($app-accent-dark-bgcolor, 0.7); 13 | $app-primary-light-color: rgba(255, 255, 255, 0.87); 14 | $app-primary-dark-color: rgba(0, 0, 0, 0.87); 15 | 16 | $app-max-zindex: 2; 17 | 18 | // media query to detect height contstrained phones 19 | $height-constrained-phone: "only screen and (orientation: portrait) and (max-height: 480px)"; 20 | -------------------------------------------------------------------------------- /src/application/components/header/_ribbon.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 3 | // 4 | // ribbon styles 5 | // ========================= 6 | 7 | .ribbon { 8 | margin: 0.25rem 0; 9 | padding-top: 0.2rem; 10 | overflow-y: hidden; 11 | @include breakpoint(medium) { 12 | margin: 1rem 0; 13 | padding-top: 0; 14 | } 15 | } 16 | 17 | .glyph { 18 | // this controls the size of the icons since icons are ems 19 | font-size: 1.6rem; 20 | @include breakpoint(medium) { 21 | font-size: 2rem; 22 | } 23 | } 24 | 25 | .icon { 26 | display: inline-block; 27 | width: 1em; 28 | height: 1em; 29 | fill: currentColor; 30 | } 31 | -------------------------------------------------------------------------------- /src/application/components/_notification.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | image/svg+xml 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/application/components/pages/Spinner.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import ReactSpinner from 'react-spinner'; 8 | 9 | class Spinner extends React.Component { 10 | static get propTypes () { 11 | return { 12 | contained: PropTypes.bool 13 | }; 14 | } 15 | 16 | render () { 17 | const spinner = ; 18 | const element = this.props.contained 19 | ? React.createElement('div', { style: { marginTop: '50%' } }, spinner) 20 | : spinner; 21 | 22 | return element; 23 | } 24 | } 25 | 26 | export default Spinner; 27 | -------------------------------------------------------------------------------- /assets/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "Contactor", 4 | "launch_path": "/", 5 | "description": "A universal, data driven example PWA", 6 | "icons": { 7 | "128": "/public/images/icon-128x128.png?v=gAA6rKkkBo", 8 | "16": "/public/images/favicon-16x16.png?v=gAA6rKkkBo", 9 | "32": "/public/images/favicon-32x32.png?v=gAA6rKkkBo", 10 | "48": "/public/images/android-chrome-48x48.png?v=gAA6rKkkBo" 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": "Contactor", 25 | "description": "A universal, data driven example PWA" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/application/actions/size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import debugLib from 'debug'; 6 | const debug = debugLib('actions:size'); 7 | 8 | /** 9 | * The size action. 10 | * Just dispatches the UPDATE_SIZE action with the given payload. 11 | * 12 | * @param {Object} context - The fluxible action context. 13 | * @param {Object} payload - The UPDATE_SIZE action payload. 14 | * @param {Function} done - The callback to execute on action completion. 15 | */ 16 | export function updateSize (context, payload, done) { 17 | debug('dispatching UPDATE_SIZE', payload); 18 | context.dispatch('UPDATE_SIZE', payload); 19 | done(); 20 | } 21 | 22 | export default updateSize; 23 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/remark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * This mocks remark and remark-html 6 | */ 7 | 'use strict'; 8 | 9 | function Remark () { 10 | } 11 | 12 | const testMu = ` 13 |

Hello World

14 |

This is a secondary header

15 |

16 | This is a paragraph. Woopdie do. 17 |

18 | `; 19 | Remark.prototype = { 20 | use: (html) => ({ 21 | process: (input, cb) => { 22 | if (html.mockError) { 23 | cb(new Error('mock error')); 24 | } else { 25 | cb(null, testMu); 26 | } 27 | } 28 | }) 29 | } 30 | 31 | function remark () { 32 | return new Remark(); 33 | } 34 | 35 | module.exports = remark; 36 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/fetch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/_result.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/actionInterface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | var actions = Object.create(null); 23 | actions.page = mockAction; 24 | actions.settings = mockAction; 25 | 26 | module.exports = { 27 | getActions: function () { 28 | return actions; 29 | }, 30 | putAction: function (name, action) { 31 | actions[name] = action; 32 | }, 33 | mockAction: mockAction 34 | }; 35 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/_styles.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 | outline-color: $app-accent-light-bgcolor; 23 | width: 100%; 24 | } 25 | } 26 | .contact-intro { 27 | height: 2.2rem; 28 | 29 | // Set the min width only for not skinny phones 30 | @media only screen and (min-width: 350px) { 31 | min-width: rem-calc(328px); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/application/components/header/Logo.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { NavLink } from 'fluxible-router'; 8 | 9 | class Logo extends React.Component { 10 | static get propTypes () { 11 | return { 12 | site: PropTypes.object.isRequired 13 | }; 14 | } 15 | 16 | render () { 17 | return ( 18 |
19 | 20 |

21 | {this.props.site.name} 22 |

23 | 24 | {this.props.site.tagLine} 25 | 26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | export default Logo; 33 | -------------------------------------------------------------------------------- /src/node_modules/utils/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import FluxibleRouteTransformer from './FluxibleRouteTransformer'; 6 | import codes from './codes'; 7 | 8 | /** 9 | * Factory to create a FluxibleRouteTransformer object. 10 | * 11 | * @param {Object} options - Options to control the object creation. 12 | * @param {Object} options.actions - The actions available for use in route transformations, and thus in the backend. 13 | */ 14 | export function createFluxibleRouteTransformer (options) { 15 | options = options || {}; 16 | return new FluxibleRouteTransformer(options.actions); 17 | } 18 | 19 | export const conformErrorStatus = codes.conformErrorStatus; 20 | 21 | export default { 22 | createFluxibleRouteTransformer, 23 | conformErrorStatus 24 | }; 25 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/sw-data.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | manifest: { 17 | debug: false, 18 | cacheId: 'app' 19 | }, 20 | assets: [ 21 | 'https://'+ hostnames[1] +'/somepath/to/some/resource', 22 | 'https://'+ hostnames[0] +'/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNV_2ngZ8dMf8fLgjYEouxg.woff2', 23 | 'https://'+ hostnames[0] +'/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlBM0YzuT7MdOe03otPbuUS0.woff', 24 | 'https://'+ hostnames[0] +'/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlEY6Fu39Tt9XkmtSosaMoEA.ttf' 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /src/tests/functional/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | }); 35 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/sw-utils-db.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 () { 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 () { 31 | if (this.error) { 32 | return Promise.reject(new Error('mock error')); 33 | } 34 | 35 | return Promise.resolve(); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | pull_request: 7 | branches-ignore: 8 | - 'snyk-**' 9 | 10 | jobs: 11 | verify: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x, 16.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3.0.0 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - name: Lint, Test, And Coverage 28 | run: npm run lint && npm run test:cover 29 | - name: Coverage Upload 30 | if: ${{ success() }} 31 | uses: coverallsapp/github-action@master 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | path-to-lcov: ./reports/coverage/lcov.info -------------------------------------------------------------------------------- /src/application/components/header/_nav.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 3 | // 4 | // nav styles 5 | // ========================= 6 | 7 | .navigation { 8 | list-style: none; 9 | padding: 0; 10 | margin: 0; 11 | } 12 | .navigation-link { 13 | line-height: 1.4; 14 | font-size: 1.4rem; 15 | font-weight: bold; 16 | padding: 0 1rem; 17 | flex-grow: 1; 18 | text-align: center; 19 | 20 | background: $app-primary-bgcolor; 21 | box-shadow: inset 0 -8px 6px -7px $app-accent-dark-shadow; 22 | z-index: $app-max-zindex - 1; 23 | 24 | @include breakpoint(medium) { 25 | font-size: 1.8rem; 26 | } 27 | 28 | &.selected { 29 | background: transparent; 30 | box-shadow: 0 7px 6px 4px $app-accent-dark-shadow; 31 | z-index: $app-max-zindex; 32 | } 33 | 34 | a { 35 | display: block; 36 | width: 100%; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/build/symlink.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import gulp from 'gulp'; 6 | import { sync as mkdirp } from 'mkdirp' 7 | 8 | /** 9 | * Factory for the symlink task. 10 | * Ensure the link target exists (allowEmpty on src will not work). 11 | * 12 | * @param {Object} settings - The project settings. 13 | * @param {Boolean} output - True to create symlink in output, false for src. 14 | * @returns {Function} the symlink task. 15 | */ 16 | export default function symlinkTaskFactory (settings, output) { 17 | const target = output ? 'output' : 'src'; 18 | 19 | return function symlink () { 20 | mkdirp(`./${settings[target].application}`); 21 | 22 | return gulp.src(`./${settings[target].application}`) 23 | .pipe( 24 | gulp.symlink(`./${settings[target].baseDir}/node_modules`) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/application/actions/init.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import debugLib from 'debug'; 6 | const debug = debugLib('actions:init'); 7 | 8 | /** 9 | * Perform the init action. 10 | * The init action is intended for perparing the app state on the server. 11 | * Extensible - Many different properties can be passed to the app on this action. 12 | * Stores that listen to this action check for properties they are interested in. 13 | * 14 | * @param {Object} context - The fluxible action context. 15 | * @param {Object} payload - The INIT_APP action payload. 16 | * @param {Function} done - The callback to execute on completion. 17 | */ 18 | export function init (context, payload, done) { 19 | debug('dispatching INIT_APP', payload); 20 | context.dispatch('INIT_APP', payload); 21 | done(); 22 | } 23 | 24 | export default init; 25 | -------------------------------------------------------------------------------- /src/build/prep.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import gulp from 'gulp'; 6 | import symlinkTaskFactory from './symlink'; 7 | import del from 'del'; 8 | 9 | /** 10 | * Factory for the prep task. 11 | * Runs before a compilation task to make the output complete/correct. 12 | * Copies json, makes symlinks. 13 | * 14 | * @param {Object} settings - The project settings. 15 | * @returns {Function} the prep task. 16 | */ 17 | export default function prepTaskFactory (settings) { 18 | return gulp.series( 19 | function clean () { 20 | return del([settings.output.baseDir]); 21 | }, 22 | function json () { 23 | return gulp.src(`${settings.src.baseDir}/**/*.json`) 24 | .pipe( 25 | gulp.dest(settings.output.baseDir) 26 | ); 27 | }, 28 | symlinkTaskFactory(settings, true) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/application/server/services/data/utils.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | 6 | /** 7 | * Find the first [object Object] with key that matches value. 8 | * 9 | * @param {String} key - The property name. 10 | * @param {String} value - The property value. 11 | * @param {Object} obj - The object to search. 12 | * @returns {Object} the object that contains key===value. Otherwise undefined. 13 | */ 14 | export function objContains (key, value, obj) { 15 | if (obj[key] === value) { 16 | return obj; 17 | } 18 | 19 | let found; 20 | 21 | Object.keys(obj).some((k) => { 22 | if (Object.prototype.toString.call(obj[k]) === '[object Object]') { 23 | found = objContains(key, value, obj[k]); 24 | return !!found; 25 | } 26 | }); 27 | 28 | return found; 29 | } 30 | 31 | export default { 32 | objContains 33 | }; 34 | -------------------------------------------------------------------------------- /src/application/server/services/page.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import data from './data'; 8 | import error from './error'; 9 | 10 | export const name = 'page'; 11 | 12 | /** 13 | * The read CRUD method definition. 14 | * Just directs work. Params are per Yahoo fetchr. 15 | * 16 | * @param {Object} req - Not used. 17 | * @param {String} resource - Not used. 18 | * @param {Object} params - The data fetch parameters. 19 | * @param {Object} config - Not used. 20 | * @param {Function} callback - The callback to execute on completion. 21 | */ 22 | export function read (req, resource, params, config, callback) { 23 | return data.fetch(params, (err, data) => { 24 | callback(error(err), data); 25 | }); 26 | } 27 | 28 | export default { 29 | name, 30 | read 31 | }; 32 | -------------------------------------------------------------------------------- /src/application/components/pages/_styles.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 | height: 20rem; 13 | width: 100%; 14 | // let content dictate, get perf elsewhere 15 | 16 | @include breakpoint(medium) { 17 | padding: 0 2rem; 18 | min-height: 20rem; 19 | min-width: 26rem; 20 | } 21 | 22 | p { 23 | @include breakpoint(medium) { 24 | font-size: 1.2rem; 25 | } 26 | } 27 | 28 | a { 29 | text-decoration: underline; 30 | } 31 | } 32 | 33 | @import "contact/styles"; 34 | 35 | @import "spinner"; 36 | // Pulled directly from the react-spinner package. 37 | @import "react-spinner"; 38 | 39 | // This was split into another bundle. 40 | // @import "settings/styles"; 41 | -------------------------------------------------------------------------------- /src/build/copy.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import gulp from 'gulp'; 6 | 7 | /** 8 | * Factory for the copy task. 9 | * 10 | * @param {Object} settings - The project settings. 11 | * @returns {Function} the copy task. 12 | */ 13 | export default function copyTaskFactory (settings) { 14 | return function copy () { 15 | return gulp.src([ 16 | '**', 17 | // Copy all assets, EXCEPT: 18 | 19 | // styles are processed by ccss 20 | '!**/styles/**', 21 | // svg is processed by svg task 22 | '!images/*.svg', 23 | // skip image designer source files 24 | '!images/*.xcf', 25 | // scripts are processed by webpack 26 | '!scripts/**' 27 | ], { 28 | cwd: settings.assets.baseDir 29 | }) 30 | .pipe( 31 | gulp.dest(settings.dist.baseDir) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/tests/unit/services/mail/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import mocks from 'test/mocks'; 9 | 10 | describe('mail/index', () => { 11 | let mail; 12 | 13 | before(function () { 14 | this.timeout(5000); 15 | 16 | mocks.mail.begin(); 17 | mail = require('application/server/services/mail/index'); 18 | }); 19 | 20 | after(() => { 21 | mocks.mail.end(); 22 | }); 23 | 24 | it('should send mail without error', (done) => { 25 | mail.send({ 26 | name: 'tom', 27 | email: 'tom@heaven.org', 28 | message: 'thinking of you' 29 | }, (err) => { 30 | done(err); 31 | }); 32 | }); 33 | 34 | it('should expose a worker method', () => { 35 | expect(mail.worker).to.be.a('function'); 36 | expect(mail).to.respondTo('worker'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/application/server/services/data/markdown.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /* global Promise */ 6 | import debugLib from 'debug'; 7 | import remark from 'remark'; 8 | import remarkHtml from 'remark-html'; 9 | 10 | const debug = debugLib('services:data:markdown'); 11 | 12 | /** 13 | * Parse markdown to markup. 14 | * 15 | * @param {String} input - The markdown to parse. 16 | * @returns {Promise} Resolves to the markup, or rejects. 17 | */ 18 | export function markdown (input) { 19 | debug('parsing markdown'); 20 | 21 | return new Promise((resolve, reject) => { 22 | remark() 23 | .use(remarkHtml) 24 | .process(input, (err, res) => { 25 | if (err) { 26 | debug('remark markdown conversion failed', err); 27 | return reject(err); 28 | } 29 | resolve(String(res)); 30 | }); 31 | }); 32 | } 33 | 34 | export default markdown; 35 | -------------------------------------------------------------------------------- /src/application/server/services/routes.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import data from './data'; 8 | import error from './error'; 9 | 10 | export const name = 'routes'; 11 | 12 | /** 13 | * The read CRUD method definition. 14 | * Directs work and mediates the response. Params are per Yahoo fetchr. 15 | * 16 | * @param {Object} req - Not used. 17 | * @param {String} resource - Not used. 18 | * @param {Object} params - The data fetch parameters. 19 | * @param {Object} config - Not used. 20 | * @param {Function} callback - The callback to execute on completion. 21 | */ 22 | export function read (req, resource, params, config, callback) { 23 | return data.fetch(params, (err, res) => { 24 | callback(error(err), res ? res.content : null); 25 | }); 26 | } 27 | 28 | export default { 29 | name, 30 | read 31 | }; 32 | -------------------------------------------------------------------------------- /src/node_modules/configs/push/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/build/perfbudget.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import gulpWebPageTest from 'gulp-webpagetest'; 6 | 7 | /** 8 | * Factory for the perfbudget ask. 9 | * Runs a perfbudget against DEPLOY_URL on webpagetest. 10 | * 11 | * @returns {Function} The perfbudget task. 12 | */ 13 | export default function perfbudgetTaskFactory () { 14 | return function perfbudget (done) { 15 | const wpt = gulpWebPageTest ({ 16 | url: process.env.DEPLOY_URL, 17 | key: process.env.WPT_API_KEY, 18 | location: 'Dulles:Chrome', 19 | firstViewOnly: true, 20 | timeout: 300, 21 | connectivity: '3G', 22 | emulateMobile: true, 23 | runs: 3, 24 | budget: { 25 | // 3000 nominal + (2 * 300) ssl negotiation 26 | SpeedIndex: 3600 27 | }, 28 | wptInstance: 'https://www.webpagetest.org' 29 | }); 30 | 31 | return wpt(done); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/_anim.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/node_modules/utils/node.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /* global Promise */ 6 | 7 | /** 8 | * Utility to promisify a Node function 9 | * 10 | * @param {Function} nodeFunc - The node function to Promisify. 11 | */ 12 | function nodeCall (nodeFunc /* args... */) { 13 | const nodeArgs = Array.prototype.slice.call(arguments, 1); 14 | 15 | return new Promise(function (resolve, reject) { 16 | /** 17 | * Resolve a node callback 18 | */ 19 | function nodeResolver (err, value) { 20 | if (err) { 21 | reject(err); 22 | } else if (arguments.length > 2) { 23 | resolve.apply(resolve, Array.prototype.slice.call(arguments, 1)); 24 | } else { 25 | resolve(value); 26 | } 27 | } 28 | 29 | nodeArgs.push(nodeResolver); 30 | nodeFunc.apply(nodeFunc, nodeArgs); 31 | }); 32 | } 33 | 34 | module.exports = { 35 | nodeCall: nodeCall 36 | }; 37 | -------------------------------------------------------------------------------- /src/application/components/footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import ByLine from './ByLine'; 8 | import SiteBullets from './SiteBullets'; 9 | import License from './License'; 10 | import LocalBusiness from './LocalBusiness'; 11 | 12 | class Footer extends React.Component { 13 | static get propTypes () { 14 | return { 15 | models: PropTypes.object.isRequired 16 | }; 17 | } 18 | 19 | render () { 20 | return ( 21 | 27 | ); 28 | } 29 | } 30 | 31 | export default Footer; 32 | -------------------------------------------------------------------------------- /src/application/server/services/contact.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import mail from './mail'; 8 | import error from './error'; 9 | 10 | export const name = 'contact'; 11 | 12 | /** 13 | * The create CRUD method definition. 14 | * Just directs work. Params are per Yahoo fetchr. 15 | * 16 | * @param {Object} req - Not used. 17 | * @param {String} resource - Not used. 18 | * @param {Object} params - The collected contact fields to send. 19 | * @param {Object} body - Not used. 20 | * @param {Object} config - Not used. 21 | * @param {Function} callback - The callback to execute on completion. 22 | */ 23 | export function create (req, resource, params, body, config, callback) { 24 | return mail.send(params, (err, data) => { 25 | callback(error(err), data); 26 | }); 27 | } 28 | 29 | export default { 30 | name, 31 | create 32 | }; 33 | -------------------------------------------------------------------------------- /src/application/components/footer/License.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | 7 | import PropTypes from 'prop-types'; 8 | 9 | class License extends React.Component { 10 | static get propTypes () { 11 | return { 12 | license: PropTypes.object.isRequired 13 | }; 14 | } 15 | 16 | render () { 17 | const statements = this.props.license.statement.split( 18 | this.props.license.type 19 | ); 20 | 21 | return ( 22 |
23 | 24 | 25 | {statements[0]} 26 | 27 | 28 | {this.props.license.type} 29 | 30 | 31 | {statements[1]} 32 | 33 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | export default License; 40 | -------------------------------------------------------------------------------- /src/application/components/footer/SiteBullets.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | 7 | import PropTypes from 'prop-types'; 8 | 9 | class SiteBullets extends React.Component { 10 | static get propTypes () { 11 | return { 12 | items: PropTypes.array.isRequired 13 | }; 14 | } 15 | 16 | render () { 17 | const items = this.props.items.map(function (item, index, arr) { 18 | return ( 19 | 20 | 21 | {item} 22 | 23 | 24 | { index < (arr.length - 1) ? ' • ' : '' } 25 | 26 | 27 | ); 28 | }); 29 | 30 | return ( 31 |
32 | 33 | {items} 34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | export default SiteBullets; 41 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/utils/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | export function getSettingsFields (context, SettingsStore) { 8 | const settingsStore = context.getStore(SettingsStore); 9 | return { 10 | hasServiceWorker: settingsStore.getHasServiceWorker(), 11 | hasPushMessaging: settingsStore.getHasPushMessaging(), 12 | hasPermissions: settingsStore.getHasPermissions(), 13 | hasNotifications: settingsStore.getHasNotifications(), 14 | pushBlocked: settingsStore.getPushBlocked(), 15 | syncBlocked: settingsStore.getSyncBlocked(), 16 | pushSubscription: settingsStore.getPushSubscription(), 17 | pushSubscriptionError: settingsStore.getPushSubscriptionError(), 18 | pushTopics: settingsStore.getPushTopics(), 19 | pushTopicsError: settingsStore.getPushTopicsError(), 20 | transition: settingsStore.getTransition() 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/application/client/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2022 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, .no-js body { 20 | font-family: "Source Sans Pro", sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /src/application/server/services/error.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | /** 8 | * Conform an error to Yahoo Fetchr requirements. 9 | * 10 | * @param {String | Object | Error} error - The error to conform, can be null. 11 | * @param {Number} [statusCode] - An optional statusCode to use to override 12 | * or define specific statusCode. 13 | * @returns {Falsy | Error | decorated} A Fetchr conformed error. 14 | */ 15 | export function decorateFetchrError (error, statusCode) { 16 | if (error) { 17 | error = typeof error === 'object' ? error : new Error(error.toString()); 18 | 19 | error.statusCode = error.statusCode || error.status || statusCode || 400; 20 | 21 | error.output = { 22 | message: error.message, 23 | full: error.toString() 24 | }; 25 | } 26 | 27 | return error; 28 | } 29 | 30 | export default decorateFetchrError; 31 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/sw-sync-push.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | const mockData = { 11 | error: false, 12 | value: undefined 13 | }; 14 | 15 | const mockError = new Error('mock error'); 16 | 17 | module.exports = { 18 | synchronize: function synchronizePushSubscription () { 19 | const error = mockData.error; 20 | const value = typeof mockData.value === 'undefined' ? 21 | false : mockData.value; 22 | 23 | return new Promise(function (resolve, reject) { 24 | if (error) { 25 | return reject(mockError); 26 | } 27 | 28 | return resolve(value); 29 | }); 30 | }, 31 | setEmulateError: function (error) { 32 | mockData.error = error; 33 | }, 34 | getMockError: function () { 35 | return mockError; 36 | }, 37 | setValue: function (value) { 38 | mockData.value = value; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/build/imagemin.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import gulp from 'gulp'; 6 | import gulpImagemin from 'gulp-imagemin'; 7 | 8 | /** 9 | * Factory for the imagemin task. 10 | * Optimizes applicable images for the project. 11 | * Outputs the optimized images to appropriate dist location. 12 | * 13 | * @param {Object} settings - The project settings. 14 | * @returns {Function} The imagemin task. 15 | */ 16 | export default function imageminTaskFactory (settings) { 17 | return function imagemin () { 18 | return gulp.src('**/*.{jpg,jpeg,png}', { 19 | cwd: settings.assets.images 20 | }) 21 | .pipe( 22 | gulpImagemin([ 23 | gulpImagemin.gifsicle(), 24 | gulpImagemin.mozjpeg({ quality: 60, progressive: true }), 25 | gulpImagemin.optipng(), 26 | gulpImagemin.svgo() 27 | ]) 28 | ) 29 | .pipe( 30 | gulp.dest(settings.dist.images) 31 | ); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/node_modules/utils/push.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | 6 | /** 7 | * Consistent method for returning a subscription id for a pushSubscription. 8 | * 9 | * @param {Object} subscription - The pushSubscription object. 10 | * @returns {String} The subscription id, null if no subscription supplied. 11 | */ 12 | export function getSubscriptionId (subscription) { 13 | if (!subscription) { 14 | return null; 15 | } 16 | 17 | let subscriptionId = null; 18 | 19 | if (subscription.endpoint) { 20 | let endpointSections = subscription.endpoint.split('/'); 21 | subscriptionId = endpointSections[endpointSections.length - 1]; 22 | } 23 | 24 | if (!subscriptionId && typeof subscription.getKey === 'function') { 25 | // This should be unique enough to act like an id for purpose. 26 | subscriptionId = subscription.getKey(); 27 | } 28 | 29 | return subscriptionId; 30 | } 31 | 32 | export default { 33 | getSubscriptionId 34 | }; 35 | -------------------------------------------------------------------------------- /src/application/components/PageContainer.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import sizeAction from 'application/actions/size'; 8 | import { fluxibleWindowResizeReporter } from 'react-element-size-reporter'; 9 | import Notification from './Notification'; 10 | 11 | class PageContainer extends React.Component { 12 | static get propTypes () { 13 | return { 14 | children: PropTypes.any.isRequired 15 | }; 16 | } 17 | 18 | render () { 19 | return ( 20 |
21 | {this.props.children} 22 | 23 |
24 | ); 25 | } 26 | } 27 | 28 | const pageContainer = fluxibleWindowResizeReporter( 29 | PageContainer, '.page', sizeAction, { 30 | resizeWait: 50, 31 | sizeReporter: { 32 | reportWidth: true, 33 | reportHeight: true, 34 | grow: { 35 | height: 10 36 | } 37 | } 38 | } 39 | ); 40 | 41 | export default pageContainer 42 | -------------------------------------------------------------------------------- /src/application/app.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import debugLib from 'debug'; 8 | import FluxibleApp from 'fluxible'; 9 | import fetchrPlugin from 'fluxible-plugin-fetchr'; 10 | import Application from './components/Application'; 11 | import BackgroundStore from './stores/BackgroundStore'; 12 | import ContentStore from './stores/ContentStore'; 13 | import ContactStore from './stores/ContactStore'; 14 | import RouteStore from './stores/RouteStore'; 15 | import ModalStore from './stores/ModalStore'; 16 | 17 | const debug = debugLib('app'); 18 | 19 | debug('Creating FluxibleApp'); 20 | const app = new FluxibleApp({ 21 | component: Application 22 | }); 23 | 24 | debug('Adding Plugins'); 25 | app.plug(fetchrPlugin({ xhrPath: '/_api' })); 26 | 27 | debug('Registering Stores'); 28 | app.registerStore(BackgroundStore); 29 | app.registerStore(ContentStore); 30 | app.registerStore(ContactStore); 31 | app.registerStore(RouteStore); 32 | app.registerStore(ModalStore); 33 | 34 | export default app; 35 | -------------------------------------------------------------------------------- /src/tests/unit/utils/codes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | 9 | describe('codes', () => { 10 | let codes, conformErrorStatus; 11 | 12 | before('codes', () => { 13 | codes = require('utils/codes'); 14 | conformErrorStatus = codes.conformErrorStatus; 15 | }); 16 | 17 | it('should expose expected operations', () => { 18 | expect(codes).to.respondTo('conformErrorStatus'); 19 | }); 20 | 21 | describe('conformErrorStatus', () => { 22 | it('should conform error status 404 to \'404\'', () => { 23 | const status = conformErrorStatus(404); 24 | expect(status).to.equal('404'); 25 | expect(status).to.not.equal(404); 26 | }); 27 | 28 | it('should conform any other status to \'500\'', () => { 29 | [ 30 | 0, 200, 300, 304, 400, 401, 403, '404', 410, 412, 499, 500, 501, 503 31 | ].forEach((status) => { 32 | expect(conformErrorStatus(status)).to.equal('500'); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/application/components/header/ModalLink.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { openModal as modalAction } from 'application/actions/modal'; 8 | 9 | class ModalLink extends React.Component { 10 | constructor (props) { 11 | super(props); 12 | this.clickHandler = this.clickHandler.bind(this); 13 | } 14 | 15 | static get propTypes () { 16 | return { 17 | data: PropTypes.object.isRequired, 18 | className: PropTypes.string, 19 | children: PropTypes.any 20 | }; 21 | } 22 | 23 | static get contextTypes () { 24 | return { 25 | executeAction: PropTypes.func.isRequired 26 | }; 27 | } 28 | 29 | render () { 30 | return ( 31 | 32 | {this.props.children} 33 | 34 | ); 35 | } 36 | 37 | clickHandler () { 38 | this.context.executeAction(modalAction, this.props.data); 39 | } 40 | } 41 | 42 | export default ModalLink; 43 | -------------------------------------------------------------------------------- /src/tests/unit/services/contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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('test/mocks'); 10 | 11 | describe('contact service', function() { 12 | var contact; 13 | 14 | before(function() { 15 | this.timeout(5000); 16 | 17 | mocks.serviceMail.begin(); 18 | contact = require('application/server/services/contact'); 19 | }); 20 | 21 | after(function() { 22 | mocks.serviceMail.end(); 23 | }); 24 | 25 | describe('object', function() { 26 | it('should have name and create members', function() { 27 | expect(contact.name).to.be.a('string'); 28 | expect(contact.create).to.be.a('function'); 29 | }); 30 | }); 31 | 32 | describe('create', function() { 33 | it('should return a valid response', function(done) { 34 | contact.create(null, null, {}, null, null, function(err) { 35 | if (err) { 36 | done(err); 37 | } 38 | done(); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /assets/images/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/application/actions/interface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | import { page } from './page'; 13 | 14 | const actions = Object.create(null); 15 | actions.page = page; 16 | 17 | /** 18 | * Get all actions available for backend reference. 19 | * 20 | * @returns {Object} Action creators available for backend reference. 21 | */ 22 | export function getActions () { 23 | return actions; 24 | } 25 | 26 | /** 27 | * Add or replace an action to make it available to backend reference. 28 | * 29 | * @param {String} name - The public name of the action. 30 | * @param {Function} action - The action creator to add. 31 | */ 32 | export function putAction (name, action) { 33 | actions[name] = action; 34 | } 35 | 36 | export default { 37 | getActions, 38 | putAction 39 | } 40 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/response.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/components/pages/settings/_styles.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /prestart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * To facilitate temporary, remote builds, update the output app symlink. 6 | */ 7 | /* eslint-disable no-console */ 8 | 9 | const fs = require('fs'); 10 | 11 | /** 12 | * Update the output symbolic link 13 | */ 14 | function updateLink () { 15 | try { 16 | fs.unlinkSync('./output/node_modules/application'); 17 | fs.symlinkSync( 18 | '../application', 19 | './output/node_modules/application', 20 | 'dir' 21 | ); 22 | console.log('*** Successfully updated output application symlink ***'); 23 | } catch(e){ 24 | console.error('*** FAILED to update output symlink ***'); 25 | console.error(e); 26 | } 27 | } 28 | 29 | /** 30 | * Conditionally update the application output symlink 31 | */ 32 | function conditionalLinkUpdate () { 33 | const shouldUpdate = 'DYNO' in process.env; 34 | 35 | if (shouldUpdate) { 36 | console.log('*** Updating symbolic output link ***'); 37 | updateLink(); 38 | } else { 39 | console.log('*** Skipping symbolic output link update ***'); 40 | } 41 | } 42 | 43 | conditionalLinkUpdate(); -------------------------------------------------------------------------------- /src/tests/unit/utils/push.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import push from 'utils/push'; 9 | 10 | describe('push', () => { 11 | it('should expose getSubscriptionId', () => { 12 | expect(push).to.respondTo('getSubscriptionId'); 13 | }); 14 | 15 | describe('getSubscriptionId', () => { 16 | const subId = '1234'; 17 | const subId2 = '5678'; 18 | const endpoint = `https://endpoint/${subId2}`; 19 | 20 | it('should return null if falsy subscription supplied', () => { 21 | expect(push.getSubscriptionId()).to.be.null; 22 | }); 23 | 24 | it('should return null if no endpoint or getKey', () => { 25 | expect(push.getSubscriptionId({})).to.be.null; 26 | }); 27 | 28 | it('should return subscriptionId from endpoint', () => { 29 | expect(push.getSubscriptionId({ 30 | endpoint 31 | })).to.equal(subId2); 32 | }); 33 | 34 | it('should use getKey if no endpoint', () => { 35 | expect(push.getSubscriptionId({ 36 | getKey: () => subId 37 | })).to.equal(subId); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/tests/unit/actions/size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import { createMockActionContext } from 'fluxible/utils'; 9 | import { updateSize as sizeAction } from 'application/actions/size'; 10 | import { BackgroundStore } from 'application/stores/BackgroundStore'; 11 | 12 | describe('size action', () => { 13 | const params = { 14 | width: 1, 15 | height: 2, 16 | top: 3, 17 | accumulate: false 18 | }; 19 | let context; 20 | 21 | // create the action context wired to BackgroundStore 22 | beforeEach(() => { 23 | context = createMockActionContext({ 24 | stores: [ BackgroundStore ] 25 | }); 26 | }); 27 | 28 | it('should update the background store', (done) => { 29 | // TODO: add listener to store to get the whole story 30 | context.executeAction(sizeAction, params, (err) => { 31 | if (err) { 32 | return done(err); 33 | } 34 | 35 | const store = context.getStore(BackgroundStore); 36 | expect(store.getTop()).to.equal(params.top); 37 | 38 | done(); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/build/webpack/inline.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import path from 'path'; 6 | import makeMode from './utils/mode'; 7 | import uglifyPluginFactory from './plugins/uglify'; 8 | 9 | /** 10 | * Generate the inline script bundle. 11 | * 12 | * @param {Object} settings - The project settings. 13 | * @param {String} type - One of ['dev', 'prod', 'perf']. 14 | * @returns {Object} The web pack config for the inline bundle. 15 | */ 16 | export default function inlineConfig (settings, type) { 17 | const config = { 18 | mode: makeMode(type), 19 | entry: `./${settings.src.inlineScript}`, 20 | output: { 21 | path: settings.webpack.absoluteOutputPath, 22 | filename: path.basename(settings.dist.inlineScript) 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.js$/, 28 | exclude: /^\/node_modules/, 29 | loader: 'babel-loader' 30 | } 31 | ] 32 | }, 33 | stats: 'verbose', 34 | optimization: type === 'prod' ? { 35 | minimizer: [ 36 | uglifyPluginFactory() 37 | ] 38 | } : undefined 39 | }; 40 | 41 | return config; 42 | } 43 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/request.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 || 'GET'; 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 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/elements.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import Input from './Input'; 7 | import Result from './Result'; 8 | 9 | const classes = { 10 | name: Input, 11 | email: Input, 12 | message: Input, 13 | result: Result 14 | }; 15 | 16 | const inputProps = { 17 | name: { 18 | inputElement: 'input', 19 | inputType: 'text', 20 | inputId: 'name-input' 21 | }, 22 | email: { 23 | inputElement: 'input', 24 | inputType: 'email', 25 | inputId: 'email-input' 26 | }, 27 | message: { 28 | inputElement: 'textarea', 29 | inputId: 'message-input' 30 | }, 31 | result: {} 32 | }; 33 | 34 | /** 35 | * Create a Contact Element 36 | * 37 | * @param {String} component - The name of the component to create. 38 | * @param {Object} props - The props to create the component with. 39 | * @returns {Object} A React Element for the given contact component name and props. 40 | */ 41 | export function createContactElement (component, props) { 42 | return React.createElement( 43 | classes[component], 44 | Object.assign(props, inputProps[component]) 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en-US", 3 | "short_name": "Contactor", 4 | "name": "Contactor Example PWA", 5 | "icons": [ 6 | { 7 | "src": "/public/images/android-chrome-36x36.png?v=gAA6rKkkBo", 8 | "sizes": "36x36", 9 | "type": "image/png", 10 | "density": 0.75 11 | }, 12 | { 13 | "src": "/public/images/android-chrome-48x48.png?v=gAA6rKkkBo", 14 | "sizes": "48x48", 15 | "type": "image/png", 16 | "density": 1 17 | }, 18 | { 19 | "src": "/public/images/android-chrome-72x72.png?v=gAA6rKkkBo", 20 | "sizes": "72x72", 21 | "type": "image/png", 22 | "density": 1.5 23 | }, 24 | { 25 | "src": "/public/images/android-chrome-96x96.png?v=gAA6rKkkBo", 26 | "sizes": "96x96", 27 | "type": "image/png", 28 | "density": 2 29 | }, 30 | { 31 | "src": "/public/images/android-chrome-144x144.png?v=gAA6rKkkBo", 32 | "sizes": "144x144", 33 | "type": "image/png", 34 | "density": 3 35 | }, 36 | { 37 | "src": "/public/images/android-chrome-192x192.png?v=gAA6rKkkBo", 38 | "sizes": "192x192", 39 | "type": "image/png", 40 | "density": 4 41 | } 42 | ], 43 | "start_url": "/?homescreen=1", 44 | "display": "standalone", 45 | "orientation": "portrait", 46 | "background_color": "#9C27B0", 47 | "theme_color": "#6A1B9A", 48 | "gcm_sender_id": "54583389178" 49 | } 50 | -------------------------------------------------------------------------------- /src/application/client/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import debugLib from 'debug'; 9 | import React from 'react'; 10 | import { render } from 'react-dom'; 11 | import { createElementWithContext } from 'fluxible-addons-react'; 12 | import app from 'application/app'; 13 | 14 | /** 15 | * This is assigned to window.AppMain via webpack. 16 | */ 17 | export default function main () { 18 | if (DEBUG) { 19 | window.React = React; // for chrome dev tool support 20 | debugLib.enable('*'); // show debug trail 21 | } 22 | 23 | const debug = debugLib('client'); 24 | const dehydratedState = window.App; // sent from the server 25 | 26 | debug('rehydrating app'); 27 | app.rehydrate(dehydratedState, (err, context) => { 28 | if (err) { 29 | throw err; 30 | } 31 | 32 | if (DEBUG) { 33 | window.context = context; 34 | } 35 | 36 | debug('rendering app'); 37 | render( 38 | createElementWithContext(context, { 39 | analytics: dehydratedState.analytics 40 | }), 41 | document.getElementById('application') 42 | ); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/application/components/header/Header.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import Ribbon from './Ribbon'; 8 | import Logo from './Logo'; 9 | import Nav from './Nav'; 10 | 11 | class Header extends React.Component { 12 | static get propTypes () { 13 | return { 14 | selected: PropTypes.string.isRequired, 15 | links: PropTypes.array.isRequired, 16 | models: PropTypes.object.isRequired, 17 | hasServiceWorker: PropTypes.bool.isRequired 18 | }; 19 | } 20 | 21 | render () { 22 | return ( 23 |
24 |
25 | 31 | 32 |
33 |
35 | ); 36 | } 37 | } 38 | 39 | export default Header; 40 | -------------------------------------------------------------------------------- /src/tests/unit/services/data/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | 9 | describe('data/utils', () => { 10 | let utils; 11 | 12 | before('utils', () => { 13 | utils = require('application/server/services/data/utils'); 14 | }); 15 | 16 | describe('objContains', () => { 17 | const testKey = 'testKey'; 18 | const testValue = 'testValue'; 19 | const test = { 20 | some: 'string' 21 | }; 22 | const object = { 23 | other: { 24 | test: { 25 | some: 'string' 26 | } 27 | }, 28 | test: {} 29 | }; 30 | 31 | before('objContains', () => { 32 | test[testKey] = testValue; 33 | object.test = Object.assign(object.test, test); 34 | }); 35 | 36 | it('should retrieve test object', () => { 37 | const result = utils.objContains(testKey, testValue, object); 38 | expect(result).to.eql(test); 39 | }); 40 | 41 | it('should returned undefined if not found', () => { 42 | const result = utils.objContains('nope', 'nothing', object); 43 | expect(result).to.be.undefined; 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/application/components/pages/settings/Topics.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | 7 | import PropTypes from 'prop-types'; 8 | 9 | class Topics extends React.Component { 10 | static get propTypes () { 11 | return { 12 | topics: PropTypes.array.isRequired, 13 | disabled: PropTypes.bool.isRequired, 14 | onChange: PropTypes.func.isRequired 15 | }; 16 | } 17 | 18 | render () { 19 | const topics = this.props.topics.map((topic) => { 20 | return ( 21 |
  • 22 |
    23 | 27 | 28 |
    29 |
    30 | {topic.label} 31 |
    32 |
  • 33 | ); 34 | }, this); 35 | 36 | return ( 37 | 40 | ); 41 | } 42 | } 43 | 44 | export default Topics; 45 | -------------------------------------------------------------------------------- /src/tests/unit/actions/init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import { createMockActionContext } from 'fluxible/utils'; 9 | import { init as initAction } from 'application/actions/init'; 10 | import { BackgroundStore } from 'application/stores/BackgroundStore'; 11 | 12 | describe('init action', function () { 13 | const params = { 14 | backgrounds: { 15 | serviceUrl: 'https://lorempixel.com', 16 | backgrounds: ['1', '2'] 17 | } 18 | }; 19 | let context; 20 | 21 | // create the action context wired to BackgroundStore 22 | beforeEach(() => { 23 | context = createMockActionContext({ 24 | stores: [ BackgroundStore ] 25 | }); 26 | }); 27 | 28 | it('should update the background store', (done) => { 29 | context.executeAction(initAction, params, (err) => { 30 | if (err) { 31 | return done(err); 32 | } 33 | 34 | const store = context.getStore(BackgroundStore); 35 | 36 | expect(store.getImageServiceUrl()).to.equal(params.backgrounds.serviceUrl); 37 | expect(Object.keys(store.backgroundUrls)).to.have.length(2); 38 | 39 | done(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/application/server/services/mail/mailer.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import mailer from 'nodemailer'; 8 | import configs from 'configs'; 9 | 10 | const contact = 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 | export function send (payload, done) { 23 | const 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 | export default { 41 | send 42 | }; 43 | -------------------------------------------------------------------------------- /src/application/components/footer/ByLine.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | 7 | import PropTypes from 'prop-types'; 8 | 9 | class ByLine extends React.Component { 10 | static get propTypes () { 11 | return { 12 | author: PropTypes.object.isRequired 13 | }; 14 | } 15 | 16 | render () { 17 | const byLine = this.props.author.byLine.replace( 18 | ` ${this.props.author.name}`, '' 19 | ); 20 | 21 | /* eslint-disable react/jsx-no-target-blank */ 22 | /* rel=noopener is desired, author.url wants referrer */ 23 | return ( 24 |
    25 | 26 | 27 | {byLine} 28 | 29 | 30 | 31 | {this.props.author.name} 32 | 33 | 34 | 35 | © 36 | 37 | 38 | {(new Date()).getFullYear()} 39 | 40 | 41 |
    42 | ); 43 | /* eslint-enable */ 44 | } 45 | } 46 | 47 | export default ByLine; 48 | -------------------------------------------------------------------------------- /src/tests/unit/services/data/markdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import mocks from 'test/mocks'; 9 | 10 | describe('markdown', () => { 11 | let remarkHtml; 12 | let markdown; 13 | const testMd = ` 14 | # Hello World 15 | 16 | ## This is A Test Heading 17 | 18 | This is a test paragraph. Whoopdie do. 19 | `; 20 | 21 | before(function () { 22 | this.timeout(5000); 23 | 24 | mocks.remarkable.begin(); 25 | markdown = require('application/server/services/data/markdown').markdown; 26 | remarkHtml = require('remark-html'); 27 | }); 28 | 29 | after(() => { 30 | mocks.remarkable.end(); 31 | }); 32 | 33 | it('should return some markup', () => { 34 | return markdown(testMd).then(markup => { 35 | expect(markup).to.contain(' { 40 | remarkHtml.mockError = true; 41 | return markdown(testMd).then(() => { 42 | expect.fail('expected markdown to reject'); 43 | }).catch(err => { 44 | expect(err.message).to.contain('error'); 45 | }).then(() => { 46 | remarkHtml.mockError = false; 47 | }); 48 | }) 49 | }); 50 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/cache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /* global Promise */ 6 | 'use strict'; 7 | 8 | var routesResponse = require('test/fixtures/routes-response'); 9 | 10 | var calledFind; 11 | var calledGet; 12 | var calledPut; 13 | 14 | module.exports = { 15 | mockReset: function () { 16 | calledFind = calledGet = calledPut = 0; 17 | delete this.findValue; 18 | }, 19 | mockCounts: function () { 20 | return { 21 | find: calledFind, 22 | get: calledGet, 23 | put: calledPut 24 | }; 25 | }, 26 | 27 | find: function () { 28 | calledFind++; 29 | return this.findValue; 30 | }, 31 | get: function (resource) { 32 | calledGet++; 33 | var result = 'hello world'; // ref: mocks/superagent.js defaultResponse 34 | 35 | if (resource === 'routes') { 36 | delete this.findValue; 37 | return routesResponse; 38 | } 39 | 40 | if (resource === 'find') { 41 | this.findValue = result; 42 | result = undefined; 43 | } 44 | 45 | if (resource === 'miss') { 46 | delete this.findValue; 47 | result = undefined; 48 | } 49 | 50 | return result; 51 | }, 52 | put: function () { 53 | calledPut++; 54 | return Promise.resolve(); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/tests/unit/services/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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('test/mocks'); 10 | 11 | describe('page service', function () { 12 | var page; 13 | 14 | before(function () { 15 | this.timeout(5000); 16 | 17 | mocks.serviceData.begin(); 18 | page = require('application/server/services/page'); 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(page.name).to.be.a('string'); 28 | expect(page.read).to.be.a('function'); 29 | }); 30 | }); 31 | 32 | describe('read', function () { 33 | it('should return a valid response', function (done) { 34 | page.read(null, null, { resource: 'home' }, null, function (err, data) { 35 | if (err) { 36 | done(err); 37 | } 38 | expect(data).to.be.an('object'); 39 | expect(data).to.have.property('models') 40 | .that.is.an('object'); 41 | expect(data).to.have.property('content') 42 | .that.is.a('string'); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/tests/unit/services/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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('test/mocks'); 11 | var routesResponse = require('test/fixtures/routes-response'); 12 | 13 | describe('routes service', function () { 14 | var routes; 15 | 16 | before(function () { 17 | this.timeout(5000); 18 | 19 | mocks.serviceData.begin(); 20 | routes = require('application/server/services/routes'); 21 | }); 22 | 23 | after(function () { 24 | mocks.serviceData.end(); 25 | }); 26 | 27 | describe('object', function () { 28 | it('should have name and read members', function () { 29 | expect(routes.name).to.be.a('string'); 30 | expect(routes.read).to.be.a('function'); 31 | }); 32 | }); 33 | 34 | describe('read', function () { 35 | it('should return a valid response', function (done) { 36 | routes.read(null, null, { resource: 'routes' }, null, function (err, data) { 37 | if (err) { 38 | done(err); 39 | } 40 | expect(data).to.be.an('object'); 41 | expect(JSON.stringify(routesResponse.home)).to.equal(JSON.stringify(data.home)); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/application/components/pages/ContentPage.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import Spinner from './Spinner'; 8 | 9 | class ContentPage extends React.Component { 10 | static get propTypes () { 11 | return { 12 | content: PropTypes.any, 13 | spinner: PropTypes.bool 14 | }; 15 | } 16 | 17 | render () { 18 | const content = this.renderContent(); 19 | 20 | return ( 21 |
    22 | {content} 23 |
    24 | ); 25 | } 26 | 27 | shouldComponentUpdate (nextProps) { 28 | const spinnerChange = this.props.spinner !== nextProps.spinner; 29 | const contentChange = this.props.content !== nextProps.content; 30 | return spinnerChange || contentChange; 31 | } 32 | 33 | /* eslint-disable react/no-danger-with-children */ 34 | renderContent () { 35 | if (this.props.spinner) { 36 | return ( 37 | 38 | ); 39 | } else { 40 | return ( 41 |
    42 |
    43 | ); 44 | } 45 | } 46 | /* eslint-enable react/no-danger-with-children */ 47 | } 48 | 49 | export default ContentPage; 50 | -------------------------------------------------------------------------------- /src/application/components/pages/settings/Switch.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | 7 | import PropTypes from 'prop-types'; 8 | 9 | class Switch extends React.Component { 10 | static get propTypes () { 11 | return { 12 | inputId: PropTypes.string.isRequired, 13 | checked: PropTypes.bool.isRequired, 14 | disabled: PropTypes.bool.isRequired, 15 | onChange: PropTypes.func.isRequired, 16 | label: PropTypes.string.isRequired, 17 | notice: PropTypes.string 18 | }; 19 | } 20 | 21 | render () { 22 | return ( 23 |
    24 |
    25 | 29 | 30 |
    31 |
    32 | {this.props.label} 33 |
    34 |
    37 | {this.props.notice} 38 |
    39 |
    40 | ); 41 | } 42 | } 43 | 44 | export default Switch; 45 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/utils/tests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 8 | /** 9 | * Extract comparable function text with code instrumentation considerations. 10 | */ 11 | function comparableFunctionText (functionText) { 12 | return functionText 13 | // remove any coverage statements 14 | .replace(/[^;]+(?:cov_[^;]+);/g, '') 15 | // remove whitespace 16 | .replace(/\s+/g, ''); 17 | } 18 | 19 | export function testTransform (expect, actual, expected) { 20 | Object.keys(expected).forEach((key) => { 21 | expect(actual[key].page).to.eql(expected[key].page); 22 | expect(actual[key].path).to.eql(expected[key].path); 23 | expect(actual[key].method).to.eql(expected[key].method); 24 | expect(actual[key].label).to.eql(expected[key].label); 25 | 26 | const expectedActionContents = 27 | comparableFunctionText(/\{([^}]+)\}/.exec(''+expected[key].action)[1]); 28 | 29 | expect(expectedActionContents).to.exist; 30 | expect(expectedActionContents).to.not.be.empty; 31 | 32 | expect(actual[key].action).to.be.a('function'); 33 | expect(actual[key].action.length).to.eql(expected[key].action.length); 34 | 35 | expect(comparableFunctionText(''+actual[key].action)) 36 | .to.contain(expectedActionContents); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/application/components/header/_logo.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 2 | // Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 3 | // 4 | // logo styles 5 | // ========================= 6 | 7 | .logo { 8 | @include vertical-block(center, shrink) { 9 | overflow-y: visible; 10 | overflow-x: visible; 11 | align-items: center; 12 | } 13 | margin: 0 2rem 0.8rem; 14 | 15 | @include breakpoint(medium) { 16 | margin: 0 2rem 1.5rem; 17 | } 18 | @media #{$height-constrained-phone} { 19 | margin: 0 1.2rem 0.5rem; 20 | } 21 | } 22 | 23 | .logo h1 { 24 | padding: 0.3em 2.2em 1em 0em; 25 | margin: 0; 26 | 27 | font-size: 1.8em; 28 | 29 | background: none, inline-image("logo.svg", "image/svg+xml") no-repeat 100% 80%; 30 | 31 | @include breakpoint(medium) { 32 | font-size: 3em; 33 | } 34 | @media #{$height-constrained-phone} { 35 | font-size: 1.4em; 36 | } 37 | } 38 | 39 | .logo .tagline { 40 | @include grid-content; 41 | overflow-y: visible; 42 | 43 | padding-left: 0; 44 | position: relative; 45 | margin-top: 1.2 * $rem-base * $base-line-height * -1; 46 | 47 | max-width: 80%; 48 | font-size: 85%; 49 | @include breakpoint(medium) { 50 | font-size: 100%; 51 | margin-top: 2 * $rem-base * $base-line-height * -1; 52 | } 53 | @media #{$height-constrained-phone} { 54 | margin-top: $rem-base * $base-line-height * -1; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/build/fixtures.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /* global Promise */ 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | import utils from 'utils/node'; 9 | 10 | /** 11 | * Factory for the fixtures task. 12 | * Runs each test fixture generator in generatorsDir in parallel. 13 | * Each fixtures generator is assumed to export a node style async function. 14 | * 15 | * @param {Object} settings - The project settings. 16 | * @returns {Function} The fixtures task. 17 | */ 18 | export default function fixturesTaskFactory (settings) { 19 | const generatorsDir = `./${settings.src.tests}/generators`; 20 | const options = { 21 | // 'script-filename.js': {} 22 | }; 23 | 24 | return function fixtures () { 25 | return utils.nodeCall(fs.readdir, generatorsDir).then((generators) => { 26 | return Promise 27 | .all(generators.map((generatorScript) => { 28 | const generator = path.resolve('.', generatorsDir, generatorScript); 29 | return utils.nodeCall( 30 | require(generator).run, 31 | options[generatorScript] 32 | ); 33 | })) 34 | .catch((error) => { 35 | throw new Error(`fixtures task failed: ${error}`); 36 | }) 37 | }).catch((error) => { 38 | throw new Error(`fixtures task failed: ${error}`); 39 | }); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/application/client/sw/assets.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 'sw/data' module is generated by the build @see src/build/service-worker. 7 | */ 8 | import toolbox from 'sw-toolbox'; 9 | import urlm from 'utils/urls'; 10 | import data from 'sw/data'; 11 | 12 | /** 13 | * Install route GET handlers for CDN requests and precache assets. 14 | * 15 | * Route handlers for CDN requests are installed everytime as a side effect 16 | * of setting up precaching. However, precaching is only carried out as a result 17 | * of an 'install' event (not everytime). 18 | * 19 | * @see sw-toolbox 20 | */ 21 | export function setupAssetRequests () { 22 | let hostname; 23 | 24 | toolbox.precache( 25 | data.assets 26 | .sort() 27 | .map(function (asset) { 28 | const next = urlm.getHostname(asset); 29 | 30 | if (hostname !== next) { 31 | hostname = next; 32 | // New hostname, so install GET handler for that host 33 | toolbox.router.get('*', toolbox.networkFirst, { 34 | origin: hostname, 35 | // any/all CDNs get 3 seconds max 36 | networkTimeoutSeconds: 3 37 | }); 38 | } 39 | 40 | // Precache the asset in 'install' 41 | return asset; 42 | }) 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/application/server/statics.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * static asset serving middleware. 6 | */ 7 | import glob from 'glob'; 8 | import express from 'express'; 9 | 10 | /** 11 | * Use simple static with far future expires. 12 | * If not found, check to see if a distinct, different version exists that would 13 | * satisfy the request. If so, serve that. 14 | * Expects to be mounted on settings.web.baseDir. 15 | * 16 | * @param {Object} settings - The application config settings. 17 | * @returns {Function} serveStatic middleware. 18 | */ 19 | export default function statics (settings) { 20 | const serveStatic = express.static( 21 | settings.dist.baseDir, { maxAge: settings.web.assetAge } 22 | ); 23 | 24 | return function serveStaticAsset (req, res, next) { 25 | serveStatic(req, res, (err) => { 26 | if (err) { 27 | return next(err); 28 | } 29 | // If a newer version exists, rewrite and serve that. 30 | const urlMatch = req.url.replace(/[a-f0-9]+\./, '*.'); 31 | glob(settings.dist.baseDir + urlMatch, { 32 | silent: true 33 | }, (matchError, matches) => { 34 | if (matchError || matches.length !== 1) { 35 | return next(); 36 | } 37 | req.url = matches[0].replace(settings.dist.baseDir, ''); 38 | return serveStatic(req, res, next); 39 | }); 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/subscription.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | pull_request: 4 | types: [closed] 5 | branches: 6 | - 'release**' 7 | - 'stage**' 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | env: 13 | DEPLOY_URL: ${{ (contains(github.ref, 'release') && 'https://pwa.localnerve.net') || 'https://pwa-stage.localnerve.net' }} 14 | APP_NAME: ${{ (contains(github.ref, 'release') && 'pwa') || 'pwa-stage' }} 15 | 16 | if: github.event.pull_request.merged 17 | steps: 18 | - name: Get Branch Name 19 | id: get_branch 20 | run: echo ::set-output name=BRANCH_NAME::${GITHUB_REF/refs\/heads\//} 21 | - uses: actions/checkout@v3 22 | - name: Setup Node 23 | uses: actions/setup-node@v3.0.0 24 | with: 25 | node-version: '12' 26 | - name: Echo Input 27 | run: | 28 | echo DEPLOY_URL=$DEPLOY_URL 29 | echo APP_NAME=$APP_NAME 30 | - name: Install Dependencies 31 | run: npm install 32 | - name: Deploy to Heroku 33 | if: ${{ success() }} 34 | uses: akhileshns/heroku-deploy@v3.12.12 35 | with: 36 | heroku_api_key: ${{ secrets.HEROKU_API_KEY }} 37 | heroku_app_name: ${{ env.APP_NAME }} 38 | heroku_email: 'alex@localnerve.com' 39 | branch: ${{ steps.get_branch.outputs.BRANCH_NAME }} 40 | healthcheck: ${{ format('https://{0}.herokuapp.com', env.APP_NAME) }} 41 | delay: 5 42 | - name: Post-deploy 43 | if: ${{ success() }} 44 | run: | 45 | echo GET '$DEPLOY_URL' 46 | curl -i $DEPLOY_URL 47 | -------------------------------------------------------------------------------- /src/application/server/services/data/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import debugLib from 'debug'; 6 | import cache from './cache-interface'; 7 | import fetchLib from './fetch'; 8 | 9 | const debug = debugLib('services:data:index'); 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 | export function fetch (params, callback) { 22 | debug(`fetching resource "${params.resource}"`); 23 | 24 | const 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 | const spec = cache.find(params.resource); 34 | if (spec) { 35 | params = spec; 36 | } 37 | } 38 | 39 | fetchLib.fetchOne(params, callback); 40 | } 41 | 42 | export const initialize = fetchLib.fetchMain; 43 | export const update = fetchLib.fetchAll; 44 | 45 | export default { 46 | fetch, 47 | initialize, 48 | update 49 | }; 50 | -------------------------------------------------------------------------------- /src/application/components/header/Nav.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import { NavLink } from 'fluxible-router'; 8 | import sizeAction from 'application/actions/size'; 9 | import { fluxibleWindowResizeReporter } from 'react-element-size-reporter'; 10 | import cx from 'classnames'; 11 | 12 | class Nav extends React.Component { 13 | static get propTypes () { 14 | return { 15 | selected: PropTypes.string.isRequired, 16 | links: PropTypes.array.isRequired 17 | }; 18 | } 19 | 20 | render () { 21 | const selected = this.props.selected, 22 | links = this.props.links, 23 | linkHTML = links.map(function (link) { 24 | return ( 25 |
  • 29 | {link.label} 30 |
  • 31 | ); 32 | }); 33 | return ( 34 | 37 | ); 38 | } 39 | } 40 | 41 | const nav = fluxibleWindowResizeReporter(Nav, '.navigation', sizeAction, { 42 | resizeWait: 50, 43 | sizeReporter: { 44 | reportTop: true, 45 | reportHeight: true, 46 | grow: { 47 | top: 5, 48 | height: 10 49 | } 50 | } 51 | }); 52 | 53 | export default nav; 54 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/Input.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | 7 | import PropTypes from 'prop-types'; 8 | 9 | class ContactInput extends React.Component { 10 | static get propTypes () { 11 | return { 12 | fieldValue: PropTypes.string, 13 | setInputReference: PropTypes.func.isRequired, 14 | label: PropTypes.object.isRequired, 15 | inputElement: PropTypes.string.isRequired, 16 | inputType: PropTypes.string, 17 | inputId: PropTypes.string.isRequired, 18 | focus: PropTypes.bool.isRequired 19 | }; 20 | } 21 | 22 | render () { 23 | const inputElement = React.createElement(this.props.inputElement, { 24 | type: this.props.inputType, 25 | id: this.props.inputId, 26 | name: this.props.inputId, 27 | key: this.props.inputId, 28 | title: this.props.label.help, 29 | placeholder: this.props.label.help, 30 | ref: this.props.setInputReference, 31 | className: 'form-value-element', 32 | autoFocus: this.props.focus, 33 | required: true, 34 | 'aria-required': true, 35 | defaultValue: this.props.fieldValue 36 | }); 37 | return ( 38 |
    39 | 42 | {inputElement} 43 |
    44 | ); 45 | } 46 | } 47 | 48 | export default ContactInput; 49 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/models-response.js: -------------------------------------------------------------------------------- 1 | /** This is a generated file **/ 2 | /** 3 | GENERATION_TIME = Tue May 17 2016 23:33:02 GMT-0400 (EDT) 4 | NODE_ENV = development 5 | FRED_URL = https://api.github.com/repos/localnerve/fred/contents/resources.json?ref=development 6 | **/ 7 | /*eslint quotes:0 */ 8 | module.exports = JSON.parse(JSON.stringify( 9 | {"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/react-pwa-reference/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/react-pwa-reference","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","split":"settings","resource":"settings","url":"https://api.github.com/repos/localnerve/fred/contents/pages/settings.json","format":"json","action":{"name":"settings","params":{}},"models":["LocalBusiness","SiteInfo","Settings"]}} 10 | )); -------------------------------------------------------------------------------- /src/application/components/_app.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/application/components/pages/settings/_topics.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/tests/unit/services/error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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('application/server/services/error').decorateFetchrError; 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 | -------------------------------------------------------------------------------- /src/node_modules/configs/images/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/404-response.js: -------------------------------------------------------------------------------- 1 | /** This is a generated file **/ 2 | /** 3 | GENERATION_TIME = Tue May 17 2016 23:33:02 GMT-0400 (EDT) 4 | NODE_ENV = development 5 | FRED_URL = https://api.github.com/repos/localnerve/fred/contents/resources.json?ref=development 6 | **/ 7 | /*eslint quotes:0 */ 8 | module.exports = JSON.parse(JSON.stringify( 9 | {"models":{"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/react-pwa-reference/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/react-pwa-reference","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","split":"settings","resource":"settings","url":"https://api.github.com/repos/localnerve/fred/contents/pages/settings.json","format":"json","action":{"name":"settings","params":{}},"models":["LocalBusiness","SiteInfo","Settings"]}},"content":"

    Not Found

    \n

    Sorry, but the page you are trying to view does not exist.

    \n"} 10 | )); -------------------------------------------------------------------------------- /src/tests/unit/utils/syncable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import syncable from 'utils/syncable'; 9 | 10 | describe('syncable', () => { 11 | it('should expose expected operations', () => { 12 | expect(syncable).to.respondTo('push'); 13 | expect(syncable).to.respondTo('contact'); 14 | expect(syncable).to.have.property('ops').that.is.an('object') 15 | .that.is.not.empty; 16 | expect(syncable).to.have.property('types').that.is.an('object') 17 | .that.is.not.empty; 18 | expect(syncable).to.have.property('propertyName').that.is.a('string') 19 | .that.is.not.empty; 20 | }); 21 | 22 | describe('push', () => { 23 | it('should do nothing for a bad input', () => { 24 | expect(syncable.push(null)).to.be.null; 25 | }); 26 | 27 | it('should create a fallback property for push', () => { 28 | const test = {}; 29 | const result = syncable.push(test); 30 | 31 | expect(result._fallback).to.have.property('type'); 32 | expect(result._fallback.type).to.equal('push'); 33 | }); 34 | }); 35 | 36 | describe('contact', () => { 37 | it('should do nothing for a bad input', () => { 38 | expect(syncable.contact(null)).to.be.null; 39 | }); 40 | 41 | it('should create a fallback property for contact', () => { 42 | const test = {}; 43 | const result = syncable.contact(test); 44 | 45 | expect(result._fallback).to.have.property('type'); 46 | expect(result._fallback.type).to.equal('contact'); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/node_modules/configs/analytics/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/node_modules/utils/urls.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | 6 | /** 7 | * Given a url, extract the hostname part. 8 | * 9 | * @param {String} url - The url from which to pull the hostname. 10 | * @returns {String} The hostname from the given url. 11 | */ 12 | export function getHostname (url) { 13 | return url.replace(/^[^/]*\/\/([^/?#:]+).*$/, (all, hostname) => hostname); 14 | } 15 | 16 | /** 17 | * Given a url, extract the last path segment. 18 | * Last path segment is the file name, or file, or any last part of the path 19 | * before ?|#|$ 20 | * 21 | * @param {String} url - The url from which to pull the last path segment. 22 | * Can be absolute or relative url, path, file, or path and qs/hash 23 | * @returns {String} The last path segment 24 | */ 25 | export function getLastPathSegment (url) { 26 | const matches = /(?:\/{1}|^)([\w\-.]+)\/?(?=\?|#|$)/.exec(url); 27 | return matches && matches[1] || ''; 28 | } 29 | 30 | /** 31 | * The significant hostname is the last hostname token before the TLD. 32 | * https://subdom.significant-hostname.com/someotherstuff 33 | * 34 | * @param {String} url - The url from which to pull the significant hostname. 35 | * @returns The second to last hostname token between dots for a given url. 36 | */ 37 | export function getSignificantHostname (url) { 38 | const hostname = getHostname(url); 39 | const names = hostname.split('.'); 40 | const significantIndex = names.length < 2 ? 0 : names.length - 2; 41 | return names[significantIndex]; 42 | } 43 | 44 | export default { 45 | getHostname, 46 | getSignificantHostname, 47 | getLastPathSegment 48 | }; 49 | -------------------------------------------------------------------------------- /src/application/stores/RouteStore.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import { RouteStore as FluxibleRouteStore } from 'fluxible-router'; 9 | import inherits from 'inherits'; 10 | import { createFluxibleRouteTransformer } from 'utils'; 11 | import actionsInterface from 'application/actions/interface'; 12 | 13 | const transformer = createFluxibleRouteTransformer({ 14 | actions: actionsInterface.getActions() 15 | }); 16 | 17 | /** 18 | * Creates a RouteStore. 19 | * 20 | * @class 21 | */ 22 | export function RouteStore () { 23 | FluxibleRouteStore.apply(this, arguments); 24 | } 25 | 26 | inherits(RouteStore, FluxibleRouteStore); 27 | 28 | RouteStore.storeName = FluxibleRouteStore.storeName; 29 | RouteStore.handlers = FluxibleRouteStore.handlers; 30 | 31 | /** 32 | * Dehydrates this object to state. 33 | * Transforms routes to json. 34 | * 35 | * @returns {Object} The RouteStore represented as state. 36 | */ 37 | RouteStore.prototype.dehydrate = function dehydrate () { 38 | const state = FluxibleRouteStore.prototype.dehydrate.apply(this, arguments); 39 | state.routes = transformer.fluxibleToJson(state.routes); 40 | return state; 41 | }; 42 | 43 | /** 44 | * Rehydrates this object from state. 45 | * Creates routes from json using transformer. 46 | */ 47 | RouteStore.prototype.rehydrate = function rehydrate (state) { 48 | state.routes = transformer.jsonToFluxible(state.routes); 49 | return FluxibleRouteStore.prototype.rehydrate.apply(this, arguments); 50 | }; 51 | 52 | export default RouteStore; 53 | -------------------------------------------------------------------------------- /src/tests/functional/run-parallel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | /*eslint no-console:0 */ 9 | /* global Promise */ 10 | 'use strict'; 11 | 12 | var exec = require('child_process').exec; 13 | var browserSpecs = require('./browsers'); 14 | 15 | var mochaArgs = process.argv[2]; 16 | var baseUrl = process.argv[3]; 17 | 18 | var browsers = Object.keys(browserSpecs); 19 | 20 | // context specific log 21 | function log (config, data) { 22 | config = (config + ' ').slice(0, 10); 23 | ('' + data).split(/(\r?\n)/g).forEach(function(line) { 24 | if (line.replace(/\033\[[0-9;]*m/g,'').trim().length >0) { 25 | console.log(config + ': ' + line.trimRight() ); 26 | } 27 | }); 28 | } 29 | 30 | // Run a mocha test for a given browser 31 | function runMocha (browser, baseUrl, done) { 32 | var env = JSON.parse(JSON.stringify(process.env)); 33 | env.TEST_BROWSER = browser; 34 | env.TEST_BASEURL = baseUrl; 35 | 36 | var mocha = exec('mocha ' + mochaArgs, { 37 | env: env 38 | }, done); 39 | 40 | mocha.stdout.on('data', log.bind(null, browser)); 41 | mocha.stderr.on('data', log.bind(null, browser)); 42 | } 43 | 44 | Promise 45 | .all(browsers.map(function (browser) { 46 | return new Promise(function (resolve, reject) { 47 | runMocha(browser, baseUrl, function (err) { 48 | if (err) { 49 | return reject(err); 50 | } 51 | resolve(); 52 | }); 53 | }); 54 | })) 55 | .then(function() { 56 | console.log('ALL TESTS SUCCESSFUL'); 57 | }); 58 | -------------------------------------------------------------------------------- /src/build/webpack/swReg.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import makeMode from './utils/mode'; 6 | import uglifyPluginFactory from './plugins/uglify'; 7 | import statsPluginFactory from './plugins/stats'; 8 | 9 | /** 10 | * Generate the webpack config for the service worker registration bundle. 11 | * 12 | * @param {Object} settings - The project settings. 13 | * @param {String} type - One of ['dev', 'prod', 'perf']. 14 | * @returns {Object} The web pack config for the sw reg bundle. 15 | */ 16 | export default function swRegConfig (settings, type) { 17 | const devtoolModuleFilenameTemplate = 'webpack:///sw-reg/[resource-path]'; 18 | 19 | const config = { 20 | mode: makeMode(type), 21 | entry: { 22 | swReg: `./${settings.src.serviceWorker.registration}` 23 | }, 24 | output: { 25 | path: settings.webpack.absoluteOutputPath, 26 | filename: type === 'prod' ? '[name].[chunkhash].min.js' : '[name].js', 27 | publicPath: settings.web.scripts, 28 | devtoolModuleFilenameTemplate: type === 'prod' 29 | ? undefined : devtoolModuleFilenameTemplate 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.js$/, 35 | exclude: /^\/node_modules/, 36 | loader: 'babel-loader' 37 | } 38 | ] 39 | }, 40 | devtool: type === 'prod' ? undefined : 'source-map', 41 | stats: 'verbose', 42 | plugins: [ 43 | statsPluginFactory(settings, false) 44 | ], 45 | optimization: type === 'prod' ? { 46 | minimizer: [ 47 | uglifyPluginFactory() 48 | ] 49 | } : undefined 50 | }; 51 | 52 | return config; 53 | } 54 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/requestLib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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('test/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 Request () { 18 | this.__options = {}; 19 | } 20 | 21 | Request.prototype = { 22 | get: function get (options, cb) { 23 | var body; 24 | var incomingMessage; 25 | var url = options.url || options.uri; 26 | 27 | // echo options for usage audit 28 | this.__options = options; 29 | 30 | // The client should not be relying on the incomingMessage if error. 31 | if (!this.emulateError) { 32 | incomingMessage = { 33 | statusCode: 200 34 | }; 35 | } 36 | 37 | if (!this.noData) { 38 | body = { 39 | content: responses[url] || defaultResponse 40 | }; 41 | } 42 | 43 | cb( 44 | this.emulateError ? new Error('mock error') : null, 45 | incomingMessage, 46 | body 47 | ); 48 | }, 49 | /*** 50 | * Mock control and audit only 51 | */ 52 | get url () { 53 | return this.__options.url; 54 | }, 55 | setEmulateError: function (value) { 56 | this.emulateError = value; 57 | }, 58 | setNoData: function (value) { 59 | this.noData = value; 60 | } 61 | }; 62 | 63 | module.exports = new Request(); 64 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/500-response.js: -------------------------------------------------------------------------------- 1 | /** This is a generated file **/ 2 | /** 3 | GENERATION_TIME = Tue May 17 2016 23:33:02 GMT-0400 (EDT) 4 | NODE_ENV = development 5 | FRED_URL = https://api.github.com/repos/localnerve/fred/contents/resources.json?ref=development 6 | **/ 7 | /*eslint quotes:0 */ 8 | module.exports = JSON.parse(JSON.stringify( 9 | {"models":{"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/react-pwa-reference/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/react-pwa-reference","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","split":"settings","resource":"settings","url":"https://api.github.com/repos/localnerve/fred/contents/pages/settings.json","format":"json","action":{"name":"settings","params":{}},"models":["LocalBusiness","SiteInfo","Settings"]}},"content":"

    Whoops!

    \n

    We are experiencing some technical difficulties, possibly due to a network or service interruption.

    \n

    Sorry for the inconvenience, Please try again later.

    \n"} 10 | )); -------------------------------------------------------------------------------- /src/tests/unit/utils/splits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import { createMockActionContext } from 'fluxible/utils'; 9 | import { splitHandlers as splits } from 'utils/splits'; 10 | 11 | describe('splits', () => { 12 | it('should expose settings split', () => { 13 | expect(splits).to.respondTo('settings'); 14 | }); 15 | 16 | describe('settings', () => { 17 | let context; 18 | const payload = { 19 | action: { 20 | name: 'settings' 21 | }, 22 | component: 'settings' 23 | }; 24 | const action = (context, payload, done) => { 25 | expect(context).to.respondTo('dispatch'); 26 | expect(context).to.respondTo('getStore'); 27 | expect(context).to.respondTo('executeAction'); 28 | if (payload.emulateError) { 29 | return done(new Error('mock')); 30 | } 31 | return done(); 32 | }; 33 | 34 | before(() => { 35 | context = createMockActionContext(); 36 | }); 37 | 38 | it('should resolve successfully', (done) => { 39 | splits.settings(context, payload, action).then(() => { 40 | done(); 41 | }).catch((error) => { 42 | done(error); 43 | }); 44 | }); 45 | 46 | it('should reject as expected', (done) => { 47 | payload.emulateError = true; 48 | 49 | function complete (error) { 50 | delete payload.emulateError; 51 | done(error); 52 | } 53 | 54 | splits.settings(context, payload, action).then(() => { 55 | complete(new Error('should have thrown an error')); 56 | }).catch(() => { 57 | complete(); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/sw-init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * Mock init 6 | */ 7 | /* global Promise */ 8 | const defaultInitData = { 9 | timestamp: 0, 10 | stores: {}, 11 | apis: {} 12 | }; 13 | 14 | const mockData = { 15 | error: false, 16 | initDataValue: undefined 17 | }; 18 | 19 | const mockError = new Error('mock error'); 20 | 21 | module.exports = { 22 | initCommand: function mockInitCommand (payload, handler) { 23 | const promise = new Promise(function (resolve, reject) { 24 | if (mockData.error) { 25 | return reject(mockError); 26 | } 27 | resolve(); 28 | }); 29 | 30 | return promise.then(() => { 31 | return handler({ 32 | error: null 33 | }); 34 | }).catch((error) => { 35 | return handler({ 36 | error: error.toString() 37 | }); 38 | }); 39 | }, 40 | 41 | initData: function mockInitData () { 42 | const value = typeof mockData.initDataValue === 'undefined' ? 43 | defaultInitData : mockData.initDataValue; 44 | 45 | return new Promise(function (resolve, reject) { 46 | 47 | if (mockData.error) { 48 | return reject(new Error('mock error')); 49 | } 50 | 51 | resolve(value); 52 | }); 53 | }, 54 | 55 | setEmulateError: function (error) { 56 | mockData.error = error; 57 | }, 58 | 59 | getMockError: function () { 60 | return mockError; 61 | }, 62 | 63 | setMockInitData: function (timestamp, stores, apis) { 64 | if (!timestamp && !stores && !apis) { 65 | mockData.initDataValue = undefined; 66 | return; 67 | } 68 | 69 | mockData.initDataValue = { 70 | timestamp, 71 | stores, 72 | apis 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/fluxible-routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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('application/actions/interface').getActions(); 13 | 14 | var params = { 15 | resource: 'test', 16 | key: '/path/to/test' 17 | }; 18 | 19 | var action = actions.page; 20 | 21 | // This code is symbolicly compared to method in fluxibleRouteTransformer 22 | function makeAction () { 23 | var copyParams = JSON.parse(JSON.stringify(params)); 24 | return function dynAction (context, payload, done) { 25 | context.executeAction(action, copyParams, done); 26 | }; 27 | } 28 | 29 | module.exports = { 30 | home: { 31 | path: '/', 32 | method: 'get', 33 | page: 'home', 34 | label: 'Home', 35 | pageTitle: '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 | pageTitle: 'About', 49 | component: 'ContentPage', 50 | mainNav: true, 51 | background: '4', 52 | order: 1, 53 | priority: 1, 54 | action: makeAction() 55 | }, 56 | contact: { 57 | path: '/contact', 58 | method: 'get', 59 | page: 'contact', 60 | label: 'Contact', 61 | pageTitle: 'Contact', 62 | component: 'Contact', 63 | mainNav: true, 64 | background: '5', 65 | order: 2, 66 | priority: 1, 67 | action: makeAction() 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/application/actions/contact.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import debugLib from 'debug'; 6 | import syncable from 'utils/syncable'; 7 | 8 | const debug = debugLib('actions:contact'); 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), {}, {}, (err) => { 22 | if (err) { 23 | debug('dispatching CREATE_CONTACT_FAILURE'); 24 | context.dispatch('CREATE_CONTACT_FAILURE', fields); 25 | return done(); 26 | } 27 | 28 | debug('dispatching CREATE_CONTACT_SUCCESS'); 29 | context.dispatch('CREATE_CONTACT_SUCCESS', fields); 30 | done(); 31 | } 32 | ); 33 | } 34 | 35 | /** 36 | * Perform the contact action. 37 | * 38 | * @param {Object} context - The fluxible context. 39 | * @param {Object} payload - The action payload. 40 | * @param {Object} payload.fields - The contact fields. 41 | * @param {Boolean} payload.complete - Flag indicating contact field gathering is complete. 42 | * @param {Function} done - The callback to execute on completion. 43 | */ 44 | export function contact (context, payload, done) { 45 | debug('dispatching UPDATE_CONTACT_FIELDS', payload.fields); 46 | context.dispatch('UPDATE_CONTACT_FIELDS', payload.fields); 47 | 48 | if (!payload.complete) { 49 | return done(); 50 | } 51 | 52 | serviceRequest(context, payload.fields, done); 53 | } 54 | 55 | export default contact; 56 | -------------------------------------------------------------------------------- /src/application/server/rewrites.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | * 5 | * Rewrite middleware. 6 | */ 7 | import rewrite from 'connect-modrewrite'; 8 | 9 | /** 10 | * Basic rewrite rules. 11 | * 12 | * @param {Object} settings - The application config settings. 13 | * @returns {Function} Basic rewrite middleware. 14 | */ 15 | export function rewriteBasics (settings) { 16 | const rewriteRules = [ 17 | // rewrite root image requests to settings.web.images 18 | '^/([^\\/]+\\.(?:png|jpg|jpeg|webp|ico|svg|gif)(?:\\?.*)?$) ' + 19 | settings.web.images + '/$1 [NC L]', 20 | // alias home to root 21 | '^/home/?$ / [L]', 22 | // forbid 404 and 500 direct requests 23 | '^/(?:404|500)/?$ [F L]' 24 | ]; 25 | 26 | return rewrite(rewriteRules); 27 | } 28 | 29 | /** 30 | * Transforms root /sw.js request to the actual asset location. 31 | * In addition, handle sw source map. 32 | * Service worker rewrites delayed so assets.json not required on app start. 33 | * 34 | * @param {Object} settings - The application config settings. 35 | * @returns {Function} Service Worker rewrite middleware. 36 | */ 37 | export function rewriteServiceWorker (settings) { 38 | return function applyServiceWorkerRewrite (req, res, next) { 39 | const swRule = new RegExp( 40 | '^(/' + settings.web.assets.swMainScript(true) + ')$', 'i' 41 | ); 42 | 43 | if (swRule.test(req.url)) { 44 | req.url = req.url.replace(swRule, settings.web.assets.swMainScript()); 45 | } else { 46 | let reSourceMap = new RegExp( 47 | '^(/' + settings.web.assets.swMainScriptMap(true) + ')$', 'i' 48 | ); 49 | if (reSourceMap.test(req.url)) { 50 | req.url = req.url.replace(reSourceMap, settings.web.assets.swMainScriptMap()); 51 | } 52 | } 53 | next(); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/tests/unit/sw/utils/idb.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import mocks from 'test/mocks'; 9 | 10 | describe('sw/utils/idb', () => { 11 | let treoMock, idb; 12 | 13 | before('setup sw/utils/idb', function () { 14 | this.timeout(5000); 15 | 16 | mocks.swUtilsIdbTreo.begin(); 17 | 18 | idb = require('application/client/sw/node_modules/sw/utils/idb'); 19 | treoMock = require('treo'); 20 | treoMock.setValue('some value'); 21 | }); 22 | 23 | after(() => { 24 | mocks.swUtilsIdbTreo.end(); 25 | }); 26 | 27 | it('should export expected things', () => { 28 | expect(idb.stores).to.be.an('object').that.is.not.empty; 29 | expect(idb).to.respondTo('all'); 30 | expect(idb).to.respondTo('batch'); 31 | expect(idb).to.respondTo('del'); 32 | expect(idb).to.respondTo('get'); 33 | expect(idb).to.respondTo('put'); 34 | }); 35 | 36 | describe('method', () => { 37 | const method = 'get'; 38 | let storeName; 39 | 40 | before(() => { 41 | storeName = Object.keys(idb.stores)[0]; 42 | }); 43 | 44 | afterEach(() => { 45 | expect(treoMock.status.getCloseCount()).to.equal(1); 46 | }); 47 | 48 | it('should execute successfully', (done) => { 49 | idb[method](storeName).then((value) => { 50 | expect(value).to.be.a('string').that.is.not.empty; 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should fail successfully', (done) => { 56 | idb[method](storeName, 'emulateError') 57 | .then(() => { 58 | done(new Error('expected failure')); 59 | }) 60 | .catch((error) => { 61 | expect(error.message).to.be.an('string').that.is.not.empty; 62 | done(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/Nav.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import cx from 'classnames'; 8 | 9 | class ContactNav extends React.Component { 10 | static get propTypes () { 11 | return { 12 | stepCurrent: PropTypes.number.isRequired, 13 | stepFinal: PropTypes.number.isRequired, 14 | onPrevious: PropTypes.func.isRequired, 15 | nav: PropTypes.object.isRequired 16 | }; 17 | } 18 | 19 | shouldComponentUpdate (nextProps) { 20 | return nextProps.stepCurrent !== this.props.stepCurrent; 21 | } 22 | 23 | render () { 24 | const last = this.props.stepCurrent === this.props.stepFinal; 25 | const nav = last ? [] : this.renderContactNav(); 26 | 27 | return ( 28 |
    32 | {nav} 33 |
    34 | ); 35 | } 36 | 37 | renderContactNav () { 38 | const complete = this.props.stepCurrent === this.props.stepFinal - 1; 39 | const nextText = complete ? this.props.nav.next.last : 40 | this.props.nav.next.text; 41 | 42 | return [ 43 | , 51 | 58 | ]; 59 | } 60 | } 61 | 62 | export default ContactNav; 63 | -------------------------------------------------------------------------------- /src/tests/unit/stores/RouteStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import { RouteStore } from 'application/stores/RouteStore'; 9 | import { getActions } from 'application/actions/interface'; 10 | import { createFluxibleRouteTransformer } from 'utils'; 11 | 12 | import routesResponseFixture from 'test/fixtures/routes-response'; 13 | import { testTransform } from 'test/utils/tests'; 14 | 15 | const transformer = createFluxibleRouteTransformer({ 16 | actions: getActions() 17 | }); 18 | 19 | describe('Route store', () => { 20 | let routesResponse, storeInstance; 21 | 22 | beforeEach(() => { 23 | storeInstance = new RouteStore(); 24 | }); 25 | 26 | it('should instantiate correctly', () => { 27 | expect(storeInstance).to.be.an('object'); 28 | expect(storeInstance._handleReceiveRoutes).to.be.a('function'); 29 | expect(storeInstance.dehydrate).to.be.a('function'); 30 | expect(storeInstance.rehydrate).to.be.a('function'); 31 | }); 32 | 33 | describe('with routes', () => { 34 | beforeEach(() => { 35 | // clone the routes-response fixture data 36 | routesResponse = JSON.parse(JSON.stringify(routesResponseFixture)); 37 | }); 38 | 39 | it('should dehydrate routes to json', () => { 40 | storeInstance._handleReceiveRoutes(transformer.jsonToFluxible(routesResponse)); 41 | const state = storeInstance.dehydrate(); 42 | expect(state.routes).to.eql(routesResponse); 43 | }); 44 | 45 | it('should rehydrate to fluxible routes', () => { 46 | storeInstance.rehydrate({ 47 | routes: routesResponse, 48 | currentNavigate: { 49 | url: '/bogus' 50 | } 51 | }); 52 | 53 | testTransform( 54 | expect, storeInstance._routes, transformer.jsonToFluxible(routesResponse) 55 | ); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/application/client/sw/activate.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import toolbox from 'sw-toolbox'; 14 | import debugLib from 'sw/utils/debug'; 15 | import data from 'sw/data'; 16 | 17 | const dataManifest = data.manifest || { 18 | debug: false, 19 | cacheId: 'app' 20 | }; 21 | const cacheId = dataManifest.cacheId; 22 | 23 | const debug = debugLib('activate'); 24 | 25 | /** 26 | * Remove any previous cache that might have been under this code's governance. 27 | * Relies on how cacheName is constructed in index.js 28 | * 29 | * Previous caches are identified using the following: 30 | * 1. starts with cacheId 31 | * 2. contains the current scope. 32 | * 3. does not end with the 'inactive$$$'. 33 | * 4. is not exactly the current sw-toolbox cacheName 34 | */ 35 | self.addEventListener('activate', function (event) { 36 | debug('activate event fired, scope: ', toolbox.options.scope); 37 | 38 | if (!toolbox.options.scope) { 39 | return debug('Unable to determine cache scope, no action taken'); 40 | } 41 | 42 | event.waitUntil( 43 | caches.keys().then(function (cacheNames) { 44 | return Promise.all( 45 | cacheNames.map(function (cacheName) { 46 | if (cacheName.indexOf(cacheId) === 0 && 47 | cacheName.indexOf(toolbox.options.scope) > -1 && 48 | !/inactive\${3}$/i.test(cacheName) && 49 | cacheName !== toolbox.options.cache.name 50 | ) { 51 | debug('deleting old cache ', cacheName); 52 | return caches.delete(cacheName); 53 | } 54 | }) 55 | ); 56 | }) 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /src/application/components/footer/_styles.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 - 2022 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 | margin: 0; 49 | 50 | @include breakpoint(medium) { 51 | margin: 0 auto; 52 | min-width: 40rem; 53 | // hack for IE, ref: #41 54 | width: 0; 55 | } 56 | 57 | [itemProp="addressLocality"]::after { 58 | content: ","; 59 | } 60 | [itemProp="addressRegion"]::before, 61 | [itemProp="addressRegion"]::after { 62 | content: " "; 63 | } 64 | } 65 | 66 | .att-line { 67 | padding-top: 0.25rem; 68 | 69 | span { 70 | line-height: 1.2; 71 | font-size: 1.2rem; 72 | font-weight: bold; 73 | 74 | @media #{$height-constrained-phone} { 75 | font-size: inherit; 76 | } 77 | } 78 | } 79 | 80 | .license { 81 | font-size: 75%; 82 | } 83 | 84 | .by-line { 85 | padding-bottom: 0.25rem; 86 | 87 | @media #{$height-constrained-phone} { 88 | font-size: 75%; 89 | overflow-y: visible; 90 | } 91 | 92 | .by-line-items { 93 | span::after { 94 | content: ' '; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/build/webpack/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /* eslint-disable no-console */ 6 | import path from 'path'; 7 | import webpack from 'webpack'; 8 | import inlineConfig from './inline'; 9 | import mainConfig from './main'; 10 | import swMainConfig from './swMain'; 11 | import swRegConfig from './swReg'; 12 | 13 | const configFactoryGroups = { 14 | main: [ 15 | inlineConfig, 16 | mainConfig, 17 | swRegConfig 18 | ], 19 | sw: [ 20 | swMainConfig 21 | ] 22 | }; 23 | 24 | /** 25 | * Creates a task for webpack bundling/compiling groups of output targets. 26 | * 27 | * @param {String} group - The grouping of compilations, ['main', 'sw']. 28 | * @param {Object} settings - The project settings. 29 | * @param {String} target - ['dev', 'perf', 'prod']. 30 | * @returns nothing, calls done when complete. 31 | */ 32 | export default function webpackTaskFactory (group, settings, target) { 33 | return function taskWebpack (done) { 34 | const configFactories = configFactoryGroups[group]; 35 | 36 | // force dist.scripts absolute output path 37 | const absoluteOutputPath = path.join(process.cwd(), settings.dist.scripts); 38 | settings.webpack = settings.webpack || {}; 39 | settings.webpack.absoluteOutputPath = absoluteOutputPath; 40 | 41 | // multi-compiler invocation 42 | webpack( 43 | configFactories.map(configFactory => configFactory(settings, target)), 44 | (err, stats) => { 45 | if (err) { 46 | console.error('webpack error', err); 47 | } 48 | if (stats) { 49 | const info = stats.toJson(); 50 | if (stats.hasErrors()) { 51 | console.error('webpack compile errors:', info.errors); 52 | } 53 | if (stats.hasWarnings()) { 54 | console.warn('webpack compile warnings:', info.warnings); 55 | } 56 | } 57 | done(err); 58 | } 59 | ); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/application/client/sw/init/update.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import * as stores from './stores'; 9 | import * as db from 'sw/utils/db'; 10 | import debugLib from 'sw/utils/debug'; 11 | 12 | const debug = debugLib('init:update'); 13 | const apis = db.stores.init({ key: 'apis' }); 14 | const timestamp = db.stores.init({ key: 'timestamp' }); 15 | 16 | /** 17 | * Update the IndexedDB init IDBObjectStore if appropriate. 18 | * 19 | * @param {Object} payload - Initial payload 20 | * @param {Object} payload.stores - The flux stores for the app. 21 | * @param {Object} payload.apis - The api information for the app. 22 | * @param {Number} payload.timestamp - The timestamp of the app state. 23 | * @return {Boolean} A promise that resolves to boolean indicating if init 24 | * got new data and should run. 25 | */ 26 | export default function update (payload) { 27 | debug('Running update'); 28 | 29 | return timestamp.read().then((currentTs) => { 30 | // If the incoming timestamp is newer, it's on. 31 | return payload.timestamp && currentTs < payload.timestamp; 32 | }, () => { 33 | // No existing timestamp found, so brand new - it's on! 34 | return true; 35 | }).then((shouldUpdate) => { 36 | if (shouldUpdate) { 37 | // Update the init.timestamp 38 | return timestamp.update(payload.timestamp) 39 | .then(() => { 40 | // Update init.stores 41 | return stores.updateInitStores(payload.stores); 42 | }) 43 | .then(() => { 44 | // Update init.apis 45 | return apis.update(payload.apis).then(() => { 46 | return true; 47 | }); 48 | }).catch((error) => { 49 | debug('Failed to update', error); 50 | throw error; // rethrow 51 | }); 52 | } else { 53 | return false; 54 | } 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/home-response.js: -------------------------------------------------------------------------------- 1 | /** This is a generated file **/ 2 | /** 3 | GENERATION_TIME = Tue May 17 2016 23:33:02 GMT-0400 (EDT) 4 | NODE_ENV = development 5 | FRED_URL = https://api.github.com/repos/localnerve/fred/contents/resources.json?ref=development 6 | **/ 7 | /*eslint quotes:0 */ 8 | module.exports = JSON.parse(JSON.stringify( 9 | {"models":{"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/react-pwa-reference/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/react-pwa-reference","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","split":"settings","resource":"settings","url":"https://api.github.com/repos/localnerve/fred/contents/pages/settings.json","format":"json","action":{"name":"settings","params":{}},"models":["LocalBusiness","SiteInfo","Settings"]}},"content":"

    Welcome To The Demo

    \n

    This is an isomorphic web app that demonstrates performance techniques and multiple backing services.

    \n

    Data Driven Flux & React

    \n

    All the content you see, including navigation, comes from a backend data service.\nThis application uses Facebook's React and Yahoo's Fluxible.

    \n"} 10 | )); -------------------------------------------------------------------------------- /src/build/webpack/swMain.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import makeMode from './utils/mode'; 6 | import uglifyPluginFactory from './plugins/uglify'; 7 | import statsPluginFactory from './plugins/stats'; 8 | 9 | /** 10 | * Generate the service worker main script bundle. 11 | * 12 | * @param {Object} settings - The project settings. 13 | * @param {String} type - One of ['dev', 'prod', 'perf']. 14 | * @returns {Object} The web pack config for the service worker bundle. 15 | */ 16 | export default function swMainConfig (settings, type) { 17 | const devtoolModuleFilenameTemplate = 'webpack:///sw/[resource-path]'; 18 | 19 | const config = { 20 | mode: makeMode(type), 21 | entry: { 22 | sw: `./${settings.src.serviceWorker.entry}` 23 | }, 24 | output: { 25 | path: settings.webpack.absoluteOutputPath, 26 | publicPath: settings.web.scripts, 27 | // One name to rule them all 28 | filename: '[name].js', 29 | devtoolModuleFilenameTemplate: type === 'prod' 30 | ? undefined : devtoolModuleFilenameTemplate 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.json$/, 36 | exclude: /^\/node_modules/, 37 | type: 'json' 38 | }, 39 | { 40 | test: /\.js$/, 41 | loader: 'string-replace-loader', 42 | options: { 43 | search: '__TEST__', 44 | replace: '""' 45 | } 46 | }, 47 | { 48 | test: /\.js$/, 49 | exclude: /(^\/node_modules|\.json$)/, 50 | loader: 'babel-loader' 51 | } 52 | ] 53 | }, 54 | devtool: type === 'prod' ? undefined : 'source-map', 55 | target: 'webworker', 56 | stats: 'verbose', 57 | plugins: [ 58 | statsPluginFactory(settings, false) 59 | ], 60 | optimization: type === 'prod' ? { 61 | minimizer: [ 62 | uglifyPluginFactory() 63 | ] 64 | } : undefined 65 | }; 66 | 67 | return config; 68 | } 69 | -------------------------------------------------------------------------------- /src/application/components/footer/LocalBusiness.jsx: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | 7 | import PropTypes from 'prop-types'; 8 | 9 | class LocalBusiness extends React.Component { 10 | static get propTypes () { 11 | return { 12 | business: PropTypes.object.isRequired 13 | }; 14 | } 15 | 16 | render () { 17 | const uriMailTo = `mailto:${this.props.business.email}`; 18 | const uriTel = `tel:+1-'${this.props.business.telephone}`; 19 | 20 | return ( 21 |
    25 |
    26 | 27 | {this.props.business.legalName} 28 | 29 |
    30 | 31 | {this.props.business.address.streetAddress} 32 | 33 |
    34 | 35 | {this.props.business.address.addressLocality} 36 | 37 | 38 | {this.props.business.address.addressRegion} 39 | 40 | 41 | {this.props.business.address.postalCode} 42 | 43 |
    44 |
    45 |
    46 |
    47 | 48 | 49 | {this.props.business.email} 50 | 51 | 52 | 53 | 54 | {this.props.business.telephone} 55 | 56 | 57 |
    58 |
    59 | ); 60 | } 61 | } 62 | 63 | export default LocalBusiness; 64 | -------------------------------------------------------------------------------- /src/application/actions/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import debugLib from 'debug'; 6 | 7 | const debug = debugLib('actions:page'); 8 | 9 | /** 10 | * Perform the service request for the page action. 11 | * 12 | * @param {Object} context - The fluxible action context. 13 | * @param {Object} payload - The action payload. 14 | * @param {String} payload.resource - The name of the content resource. 15 | * @param {String} payload.pageTitle - The page title. 16 | * @param {Function} done - The callback to execute on request completion. 17 | */ 18 | function serviceRequest (context, payload, done) { 19 | debug('Page service request start'); 20 | 21 | context.service.read('page', payload, {}, function (err, data) { 22 | debug('Page service request complete', data); 23 | 24 | if (err) { 25 | return done(err); 26 | } 27 | 28 | if (!data) { 29 | debug('no data found', payload.resource); 30 | 31 | const noData = new Error('Page not found'); 32 | noData.statusCode = 404; 33 | return done(noData); 34 | } 35 | 36 | context.dispatch('RECEIVE_PAGE_CONTENT', { 37 | resource: payload.resource, 38 | data: data 39 | }); 40 | 41 | return done(); 42 | }); 43 | } 44 | 45 | /** 46 | * The page action. 47 | * 48 | * @param {Object} context - The fluxible action context. 49 | * @param {Object} payload - The action payload. 50 | * @param {String} payload.resource - The name of the content resource. 51 | * @param {String} payload.pageTitle - The page title. 52 | * @param {Function} done - The callback to execute on action completion. 53 | */ 54 | export function page (context, payload, done) { 55 | const data = context.getStore('ContentStore').get(payload.resource); 56 | 57 | if (data) { 58 | debug(`Found ${payload.resource} in cache`); 59 | 60 | context.dispatch('RECEIVE_PAGE_CONTENT', { 61 | resource: payload.resource, 62 | data: data 63 | }); 64 | 65 | return done(); 66 | } 67 | 68 | serviceRequest(context, payload, done); 69 | } 70 | 71 | export default page; 72 | -------------------------------------------------------------------------------- /src/application/components/pages/contact/Steps.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import cx from 'classnames'; 8 | 9 | class ContactSteps extends React.Component { 10 | static get propTypes () { 11 | return { 12 | steps: PropTypes.array.isRequired, 13 | stepCurrent: PropTypes.number.isRequired, 14 | stepFinal: PropTypes.number.isRequired, 15 | failure: PropTypes.bool.isRequired, 16 | resultMessage: PropTypes.string, 17 | retry: PropTypes.func.isRequired 18 | }; 19 | } 20 | 21 | shouldComponentUpdate (nextProps) { 22 | return nextProps.stepCurrent !== this.props.stepCurrent || 23 | nextProps.failure !== this.props.failure; 24 | } 25 | 26 | render () { 27 | const contactSteps = this.renderContactSteps(); 28 | return ( 29 | 32 | ); 33 | } 34 | 35 | renderContactSteps () { 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 | const 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 | export default ContactSteps; 68 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/settings-response.js: -------------------------------------------------------------------------------- 1 | /** This is a generated file **/ 2 | /** 3 | GENERATION_TIME = Tue May 17 2016 23:33:02 GMT-0400 (EDT) 4 | NODE_ENV = development 5 | FRED_URL = https://api.github.com/repos/localnerve/fred/contents/resources.json?ref=development 6 | **/ 7 | /*eslint quotes:0 */ 8 | module.exports = JSON.parse(JSON.stringify( 9 | {"models":{"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/react-pwa-reference/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/react-pwa-reference","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","split":"settings","resource":"settings","url":"https://api.github.com/repos/localnerve/fred/contents/pages/settings.json","format":"json","action":{"name":"settings","params":{}},"models":["LocalBusiness","SiteInfo","Settings"]}},"content":{"name":"settings","heading":"Settings","settingsNotSupported":"Settings Not Available On This Device","pushNotifications":{"enable":"Enable Push Notifications","notificationsNotSupported":"Notifications Not Supported","notificationsBlocked":"Notifications Blocked","pushMessagingNotSupported":"Push Messaging Not Supported","topics":[{"label":"Alerts","tag":"push-alerts-tag"},{"label":"Upcoming Events","tag":"push-upcoming-events-tag"}]},"backgroundSync":{"enable":"Enable Background Sync","notSupported":"Background Sync Not Supported"},"demo":{"pushNotification":true,"backgroundSync":true}}} 10 | )); -------------------------------------------------------------------------------- /src/tests/unit/services/data/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | 7 | import { expect } from 'chai'; 8 | import mocks from 'test/mocks'; 9 | 10 | describe('data/index', () => { 11 | let data, cache, fetchLib; 12 | 13 | before(function () { 14 | this.timeout(5000); 15 | 16 | mocks.fetch.begin(); 17 | data = require('application/server/services/data'); 18 | cache = require('./cache-interface'); 19 | fetchLib = require('./fetch'); 20 | }); 21 | 22 | after(() => { 23 | mocks.fetch.end(); 24 | }); 25 | 26 | beforeEach(() => { 27 | cache.mockReset(); 28 | fetchLib.mockReset(); 29 | }); 30 | 31 | describe('fetch', () => { 32 | it('should pull from cache if exists', (done) => { 33 | data.fetch({}, (err, res) => { 34 | if (err) { 35 | done(err); 36 | } 37 | 38 | expect(res).to.equal(cache.get()); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('should fetch if not in cache', (done) => { 44 | data.fetch({ resource: 'miss' }, (err, res) => { 45 | if (err) { 46 | done(err); 47 | } 48 | 49 | expect(res).to.equal('fetch'); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('should fetch using find spec if not in cache', (done) => { 55 | data.fetch({ resource: 'find' }, (err, res) => { 56 | if (err) { 57 | done(err); 58 | } 59 | 60 | const callCounts = cache.mockCounts(); 61 | const params = fetchLib.mockParams(); 62 | 63 | expect(callCounts.get).to.equal(1); 64 | expect(callCounts.find).to.equal(1); 65 | expect(params).to.equal(cache.find()); 66 | expect(res).to.equal('fetch'); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('initialize', () => { 73 | it('should initialize', (done) => { 74 | data.initialize(done); 75 | }); 76 | }); 77 | 78 | describe('update', () => { 79 | it('should update', (done) => { 80 | data.update(done); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/application/server/sitemap.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | import debugLib from 'debug'; 11 | import urlLib from 'url'; 12 | import sitemapLib from 'sitemap-xml'; 13 | import utils from 'utils/node'; 14 | import configs from 'configs'; 15 | import serviceData from './services/data'; 16 | 17 | const debug = debugLib('server:sitemap'); 18 | 19 | const config = configs.create(); 20 | const settings = config.settings; 21 | 22 | /** 23 | * Handle requests for sitemap.xml. 24 | * 25 | * @param {Object} req - The request object, not used. 26 | * @param {Object} res - The response object. 27 | * @param {Object} next - The next object. 28 | */ 29 | export default function sitemap (req, res, next) { 30 | debug('Read routes'); 31 | 32 | utils 33 | .nodeCall(serviceData.fetch, { 34 | resource: config.data.FRED.mainResource 35 | }) 36 | .then((result) => { 37 | const routes = result.content, 38 | ssl = settings.web.ssl || settings.web.sslRemote, 39 | stream = sitemapLib(); 40 | 41 | res.header('Content-Type', 'text/xml'); 42 | stream.pipe(res); 43 | 44 | Object.keys(routes) 45 | .filter((key) => { 46 | return routes[key].mainNav; 47 | }) 48 | .forEach((key) => { 49 | stream.write({ 50 | loc: urlLib.format({ 51 | protocol: ssl ? 'https' : 'http', 52 | hostname: settings.web.appHostname, 53 | pathname: routes[key].path 54 | }), 55 | priority: routes[key].siteMeta ? 56 | routes[key].siteMeta.priority : 1.0, 57 | changefreq: routes[key].siteMeta ? 58 | routes[key].siteMeta.changefreq : 'monthly' 59 | }); 60 | }); 61 | 62 | stream.end(); 63 | }) 64 | .catch((err) => { 65 | debug('Request failed: ', err); 66 | err.status = err.statusCode = (err.statusCode || err.status || 500); 67 | next(err); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/application/actions/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | import debugLib from 'debug'; 6 | import { createFluxibleRouteTransformer } from 'utils'; 7 | 8 | const debug = debugLib('actions:routes'); 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 | export function routes (context, payload, done) { 24 | // This is done late in case routes (this) in interface, TODO: revisit. 25 | const actions = require('application/actions/interface').getActions(); 26 | 27 | const transformer = (typeof payload.transform === 'function' ? 28 | payload.transform : createFluxibleRouteTransformer({ 29 | actions 30 | }).jsonToFluxible); 31 | 32 | if (payload.routes) { 33 | let fluxibleRoutes = payload.routes; 34 | 35 | if (payload.transform) { 36 | debug('transforming routes'); 37 | 38 | fluxibleRoutes = transformer(payload.routes); 39 | } 40 | 41 | context.dispatch('RECEIVE_ROUTES', fluxibleRoutes); 42 | return done(); 43 | } 44 | 45 | debug('Routes request start'); 46 | context.service.read('routes', payload, {}, function (err, routes) { 47 | debug('Routes request complete'); 48 | 49 | if (err) { 50 | return done(err); 51 | } 52 | 53 | const fluxibleRoutes = transformer(routes); 54 | context.dispatch('RECEIVE_ROUTES', fluxibleRoutes); 55 | done(null, fluxibleRoutes); 56 | }); 57 | } 58 | 59 | export default routes; 60 | -------------------------------------------------------------------------------- /src/node_modules/configs/index.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/build/nodemon.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /* global Set */ 6 | import path from 'path'; 7 | import gulpNodemon from 'gulp-nodemon'; 8 | 9 | /** 10 | * Factory for the nodemon task. 11 | * 12 | * @param {Object} settings - The project settings. 13 | * @param {String} target - One of ['dev', 'debug', 'perf', 'prod']. 14 | * @returns {Function} The nodemon task. 15 | */ 16 | export default function nodemonTaskFactory (settings, target) { 17 | const legacyWatchOptions = { 18 | legacyWatch: true, 19 | pollingInterval: 250 20 | }; 21 | 22 | const debugTarget = ['debug', 'inspect'].indexOf(target) > -1; 23 | 24 | const options = { 25 | ignore: [ 26 | settings.src.serviceWorker.data, 27 | settings.src.serviceWorker.precache, 28 | settings.src.assetsJson, 29 | settings.src.assetsRevManifest 30 | ], 31 | ignoreRoot: [ 32 | 'build', 33 | 'tests', 34 | 'node_modules/application' 35 | ], 36 | verbose: true, 37 | ext: 'js jsx scss', 38 | watch: settings.src.baseDir, 39 | tasks: (changedFiles) => { 40 | const buildTarget = debugTarget ? 'dev' : target; 41 | const tasks = new Set(); 42 | 43 | changedFiles.forEach((file) => { 44 | if (/\.js.?$/.test(file)) { 45 | if (/\/sw\/?/.test(path.dirname(file))) { 46 | tasks.add(`bundlesSw_${buildTarget}`); 47 | } else { 48 | tasks.add(`bundlesMain_${buildTarget}`); 49 | } 50 | } else { 51 | // Must be scss 52 | tasks.add(`ccss_${buildTarget}`); 53 | } 54 | }); 55 | 56 | return Array.from(tasks); 57 | } 58 | }; 59 | 60 | // This is a workaround for an OSX/chokidar issue I'm experiencing (#217) 61 | if (process.platform === 'darwin') { 62 | Object.assign(options, legacyWatchOptions); 63 | } 64 | 65 | if (debugTarget) { 66 | options.nodeArgs = ['--debug-brk']; 67 | if (target === 'inspect') { 68 | options.nodeArgs.push('--inspect'); 69 | } 70 | } 71 | 72 | return function nodemon (done) { 73 | gulpNodemon(options); 74 | done(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/mocks/sw-utils-idb-treo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/about-response.js: -------------------------------------------------------------------------------- 1 | /** This is a generated file **/ 2 | /** 3 | GENERATION_TIME = Tue May 17 2016 23:33:02 GMT-0400 (EDT) 4 | NODE_ENV = development 5 | FRED_URL = https://api.github.com/repos/localnerve/fred/contents/resources.json?ref=development 6 | **/ 7 | /*eslint quotes:0 */ 8 | module.exports = JSON.parse(JSON.stringify( 9 | {"models":{"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/react-pwa-reference/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/react-pwa-reference","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","split":"settings","resource":"settings","url":"https://api.github.com/repos/localnerve/fred/contents/pages/settings.json","format":"json","action":{"name":"settings","params":{}},"models":["LocalBusiness","SiteInfo","Settings"]}},"content":"

    About This Demo

    \n

    This demo is only meant to be a reference application. It serves as a possible source of inspiration for solving problems during isomorphic application development, or as a basis for a progressive application.

    \n

    The outgoing mail queue is currently disconnected from this application on Heroku, so contact form completions will not succeed.

    \n

    Please visit the project documentation for more detailed info, or contact me @localnerve.

    \n"} 10 | )); -------------------------------------------------------------------------------- /src/tests/unit/services/subscription.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016 - 2022 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('test/mocks'); 10 | 11 | describe('subscription service', function () { 12 | var subscription; 13 | 14 | before(function () { 15 | this.timeout(5000); 16 | 17 | mocks.serviceSubscription.begin(); 18 | subscription = require('application/server/services/subscription'); 19 | }); 20 | 21 | after(function () { 22 | mocks.serviceSubscription.end(); 23 | }); 24 | 25 | function subCall (method, done) { 26 | var args = []; 27 | if (method === 'read' || method === 'delete') { 28 | args.push( 29 | null, null, {}, null 30 | ); 31 | } else { 32 | args.push( 33 | null, null, {}, {}, null 34 | ); 35 | } 36 | args.push(function (err) { 37 | if (err) { 38 | return done(err); 39 | } 40 | done(); 41 | }); 42 | subscription[method].apply(subscription, args); 43 | } 44 | 45 | describe('object', function () { 46 | it('should have name and create members', function () { 47 | expect(subscription.name).to.be.a('string'); 48 | expect(subscription.create).to.be.a('function'); 49 | expect(subscription.read).to.be.a('function'); 50 | expect(subscription.update).to.be.a('function'); 51 | expect(subscription.delete).to.be.a('function'); 52 | }); 53 | }); 54 | 55 | describe('create', function () { 56 | it('should return a valid response', function (done) { 57 | subCall('create', done); 58 | }); 59 | }); 60 | 61 | describe('read', function () { 62 | it('should return a valid response', function (done) { 63 | subCall('read', done); 64 | }); 65 | }); 66 | 67 | describe('update', function () { 68 | it('should return a valid response', function (done) { 69 | subCall('update', done); 70 | }); 71 | }); 72 | 73 | describe('delete', function () { 74 | it('should return a valid response', function (done) { 75 | subCall('delete', done); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/tests/workers/contact/contact.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) 2016 - 2022 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 | /*eslint-disable no-console */ 19 | const spawn = require('child_process').spawn; 20 | 21 | require('@babel/register')({ 22 | presets: [ 23 | '@babel/env' 24 | ], 25 | ignore: [] 26 | }); 27 | 28 | const mail = require('application/server/services/mail'); 29 | const contact = require('configs').create().contact; 30 | 31 | const workerProcess = 'application/server/workers/contact/bin/contact'; 32 | const workTime = 10000; 33 | 34 | const prereqs = contact.mail.username() && contact.mail.password() && 35 | contact.mail.service(); 36 | 37 | if (!prereqs) { 38 | console.error( 39 | 'mail service environmental prerequisites missing. Check environment.' 40 | ); 41 | console.error('mail config'); 42 | console.error(`service = ${contact.mail.service()}`); 43 | console.error(`to = ${contact.mail.to()}`); 44 | console.error(`from = ${contact.mail.from()}`); 45 | console.error(`username = ${contact.mail.username()}`); 46 | console.error(`password = ${contact.mail.password()}`); 47 | process.exit(); 48 | } 49 | 50 | mail.send({ 51 | name: 'Manual Test', 52 | email: 'manual@test.local', 53 | message: 'This is a test message from the manual test harness.' 54 | }, function (err) { 55 | if (err) { 56 | throw err; 57 | } 58 | 59 | const cp = spawn(require.resolve(workerProcess)); 60 | 61 | cp.on('close', function () { 62 | console.log(`${workerProcess} complete`); 63 | process.exit(); 64 | }); 65 | 66 | setTimeout(function () { 67 | cp.kill('SIGINT'); 68 | }, workTime); 69 | }); 70 | -------------------------------------------------------------------------------- /src/tests/node_modules/test/fixtures/routes-response.js: -------------------------------------------------------------------------------- 1 | /** This is a generated file **/ 2 | /** 3 | GENERATION_TIME = Tue May 17 2016 23:33:02 GMT-0400 (EDT) 4 | NODE_ENV = development 5 | FRED_URL = https://api.github.com/repos/localnerve/fred/contents/resources.json?ref=development 6 | **/ 7 | /*eslint quotes:0 */ 8 | module.exports = JSON.parse(JSON.stringify( 9 | {"404":{"path":"/404","method":"get","page":"404","label":"Not Found","pageTitle":"Page Not Found","component":"ContentPage","mainNav":false,"background":"","order":0,"priority":0,"action":{"name":"page","params":{"resource":"404","url":"https://api.github.com/repos/localnerve/fred/contents/pages/404.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"]}}},"500":{"path":"/500","method":"get","page":"500","label":"Error","pageTitle":"Application Error","component":"ContentPage","mainNav":false,"background":"","order":0,"priority":0,"action":{"name":"page","params":{"resource":"500","url":"https://api.github.com/repos/localnerve/fred/contents/pages/500.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"]}}},"home":{"path":"/","method":"get","page":"home","label":"Home","pageTitle":"An Example Isomorphic Application","component":"ContentPage","mainNav":true,"background":"3.jpg","order":0,"priority":1,"siteMeta":{"priority":1,"changefreq":"weekly"},"action":{"name":"page","params":{"resource":"home","url":"https://api.github.com/repos/localnerve/fred/contents/pages/home.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"]}}},"about":{"path":"/about","method":"get","page":"about","label":"About","pageTitle":"About","component":"ContentPage","mainNav":true,"background":"4.jpg","order":1,"priority":1,"siteMeta":{"priority":0.5,"changefreq":"monthly"},"action":{"name":"page","params":{"resource":"about","url":"https://api.github.com/repos/localnerve/fred/contents/pages/about.md","format":"markdown","models":["LocalBusiness","SiteInfo","Settings"]}}},"contact":{"path":"/contact","method":"get","page":"contact","label":"Contact","pageTitle":"Contact","component":"Contact","mainNav":true,"background":"5.jpg","order":2,"priority":1,"siteMeta":{"priority":0.8,"changefreq":"monthly"},"action":{"name":"page","params":{"resource":"contact","url":"https://api.github.com/repos/localnerve/fred/contents/pages/contact.json","format":"json","models":["LocalBusiness","SiteInfo","Settings"]}}}} 10 | )); --------------------------------------------------------------------------------