├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .postcssrc.js ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── logo.png ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── e2e ├── .editorconfig ├── .eslintrc.js └── appStartup.e2e.js ├── jsconfig.json ├── netlify.toml ├── package-lock.json ├── package.json ├── src ├── .editorconfig ├── .eslintrc.js ├── .stylelintrc.js ├── assets │ └── logo.png ├── components │ ├── App.vue │ ├── animations │ │ └── Animation.vue │ ├── containers │ │ ├── Card.vue │ │ └── Page.vue │ ├── index.js │ ├── pages │ │ ├── PageDemo.vue │ │ └── PageHome.vue │ ├── snippets │ │ ├── Bitmap.vue │ │ ├── Dump.vue │ │ ├── Ellipsis.vue │ │ ├── ExternalLink.vue │ │ ├── Icon.vue │ │ ├── Spinner.vue │ │ └── Vector.vue │ └── transitions │ │ ├── CustomTransition.vue │ │ └── Fade.vue ├── config │ ├── analytics.js │ ├── build.js │ ├── dev │ │ ├── analytics.js │ │ ├── build.js │ │ ├── meta.js │ │ ├── paths.js │ │ ├── router.js │ │ └── styles.js │ ├── meta.js │ ├── paths.js │ ├── router.js │ ├── styles.js │ └── tooling │ │ ├── aliases.js │ │ ├── manifest.js │ │ ├── offline.js │ │ ├── robotsTxt.js │ │ ├── routes.js │ │ ├── sitemap.js │ │ └── svgo.js ├── directives │ └── global │ │ ├── imagesLoaded.js │ │ └── index.js ├── filters │ └── global │ │ ├── ceil.js │ │ ├── decimal.js │ │ ├── floor.js │ │ ├── index.js │ │ └── round.js ├── fonts │ └── .gitkeep ├── index.html.ejs ├── main.js ├── mixins │ ├── global │ │ ├── classes.js │ │ └── index.js │ └── persist.js ├── models │ └── DemoObject.js ├── services │ ├── events.js │ ├── network.js │ ├── time.js │ └── viewport.js ├── store │ ├── .eslintrc.js │ ├── index.js │ └── myModule.js ├── styles │ ├── defaults │ │ ├── body.scss │ │ ├── forms.scss │ │ ├── headings.scss │ │ ├── links.scss │ │ └── lists.scss │ ├── global.scss │ ├── keyframes │ │ ├── keyframes-fade-in.scss │ │ ├── keyframes-pulse.scss │ │ └── keyframes-spin.scss │ ├── mixins │ │ └── mixin-type.scss │ ├── shared.scss │ ├── transitions │ │ └── transition-fade.scss │ ├── utilities │ │ └── utility-bodytext.scss │ └── variables.scss ├── svg │ ├── Cog.svg │ ├── Logo.svg │ └── index.js ├── util │ ├── blur.js │ ├── clearSelection.js │ ├── composeClassnames.js │ ├── createInstance.js │ ├── destroyInstances.js │ ├── eventHasMetaKey.js │ ├── isAbsoluteUrl.js │ └── trimWhitespace.js └── vendor │ ├── offline-plugin-runtime.js │ ├── vue-analytics.js │ ├── vue-meta.js │ ├── vue-router.js │ ├── vue.js │ └── vuex.js ├── static ├── .gitkeep ├── _redirects ├── favicon.png ├── icon-114.png ├── icon-120.png ├── icon-144.png ├── icon-150.png ├── icon-152.png ├── icon-192.png ├── icon-256.png ├── icon-310.png ├── icon-48.png ├── icon-57.png ├── icon-70.png ├── icon-72.png ├── icon-76.png └── icon-96.png ├── test ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ └── runner.js └── unit │ ├── .eslintrc │ ├── jest.conf.js │ ├── setup.js │ └── stubs │ ├── scssShared.stub.js │ └── svg.stub.js └── unit ├── .editorconfig ├── .eslintrc.js ├── components ├── pages │ └── PageHome.spec.js └── snippets │ ├── Bitmap.spec.js │ └── ExternalLink.spec.js ├── services ├── events.spec.js └── network.spec.js ├── store └── myModule.spec.js └── util ├── composeClassnames.spec.js ├── isAbsoluteUrl.spec.js └── trimWhitespace.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | rules: { 20 | // allow async-await 21 | 'generator-star-spacing': 'off', 22 | // allow debugger during development 23 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | # .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.14.2 2 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "postcss-import": {}, 7 | "autoprefixer": {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "wayou.vscode-todo-highlight", 4 | "shinnn.stylelint", 5 | "dbaeumer.vscode-eslint", 6 | "EditorConfig.editorconfig", 7 | "octref.vetur" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/tooling/dev-server.js" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | // NOTE: This will be in version control and apply for all devs 3 | { 4 | "files.exclude": { 5 | 6 | // Defaults 7 | "**/.git": true, 8 | "**/.DS_Store": true, 9 | 10 | // This excludes all node_modules folders from the explore tree 11 | "**/node_modules": true, 12 | 13 | // Root only 14 | // "node_modules": true, 15 | 16 | // Build dir 17 | "dist": true 18 | 19 | }, 20 | 21 | // Treat certain configuration files as JSON 22 | "files.associations": { 23 | "*.babelrc": "json", 24 | "*.eslintrc": "json", 25 | "*.htmllintrc": "json", 26 | "*.stylelintrc": "json" 27 | }, 28 | 29 | // Vetur tooling configuration 30 | "vetur.format.scriptInitialIndent": false, 31 | "vetur.format.styleInitialIndent": false, 32 | "vetur.validation.script": true, 33 | "vetur.validation.style": true, 34 | "vetur.validation.template": true, 35 | 36 | // Emmet should be enabled on .vue files 37 | "emmet.syntaxProfiles": { 38 | "vue-html": "html", 39 | "vue": "html" 40 | }, 41 | 42 | // CSS linting with Stylelint extension 43 | "stylelint.enable": true, 44 | "css.validate": false, 45 | "scss.validate": false, 46 | 47 | "eslint.options": { 48 | "extensions": [ 49 | ".js", 50 | ".vue" 51 | ] 52 | }, 53 | 54 | // Settings for VS Code's ESLint extension 55 | "eslint.validate": [ 56 | 57 | // Defaults 58 | { 59 | "language": "javascript", 60 | "autoFix": true 61 | }, 62 | { 63 | "language": "javascriptreact", 64 | "autoFix": true 65 | }, 66 | 67 | // Inline HTML 68 | { 69 | "language": "html", 70 | "autoFix": true 71 | }, 72 | 73 | // Should also go through .vue files 74 | "vue" 75 | 76 | ] 77 | 78 | } 79 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // https: //code.visualstudio.com/docs/editor/tasks 3 | "version": "0.1.0", 4 | "tasks": [ 5 | 6 | // // Show all problems in the project 7 | // { 8 | // "taskName": "Show all problems in project", 9 | // "command": "eslint", 10 | // "isShellCommand": true, 11 | // "args": [ 12 | // "-w", 13 | // "-p", 14 | // ".", 15 | // "--noEmit" 16 | // ], 17 | // "showOutput": "silent", 18 | // "isBackground": true, 19 | // "problemMatcher": "$eslint-stylish" 20 | // } 21 | 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bellevue 2 | 3 | **Bellevue** is a full-featured frontend project template for modern single-page applications built on Vue.js and Webpack. 4 | 5 | - Demo: [bellevue.netlify.com](https://bellevue.netlify.com/demo) 6 | - Documentation: [eiskis.gitbooks.io/bellevue](https://eiskis.gitbooks.io/bellevue/) 7 | - Source and issues: [github.com/Eiskis/bellevue](https://github.com/Eiskis/bellevue) 8 | 9 | Bellevue is based on the official `vuejs-templates/webpack` template, but extends it with many additional tooling features such as preconfigured SCSS support, SVG pipeline, extensive linting and centralised configuration. 10 | 11 | While the official template is only a _Hello world_, Bellevue's goal is to set you up with a well-documented, [thought-out application structure](https://eiskis.gitbooks.io/bellevue/app/overview.html) with all the patterns you need for building a complex application such as SVG compilation, routing, state management, persistence and more (see [feature comparison](https://eiskis.gitbooks.io/bellevue/overview/comparison.html)). 12 | 13 | ## Requirements 14 | 15 | 1. The Node version defined in [.nvmrc](./nvmrc) 16 | 17 | **Protip:** manage node versions easily with [nvm](https://github.com/creationix/nvm). 18 | 19 | ## Build Setup 20 | 21 | ``` bash 22 | # install dependencies 23 | npm install 24 | 25 | # serve with hot reload at localhost:8080 26 | npm run dev 27 | 28 | # serve with hot reload at custom port 29 | PORT=1234 npm run dev 30 | 31 | # build for production with minification 32 | npm run build 33 | 34 | # build for production and view the bundle analyzer report 35 | npm run build:report 36 | 37 | # run unit tests 38 | npm run unit 39 | 40 | # run unit tests and show coverage report 41 | npm run unit:report 42 | 43 | # run unit tests and with hot reload (`jest --watch`) 44 | # NOTE: You have more options in the terminal after you run this command 45 | # NOTE: You can change this to `--watchAll` in `package.json` in case of issues 46 | # NOTE: See https://github.com/facebook/jest/issues/4883 47 | npm run unit:watch 48 | 49 | # run e2e tests 50 | npm run e2e 51 | 52 | # run all tests 53 | npm test 54 | ``` 55 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using `ts-loader`, setting this to `true` will make TypeScript errors show up during build 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 5 | 6 | hotClient.subscribe(function (event) { 7 | if (event.action === 'reload') { 8 | window.location.reload() 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | const config = require('../config') 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 7 | } 8 | 9 | const opn = require('opn') 10 | const path = require('path') 11 | const express = require('express') 12 | const webpack = require('webpack') 13 | const proxyMiddleware = require('http-proxy-middleware') 14 | const webpackConfig = (process.env.NODE_ENV === 'testing' || process.env.NODE_ENV === 'production') 15 | ? require('./webpack.prod.conf') 16 | : require('./webpack.dev.conf') 17 | 18 | // default port where dev server listens for incoming traffic 19 | const port = process.env.PORT || config.dev.port 20 | // automatically open browser, if not set will be false 21 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 22 | // Define HTTP proxies to your custom API backend 23 | // https://github.com/chimurai/http-proxy-middleware 24 | const proxyTable = config.dev.proxyTable 25 | 26 | const app = express() 27 | const compiler = webpack(webpackConfig) 28 | 29 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 30 | publicPath: webpackConfig.output.publicPath, 31 | quiet: true 32 | }) 33 | 34 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 35 | log: false, 36 | heartbeat: 2000 37 | }) 38 | // force page reload when html-webpack-plugin template changes 39 | // currently disabled until this is resolved: 40 | // https://github.com/jantimon/html-webpack-plugin/issues/680 41 | // compiler.plugin('compilation', function (compilation) { 42 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 43 | // hotMiddleware.publish({ action: 'reload' }) 44 | // cb() 45 | // }) 46 | // }) 47 | 48 | // enable hot-reload and state-preserving 49 | // compilation error display 50 | app.use(hotMiddleware) 51 | 52 | // proxy api requests 53 | Object.keys(proxyTable).forEach(function (context) { 54 | let options = proxyTable[context] 55 | if (typeof options === 'string') { 56 | options = { target: options } 57 | } 58 | app.use(proxyMiddleware(options.filter || context, options)) 59 | }) 60 | 61 | // handle fallback for HTML5 history API 62 | app.use(require('connect-history-api-fallback')()) 63 | 64 | // serve webpack bundle output 65 | app.use(devMiddleware) 66 | 67 | // serve pure static assets 68 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 69 | app.use(staticPath, express.static('./static')) 70 | 71 | const uri = 'http://localhost:' + port 72 | 73 | var _resolve 74 | var _reject 75 | var readyPromise = new Promise((resolve, reject) => { 76 | _resolve = resolve 77 | _reject = reject 78 | }) 79 | 80 | var server 81 | var portfinder = require('portfinder') 82 | portfinder.basePort = port 83 | 84 | console.log('> Starting dev server...') 85 | devMiddleware.waitUntilValid(() => { 86 | portfinder.getPort((err, port) => { 87 | if (err) { 88 | _reject(err) 89 | } 90 | process.env.PORT = port 91 | var uri = 'http://localhost:' + port 92 | console.log('> Listening at ' + uri + '\n') 93 | // when env is testing, don't need open it 94 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 95 | opn(uri) 96 | } 97 | server = app.listen(port) 98 | _resolve() 99 | }) 100 | }) 101 | 102 | module.exports = { 103 | ready: readyPromise, 104 | close: () => { 105 | server.close() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | const scssResourcesLoaderOptions = { 33 | loader: 'sass-resources-loader', 34 | options: { 35 | resources: [ 36 | path.resolve(__dirname, '../src/styles/shared.scss') 37 | ] 38 | } 39 | } 40 | 41 | // generate loader string to be used with extract text plugin 42 | function generateLoaders (loader, loaderOptions) { 43 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 44 | 45 | if (loader) { 46 | loaders.push({ 47 | loader: loader + '-loader', 48 | options: Object.assign({}, loaderOptions, { 49 | sourceMap: options.sourceMap 50 | }) 51 | }) 52 | } 53 | 54 | // Extract CSS when that option is specified 55 | // (which is the case during production build) 56 | if (options.extract) { 57 | return ExtractTextPlugin.extract({ 58 | use: loaders, 59 | fallback: 'vue-style-loader' 60 | }) 61 | } else { 62 | return ['vue-style-loader'].concat(loaders) 63 | } 64 | } 65 | 66 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 67 | return { 68 | css: generateLoaders(), 69 | postcss: generateLoaders(), 70 | less: generateLoaders('less'), 71 | sass: generateLoaders('sass', { indentedSyntax: true }).concat(scssResourcesLoaderOptions), 72 | scss: generateLoaders('sass').concat(scssResourcesLoaderOptions), 73 | stylus: generateLoaders('stylus'), 74 | styl: generateLoaders('stylus') 75 | } 76 | } 77 | 78 | // Generate loaders for standalone style files (outside of .vue) 79 | exports.styleLoaders = function (options) { 80 | const output = [] 81 | const loaders = exports.cssLoaders(options) 82 | 83 | for (const extension in loaders) { 84 | const loader = loaders[extension] 85 | output.push({ 86 | test: new RegExp('\\.' + extension + '$'), 87 | use: loader 88 | }) 89 | } 90 | 91 | return output 92 | } 93 | 94 | exports.createNotifierCallback = () => { 95 | const notifier = require('node-notifier') 96 | 97 | return (severity, errors) => { 98 | if (severity !== 'error') return 99 | 100 | const error = errors[0] 101 | const filename = error.file && error.file.split('!').pop() 102 | 103 | notifier.notify({ 104 | title: packageConfig.name, 105 | message: severity + ': ' + error.name, 106 | subtitle: filename || '', 107 | icon: path.join(__dirname, 'logo.png') 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const _ = require('lodash') 3 | const path = require('path') 4 | const utils = require('./utils') 5 | const config = require('../config') 6 | const vueLoaderConfig = require('./vue-loader.conf') 7 | 8 | // Centralized config 9 | const aliasConfig = require('../src/config/tooling/aliases.js') 10 | const svgoConfig = require('../src/config/tooling/svgo.js') 11 | 12 | const includeDirs = [ 13 | resolve('src'), 14 | resolve('e2e'), 15 | resolve('unit'), 16 | resolve('test') 17 | ] 18 | 19 | // Modify the SVGO config to match the format the plugin expects 20 | let svgoPlugins = [] 21 | for (let pluginName in svgoConfig) { 22 | svgoPlugins.push({ 23 | [pluginName]: svgoConfig[pluginName] 24 | }) 25 | } 26 | 27 | function resolve (dir) { 28 | return path.join(__dirname, '..', dir) 29 | } 30 | 31 | const createLintingRule = () => ({ 32 | test: /\.(js|vue)$/, 33 | loader: 'eslint-loader', 34 | enforce: 'pre', 35 | include: includeDirs, 36 | options: { 37 | formatter: require('eslint-friendly-formatter'), 38 | emitWarning: !config.dev.showEslintErrorsInOverlay 39 | } 40 | }) 41 | 42 | // Fetch aliases from config, and add one magical alias for Vue 43 | let aliases = _.merge({}, _.mapValues(aliasConfig, (value) => { 44 | return resolve(value) 45 | }), { 46 | 'vue$': 'vue/dist/vue.esm.js' 47 | }) 48 | 49 | module.exports = { 50 | context: path.resolve(__dirname, '../'), 51 | entry: { 52 | app: './src/main.js' 53 | }, 54 | output: { 55 | path: config.build.assetsRoot, 56 | filename: '[name].js', 57 | publicPath: process.env.NODE_ENV === 'production' 58 | ? config.build.assetsPublicPath 59 | : config.dev.assetsPublicPath 60 | }, 61 | resolve: { 62 | extensions: ['.js', '.vue', '.scss', '.json'], 63 | alias: aliases 64 | }, 65 | resolveLoader: { 66 | alias: { 67 | 'sass-to-js': 'sass-vars-to-js-loader?preserveKeys=false', 68 | }, 69 | }, 70 | module: { 71 | rules: [ 72 | ...(config.dev.useEslint ? [createLintingRule()] : []), 73 | { 74 | test: /\.vue$/, 75 | loader: 'vue-loader', 76 | options: vueLoaderConfig 77 | }, 78 | { 79 | test: /\.js$/, 80 | loader: 'babel-loader', 81 | include: includeDirs 82 | }, 83 | { 84 | test: /\.svg$/, 85 | loader: 'vue-svg-loader', 86 | options: { 87 | svgo: { 88 | plugins: svgoPlugins 89 | } 90 | } 91 | }, 92 | { 93 | test: /\.(png|jpe?g|gif)(\?.*)?$/, 94 | loader: 'url-loader', 95 | options: { 96 | limit: 10000, 97 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 98 | } 99 | }, 100 | { 101 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 102 | loader: 'url-loader', 103 | options: { 104 | limit: 10000, 105 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 106 | } 107 | }, 108 | { 109 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 110 | loader: 'url-loader', 111 | options: { 112 | limit: 10000, 113 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 114 | } 115 | } 116 | ] 117 | }, 118 | node: { 119 | // prevent webpack from injecting useless setImmediate polyfill because Vue 120 | // source contains it (although only uses it if it's native). 121 | setImmediate: false, 122 | // prevent webpack from injecting mocks to Node native modules 123 | // that does not make sense for the client 124 | dgram: 'empty', 125 | fs: 'empty', 126 | net: 'empty', 127 | tls: 'empty', 128 | child_process: 'empty' 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const _ = require('lodash') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const HtmlWebpackPlugin = require('html-webpack-plugin') 9 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 10 | const StylelintPlugin = require('stylelint-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | 17 | 18 | // Custom config files (must be merged with dev values) 19 | const buildConfig = require('../src/config/dev/build.js') 20 | const metaConfig = require('../src/config/dev/meta.js') 21 | const pathsConfig = require('../src/config/dev/paths.js') 22 | 23 | const manifestConfig = require('../src/config/tooling/manifest.js') 24 | const offlineConfig = require('../src/config/tooling/offline.js') 25 | 26 | // Passed to `index.html.ejs` 27 | const templateConfig = { 28 | build: buildConfig, 29 | meta: metaConfig, 30 | paths: pathsConfig 31 | } 32 | 33 | 34 | 35 | const devWebpackConfig = merge(baseWebpackConfig, { 36 | module: { 37 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 38 | }, 39 | // cheap-module-eval-source-map is faster for development 40 | devtool: config.dev.devtool, 41 | 42 | // these devServer options should be customized in /config/index.js 43 | devServer: { 44 | clientLogLevel: 'warning', 45 | historyApiFallback: true, 46 | hot: true, 47 | compress: true, 48 | host: HOST || config.dev.host, 49 | port: PORT || config.dev.port, 50 | open: config.dev.autoOpenBrowser, 51 | overlay: config.dev.errorOverlay 52 | ? { warnings: false, errors: true } 53 | : false, 54 | publicPath: config.dev.assetsPublicPath, 55 | proxy: config.dev.proxyTable, 56 | quiet: true, // necessary for FriendlyErrorsPlugin 57 | watchOptions: { 58 | poll: config.dev.poll, 59 | } 60 | }, 61 | plugins: [ 62 | new webpack.DefinePlugin({ 63 | 'process.env': require('../config/dev.env') 64 | }), 65 | new webpack.HotModuleReplacementPlugin(), 66 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 67 | new webpack.NoEmitOnErrorsPlugin(), 68 | // https://github.com/ampedandwired/html-webpack-plugin 69 | new HtmlWebpackPlugin({ 70 | filename: 'index.html', 71 | template: 'src/index.html.ejs', 72 | favicon: null, // Favicon comes from static (like other app icons) 73 | config: templateConfig, 74 | inject: true 75 | }), 76 | new StylelintPlugin({ 77 | 78 | // Normally the syntax is detected by style type, 79 | // but I need to set this for .vue files 80 | syntax: 'scss', 81 | configFile: 'src/.stylelintrc.js', 82 | files: [ 83 | 'src/**/*.s?(a|c)ss', 84 | 'src/**/*.vue' 85 | ] 86 | 87 | }), 88 | ] 89 | }) 90 | 91 | // Enable offline plugin 92 | if (buildConfig.offline) { 93 | webpackConfig.plugins.push(new OfflinePlugin(offlineConfig)) 94 | } 95 | 96 | module.exports = new Promise((resolve, reject) => { 97 | portfinder.basePort = process.env.PORT || config.dev.port 98 | portfinder.getPort((err, port) => { 99 | if (err) { 100 | reject(err) 101 | } else { 102 | // publish the new Port, necessary for e2e tests 103 | process.env.PORT = port 104 | // add port to devServer config 105 | devWebpackConfig.devServer.port = port 106 | 107 | // Add FriendlyErrorsPlugin 108 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 109 | compilationSuccessInfo: { 110 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 111 | }, 112 | onErrors: config.dev.notifyOnErrors 113 | ? utils.createNotifierCallback() 114 | : undefined 115 | })) 116 | 117 | resolve(devWebpackConfig) 118 | } 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | // https://www.npmjs.com/package/webapp-manifest-plugin 15 | const WebappManifest = require('webapp-manifest-plugin') 16 | const WebappManifestPlugin = WebappManifest.default 17 | 18 | // https://github.com/NekR/offline-plugin 19 | const OfflinePlugin = require('offline-plugin') 20 | 21 | // https://www.npmjs.com/package/robotstxt-webpack-plugin 22 | const RobotstxtPlugin = require('robotstxt-webpack-plugin').default 23 | 24 | // https://www.npmjs.com/package/sitemap-webpack-plugin 25 | const SitemapPlugin = require('sitemap-webpack-plugin').default 26 | 27 | const env = process.env.NODE_ENV === 'testing' 28 | ? require('../config/test.env') 29 | : require('../config/prod.env') 30 | 31 | 32 | 33 | // Custom config files 34 | const buildConfig = require('../src/config/build.js') 35 | const metaConfig = require('../src/config/meta.js') 36 | const pathsConfig = require('../src/config/paths.js') 37 | 38 | const manifestConfig = require('../src/config/tooling/manifest.js') 39 | const offlineConfig = require('../src/config/tooling/offline.js') 40 | const robotsTxtConfig = require('../src/config/tooling/robotsTxt.js') 41 | const sitemapConfig = require('../src/config/tooling/sitemap.js') 42 | 43 | // Passed to `index.html.ejs` 44 | const templateConfig = { 45 | build: buildConfig, 46 | meta: metaConfig, 47 | paths: pathsConfig 48 | } 49 | 50 | 51 | 52 | const webpackConfig = merge(baseWebpackConfig, { 53 | module: { 54 | rules: utils.styleLoaders({ 55 | sourceMap: config.build.productionSourceMap, 56 | extract: true, 57 | usePostCSS: true 58 | }) 59 | }, 60 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 61 | output: { 62 | path: config.build.assetsRoot, 63 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 64 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 65 | }, 66 | plugins: [ 67 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 68 | new webpack.DefinePlugin({ 69 | 'process.env': env 70 | }), 71 | new UglifyJsPlugin({ 72 | uglifyOptions: { 73 | compress: { 74 | warnings: false 75 | } 76 | }, 77 | sourceMap: config.build.productionSourceMap, 78 | parallel: true 79 | }), 80 | // extract css into its own file 81 | new ExtractTextPlugin({ 82 | filename: utils.assetsPath('css/[name].[contenthash].css'), 83 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 84 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 85 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 86 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 87 | allChunks: true, 88 | }), 89 | // Compress extracted CSS. We are using this plugin so that possible 90 | // duplicated CSS from different components can be deduped. 91 | new OptimizeCSSPlugin({ 92 | cssProcessorOptions: config.build.productionSourceMap 93 | ? { safe: true, map: { inline: false } } 94 | : { safe: true } 95 | }), 96 | // generate dist index.html with correct asset hash for caching. 97 | // you can customize output by editing /index.html 98 | // see https://github.com/ampedandwired/html-webpack-plugin 99 | new HtmlWebpackPlugin({ 100 | filename: process.env.NODE_ENV === 'testing' 101 | ? 'index.html' 102 | : config.build.index, 103 | template: 'src/index.html.ejs', 104 | favicon: null, // Favicon comes from static (like other app icons) 105 | config: templateConfig, // Passed to `index.html.ejs` 106 | inject: true, 107 | minify: { 108 | removeComments: true, 109 | collapseWhitespace: true, 110 | removeAttributeQuotes: true 111 | // more options: 112 | // https://github.com/kangax/html-minifier#options-quick-reference 113 | }, 114 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 115 | chunksSortMode: 'dependency' 116 | }), 117 | // keep module.id stable when vender modules does not change 118 | new webpack.HashedModuleIdsPlugin(), 119 | // enable scope hoisting 120 | new webpack.optimize.ModuleConcatenationPlugin(), 121 | // split vendor js into its own file 122 | new webpack.optimize.CommonsChunkPlugin({ 123 | name: 'vendor', 124 | minChunks (module) { 125 | // any required modules inside node_modules are extracted to vendor 126 | return ( 127 | module.resource && 128 | /\.js$/.test(module.resource) && 129 | module.resource.indexOf( 130 | path.join(__dirname, '../node_modules') 131 | ) === 0 132 | ) 133 | } 134 | }), 135 | // extract webpack runtime and module manifest to its own file in order to 136 | // prevent vendor hash from being updated whenever app bundle is updated 137 | new webpack.optimize.CommonsChunkPlugin({ 138 | name: 'manifest', 139 | minChunks: Infinity 140 | }), 141 | // This instance extracts shared chunks from code splitted chunks and bundles them 142 | // in a separate chunk, similar to the vendor chunk 143 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 144 | new webpack.optimize.CommonsChunkPlugin({ 145 | name: 'app', 146 | async: 'vendor-async', 147 | children: true, 148 | minChunks: 3 149 | }), 150 | 151 | // copy custom static assets 152 | new CopyWebpackPlugin([ 153 | { 154 | from: path.resolve(__dirname, '../static'), 155 | to: config.build.assetsSubDirectory, 156 | ignore: ['.*'] 157 | } 158 | ]) 159 | ] 160 | }) 161 | 162 | // Enable offline plugin 163 | if (buildConfig.offline) { 164 | webpackConfig.plugins.push(new OfflinePlugin(offlineConfig)) 165 | } 166 | 167 | // Generate manifest.json if set in config 168 | if (manifestConfig) { 169 | webpackConfig.plugins.push(new WebappManifestPlugin(manifestConfig)) 170 | } 171 | 172 | // Generate robots.txt 173 | if (robotsTxtConfig && robotsTxtConfig.host && robotsTxtConfig.policy) { 174 | webpackConfig.plugins.push(new RobotstxtPlugin(robotsTxtConfig)) 175 | } 176 | 177 | // Generate sitemap.xml 178 | if (sitemapConfig && sitemapConfig.base) { 179 | webpackConfig.plugins.push( 180 | new SitemapPlugin( 181 | sitemapConfig.base, 182 | sitemapConfig.paths, 183 | sitemapConfig.options 184 | ) 185 | ) 186 | } 187 | 188 | if (config.build.productionGzip) { 189 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 190 | 191 | webpackConfig.plugins.push( 192 | new CompressionWebpackPlugin({ 193 | asset: '[path].gz[query]', 194 | algorithm: 'gzip', 195 | test: new RegExp( 196 | '\\.(' + 197 | config.build.productionGzipExtensions.join('|') + 198 | ')$' 199 | ), 200 | threshold: 10240, 201 | minRatio: 0.8 202 | }) 203 | ) 204 | } 205 | 206 | if (config.build.bundleAnalyzerReport) { 207 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 208 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 209 | } 210 | 211 | module.exports = webpackConfig 212 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.2.6 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | // CSS Sourcemaps off by default because relative paths are "buggy" 44 | // with this option, according to the CSS-Loader README 45 | // (https://github.com/webpack/css-loader#sourcemaps) 46 | // In our experience, they generally work as expected, 47 | // just be aware of this issue when enabling this option. 48 | cssSourceMap: false, 49 | }, 50 | 51 | build: { 52 | // Template for index.html 53 | index: path.resolve(__dirname, '../dist/index.html'), 54 | 55 | // Paths 56 | assetsRoot: path.resolve(__dirname, '../dist'), 57 | assetsSubDirectory: '', 58 | assetsPublicPath: '/', 59 | 60 | /** 61 | * Source Maps 62 | */ 63 | 64 | productionSourceMap: true, 65 | // https://webpack.js.org/configuration/devtool/#production 66 | devtool: '#source-map', 67 | 68 | // Gzip off by default as many popular static hosts such as 69 | // Surge or Netlify already gzip all static assets for you. 70 | // Before setting to `true`, make sure to: 71 | // npm install --save-dev compression-webpack-plugin 72 | productionGzip: false, 73 | productionGzipExtensions: ['js', 'css'], 74 | 75 | // Run the build command with an extra argument to 76 | // View the bundle analyzer report after build finishes: 77 | // `npm run build --report` 78 | // Set to `true` or `false` to always turn it on or off 79 | bundleAnalyzerReport: process.env.npm_config_report 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /e2e/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var base = require('../src/.eslintrc'); 3 | 4 | module.exports = _.merge( 5 | {}, 6 | base, 7 | { 8 | // env: {}, 9 | // globals: {} 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /e2e/appStartup.e2e.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 6 | 'default e2e tests': (browser) => { 7 | 8 | // automatically uses dev Server port from /config.index.js 9 | // default: http://localhost:8080 10 | // see nightwatch.conf.js 11 | const devServer = browser.globals.devServerURL 12 | 13 | browser 14 | .url(devServer) 15 | .waitForElementVisible('.c-app', 5000) 16 | .assert.elementPresent('.c-page-home') 17 | .assert.containsText('h1', 'Hello world!') 18 | .assert.elementCount('h1', 1) 19 | .end() 20 | 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./e2e/**/*", 4 | "./src/**/*", 5 | "./unit/**/*" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@components/*": [ 11 | "src/components/*" 12 | ], 13 | "@assets/*": [ 14 | "src/assets/*" 15 | ], 16 | "@config/*": [ 17 | "src/config/dev/*" 18 | ], 19 | "@directives/*": [ 20 | "src/directives/*" 21 | ], 22 | "@fonts/*": [ 23 | "src/fonts/*" 24 | ], 25 | "@mixins/*": [ 26 | "src/mixins/*" 27 | ], 28 | "@models/*": [ 29 | "src/models/*" 30 | ], 31 | "@services/*": [ 32 | "src/services/*" 33 | ], 34 | "@svg/*": [ 35 | "src/svg/*" 36 | ], 37 | "@util/*": [ 38 | "src/util/*" 39 | ], 40 | "@vendor/*": [ 41 | "src/vendor/*" 42 | ] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # Netlify config 2 | # See reference here: 3 | # https://www.netlify.com/docs/netlify-toml-reference/ 4 | 5 | # The following redirect is intended for use with most SPA's that handles routing internally. 6 | [[redirects]] 7 | from = "/*" 8 | to = "/index.html" 9 | status = 200 10 | 11 | # Build config 12 | # NOTE: these will override values set in Netlify UI 13 | [build] 14 | publish = "dist" 15 | command = "npm run ondeploy" 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bellevue", 3 | "version": "2.4.0", 4 | "description": "A full-featured frontend project template for modern single-page applications built on Vue.js and Webpack.", 5 | "author": "Jerry Jäppinen ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "dist": "npx lite-server --baseDir=dist", 11 | "unit": "jest --config test/unit/jest.conf.js", 12 | "unit:report": "jest --config test/unit/jest.conf.js --coverage", 13 | "unit:watch": "jest --watch --config test/unit/jest.conf.js", 14 | "e2e": "node test/e2e/runner.js", 15 | "test": "npm run unit && npm run e2e", 16 | "lint": "npm run lint:js && npm run lint:styles", 17 | "lint:js": "eslint --ext .js,.vue src e2e unit", 18 | "lint:styles": "npm run lint:styles:global && npm run lint:styles:vue", 19 | "lint:styles:global": "stylelint src/**/*.css src/**/*.scss --config 'src/.stylelintrc.js'", 20 | "lint:styles:vue": "stylelint 'src/**/*.vue' --config 'src/.stylelintrc.js'", 21 | "build": "node build/build.js", 22 | "build:report": "npm run build --report", 23 | "ondeploy": "npm run build" 24 | }, 25 | "dependencies": { 26 | "lodash": "^4.17.11", 27 | "moabit": "0.0.4", 28 | "raf": "^3.4.1", 29 | "vue": "^2.5.22", 30 | "vue-analytics": "^5.12.2", 31 | "vue-images-loaded": "^1.1.2", 32 | "vue-meta": "^1.5.8", 33 | "vue-router": "^3.0.2", 34 | "vuex": "^3.0.1" 35 | }, 36 | "devDependencies": { 37 | "autoprefixer": "^8.6.5", 38 | "babel-core": "^6.26.3", 39 | "babel-eslint": "^8.2.6", 40 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 41 | "babel-jest": "^22.4.4", 42 | "babel-loader": "^7.1.5", 43 | "babel-plugin-dynamic-import-node": "^1.2.0", 44 | "babel-plugin-syntax-jsx": "^6.18.0", 45 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 46 | "babel-plugin-transform-runtime": "^6.23.0", 47 | "babel-plugin-transform-vue-jsx": "^3.7.0", 48 | "babel-preset-env": "^1.7.0", 49 | "babel-preset-stage-2": "^6.24.1", 50 | "babel-register": "^6.26.0", 51 | "chalk": "^2.4.2", 52 | "chromedriver": "^2.38.3", 53 | "copy-webpack-plugin": "^4.5.1", 54 | "cross-spawn": "^6.0.5", 55 | "css-loader": "^0.28.11", 56 | "eslint": "^4.19.1", 57 | "eslint-config-standard": "^10.2.1", 58 | "eslint-friendly-formatter": "^3.0.0", 59 | "eslint-loader": "^1.9.0", 60 | "eslint-plugin-html": "^4.0.6", 61 | "eslint-plugin-import": "^2.12.0", 62 | "eslint-plugin-jest": "^21.15.2", 63 | "eslint-plugin-node": "^6.0.1", 64 | "eslint-plugin-promise": "^3.8.0", 65 | "eslint-plugin-standard": "^3.1.0", 66 | "eslint-plugin-vue": "^4.5.0", 67 | "eventsource-polyfill": "^0.9.6", 68 | "extract-text-webpack-plugin": "^3.0.2", 69 | "file-loader": "^1.1.11", 70 | "friendly-errors-webpack-plugin": "^1.7.0", 71 | "html-webpack-plugin": "^2.30.1", 72 | "jest": "^22.4.4", 73 | "jest-css-modules": "^1.1.0", 74 | "jest-serializer-vue": "^1.0.0", 75 | "jest-static-stubs": "0.0.1", 76 | "nightwatch": "^0.9.21", 77 | "node-notifier": "^5.2.1", 78 | "node-sass": "^4.9.0", 79 | "offline-plugin": "^4.9.1", 80 | "optimize-css-assets-webpack-plugin": "^3.2.0", 81 | "ora": "^1.4.0", 82 | "portfinder": "^1.0.20", 83 | "postcss-import": "^11.1.0", 84 | "postcss-loader": "^2.1.5", 85 | "rimraf": "^2.6.3", 86 | "robotstxt-webpack-plugin": "^4.0.1", 87 | "sass-loader": "^6.0.7", 88 | "sass-resources-loader": "^1.3.5", 89 | "sass-vars-to-js-loader": "^2.0.3", 90 | "selenium-server": "^3.12.0", 91 | "semver": "^5.5.0", 92 | "shelljs": "^0.8.3", 93 | "sitemap-webpack-plugin": "^0.5.1", 94 | "stylelint": "^9.2.1", 95 | "stylelint-config-standard": "^18.2.0", 96 | "stylelint-processor-html": "^1.0.0", 97 | "stylelint-scss": "^2.5.0", 98 | "stylelint-webpack-plugin": "^0.10.5", 99 | "uglifyjs-webpack-plugin": "^1.2.5", 100 | "url-loader": "^0.6.2", 101 | "vue-jest": "^2.6.0", 102 | "vue-loader": "^14.2.4", 103 | "vue-style-loader": "^4.1.2", 104 | "vue-svg-loader": "^0.5.0", 105 | "vue-template-compiler": "^2.5.22", 106 | "vue-test-utils": "^1.0.0-beta.11", 107 | "webapp-manifest-plugin": "0.0.4", 108 | "webpack": "^3.12.0", 109 | "webpack-bundle-analyzer": "^2.13.1", 110 | "webpack-dev-server": "^2.11.3", 111 | "webpack-merge": "^4.1.2" 112 | }, 113 | "engines": { 114 | "node": "10.14.2", 115 | "npm": "6.4.1" 116 | }, 117 | "browserslist": [ 118 | "> 1%", 119 | "last 2 versions", 120 | "not ie <= 10" 121 | ] 122 | } 123 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // ESLint only supports JS, not (S)CSS or HTML. 2 | // http://eslint.org/docs/user-guide/configuring 3 | // http://eslint.org/docs/user-guide/rules 4 | 5 | 6 | 7 | // 8 | // NOTE 9 | // 10 | // - This file should contain the rules for OUR custom JS code. 11 | // - The rules defined here should be accompanied with comments explaining the rationale behind the convention. 12 | // - Using the severity 'error' will break builds even during development, so it's better to reserve it for most serious things only. 13 | // - All warnings should also be cleaned up from production code however. 14 | // 15 | // Other linter files 16 | // 17 | // - Please keep the ROOT `/.eslintrc.js` without modifications, as it is the default for most Vue projcts. 18 | // - You can specify further rules in .eslintrc files under sub folders for more granular, cascading rulesets. 19 | // - You can import these rules in other places, like under tests. 20 | 21 | module.exports = { 22 | plugins: [ 23 | 'import', 24 | 'vue' 25 | ], 26 | extends: [ 27 | 'standard', 28 | 'plugin:vue/recommended' 29 | ], 30 | rules: { 31 | 32 | // Stylistic issues 33 | 34 | // Indent with tabs, because spaces are not user-adjustable in IDEs, are harder to target with mouse cursors and will always have indentation errors 35 | 'no-tabs': ['off'], 36 | 'indent': ['warn', 'tab'], 37 | 38 | // Semi colons required to avoid any gotchas 39 | 'semi': [ 40 | 'warn', 41 | 'never' 42 | ], 43 | 44 | // Single quotes should be used 45 | // NOTE: template literals not allowed currently, but can be enabled if we have legitimate use cases for them 46 | 'quotes': [ 47 | 'warn', 48 | 'single', 49 | { 50 | 'avoidEscape': false, 51 | 'allowTemplateLiterals': false 52 | } 53 | ], 54 | 55 | // Leftovers 56 | 'no-unused-expressions': 'warn', 57 | 58 | // Number of consecutive blank lines allowed 59 | 'no-multiple-empty-lines': [ 60 | 'warn', 61 | { 62 | 'max': 3, 63 | 'maxBOF': 2, 64 | 'maxEOF': 1 65 | } 66 | ], 67 | 68 | 69 | 70 | // Guard against duplicate variable names in one scope 71 | 'no-shadow': ['error'], 72 | 'no-param-reassign': ['warn', { 73 | 'props': true, 74 | 'ignorePropertyModificationsFor': [] 75 | }], 76 | 77 | 78 | 79 | // Language features 80 | 81 | // Misc. 82 | 'padded-blocks': 'off', // Weird rule, we need whitespace sometimes 83 | 'no-empty': 'warn', // Empty blocks should be cleaned up 84 | 'no-unreachable': 'warn', // Unreachable code should be cleaned up 85 | 'no-else-return': 'error', // Smelly, code will break when refactoring 86 | 'no-useless-escape': 'off', // Sometimes escaping certain characters is not useless 87 | 88 | // Variables should be declared when they are used for the first time 89 | // This makes it easier to move them from one scope to another when refactoring 90 | 'one-var': [ 91 | 'error', 92 | 'never' 93 | ], 94 | 95 | // Allow balancing object notation key-value pairs 96 | 'key-spacing': ['warn'], 97 | 98 | // config.someItems['foo'] is sometimes useful 99 | // It can highlight that we're referring to an item with a very specific, hardcoded name (that probably should be a variable) 100 | 'dot-notation': ['off'], 101 | 102 | // Make arrow functions slightly less dangerous and confusing 103 | 'no-confusing-arrow': ['error'], 104 | 'arrow-parens': ['error', 'always'], 105 | 'arrow-body-style': ['error', 'always'], 106 | 'arrow-spacing': [ 107 | 'error', 108 | { 109 | 'before': true, 110 | 'after': true 111 | } 112 | ], 113 | 114 | // Would normally prefer the same traditional object syntax everywhere, because shorthand cannot always be used. 115 | // It's better to have only one format in the codebase. However imports and exports have similar shorthand syntax anyway. 116 | 'object-shorthand': [ 117 | 'warn', 118 | 'always' 119 | ], 120 | 121 | // Destructuring assignments 122 | // Think twice about how to use them 123 | // http://teeohhem.com/why-destructuring-is-a-terrible-idea-in-es6/ 124 | 'no-useless-rename': ['warn'], 125 | 'prefer-destructuring': [ 126 | 'error', 127 | { 128 | 'array': false, 129 | 'object': false 130 | } 131 | ], 132 | 133 | // Allow long ternary (not always 'unneeded') 134 | // See http://stackoverflow.com/questions/2100758/javascript-or-variable-assignment-explanation 135 | 'no-unneeded-ternary': ['off'], 136 | 137 | // New Foo() is fine 138 | 'no-new': ['off'], 139 | 140 | 141 | 142 | // Rules for imports plugin 143 | // https://github.com/benmosher/eslint-plugin-import 144 | 145 | // FIXME: we should enable these once this issue with aliases is resolved: 146 | // https://github.com/benmosher/eslint-plugin-import/issues/779 147 | // 'import/named': ['error'], 148 | // 'import/default': ['error'], 149 | // 'import/no-extraneous-dependencies': ['error'], 150 | 151 | 'import/no-absolute-path': ['error'], 152 | 'import/no-webpack-loader-syntax': ['error'], 153 | 'import/export': ['warn'], 154 | 'import/no-named-as-default': ['warn'], 155 | 'import/first': ['warn'], 156 | 'import/no-duplicates': ['warn'], 157 | 'import/extensions': [ 158 | 'warn', 159 | 'never', 160 | { 161 | 'svg': 'always' 162 | } 163 | ], 164 | 165 | // We turn this off, as sometimes we want default to be an object containing all the named exports 166 | 'import/no-named-as-default-member': ['off'], 167 | 168 | 169 | 170 | // Vue-specific 171 | // https://github.com/vuejs/eslint-plugin-vue#bulb-rules 172 | 'vue/html-indent': [ 173 | 'error', 174 | 'tab', 175 | { 176 | 'attribute': 1, 177 | 'closeBracket': 0, 178 | 'ignores': [] 179 | } 180 | ], 181 | 'vue/max-attributes-per-line': [ 182 | 'warn', 183 | { 184 | 'singleline': 3, 185 | 'multiline': { 186 | 'max': 1, 187 | 'allowFirstLine': false 188 | } 189 | } 190 | ], 191 | 'vue/name-property-casing': ['error', 'kebab-case'], 192 | 'vue/require-default-prop': ['off'], 193 | 'vue/require-prop-types': ['off'], 194 | 'vue/attributes-order': ['off'] 195 | 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | // Stylelint supports CSS and SCSS 2 | // https://stylelint.io/user-guide/configuration/ 3 | // https://stylelint.io/user-guide/rules/ 4 | // https://www.npmjs.com/package/stylelint-scss 5 | 6 | // 7 | // NOTE 8 | // 9 | // - This file should contain the rules for OUR custom (S)CSS code. 10 | // - The rules defined here should be accompanied with comments explaining the rationale behind the convention. 11 | // 12 | // Other linter files 13 | // 14 | // - Please keep the ROOT `/.stylelintrc` without modifications, as it is the default for most Vue projcts. 15 | // - You can specify further rules in .eslintrc files under sub folders for more granular, cascading rulesets. 16 | // - You can import these rules in other places, like under tests. 17 | 18 | // NOTE 19 | // Apparently this does NOT extend root configuration as one would expect 20 | // All settings must be defined here, including processors and plugins 21 | 22 | module.exports = { 23 | 24 | // Treat CSS issues as warnings rather than errors 25 | defaultSeverity: 'warning', 26 | 27 | processors: [ 28 | 'stylelint-processor-html' 29 | ], 30 | 31 | plugins: [ 32 | 'stylelint-scss' 33 | ], 34 | 35 | 36 | 37 | // Coding style 38 | 39 | extends: [ 40 | 'stylelint-config-standard' 41 | ], 42 | 43 | rules: { 44 | 45 | // Indent with tabs 46 | 'indentation': ['tab'], 47 | 48 | // These will cause false positives 49 | 'no-empty-source': null, 50 | 'at-rule-no-unknown': null, 51 | 52 | // We're not quite this strict 53 | 'declaration-empty-line-before': null, 54 | 'block-closing-brace-empty-line-before': null, 55 | 'rule-empty-line-before': null, 56 | 57 | // Comments 58 | 'comment-no-empty': true, 59 | 'comment-empty-line-before': null, // Would like to use this, but will warn on commented single-line rules 60 | 'comment-whitespace-inside': null, 61 | 62 | // Misc basic things 63 | 'color-no-invalid-hex': true, 64 | 'function-calc-no-unspaced-operator': true, 65 | 'number-leading-zero': 'always', 66 | 'length-zero-no-unit': true, 67 | 'no-extra-semicolons': true, 68 | 'no-eol-whitespace': true, 69 | 'no-missing-end-of-source-newline': true, 70 | 'no-invalid-double-slash-comments': true, 71 | 'declaration-block-no-duplicate-properties': true, 72 | 'declaration-colon-newline-after': null, 73 | 'function-parentheses-newline-inside': 'always-multi-line', 74 | 'block-closing-brace-newline-after': 'always', 75 | 'block-no-empty': true, 76 | 77 | // Quotes 78 | // We'd like to enforce single quotes for everything but attribute selectors, but currently this is not possible 79 | // 'string-quotes': 'single', 80 | 'font-family-name-quotes': 'always-unless-keyword', 81 | 'function-url-quotes': 'always', 82 | 83 | // At rules 84 | 'at-rule-name-space-after': 'always-single-line', 85 | 'at-rule-semicolon-space-before': 'never', 86 | 'at-rule-name-newline-after': null, // 'always-multi-line' is close but sometimes weird 87 | 'at-rule-empty-line-before': [ 88 | 'always', 89 | { 90 | 'ignore': [ 91 | 'after-comment', 92 | 'inside-block', 93 | 'blockless-after-blockless' 94 | ] 95 | } 96 | ], 97 | 98 | // Prefer lowercase in general 99 | 'unit-case': 'lower', 100 | 'color-hex-case': 'lower', 101 | 'function-name-case': 'lower', 102 | 'value-keyword-case': 'lower', 103 | 'at-rule-name-case': 'lower', 104 | 105 | // Misc rules 106 | 'shorthand-property-no-redundant-values': null, 107 | 'number-no-trailing-zeros': null, 108 | 'selector-pseudo-element-colon-notation': 'single', 109 | 'declaration-block-no-redundant-longhand-properties': null, 110 | 111 | // No vendor prefixes in source 112 | // This is because Autoprefixer enforces correct vendor prefixes better than us humans and will remove them up anyway 113 | 'at-rule-no-vendor-prefix': true, 114 | 'media-feature-name-no-vendor-prefix': true, 115 | 'selector-no-vendor-prefix': true, 116 | 'property-no-vendor-prefix': true, 117 | 'value-no-vendor-prefix': true, 118 | 119 | // Disallow excessive nesting 120 | // NOTE 121 | // - While the intention is good, a blanket limit does not do the job well 122 | // - Many times you should not be nesting even beyond two levels 123 | // - And sometimes nesting beyond this limit might make sense 124 | 'max-nesting-depth': [ 125 | 4 126 | ], 127 | 128 | // Allow convenient spacing, but don't go overboard 129 | 'selector-max-empty-lines': 3, 130 | 'function-max-empty-lines': 3, 131 | 'value-list-max-empty-lines': 3, 132 | 'max-empty-lines': [ 133 | 3 134 | ], 135 | 136 | 137 | 138 | // SCSS-specific linting 139 | 140 | // Stylistic 141 | 'scss/dollar-variable-colon-space-after': 'always', 142 | 'scss/dollar-variable-colon-space-before': 'never', 143 | 144 | // Confusing, not clear if compiles to shorthand or individual properties 145 | 'scss/declaration-nested-properties': 'never' 146 | 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /src/components/animations/Animation.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 83 | 84 | -------------------------------------------------------------------------------- /src/components/containers/Card.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/containers/Page.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 38 | 39 | 57 | 58 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as App } from './App' 2 | 3 | // Animations 4 | export { default as Animation } from './animations/Animation' 5 | 6 | // Containers 7 | export { default as Card } from './containers/Card' 8 | export { default as Page } from './containers/Page' 9 | 10 | // Pages 11 | export { default as PageDemo } from './pages/PageDemo' 12 | export { default as PageHome } from './pages/PageHome' 13 | 14 | // Snippets 15 | export { default as Bitmap } from './snippets/Bitmap' 16 | export { default as Dump } from './snippets/Dump' 17 | export { default as Ellipsis } from './snippets/Ellipsis' 18 | export { default as ExternalLink } from './snippets/ExternalLink' 19 | export { default as Icon } from './snippets/Icon' 20 | export { default as Spinner } from './snippets/Spinner' 21 | export { default as Vector } from './snippets/Vector' 22 | 23 | // Transitions 24 | export { default as CustomTransition } from './transitions/CustomTransition' 25 | export { default as Fade } from './transitions/Fade' 26 | -------------------------------------------------------------------------------- /src/components/pages/PageDemo.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 275 | 276 | 343 | 344 | -------------------------------------------------------------------------------- /src/components/pages/PageHome.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/snippets/Bitmap.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 105 | 106 | 116 | -------------------------------------------------------------------------------- /src/components/snippets/Dump.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 28 | 29 | 36 | -------------------------------------------------------------------------------- /src/components/snippets/Ellipsis.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 51 | -------------------------------------------------------------------------------- /src/components/snippets/ExternalLink.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 28 | -------------------------------------------------------------------------------- /src/components/snippets/Icon.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | 33 | -------------------------------------------------------------------------------- /src/components/snippets/Spinner.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 60 | 61 | 97 | -------------------------------------------------------------------------------- /src/components/snippets/Vector.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 55 | -------------------------------------------------------------------------------- /src/components/transitions/CustomTransition.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 77 | -------------------------------------------------------------------------------- /src/components/transitions/Fade.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /src/config/analytics.js: -------------------------------------------------------------------------------- 1 | // NOTE: you also need to import vue-analytics library in `vue.js` 2 | module.exports = { 3 | id: 'UA-XXX-X' 4 | } 5 | -------------------------------------------------------------------------------- /src/config/build.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isDebug: false, 3 | 4 | // Set to true to enable https://github.com/NekR/offline-plugin 5 | // NOTE: also ensure plugin code is imported in `main.js` 6 | offline: false 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/config/dev/analytics.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash').merge 2 | const baseConfig = require('../analytics') 3 | module.exports = merge({}, baseConfig, { 4 | 5 | // We can disable tracking for development by setting this to `null` 6 | id: null 7 | 8 | }) 9 | -------------------------------------------------------------------------------- /src/config/dev/build.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash').merge 2 | const baseConfig = require('../build') 3 | module.exports = merge({}, baseConfig, { 4 | 5 | // Set debug flag when importing this file 6 | isDebug: true, 7 | 8 | // NOTE: Better to keep this disabled on dev server 9 | offline: false 10 | 11 | }) 12 | -------------------------------------------------------------------------------- /src/config/dev/meta.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash').merge 2 | const baseConfig = require('../meta') 3 | module.exports = merge({}, baseConfig, { 4 | 5 | // ... 6 | 7 | }) 8 | -------------------------------------------------------------------------------- /src/config/dev/paths.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash').merge 2 | const baseConfig = require('../paths') 3 | module.exports = merge({}, baseConfig, { 4 | host: 'http://localhost:8080/', 5 | staticAssetsPath: '/static/' 6 | }) 7 | -------------------------------------------------------------------------------- /src/config/dev/router.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../router') 2 | -------------------------------------------------------------------------------- /src/config/dev/styles.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../styles') 2 | -------------------------------------------------------------------------------- /src/config/meta.js: -------------------------------------------------------------------------------- 1 | // NOTE: this file will be imported during tooling before ES6 is supported 2 | module.exports = { 3 | 4 | // Will be used in base HTML templating 5 | title: 'Bellevue', 6 | description: 'A full-featured frontend project template for modern single-page applications built on Vue.js and Webpack.', 7 | themeColor: '#42b983', 8 | backgroundColor: '#fafafa', 9 | 10 | // Localisation 11 | defaultLocale: 'en', 12 | fallbackLocale: 'en', 13 | 14 | // Link to native iOS app's App Store page 15 | appStore: { 16 | appId: '', 17 | affiliate: '', 18 | appArgument: '' 19 | }, 20 | 21 | // Format detection meta tag for iOS 22 | // https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html 23 | formatDetection: { 24 | 'telephone': 'yes' 25 | }, 26 | 27 | // Enable some meta tags that optimize the mobile experience 28 | mobile: true, 29 | defaultTouchHighlight: false, 30 | iosStatusBarStyle: 'black-translucent', 31 | 32 | // Viewport control for mobile devices 33 | // https://developer.mozilla.org/en/docs/Mozilla/Mobile/Viewport_meta_tag 34 | // https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html 35 | // NOTE: keys must be in kebab-case 36 | viewport: { 37 | 'width': 'device-width', 38 | 'height': null, 39 | 'initial-scale': 1, 40 | 'minimum-scale': null, 41 | 'maximum-scale': 1, 42 | 'user-scalable': 'no', 43 | 'viewport-fit': 'cover' 44 | }, 45 | 46 | // http://www.robotstxt.org/meta.html 47 | robotsMeta: [ 48 | // 'index', 49 | // 'noindex', 50 | // 'follow', 51 | // 'nofollow' 52 | ] 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/config/paths.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Canonical production URL 4 | // Needed for robots.txt and sitemap.xml generation in production builds 5 | host: 'https://example.com/', 6 | 7 | // Files under static (during runtime) 8 | staticAssetsPath: '/', 9 | faviconFilename: 'favicon.png', 10 | appleIconFilename: 'favicon.png', 11 | 12 | // Links to static or externally hosted JS that need a script tag in `index.html.ejs` 13 | scriptLinks: [ 14 | // '//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js', 15 | // '/static/vendor.js' 16 | ], 17 | 18 | // Links to static or externally hosted CSS that need a style tag in `index.html.ejs` 19 | styleLinks: [ 20 | '//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600|Roboto Mono|Dosis' 21 | // '//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css', 22 | // '/static/vendor.css' 23 | ], 24 | 25 | // List of URLs to add a prefetch meta tag for 26 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Link_prefetching_FAQ 27 | prefetch: [] 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/config/router.js: -------------------------------------------------------------------------------- 1 | // See docs at https://router.vuejs.org/en/api/options.html 2 | module.exports = { 3 | 4 | // 'hash': extra hash characters in URLs, no server configuration needed for routing to work 5 | // 'history': no extra characters in URLs, but requires server configuration 6 | mode: 'history', 7 | 8 | // Class names used by 9 | // NOTE: these should conform to our class naming conventions 10 | linkActiveClass: 'is-active', 11 | linkExactActiveClass: 'is-exact-active' 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/config/styles.js: -------------------------------------------------------------------------------- 1 | // Get SCSS variables via a special loader 2 | // eslint-disable-next-line import/no-webpack-loader-syntax 3 | module.exports = require('!!sass-to-js!@styles-variables') 4 | -------------------------------------------------------------------------------- /src/config/tooling/aliases.js: -------------------------------------------------------------------------------- 1 | // We can switch paths per environment, so we won't bundle extra code to runtime package 2 | const isDev = (process.env.NODE_ENV !== 'production') ? true : false 3 | 4 | // Aliases usable in runtime codebase when doing imports and resolving URLs 5 | // 6 | // NOTE: Order matters here! 7 | // - Put `@foo-bar` before `@foo` 8 | // - Put `@foo` before `@` 9 | module.exports = { 10 | 11 | // The base configuration files (alias is mostly for the client) 12 | '@config': isDev ? 'src/config/dev' : 'src/config', 13 | '@routes': 'src/config/tooling/routes', 14 | 15 | // Assets 16 | '@assets': 'src/assets', 17 | '@fonts': 'src/fonts', 18 | // '@locales': 'src/locales', 19 | '@svg': 'src/svg', 20 | 21 | // Vendor code, services, utilities etc. 22 | '@models': 'src/models', 23 | '@services': 'src/services', 24 | '@util': 'src/util', 25 | '@vendor': 'src/vendor', 26 | 27 | // Vue application code 28 | '@components': 'src/components', 29 | '@directives-global': 'src/directives/global', 30 | '@directives': 'src/directives', 31 | '@filters-global': 'src/filters/global', 32 | '@filters': 'src/filters', 33 | '@mixins-global': 'src/mixins/global', 34 | '@mixins': 'src/mixins', 35 | '@store': 'src/store', 36 | 37 | // Global styles 38 | '@styles-variables': 'src/styles/variables.scss', 39 | '@styles-global': 'src/styles/global.scss' 40 | 41 | // Test cases 42 | // '@unit': 'unit' 43 | // '@e2e': 'e2e' 44 | // '@test-tooling': 'test' 45 | 46 | // `src/` root 47 | // NOTE: prefer the other aliases over this (so we keep this last) 48 | // '@': 'src' 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/config/tooling/manifest.js: -------------------------------------------------------------------------------- 1 | const meta = require('../meta') 2 | 3 | // NOTE: this file will be imported during tooling before ES6 is supported 4 | // https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 5 | // https://www.npmjs.com/package/webapp-manifest-plugin 6 | module.exports = { 7 | name: meta.title, 8 | title: meta.title, 9 | description: meta.description, 10 | // dir: 'auto', 11 | lang: 'en-US', 12 | display: 'standalone', 13 | orientation: 'portrait', 14 | // startUrl: '/', 15 | preferRelatedApplications: false, 16 | backgroundColor: meta.backgroundColor, 17 | themeColor: meta.themeColor, 18 | scope: '/', 19 | icons: [ 20 | { 21 | type: 'image/png', 22 | src: 'icon-48.png', 23 | sizes: '48x48' 24 | }, 25 | { 26 | type: 'image/png', 27 | src: 'icon-57.png', 28 | sizes: '57x57' 29 | }, 30 | { 31 | type: 'image/png', 32 | src: 'icon-70.png', 33 | sizes: '70x70' 34 | }, 35 | { 36 | type: 'image/png', 37 | src: 'icon-72.png', 38 | sizes: '72x72' 39 | }, 40 | { 41 | type: 'image/png', 42 | src: 'icon-76.png', 43 | sizes: '76x76' 44 | }, 45 | { 46 | type: 'image/png', 47 | src: 'icon-96.png', 48 | sizes: '96x96' 49 | }, 50 | { 51 | type: 'image/png', 52 | src: 'icon-114.png', 53 | sizes: '114x114' 54 | }, 55 | { 56 | type: 'image/png', 57 | src: 'icon-120.png', 58 | sizes: '120x120' 59 | }, 60 | { 61 | type: 'image/png', 62 | src: 'icon-144.png', 63 | sizes: '144x144' 64 | }, 65 | { 66 | type: 'image/png', 67 | src: 'icon-150.png', 68 | sizes: '150x150' 69 | }, 70 | { 71 | type: 'image/png', 72 | src: 'icon-152.png', 73 | sizes: '152x152' 74 | }, 75 | { 76 | type: 'image/png', 77 | src: 'icon-192.png', 78 | sizes: '192x192' 79 | }, 80 | { 81 | type: 'image/png', 82 | src: 'icon-256.png', 83 | sizes: '256x256' 84 | }, 85 | { 86 | type: 'image/png', 87 | src: 'icon-310.png', 88 | sizes: '310x310' 89 | } 90 | ], 91 | 92 | // preferRelatedApplications: false, 93 | relatedApplications: [ 94 | // { 95 | // platform: 'play', 96 | // id: 'com.google.samples.apps.iosched' 97 | // } 98 | ] 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/config/tooling/offline.js: -------------------------------------------------------------------------------- 1 | // Use `offline-plugin` to let users cache your app 2 | // NOTE: to enable plugin, set options in `main.js`, `config.build.js` and `config.dev.build.js` 3 | 4 | // This file only defines the options: https://github.com/NekR/offline-plugin/blob/master/docs/options.md 5 | module.exports = { 6 | 7 | // Plugin options 8 | // appShell: null, 9 | // caches: 'all', 10 | // publicPath: '', 11 | 12 | // responseStrategy: 'network-first', 13 | // updateStrategy: 'all', 14 | // externals: [], 15 | // excludes: [], 16 | // relativePaths: false, 17 | autoUpdate: true 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/config/tooling/robotsTxt.js: -------------------------------------------------------------------------------- 1 | const paths = require('../paths') 2 | 3 | // robots.txt (only in production) 4 | // https://github.com/itgalaxy/generate-robotstxt 5 | // http://www.robotstxt.org 6 | module.exports = { 7 | sitemap: paths.host + 'sitemap.xml', 8 | host: paths.host, 9 | policy: [ 10 | // { 11 | // userAgent: '*', 12 | // allow: '/', 13 | // disallow: '/foo', 14 | // crawlDelay: 10, 15 | // cleanParam: 'ref /foo/' 16 | // } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/config/tooling/routes.js: -------------------------------------------------------------------------------- 1 | // NOTE: dependency to @components might cause webpack side effects when imported 2 | import * as components from '@components' 3 | 4 | export default [ 5 | 6 | { 7 | path: '/', 8 | name: 'home', 9 | component: components.PageHome 10 | }, 11 | 12 | { 13 | path: '/demo', 14 | name: 'demo', 15 | component: components.PageDemo 16 | } 17 | 18 | ] 19 | -------------------------------------------------------------------------------- /src/config/tooling/sitemap.js: -------------------------------------------------------------------------------- 1 | const paths = require('../paths') 2 | 3 | // sitemap.xml (only in production) 4 | // https://www.npmjs.com/package/sitemap-webpack-plugin 5 | module.exports = { 6 | base: paths.host, 7 | paths: [ 8 | '/' 9 | ], 10 | options: { 11 | fileName: 'sitemap.xml' 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/config/tooling/svgo.js: -------------------------------------------------------------------------------- 1 | // https://github.com/karify/external-svg-sprite-loader/blob/master/index.js 2 | // NOTE: normally SVGO wants its configuration values in a really weird format, but we will normalize it later 3 | module.exports = { 4 | removeViewBox: false, 5 | removeTitle: true, 6 | convertColors: { 7 | names2hex: true, 8 | rgb2hex: true, 9 | shorthex: true, 10 | shortname: true, 11 | 12 | // Convert this color code in fills/strokes/etc. to currentColor (used to generate mono-capable assets) 13 | // NOTE: must be exact, case-sensitive match before any other conversions 14 | // Assets must be authored with this in mind 15 | // https://github.com/svg/svgo/blob/master/plugins/_collections.js#L2527 16 | // https://github.com/svg/svgo/blob/master/plugins/convertColors.js#L61 17 | currentColor: '#FF00FF' 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/directives/global/imagesLoaded.js: -------------------------------------------------------------------------------- 1 | import ImagesLoaded from 'vue-images-loaded' 2 | export default ImagesLoaded 3 | -------------------------------------------------------------------------------- /src/directives/global/index.js: -------------------------------------------------------------------------------- 1 | export { default as ImagesLoaded } from './imagesLoaded' 2 | -------------------------------------------------------------------------------- /src/filters/global/ceil.js: -------------------------------------------------------------------------------- 1 | export default (value) => { 2 | return Math.ceil(value) 3 | } 4 | -------------------------------------------------------------------------------- /src/filters/global/decimal.js: -------------------------------------------------------------------------------- 1 | export default (value, length) => { 2 | return Number.parseFloat(value).toFixed(length ? length : 2) 3 | } 4 | -------------------------------------------------------------------------------- /src/filters/global/floor.js: -------------------------------------------------------------------------------- 1 | export default (value) => { 2 | return Math.floor(value) 3 | } 4 | -------------------------------------------------------------------------------- /src/filters/global/index.js: -------------------------------------------------------------------------------- 1 | export { default as ceil } from './ceil' 2 | export { default as decimal } from './decimal' 3 | export { default as floor } from './floor' 4 | export { default as round } from './round' 5 | -------------------------------------------------------------------------------- /src/filters/global/round.js: -------------------------------------------------------------------------------- 1 | export default (value) => { 2 | return Math.round(value) 3 | } 4 | -------------------------------------------------------------------------------- /src/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/src/fonts/.gitkeep -------------------------------------------------------------------------------- /src/index.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <% /* Title */ %> 8 | <%= htmlWebpackPlugin.options.config.meta.title %> 9 | 10 | 11 | <% /* Description */ %> 12 | <% if (htmlWebpackPlugin.options.config.meta.description && htmlWebpackPlugin.options.config.meta.description.length) { %> 13 | 14 | <% } %> 15 | 16 | <% /* Mobile tags */ %> 17 | <% if (htmlWebpackPlugin.options.config.meta.mobile) { %> 18 | 19 | 26 | <% } %> 27 | 28 | <% /* iOS Safari ison */ %> 29 | <% /* NOTE: `favicon.png` is a magical filename */ %> 30 | 31 | 32 | 33 | <% /* Mobile tags */ %> 34 | <% 35 | var robotsMeta = []; 36 | if (htmlWebpackPlugin.options.config.meta.robotsMeta && htmlWebpackPlugin.options.config.meta.robotsMeta.length) { 37 | for (var i = 0; i < htmlWebpackPlugin.options.config.meta.robotsMeta.length; i++) { 38 | robotsMeta.push(('' + htmlWebpackPlugin.options.config.meta.robotsMeta[i]).toUpperCase()); 39 | } 40 | } 41 | %> 42 | <% if (robotsMeta.length) { %> 43 | 44 | <% } %> 45 | 46 | <% 47 | var appStoreDefinitions = []; 48 | if ( 49 | htmlWebpackPlugin.options.config.meta.appStore && 50 | htmlWebpackPlugin.options.config.meta.appStore.appId && 51 | ('' + htmlWebpackPlugin.options.config.meta.appStore.appId).length 52 | ) { 53 | appStoreDefinitions.push('app-id=' + htmlWebpackPlugin.options.config.meta.appStore.appId); 54 | if (htmlWebpackPlugin.options.config.meta.appStore.affiliate && htmlWebpackPlugin.options.config.meta.appStore.affiliate.length) { 55 | appStoreDefinitions.push('affiliate-data=' + htmlWebpackPlugin.options.config.meta.appStore.affiliate); 56 | } 57 | if (htmlWebpackPlugin.options.config.meta.appStore.affiliate && htmlWebpackPlugin.options.config.meta.appStore.affiliate.length) { 58 | appStoreDefinitions.push('app-argument=' + htmlWebpackPlugin.options.config.meta.appStore.appArgument); 59 | } 60 | } 61 | %> 62 | <% if (appStoreDefinitions.length) { %> 63 | 64 | <% } %> 65 | 66 | <% /* Touch highlight settings */ %> 67 | <% if (!htmlWebpackPlugin.options.config.meta.defaultTouchHighlight) { %> 68 | 69 | 70 | <% } %> 71 | 72 | <% /* Format detection meta tags */ %> 73 | <% if (htmlWebpackPlugin.options.config.meta.formatDetection) { %> 74 | <% 75 | var formatDetectionRuleStrings = []; 76 | for (var key in htmlWebpackPlugin.options.config.meta.formatDetection) { 77 | if ( 78 | htmlWebpackPlugin.options.config.meta.formatDetection[key] !== null && 79 | htmlWebpackPlugin.options.config.meta.formatDetection[key] !== undefined 80 | ) { 81 | formatDetectionRuleStrings.push(key + '=' + htmlWebpackPlugin.options.config.meta.formatDetection[key]); 82 | } 83 | } 84 | %> 85 | 86 | <% } %> 87 | 88 | <% /* Viewport definitions as meta tags */ %> 89 | <% if (htmlWebpackPlugin.options.config.meta.viewport) { %> 90 | <% 91 | var viewportRuleStrings = []; 92 | for (var key in htmlWebpackPlugin.options.config.meta.viewport) { 93 | if ( 94 | htmlWebpackPlugin.options.config.meta.viewport[key] !== null && 95 | htmlWebpackPlugin.options.config.meta.viewport[key] !== undefined 96 | ) { 97 | viewportRuleStrings.push(key + '=' + htmlWebpackPlugin.options.config.meta.viewport[key]); 98 | } 99 | } 100 | %> 101 | 102 | <% } %> 103 | 104 | <% /* Viewport definitions as CSS */ %> 105 | <% 106 | var viewportCssRuleStrings = []; 107 | 108 | if (htmlWebpackPlugin.options.config.meta.viewport.width) { 109 | var viewportWidth = htmlWebpackPlugin.options.config.meta.viewport.width; 110 | if (viewportWidth && viewportWidth !== 'device-width') { 111 | viewportWidth += 'px'; 112 | } 113 | viewportCssRuleStrings.push('width: ' + viewportWidth + ';'); 114 | } 115 | 116 | if (htmlWebpackPlugin.options.config.meta.viewport.height) { 117 | var viewportHeight = htmlWebpackPlugin.options.config.meta.viewport.height; 118 | if (viewportHeight && viewportHeight !== 'device-height') { 119 | viewportHeight += 'px'; 120 | } 121 | viewportCssRuleStrings.push('height: ' + viewportHeight + ';'); 122 | } 123 | 124 | viewportCssRuleStrings = viewportCssRuleStrings.join(''); 125 | %> 126 | 131 | 132 | 133 | <% /* Windows: pinned site meta info (https://msdn.microsoft.com/en-us/library/dn255024(v=vs.85).aspx) */ %> 134 | 135 | 136 | 137 | 138 | <% /* Theme color */ %> 139 | <% if (htmlWebpackPlugin.options.config.meta.themeColor && htmlWebpackPlugin.options.config.meta.themeColor.length) { %> 140 | 141 | <% } %> 142 | 143 | <% /* iOS: Status bar style */ %> 144 | <% if (htmlWebpackPlugin.options.config.meta.iosStatusBarStyle && htmlWebpackPlugin.options.config.meta.iosStatusBarStyle.length) { %> 145 | 146 | <% } %> 147 | 148 | <% /* Prefetch meta tags */ %> 149 | <% for (var i = 0; i < htmlWebpackPlugin.options.config.paths.prefetch.length; i++) { %> 150 | 151 | <% } %> 152 | 153 | <% /* Links to static CSS files */ %> 154 | <% for (var i = 0; i < htmlWebpackPlugin.options.config.paths.styleLinks.length; i++) { %> 155 | 156 | <% } %> 157 | 158 | 159 | 160 |
161 | 162 | <% /* Built files will be auto injected */ %> 163 | 164 | <% /* Links to static JS files */ %> 165 | <% for (var i = 0; i < htmlWebpackPlugin.options.config.paths.scriptLinks.length; i++) { %> 166 | 167 | <% } %> 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { Vue, options } from '@vendor/vue' 2 | 3 | // NOTE: to enable offline plugin, uncomment the following line and ensure it is enabled in `config.build.js` 4 | // import '@vendor/offline-plugin-runtime' 5 | 6 | // Main Vue instance 7 | new Vue(options) 8 | -------------------------------------------------------------------------------- /src/mixins/global/classes.js: -------------------------------------------------------------------------------- 1 | import composeClassnames from '@util/composeClassnames' 2 | 3 | // Set a computed property with prefixed classnames 4 | // https://vuejs.org/v2/guide/mixins.html 5 | export default { 6 | 7 | computed: { 8 | 9 | classes () { 10 | const prefix = 'c-' + this.$options.name 11 | let classes = [prefix] 12 | 13 | if (this._classes) { 14 | classes = classes.concat(composeClassnames(this._classes, prefix)) 15 | } 16 | 17 | return classes 18 | } 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/mixins/global/index.js: -------------------------------------------------------------------------------- 1 | export { default as classes } from './classes' 2 | -------------------------------------------------------------------------------- /src/mixins/persist.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash' 2 | 3 | // Set a computed property to automatically store in localStorage 4 | // https://vuejs.org/v2/guide/mixins.html 5 | export default { 6 | 7 | computed: { 8 | 9 | // NOTE: This can be undefined especially for non-components 10 | persistKey () { 11 | return this.$options.name 12 | } 13 | 14 | }, 15 | 16 | watch: { 17 | 18 | // Store serialized data into localStorage when it changes (throttled) 19 | persist: debounce((data) => { 20 | if (this.persistKey) { 21 | localStorage.setItem(this.persistKey, JSON.stringify(data)) 22 | } 23 | }, 500) 24 | 25 | }, 26 | 27 | created () { 28 | if (this.persistKey && this.persist) { 29 | 30 | // Load serialized data from localStorage 31 | // NOTE: this is a synchronous operation, theoretically it might slow things down 32 | var data = localStorage.getItem(this.persistKey) 33 | 34 | if (data) { 35 | try { 36 | data = JSON.parse(data) 37 | 38 | // We found data in local storage, let's load it up 39 | if (data) { 40 | this.persist = data 41 | } 42 | 43 | } catch (error) { 44 | console.error(error) 45 | } 46 | } 47 | 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/models/DemoObject.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default Vue.extend({ 4 | 5 | props: { 6 | 7 | title: { 8 | type: String, 9 | required: true 10 | }, 11 | 12 | description: { 13 | type: String, 14 | required: false, 15 | default: null 16 | } 17 | 18 | }, 19 | 20 | computed: { 21 | 22 | titleAndDescription () { 23 | return this.title + (this.description ? (': ' + this.description) : '') 24 | } 25 | 26 | } 27 | 28 | }) 29 | -------------------------------------------------------------------------------- /src/services/events.js: -------------------------------------------------------------------------------- 1 | import { camelCase, map } from 'lodash' 2 | import Vue from 'vue' 3 | 4 | export default new Vue({ 5 | 6 | methods: { 7 | 8 | formatEventName (...eventNameParts) { 9 | return map(eventNameParts, (eventNamePart) => { 10 | return camelCase(eventNamePart) 11 | }).join(':') 12 | }, 13 | 14 | 15 | 16 | // These are passed to Vue's internal event API 17 | 18 | addListener (eventName, callback) { 19 | return this.$on(eventName, callback) 20 | }, 21 | 22 | removeListener (eventName, callback) { 23 | return this.$off(eventName, callback) 24 | }, 25 | 26 | emit (eventName, ...payload) { 27 | return this.$emit(eventName, ...payload) 28 | } 29 | 30 | } 31 | 32 | }) 33 | -------------------------------------------------------------------------------- /src/services/network.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default new Vue({ 4 | 5 | data () { 6 | return { 7 | isOnline: false 8 | } 9 | }, 10 | 11 | computed: { 12 | isOffline () { 13 | return !this.isOnline 14 | } 15 | }, 16 | 17 | created () { 18 | this.updateOnlineStatus() 19 | this.setListeners() 20 | }, 21 | 22 | beforeDestroy () { 23 | this.removeListeners() 24 | }, 25 | 26 | methods: { 27 | 28 | getOnlineStatus () { 29 | return window.navigator.onLine ? true : false 30 | }, 31 | 32 | updateOnlineStatus () { 33 | this.isOnline = this.getOnlineStatus() 34 | }, 35 | 36 | setListeners () { 37 | window.addEventListener('online', this.updateOnlineStatus) 38 | window.addEventListener('offline', this.updateOnlineStatus) 39 | }, 40 | 41 | removeListeners () { 42 | window.removeEventListener('online', this.updateOnlineStatus) 43 | window.removeEventListener('offline', this.updateOnlineStatus) 44 | } 45 | 46 | } 47 | 48 | }) 49 | -------------------------------------------------------------------------------- /src/services/time.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import requestAnimationFrame from 'raf' 3 | 4 | const intervalDuration = 1000 5 | 6 | export default new Vue({ 7 | 8 | data () { 9 | return { 10 | $_timer: null, 11 | current: new Date() 12 | } 13 | }, 14 | 15 | created () { 16 | this.$_onTimerUpdate() 17 | this.$_startTimer() 18 | }, 19 | 20 | beforeDestroy () { 21 | this.$_stopTimer() 22 | }, 23 | 24 | methods: { 25 | 26 | $_setCurrentTime () { 27 | this.current = new Date() 28 | }, 29 | 30 | $_onTimerUpdate () { 31 | requestAnimationFrame(this.$_setCurrentTime) 32 | }, 33 | 34 | $_startTimer () { 35 | this.$_stopTimer() 36 | this.$_onTimerUpdate() 37 | this.$_timer = setInterval(this.$_onTimerUpdate, intervalDuration) 38 | }, 39 | 40 | // NOTE: `current` will keep the last time but won't be updated 41 | $_stopTimer () { 42 | if (this.$_timer) { 43 | clearInterval(this.$_timer) 44 | this.$_timer = null 45 | } 46 | } 47 | 48 | } 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /src/services/viewport.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash' 2 | import Vue from 'vue' 3 | 4 | // Scroll position or dimensions are updated at most once per this amount of ms 5 | const debounceDelay = 10 6 | 7 | export default new Vue({ 8 | 9 | data () { 10 | return { 11 | width: 0, 12 | height: 0, 13 | scrollX: 0, 14 | scrollY: 0 15 | } 16 | }, 17 | 18 | computed: { 19 | 20 | isScrolled () { 21 | return this.scrollY > 0 ? true : false 22 | }, 23 | 24 | isScrolledX () { 25 | return this.scrollX > 0 ? true : false 26 | }, 27 | 28 | isLandscape () { 29 | return this.width > this.height ? true : false 30 | }, 31 | 32 | isPortrait () { 33 | return !this.isLandscape 34 | } 35 | 36 | }, 37 | 38 | created () { 39 | this.$_updateDimensions() 40 | this.$_updateScrollValues() 41 | 42 | // Bind listeners 43 | // FIXME: can/should I use this.$on (i.e. Vue's vustom events, NOT a wrapper for addEventListener)? 44 | window.addEventListener('resize', this.onResize) 45 | window.addEventListener('scroll', this.onScroll) 46 | 47 | }, 48 | 49 | beforeDestroy () { 50 | 51 | // Remove listeners 52 | window.removeEventListener('resize', this.onResize) 53 | window.removeEventListener('scroll', this.onScroll) 54 | 55 | }, 56 | 57 | methods: { 58 | 59 | 60 | 61 | // Helpers 62 | 63 | $_getScrollX () { 64 | return (window.pageXOffset || window.document.scrollLeft || 0) - (window.document.clientLeft || 0) 65 | }, 66 | 67 | $_getScrollY () { 68 | return (window.pageYOffset || window.document.scrollTop || 0) - (window.document.clientTop || 0) 69 | }, 70 | 71 | $_getWidth () { 72 | return window.innerWidth 73 | }, 74 | 75 | $_getHeight () { 76 | return window.innerHeight 77 | }, 78 | 79 | 80 | 81 | // Throttled updaters 82 | 83 | $_updateDimensions () { 84 | this.width = this.$_getWidth() 85 | this.height = this.$_getHeight() 86 | }, 87 | 88 | $_updateScrollValues () { 89 | this.scrollX = this.$_getScrollX() 90 | this.scrollY = this.$_getScrollY() 91 | }, 92 | 93 | // NOTE: won't work with arrow function since `this` scope will be different 94 | onResize: debounce(function () { 95 | this.$_updateDimensions() 96 | }, debounceDelay, { 97 | leading: true 98 | }), 99 | 100 | onScroll: debounce(function () { 101 | this.$_updateScrollValues() 102 | }, debounceDelay, { 103 | leading: true 104 | }) 105 | 106 | } 107 | 108 | }) 109 | -------------------------------------------------------------------------------- /src/store/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // When writing vuex code, some patterns that are normally disallowed can be common 2 | module.exports = { 3 | rules: { 4 | 5 | // We turn this off so we can comfortably mutate state that is passed as an argument 6 | 'no-param-reassign': ['off'], 7 | 8 | // We frequently break this rule by reusing common variable names in the same file 9 | 'no-shadow': ['off'] 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import buildConfig from '@config/build' 2 | 3 | import myModule from './myModule' 4 | 5 | export default { 6 | 7 | // https://vuex.vuejs.org/en/strict.html 8 | strict: (buildConfig.isDebug ? true : false), 9 | 10 | // https://vuex.vuejs.org/en/plugins.html 11 | // plugins: [], 12 | 13 | // State split into modules 14 | // https://vuex.vuejs.org/en/modules.html 15 | modules: { 16 | myModule 17 | }, 18 | 19 | // Global state 20 | state: {}, 21 | getters: {}, 22 | mutations: {}, 23 | actions: {} 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/store/myModule.js: -------------------------------------------------------------------------------- 1 | // https://vuex.vuejs.org/en/state.html 2 | export const state = () => { 3 | return { 4 | count: 0 5 | } 6 | } 7 | 8 | 9 | 10 | // https://vuex.vuejs.org/en/getters.html 11 | export const getters = { 12 | 13 | // args: (state, getters, rootState) 14 | doubleCount (state) { 15 | return state.count * 2 16 | } 17 | 18 | } 19 | 20 | 21 | 22 | // https://vuex.vuejs.org/en/mutations.html 23 | export const mutations = { 24 | 25 | increment (state) { 26 | state.count++ 27 | }, 28 | 29 | incrementBy (state, n) { 30 | state.count += n 31 | } 32 | 33 | } 34 | 35 | 36 | 37 | // https://vuex.vuejs.org/en/actions.html 38 | export const actions = { 39 | 40 | // NOTE: an action will be passed one `context` argument that we destructure here 41 | // args: (state, getters, rootState) 42 | increment ({ commit }) { 43 | commit('increment') 44 | }, 45 | 46 | incrementToEven ({ state, commit }) { 47 | const remainder = (state.count % 2) 48 | commit('incrementBy', (2 - Math.abs(remainder))) 49 | } 50 | 51 | } 52 | 53 | 54 | 55 | export default { 56 | namespaced: true, 57 | state, 58 | getters, 59 | mutations, 60 | actions 61 | } 62 | -------------------------------------------------------------------------------- /src/styles/defaults/body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: $default-font; 3 | font-size: $default-font-size; 4 | color: $default-color-text; 5 | background-color: $default-color-background; 6 | line-height: $default-line-height; 7 | } 8 | 9 | // NOTE: we can simply provide transition-property to add default transitions to element 10 | * { 11 | transition-property: none; 12 | transition-duration: $default-transition-duration; 13 | transition-timing-function: $default-transition-easing; 14 | } 15 | 16 | // Element defaults 17 | html, 18 | body, 19 | div, 20 | span, 21 | object, 22 | iframe, 23 | h1, 24 | h2, 25 | h3, 26 | h4, 27 | h5, 28 | h6, 29 | p, 30 | blockquote, 31 | pre, 32 | abbr, 33 | address, 34 | cite, 35 | code, 36 | del, 37 | dfn, 38 | em, 39 | img, 40 | ins, 41 | kbd, 42 | q, 43 | samp, 44 | a, 45 | small, 46 | strong, 47 | sub, 48 | sup, 49 | var, 50 | i, 51 | b, 52 | dl, 53 | dt, 54 | dd, 55 | ol, 56 | ul, 57 | li, 58 | fieldset, 59 | form, 60 | label, 61 | legend, 62 | table, 63 | caption, 64 | tbody, 65 | tfoot, 66 | thead, 67 | tr, 68 | th, 69 | td, 70 | article, 71 | aside, 72 | canvas, 73 | details, 74 | figcaption, 75 | figure, 76 | footer, 77 | header, 78 | hgroup, 79 | menu, 80 | nav, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video, 87 | button, 88 | input, 89 | textarea { 90 | border-color: $default-color-border; 91 | 92 | &:before, 93 | &:after { 94 | vertical-align: middle; 95 | border-color: $default-color-border; 96 | } 97 | 98 | } 99 | 100 | strong, 101 | dt, 102 | mark, 103 | th { 104 | @include type-strong; 105 | } 106 | 107 | blockquote, 108 | q { 109 | @include type-serif; 110 | } 111 | 112 | pre, 113 | code, 114 | kbd, 115 | samp { 116 | @include type-mono; 117 | } 118 | 119 | abbr[title], 120 | dfn[title] { 121 | @include cursor-help; 122 | text-decoration: underline; 123 | } 124 | -------------------------------------------------------------------------------- /src/styles/defaults/forms.scss: -------------------------------------------------------------------------------- 1 | button, 2 | input { 3 | padding: 0; 4 | color: inherit; 5 | font-size: inherit; 6 | vertical-align: baseline; 7 | } 8 | 9 | button { 10 | border-width: 0; 11 | background-color: transparent; 12 | @include cursor-pointer; 13 | @include type-link; 14 | 15 | @include transition-hover-active; 16 | @include transition-properties-common; 17 | 18 | &:hover, 19 | &:active { 20 | color: $default-color-link-active; 21 | } 22 | 23 | &:focus { 24 | outline-width: 0; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/defaults/headings.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | @include type-h1; 3 | } 4 | 5 | h2 { 6 | @include type-h2; 7 | } 8 | 9 | h3 { 10 | @include type-h3; 11 | } 12 | 13 | h4 { 14 | @include type-h4; 15 | } 16 | 17 | h5 { 18 | @include type-h5; 19 | } 20 | 21 | h6 { 22 | @include type-h6; 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/defaults/links.scss: -------------------------------------------------------------------------------- 1 | a { 2 | color: inherit; 3 | display: inline-block; 4 | @include cursor-pointer; 5 | 6 | @include transition-properties-common; 7 | @include transition-hover-active; 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/defaults/lists.scss: -------------------------------------------------------------------------------- 1 | ul, 2 | ol { 3 | list-style-type: none; 4 | padding-left: 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | // Shared scss tools (no output from these) 2 | @import './shared'; 3 | 4 | // Keyframe animations 5 | @import './keyframes/keyframes-fade-in'; 6 | @import './keyframes/keyframes-pulse'; 7 | @import './keyframes/keyframes-spin'; 8 | 9 | // Baseline stuff 10 | @import '~moabit/font-face'; 11 | @import '~moabit/normalize'; 12 | 13 | // Defaults 14 | @import './defaults/body'; 15 | @import './defaults/forms'; 16 | @import './defaults/headings'; 17 | @import './defaults/links'; 18 | @import './defaults/lists'; 19 | 20 | // Utilities 21 | @import './utilities/utility-bodytext'; 22 | 23 | // Transitions 24 | @import './transitions/transition-fade'; 25 | -------------------------------------------------------------------------------- /src/styles/keyframes/keyframes-fade-in.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade-in { 2 | 3 | 0% { 4 | opacity: 0; 5 | } 6 | 7 | 100% { 8 | opacity: 1; 9 | } 10 | 11 | } 12 | 13 | @keyframes fade-in-delayed { 14 | 15 | 0%, 16 | 50% { 17 | opacity: 0; 18 | } 19 | 20 | 100% { 21 | opacity: 1; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/keyframes/keyframes-pulse.scss: -------------------------------------------------------------------------------- 1 | @keyframes pulse { 2 | 3 | 0%, 4 | 100% { 5 | opacity: 1; 6 | } 7 | 8 | 50% { 9 | opacity: 0; 10 | } 11 | 12 | } 13 | 14 | @keyframes pulse-delayed { 15 | 16 | 0%, 17 | 40%, 18 | 100% { 19 | opacity: 1; 20 | } 21 | 22 | 20% { 23 | opacity: 0; 24 | } 25 | 26 | } 27 | 28 | @keyframes pulse-scale { 29 | 30 | 0%, 31 | 100% { 32 | opacity: 1; 33 | transform: scale(1); 34 | } 35 | 36 | 50% { 37 | opacity: 0; 38 | transform: scale(0); 39 | } 40 | 41 | } 42 | 43 | @keyframes pulse-scale-delayed { 44 | 45 | 0%, 46 | 40%, 47 | 100% { 48 | opacity: 1; 49 | transform: scale(1); 50 | } 51 | 52 | 20% { 53 | opacity: 0; 54 | transform: scale(0); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/keyframes/keyframes-spin.scss: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 3 | 100% { 4 | transform: rotate(360deg); 5 | } 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/mixins/mixin-type.scss: -------------------------------------------------------------------------------- 1 | // From `moabit` 2 | // NOTE: You can simply override mixins in Sass 3 | @mixin type-mono { 4 | font-family: $font-mono; 5 | font-size: 0.85em; 6 | } 7 | 8 | 9 | 10 | // Font styles 11 | @mixin type-uppercase { 12 | // font-size: $font-size-uppercase; 13 | font-weight: $font-weight-bold; 14 | text-transform: uppercase; 15 | } 16 | 17 | @mixin type-small { 18 | font-size: $font-size-small; 19 | } 20 | 21 | @mixin type-large { 22 | font-size: $font-size-large; 23 | // line-height: $line-height-loose; 24 | 25 | @include viewport-over-small { 26 | font-size: 1.4em; 27 | } 28 | 29 | } 30 | 31 | @mixin type-display { 32 | 33 | @if $font-display { 34 | font-family: $font-display; 35 | } 36 | 37 | font-size: $font-size-display; 38 | font-weight: inherit; 39 | 40 | line-height: $line-height-tight; 41 | 42 | // @include viewport-over-tiny { 43 | // font-size: 1.4em; 44 | // } 45 | 46 | } 47 | 48 | @mixin type-discreet { 49 | color: $default-color-discreet; 50 | } 51 | 52 | @mixin type-strong { 53 | font-weight: $font-weight-bold; 54 | } 55 | 56 | @mixin type-discreet-small { 57 | @include type-discreet; 58 | @include type-small; 59 | } 60 | 61 | @mixin type-warning { 62 | color: $color-red; 63 | } 64 | 65 | 66 | 67 | // Headings 68 | 69 | @mixin type-h1 { 70 | @include type-display; 71 | font-weight: bold; 72 | font-size: 1.6em; 73 | letter-spacing: -0.015em; 74 | 75 | @include viewport-over-tiny { 76 | font-size: 1.8em; 77 | } 78 | 79 | @include viewport-over-small { 80 | font-size: 2.2em; 81 | } 82 | 83 | @include viewport-over-medium { 84 | font-size: 2.4em; 85 | } 86 | 87 | @include viewport-over-large { 88 | font-size: 2.6em; 89 | } 90 | 91 | } 92 | 93 | @mixin type-h2 { 94 | font-size: 1.4em; 95 | } 96 | 97 | @mixin type-h3 { 98 | font-size: inherit; 99 | @include type-uppercase; 100 | 101 | // @include viewport-over-tiny { 102 | // @include type-strong; 103 | // } 104 | 105 | } 106 | 107 | @mixin type-h4 { 108 | @include type-discreet-small; 109 | @include type-uppercase; 110 | letter-spacing: 0.1em; 111 | } 112 | 113 | @mixin type-h5 { 114 | font-size: inherit; 115 | @include type-uppercase; 116 | @include type-discreet; 117 | } 118 | 119 | @mixin type-h6 { 120 | @include type-h5; 121 | } 122 | 123 | 124 | 125 | // Other elements 126 | 127 | @mixin type-link { 128 | // font-weight: $font-weight-bold; 129 | color: $default-color-link; 130 | } 131 | -------------------------------------------------------------------------------- /src/styles/shared.scss: -------------------------------------------------------------------------------- 1 | // Shared scss tools (no output from these) 2 | @import '~moabit/variables'; 3 | @import '~moabit/functions'; 4 | @import '~moabit/mixins'; 5 | 6 | // Pick and choose mixins if you want 7 | // @import '~moabit/mixins/mixin-align'; 8 | // @import '~moabit/mixins/mixin-animations'; 9 | // @import '~moabit/mixins/mixin-background'; 10 | // @import '~moabit/mixins/mixin-box-model'; 11 | // @import '~moabit/mixins/mixin-buffer'; 12 | // @import '~moabit/mixins/mixin-cursor'; 13 | // @import '~moabit/mixins/mixin-dimensions'; 14 | // @import '~moabit/mixins/mixin-display'; 15 | // @import '~moabit/mixins/mixin-fill'; 16 | // @import '~moabit/mixins/mixin-flex'; 17 | // @import '~moabit/mixins/mixin-font-face'; 18 | // @import '~moabit/mixins/mixin-gradient'; 19 | // @import '~moabit/mixins/mixin-hide'; 20 | // @import '~moabit/mixins/mixin-keep'; 21 | // @import '~moabit/mixins/mixin-limit'; 22 | // @import '~moabit/mixins/mixin-nudge'; 23 | // @import '~moabit/mixins/mixin-overflow'; 24 | // @import '~moabit/mixins/mixin-pad'; 25 | // @import '~moabit/mixins/mixin-position'; 26 | // @import '~moabit/mixins/mixin-push'; 27 | // @import '~moabit/mixins/mixin-radius'; 28 | // @import '~moabit/mixins/mixin-shadow'; 29 | // @import '~moabit/mixins/mixin-transform'; 30 | // @import '~moabit/mixins/mixin-transitions'; 31 | // @import '~moabit/mixins/mixin-type'; 32 | // @import '~moabit/mixins/mixin-viewport'; 33 | 34 | // Custom overrides 35 | @import './variables'; 36 | 37 | // Custom mixins 38 | @import './mixins/mixin-type'; 39 | -------------------------------------------------------------------------------- /src/styles/transitions/transition-fade.scss: -------------------------------------------------------------------------------- 1 | // Transition code 2 | // https://vuejs.org/v2/guide/transitions.html 3 | // https://vuejs.org/images/transition.png 4 | 5 | // While either transition is playing 6 | // FIXME: would be better to not specify the opacity value 1, but let the default or component's defined value stand 7 | .transition-fade-enter-active, 8 | .transition-fade-leave-active { 9 | opacity: 1; 10 | @include transition-fast; 11 | @include transition-properties(opacity); 12 | } 13 | 14 | // Start state for enter 15 | .transition-fade-enter, 16 | 17 | // End state for exit 18 | .transition-fade-leave-to { 19 | opacity: 0; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/utilities/utility-bodytext.scss: -------------------------------------------------------------------------------- 1 | // Text context 2 | 3 | // This is used for rich body text. Elements commonly used in building components, like links, are robbed of their sensible defaults. Attach this utility class on a body text container to style the rich text elements in a way that makes sense for natural article content. 4 | 5 | .bodytext { 6 | 7 | a { 8 | @include type-link; 9 | 10 | &:hover, 11 | &:active { 12 | color: $default-color-link-active; 13 | } 14 | 15 | } 16 | 17 | strong { 18 | font-weight: $font-weight-bold; 19 | } 20 | 21 | 22 | 23 | // Code 24 | 25 | code { 26 | padding: 0 0.2em; 27 | @include radius-tight; 28 | background-color: $color-very-very-light-grey; 29 | box-shadow: 0 0 0 1px $color-very-light-grey; 30 | } 31 | 32 | pre { 33 | @include radius-loose; 34 | @include pad-loose; 35 | background-color: $color-very-very-light-grey; 36 | box-shadow: 0 0 0 1px $color-very-light-grey; 37 | } 38 | 39 | dt, 40 | h3, 41 | h4 { 42 | code { 43 | text-transform: none; 44 | } 45 | } 46 | 47 | 48 | 49 | // Lists 50 | 51 | ul { 52 | list-style-type: disc; 53 | } 54 | 55 | ol { 56 | list-style-type: decimal; 57 | } 58 | 59 | ul, 60 | ol { 61 | padding-left: 2em; 62 | 63 | ul, 64 | ol { 65 | margin-bottom: 0.8em; 66 | } 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // This template uses Moabit by default 2 | // If you want to keep using it, see what variables are loaded by default here: 3 | // https://github.com/Eiskis/moabit/blob/master/variables.scss 4 | $color-vue: #4fc08d; 5 | 6 | $color-very-very-light-grey: #fafafa; 7 | $color-very-light-grey: #f6f6f6; 8 | $color-light-grey: #eaeaea; 9 | $color-grey: #b0b0b0; 10 | $color-dark: #2c3e50; 11 | $color-very-dark: #101010; 12 | 13 | // Typography 14 | $font-sans: 'Source Sans Pro', 'Source Sans', 'system-ui', 'San Francisco', 'Helvetica Neue', 'Helvetica', 'Segoe UI', 'Segoe WP', 'Arial', sans-serif; 15 | $font-serif: 'Droid Serif', 'Georgia', serif; 16 | $font-mono: 'Roboto Mono', 'Monaco', 'Menlo', 'Segoe UI Mono', 'Consolas', courier, monospace; 17 | $font-display: 'Dosis', 'Avenir', $font-sans; 18 | 19 | 20 | 21 | // Local web fonts to generate a font-face definitions for 22 | // 23 | // NOTE: load hosted fonts via `config.paths.js` (Google Fonts etc.) 24 | // 25 | // NOTE: 26 | // - Remember to use trailing commas with Sass nested lists to avoid gotchas 27 | // - File paths should be defined WITHOUT file extensions 28 | // $local-web-fonts: ( 29 | // ( 30 | // font-family: 'Source Sans Pro', 31 | // font-weight: 400, 32 | // font-style: normal, 33 | // path: '~@fonts/source-sans/sourcesanspro-regular-webfont', 34 | // ), 35 | // ( 36 | // font-family: 'Source Sans Pro', 37 | // font-weight: 400, 38 | // font-style: italic, 39 | // path: '~@fonts/source-sans/sourcesanspro-italic-webfont', 40 | // ), 41 | // ( 42 | // font-family: 'Source Sans Pro', 43 | // font-weight: 700, 44 | // font-style: normal, 45 | // path: '~@fonts/source-sans/sourcesanspro-bold-webfont', 46 | // ), 47 | // ( 48 | // font-family: 'Source Sans Pro', 49 | // font-weight: 700, 50 | // font-style: italic, 51 | // path: '~@fonts/source-sans/sourcesanspro-bolditalic-webfont', 52 | // ), 53 | // ); 54 | 55 | 56 | 57 | $pad-loose-vertical: 10px; 58 | 59 | 60 | 61 | // Defaults 62 | $default-font: $font-sans; 63 | $default-font-size: 18px; 64 | 65 | $default-color-background: $color-very-very-light-grey; 66 | $default-color-text: $color-dark; 67 | $default-color-link: $color-vue; 68 | $default-color-link-active: color-saturate($color-vue); 69 | $default-color-border: $color-light-grey; 70 | $default-shadow-color: $color-dark; 71 | -------------------------------------------------------------------------------- /src/svg/Cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | src/svg/Cog 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/svg/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | svg/Logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 43 | 44 | -------------------------------------------------------------------------------- /src/svg/index.js: -------------------------------------------------------------------------------- 1 | export { default as Cog } from './Cog.svg' 2 | export { default as Logo } from './Logo.svg' 3 | -------------------------------------------------------------------------------- /src/util/blur.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | window.document.activeElement.blur() 3 | } 4 | -------------------------------------------------------------------------------- /src/util/clearSelection.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | 3 | if (window.document.selection && window.document.selection.empty) { 4 | window.document.selection.empty() 5 | 6 | } else if (window.getSelection) { 7 | window.getSelection().removeAllRanges() 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/util/composeClassnames.js: -------------------------------------------------------------------------------- 1 | import { isNumber, isString, kebabCase } from 'lodash' 2 | 3 | const normalizePrefix = (prefix) => { 4 | if (!prefix && prefix !== '') { 5 | return 'is' 6 | } 7 | return prefix 8 | } 9 | 10 | const composeClassname = (key, value, prefix) => { 11 | 12 | if (value) { 13 | const normalizedPrefix = normalizePrefix(prefix) 14 | let classname = '' + key 15 | 16 | // String/number value goes into the class name 17 | if (isString(value) || isNumber(value)) { 18 | classname = classname + '-' + value 19 | 20 | // Otherwise we use boolean classnames 21 | } else { 22 | 23 | // Prevent duplicating prefixes if they're passed in the keys 24 | if (classname.substr(0, normalizedPrefix.length) === normalizedPrefix) { 25 | classname = classname.substr(normalizedPrefix.length) 26 | } 27 | 28 | } 29 | 30 | // Add formatted classname to result list 31 | return kebabCase(normalizedPrefix + '-' + classname) 32 | } 33 | 34 | return null 35 | } 36 | 37 | // Generate HTML/CSS class names based on a set of state, with prefixes and negatives added 38 | export default (stateHash, prefix) => { 39 | const classes = [] 40 | 41 | // Treat each class 42 | for (const key in stateHash) { 43 | const classname = composeClassname(key, stateHash[key], prefix) 44 | 45 | // We only add the classname if the value is truthy 46 | if (classname) { 47 | classes.push(classname) 48 | } 49 | 50 | } 51 | 52 | return classes 53 | } 54 | -------------------------------------------------------------------------------- /src/util/createInstance.js: -------------------------------------------------------------------------------- 1 | import { camelCase, isString, upperFirst } from 'lodash' 2 | 3 | export default (modelOrModelName, propsData) => { 4 | let Model = modelOrModelName 5 | 6 | // Require `Model` dynamically 7 | if (isString(Model)) { 8 | const normalizedModelName = upperFirst(camelCase(Model)) 9 | 10 | try { 11 | Model = require('@models/' + normalizedModelName).default 12 | } catch (error) { 13 | console.warn('newModelInstance: Model not found ("' + normalizedModelName + '").') 14 | return null 15 | } 16 | 17 | } 18 | 19 | return new Model({ 20 | propsData 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/util/destroyInstances.js: -------------------------------------------------------------------------------- 1 | import { isArray } from 'lodash' 2 | 3 | const destroyModelInstance = (modelInstance) => { 4 | return modelInstance.$destroy() 5 | } 6 | 7 | const isInstance = (obj) => { 8 | return obj && obj.$destroy ? true : false 9 | } 10 | 11 | export default (arrayOrInstance, callback) => { 12 | 13 | // One instance 14 | if (isInstance(arrayOrInstance)) { 15 | destroyModelInstance(arrayOrInstance) 16 | 17 | // Array 18 | } else if (isArray(arrayOrInstance)) { 19 | arrayOrInstance.forEach((instance, key) => { 20 | destroyModelInstance(instance) 21 | }) 22 | } 23 | 24 | if (callback) { 25 | callback() 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/util/eventHasMetaKey.js: -------------------------------------------------------------------------------- 1 | // Check if keyboard event object includes meta key being pressed 2 | export default (event) => { 3 | return (event.ctrlKey || event.metaKey || event.shiftKey) 4 | } 5 | -------------------------------------------------------------------------------- /src/util/isAbsoluteUrl.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative 2 | export default (string) => { 3 | return new RegExp('^(?:[a-z]+:)?//', 'i').test(string) ? true : false 4 | } 5 | -------------------------------------------------------------------------------- /src/util/trimWhitespace.js: -------------------------------------------------------------------------------- 1 | // Condence all whitespace in a string to maximum of one space 2 | export default (string) => { 3 | return string.trim().replace(/\s+/g, ' ') 4 | } 5 | -------------------------------------------------------------------------------- /src/vendor/offline-plugin-runtime.js: -------------------------------------------------------------------------------- 1 | import * as OfflinePluginRuntime from 'offline-plugin/runtime' 2 | import buildConfig from '@config/build' 3 | 4 | const runtime = (buildConfig.offline ? OfflinePluginRuntime : null) 5 | if (runtime) { 6 | runtime.install() 7 | } 8 | 9 | export default runtime 10 | -------------------------------------------------------------------------------- /src/vendor/vue-analytics.js: -------------------------------------------------------------------------------- 1 | // https://matteogabriele.gitbooks.io/vue-analytics/content/docs/installation.html 2 | import Vue from 'vue' 3 | import VueAnalytics from 'vue-analytics' 4 | 5 | import analyticsConfig from '@config/analytics' 6 | import routerInstance from './vue-router' 7 | 8 | // Avoid registering this without ID 9 | // NOTE: if you import this file, the library will still be in your bundle, increasing file size 10 | if (analyticsConfig.id) { 11 | Vue.use(VueAnalytics, { 12 | ...analyticsConfig, 13 | router: routerInstance 14 | }) 15 | } 16 | 17 | export default VueAnalytics 18 | -------------------------------------------------------------------------------- /src/vendor/vue-meta.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Meta from 'vue-meta' 3 | 4 | Vue.use(Meta) 5 | 6 | export default Meta 7 | -------------------------------------------------------------------------------- /src/vendor/vue-router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import routerConfig from '@config/router' 5 | import routes from '@routes' 6 | 7 | Vue.use(VueRouter) 8 | 9 | export default new VueRouter({ 10 | ...routerConfig, 11 | routes 12 | }) 13 | -------------------------------------------------------------------------------- /src/vendor/vue.js: -------------------------------------------------------------------------------- 1 | // This file sets up the main Vue instance 2 | // Conventionally this is done in main.js, but here we set it up like any vendor library 3 | // You can also use this file to set up Vue in your test harness or for other similar use cases 4 | 5 | 6 | 7 | // The Vue build version to load with the `import` command 8 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 9 | import Vue from 'vue' 10 | 11 | // Helpers 12 | import { camelCase, kebabCase } from 'lodash' 13 | 14 | // Each Vue plugin that needs setup 15 | import './vue-meta' 16 | import router from './vue-router' 17 | import vuex from './vuex' 18 | 19 | // NOTE: uncomment to enable `vue-analytics` (also see `config.analytics.js`) 20 | import './vue-analytics' 21 | 22 | // Globally registered Vue bits 23 | import * as components from '@components' 24 | import * as svgComponents from '@svg' 25 | import * as directives from '@directives-global' 26 | import * as filters from '@filters-global' 27 | import * as mixins from '@mixins-global' 28 | 29 | 30 | 31 | // Vue setup 32 | 33 | // Config 34 | Vue.config.productionTip = false 35 | 36 | // Register all components on the top level 37 | for (const componentName in components) { 38 | Vue.component(kebabCase(componentName), components[componentName]) 39 | } 40 | for (const svgName in svgComponents) { 41 | Vue.component(kebabCase('svg-' + svgName), svgComponents[svgName]) 42 | } 43 | 44 | // Register global directives on the top level 45 | for (const directiveName in directives) { 46 | Vue.directive(camelCase(directiveName), directives[directiveName]) 47 | } 48 | 49 | // Register global filters on the top level 50 | for (const filterName in filters) { 51 | Vue.filter(camelCase(filterName), filters[filterName]) 52 | } 53 | 54 | // Register global mixins on the top level 55 | for (const mixinName in mixins) { 56 | Vue.mixin(mixins[mixinName]) 57 | } 58 | 59 | 60 | 61 | const options = { 62 | el: '#app', 63 | router, 64 | store: vuex, 65 | template: '' 66 | } 67 | 68 | // Everything that's bootstrapped 69 | export { 70 | Vue, 71 | options 72 | } 73 | -------------------------------------------------------------------------------- /src/vendor/vuex.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import store from '@store' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store(store) 9 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/.gitkeep -------------------------------------------------------------------------------- /static/_redirects: -------------------------------------------------------------------------------- 1 | # Redirect all requests to app 2 | /* /index.html 200 3 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/favicon.png -------------------------------------------------------------------------------- /static/icon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-114.png -------------------------------------------------------------------------------- /static/icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-120.png -------------------------------------------------------------------------------- /static/icon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-144.png -------------------------------------------------------------------------------- /static/icon-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-150.png -------------------------------------------------------------------------------- /static/icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-152.png -------------------------------------------------------------------------------- /static/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-192.png -------------------------------------------------------------------------------- /static/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-256.png -------------------------------------------------------------------------------- /static/icon-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-310.png -------------------------------------------------------------------------------- /static/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-48.png -------------------------------------------------------------------------------- /static/icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-57.png -------------------------------------------------------------------------------- /static/icon-70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-70.png -------------------------------------------------------------------------------- /static/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-72.png -------------------------------------------------------------------------------- /static/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-76.png -------------------------------------------------------------------------------- /static/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryjappinen/bellevue/0f0f724c7315b1b90a6753ee5f23f4f1fc33be23/static/icon-96.png -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['e2e'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const path = require('path') 3 | 4 | // We need to turn aliases to something like this for Jest: 5 | // moduleNameMapper: { 6 | // '^@/(.*)$': '/src/$1' 7 | // }, 8 | 9 | const aliasConfig = require('../../src/config/tooling/aliases.js') 10 | 11 | function escapeJestRegexp (str) { 12 | return str 13 | } 14 | 15 | function treatAliasKey (aliasValue, aliasKey) { 16 | return '^' + escapeJestRegexp(aliasKey) + '/(.*)$' 17 | } 18 | 19 | function treatAliasValue (aliasValue) { 20 | return '/' + escapeJestRegexp(aliasValue) + '/$1' 21 | } 22 | 23 | function treatAliasKeySingle (aliasValue, aliasKey) { 24 | return '^' + escapeJestRegexp(aliasKey) 25 | } 26 | 27 | function treatAliasValueSingle (aliasValue) { 28 | return '/' + escapeJestRegexp(aliasValue) 29 | } 30 | 31 | let jestAliases = _.mapKeys(_.mapValues(aliasConfig, treatAliasValue), treatAliasKey) 32 | _.merge(jestAliases, _.mapKeys(_.mapValues(aliasConfig, treatAliasValueSingle), treatAliasKeySingle)) 33 | 34 | module.exports = { 35 | rootDir: path.resolve(__dirname, '../../'), 36 | moduleFileExtensions: [ 37 | 'js', 38 | 'json', 39 | 'vue' 40 | ], 41 | moduleNameMapper: _.merge({}, jestAliases, { 42 | 43 | // NOTE: Jest can't handle static assets 44 | '^.+\\.(jpg|jpeg|gif|png|mp4|mkv|avi|webm|swf|wav|mid)$': 'jest-static-stubs/$1', 45 | 46 | // NOTE: Jest can't handle this SCSS variables loader 47 | '!!sass-to-js!@styles-variables': '/test/unit/stubs/scssShared.stub.js', 48 | 49 | // NOTE: Jest can't handle this SVG loader 50 | '^.+\\.(svg)$': '/test/unit/stubs/svg.stub.js' 51 | 52 | }), 53 | transform: { 54 | '^.+\\.js$': '/node_modules/babel-jest', 55 | '.*\\.(scss)$': '/node_modules/jest-css-modules', 56 | '.*\\.(vue)$': '/node_modules/vue-jest' 57 | }, 58 | testPathIgnorePatterns: [ 59 | '/test/e2e' 60 | ], 61 | testURL: 'http://localhost/', 62 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 63 | setupFiles: ['/test/unit/setup'], 64 | coverageDirectory: '/test/unit/coverage', 65 | collectCoverageFrom: [ 66 | 'src/**/*.{js,vue}', 67 | '!src/svg/index.js', 68 | '!src/main.js', 69 | '!**/node_modules/**' 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | // import { Vue } from '@vendor/vue' 2 | import Vue from 'vue' 3 | 4 | Vue.config.productionTip = false 5 | -------------------------------------------------------------------------------- /test/unit/stubs/scssShared.stub.js: -------------------------------------------------------------------------------- 1 | // See jest.conf.js 2 | module.exports = { 3 | colorGreen: '#00ff00' 4 | } 5 | -------------------------------------------------------------------------------- /test/unit/stubs/svg.stub.js: -------------------------------------------------------------------------------- 1 | // See jest.conf.js 2 | module.exports = '' 3 | -------------------------------------------------------------------------------- /unit/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var base = require('../src/.eslintrc'); 3 | 4 | module.exports = _.merge( 5 | {}, 6 | base, 7 | { 8 | plugins: base.plugins.concat([ 9 | 'jest' 10 | ]), 11 | extends: base.extends.concat([ 12 | 'plugin:jest/recommended' 13 | ]), 14 | env: { 15 | jest: true 16 | }, 17 | // globals: {}, 18 | rules: {} 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /unit/components/pages/PageHome.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'vue-test-utils' 2 | import { PageHome } from '@components' 3 | // import { Vue } from '@vendor/vue' 4 | 5 | describe('PageHome.vue', () => { 6 | // const Constructor = Vue.extend(PageHome) 7 | // const vm = new Constructor().$mount() 8 | const wrapper = mount(PageHome, {}) 9 | 10 | it('should render title element', () => { 11 | expect(wrapper.contains('h1')).toBeTruthy() 12 | }) 13 | 14 | it('should render correct contents', () => { 15 | expect(wrapper.vm.$el.querySelector('h1').textContent) 16 | .toEqual('Hello world!') 17 | }) 18 | 19 | }) 20 | -------------------------------------------------------------------------------- /unit/components/snippets/Bitmap.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'vue-test-utils' 2 | import { Bitmap } from '@components' 3 | 4 | describe('Bitmap.vue', () => { 5 | 6 | describe('should render relative src', () => { 7 | 8 | const wrapper = mount(Bitmap, { 9 | propsData: { 10 | src: 'logo.png', 11 | showUnloaded: true, 12 | title: 'foo' 13 | } 14 | }) 15 | 16 | it('with absolute mode detected', () => { 17 | expect(wrapper.vm.srcIsAbsolute).toEqual(false) 18 | }) 19 | 20 | it('as ', () => { 21 | expect(wrapper.contains('img')).toBeTruthy() 22 | }) 23 | 24 | it('with alt', () => { 25 | expect(wrapper.vm.$el.getAttribute('alt')).toEqual('foo') 26 | }) 27 | 28 | it('with title', () => { 29 | expect(wrapper.vm.$el.getAttribute('title')).toEqual('foo') 30 | }) 31 | 32 | }) 33 | 34 | describe('should render absolute src', () => { 35 | 36 | const wrapper = mount(Bitmap, { 37 | propsData: { 38 | src: '//logo.png' 39 | } 40 | }) 41 | 42 | it('with absolute mode detected', () => { 43 | expect(wrapper.vm.srcIsAbsolute).toEqual(true) 44 | }) 45 | 46 | }) 47 | 48 | }) 49 | -------------------------------------------------------------------------------- /unit/components/snippets/ExternalLink.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'vue-test-utils' 2 | import { ExternalLink } from '@components' 3 | 4 | describe('ExternalLink.vue', () => { 5 | 6 | describe('should render relative src', () => { 7 | 8 | const url = '//foo' 9 | const wrapper = mount(ExternalLink, { 10 | propsData: { 11 | href: url 12 | } 13 | }) 14 | 15 | it('as ', () => { 16 | expect(wrapper.contains('a')).toBeTruthy() 17 | }) 18 | 19 | it('with href', () => { 20 | expect(wrapper.vm.$el.getAttribute('href')).toEqual(url) 21 | }) 22 | 23 | it('with target', () => { 24 | expect(wrapper.vm.$el.getAttribute('target')).toEqual('_blank') 25 | }) 26 | 27 | it('with rel', () => { 28 | expect(wrapper.vm.$el.getAttribute('rel')).toEqual('nofollow') 29 | }) 30 | 31 | }) 32 | 33 | }) 34 | -------------------------------------------------------------------------------- /unit/services/events.spec.js: -------------------------------------------------------------------------------- 1 | import { defer } from 'lodash' 2 | import events from '@services/events' 3 | 4 | describe('Service events', () => { 5 | 6 | // Event names 7 | 8 | describe('Service event formatEventName', () => { 9 | 10 | it('should generate simple name', () => { 11 | expect(events.formatEventName( 12 | 'foo' 13 | )).toEqual('foo') 14 | }) 15 | 16 | it('should join two parts with colon', () => { 17 | expect(events.formatEventName( 18 | 'foo', 'bar' 19 | )).toEqual('foo:bar') 20 | }) 21 | 22 | it('should join three parts with colons', () => { 23 | expect(events.formatEventName( 24 | 'foo', 'bar', 'baz' 25 | )).toEqual('foo:bar:baz') 26 | }) 27 | 28 | it('should convert parts to camelCase', () => { 29 | expect(events.formatEventName( 30 | 'fooBar', 'bar-Bar', 'BAZ_BAZ' 31 | )).toEqual( 32 | 'fooBar:barBar:bazBaz' 33 | ) 34 | }) 35 | 36 | }) 37 | 38 | 39 | 40 | // Listener execution 41 | 42 | describe('Service event listeners execute', () => { 43 | 44 | it('after emit', () => { 45 | let testVar = false 46 | const testListener = () => { 47 | testVar = true 48 | } 49 | 50 | events.addListener('foo', testListener) 51 | events.emit('foo') 52 | 53 | defer(() => { 54 | events.removeListener('foo', testListener) 55 | expect(testVar).toEqual(true) 56 | }) 57 | }) 58 | 59 | it('with payload', () => { 60 | let testVar = false 61 | const testListener = (foo, bar) => { 62 | if (foo && bar) { 63 | testVar = true 64 | } 65 | } 66 | 67 | events.addListener('bar', testListener) 68 | events.emit('bar', true, true) 69 | 70 | defer(() => { 71 | events.removeListener('bar', testListener) 72 | expect(testVar).toEqual(true) 73 | }) 74 | }) 75 | 76 | }) 77 | 78 | }) 79 | -------------------------------------------------------------------------------- /unit/services/network.spec.js: -------------------------------------------------------------------------------- 1 | import { defer } from 'lodash' 2 | import network from '@services/network' 3 | 4 | describe('Service network', () => { 5 | 6 | beforeEach(() => { 7 | network.isOnline = false 8 | }) 9 | 10 | 11 | 12 | it('isOffline when not isOnline', () => { 13 | network.isOnline = false 14 | expect(network.isOffline).toEqual(true) 15 | }) 16 | 17 | it('not isOffline when isOnline', () => { 18 | network.isOnline = true 19 | expect(network.isOffline).toEqual(false) 20 | }) 21 | 22 | it('goes online with window event', () => { 23 | network.isOnline = false 24 | window.dispatchEvent(new Event('online')) 25 | defer(() => { 26 | expect(network.isOnline).toEqual(true) 27 | }) 28 | }) 29 | 30 | it('goes offline with window event', () => { 31 | network.isOnline = true 32 | window.dispatchEvent(new Event('offline')) 33 | defer(() => { 34 | expect(network.isOnline).toEqual(true) 35 | }) 36 | }) 37 | 38 | }) 39 | -------------------------------------------------------------------------------- /unit/store/myModule.spec.js: -------------------------------------------------------------------------------- 1 | import { getters, mutations, actions } from '@store/myModule' 2 | 3 | describe('Vuex myModule', () => { 4 | 5 | 6 | 7 | describe('getters', () => { 8 | 9 | describe('doubleCount', () => { 10 | 11 | const assert = function (count, expected) { 12 | expect( 13 | getters.doubleCount({ 14 | count 15 | }) 16 | ).toEqual(expected) 17 | } 18 | 19 | it('is 0', () => { 20 | assert(0, 0) 21 | }) 22 | 23 | it('is 4', () => { 24 | assert(2, 4) 25 | }) 26 | 27 | it('is -4', () => { 28 | assert(-2, -4) 29 | }) 30 | 31 | }) 32 | 33 | }) 34 | 35 | 36 | 37 | describe('mutations', () => { 38 | 39 | describe('increment', () => { 40 | 41 | const assert = function (from, to) { 42 | const state = { count: from } 43 | mutations.increment(state) 44 | expect(state.count).toEqual(to) 45 | } 46 | 47 | it('gives -1', () => { 48 | assert(-2, -1) 49 | }) 50 | 51 | it('gives 0', () => { 52 | assert(-1, 0) 53 | }) 54 | 55 | it('gives 1', () => { 56 | assert(0, 1) 57 | }) 58 | 59 | it('gives 2', () => { 60 | assert(1, 2) 61 | }) 62 | 63 | it('gives 3', () => { 64 | assert(2, 3) 65 | }) 66 | 67 | }) 68 | 69 | }) 70 | 71 | 72 | 73 | describe('actions', () => { 74 | 75 | describe('incrementToEven', () => { 76 | 77 | // Mock state and commit for action 78 | const assert = function (from, expectedCount) { 79 | 80 | const state = { 81 | count: from 82 | } 83 | 84 | const commit = (mutation, payload) => { 85 | expect(state.count + payload).toEqual(expectedCount) 86 | } 87 | 88 | actions.incrementToEven({ state, commit }) 89 | } 90 | 91 | it('increments -3 to -2', () => { 92 | assert(-3, -2) 93 | }) 94 | 95 | it('increments -2 to 0', () => { 96 | assert(-2, 0) 97 | }) 98 | 99 | it('increments -1 to 0', () => { 100 | assert(-1, 0) 101 | }) 102 | 103 | it('increments 0 to 2', () => { 104 | assert(0, 2) 105 | }) 106 | 107 | it('increments 1 to 2', () => { 108 | assert(1, 2) 109 | }) 110 | 111 | it('increments 2 to 4', () => { 112 | assert(2, 4) 113 | }) 114 | 115 | it('increments 3 to 4', () => { 116 | assert(3, 4) 117 | }) 118 | 119 | }) 120 | 121 | }) 122 | 123 | 124 | 125 | }) 126 | -------------------------------------------------------------------------------- /unit/util/composeClassnames.spec.js: -------------------------------------------------------------------------------- 1 | import composeClassnames from '@util/composeClassnames' 2 | 3 | describe('Util composeClassnames', () => { 4 | 5 | it('should have default prefix', () => { 6 | expect(composeClassnames({ 7 | foo: true 8 | })).toEqual([ 9 | 'is-foo' 10 | ]) 11 | }) 12 | 13 | it('should have default prefix with value', () => { 14 | expect(composeClassnames({ 15 | foo: 'bar' 16 | })).toEqual([ 17 | 'is-foo-bar' 18 | ]) 19 | }) 20 | 21 | it('should output multiple classnames', () => { 22 | expect(composeClassnames({ 23 | foo: true, 24 | bar: 'bar', 25 | count: 1 26 | })).toEqual([ 27 | 'is-foo', 28 | 'is-bar-bar', 29 | 'is-count-1' 30 | ]) 31 | }) 32 | 33 | it('should not have falsy classnames', () => { 34 | expect(composeClassnames({ 35 | foo: false, 36 | bar: null, 37 | count: 0, 38 | undef: undefined, 39 | empty: '' 40 | })).toEqual([]) 41 | }) 42 | 43 | it('should output kebab-case keys', () => { 44 | expect(composeClassnames({ 45 | fooBar: true, 46 | FooBar: true, 47 | 'foo-bar': true, 48 | 'Foo-Bar': true, 49 | 'Foo_Bar': true 50 | }, '')).toEqual([ 51 | 'foo-bar', 52 | 'foo-bar', 53 | 'foo-bar', 54 | 'foo-bar', 55 | 'foo-bar' 56 | ]) 57 | }) 58 | 59 | it('should output kebab-case values', () => { 60 | expect(composeClassnames({ 61 | 'foo1': 'foo', 62 | 'foo2': 'FOO', 63 | 'foo3': 'FooBar', 64 | 'foo4': 'foo-BAR', 65 | 'foo5': 'foo_BAR' 66 | }, '')).toEqual([ 67 | 'foo-1-foo', 68 | 'foo-2-foo', 69 | 'foo-3-foo-bar', 70 | 'foo-4-foo-bar', 71 | 'foo-5-foo-bar' 72 | ]) 73 | }) 74 | 75 | it('should insert prefixes', () => { 76 | expect(composeClassnames({ 77 | foo: true, 78 | bar: 'bar' 79 | }, 'prefix')).toEqual([ 80 | 'prefix-foo', 81 | 'prefix-bar-bar' 82 | ]) 83 | }) 84 | 85 | }) 86 | -------------------------------------------------------------------------------- /unit/util/isAbsoluteUrl.spec.js: -------------------------------------------------------------------------------- 1 | import isAbsoluteUrl from '@util/isAbsoluteUrl' 2 | 3 | // Test cases 4 | // These will be tested at the start, end and in the middle of the string 5 | const testCases = { 6 | 'http://example.com': true, // regular http absolute URL 7 | 'HTTP://EXAMPLE.COM': true, // HTTP upper-case absolute URL 8 | 'https://www.exmaple.com': true, // secure http absolute URL 9 | 'ftp://example.com/file.txt': true, // file transfer absolute URL 10 | '//cdn.example.com/lib.js': true, // protocol-relative absolute URL 11 | '/myfolder/test.txt': false, // relative URL 12 | 'test': false // also relative URL 13 | } 14 | 15 | describe('Util isAbsoluteUrl', function () { 16 | for (const string in testCases) { 17 | it('"' + string + ' should be `' + testCases[string] + '`', function () { 18 | expect(isAbsoluteUrl(string)).toEqual(testCases[string]) 19 | }) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /unit/util/trimWhitespace.spec.js: -------------------------------------------------------------------------------- 1 | import trimWhitespace from '@util/trimWhitespace' 2 | 3 | // Test cases 4 | // These will be tested at the start, end and in the middle of the string 5 | const whitespaceStrings = { 6 | space: ' ', 7 | spaces: ' ', 8 | tab: '\t', 9 | tabAndSpace1: ' ' + '\t', 10 | tabAndSpace2: ' ' + '\t', 11 | tabAndSpace3: ' ' + '\t' + ' ', 12 | tabAndMultipleSpaces1: ' ' + '\t', 13 | tabAndMultipleSpaces2: ' ' + '\t', 14 | tabAndMultipleSpaces3: ' ' + '\t' + ' ', 15 | tabAndMultipleSpacesAndTabs1: ' ' + '\t', 16 | tabAndMultipleSpacesAndTabs2: ' ' + '\t', 17 | tabAndMultipleSpacesAndTabs3: ' ' + '\t' + ' ', 18 | newline: '\n', 19 | newlines: '\n\n\n' 20 | } 21 | 22 | describe('Util trimWhitespace with trailing whitespace', function () { 23 | 24 | // Expected result is the same for all these test cases 25 | const expectedResult = 'Foooo' 26 | 27 | // Test this with each of the test cases provided above 28 | for (let key in whitespaceStrings) { 29 | it('should trim ' + key, function () { 30 | 31 | // Whitespace is at the end of string 32 | expect(trimWhitespace(expectedResult + whitespaceStrings[key])).toEqual(expectedResult) 33 | 34 | }) 35 | } 36 | 37 | }) 38 | 39 | describe('Util trimWhitespace with leading whitespace', function () { 40 | 41 | // Expected result is the same for all these test cases 42 | const expectedResult = 'Foooo' 43 | 44 | // Test this with each of the test cases provided above 45 | for (let key in whitespaceStrings) { 46 | it('should trim ' + key, function () { 47 | 48 | // Whitespace is at the start of string 49 | expect(trimWhitespace(whitespaceStrings[key] + expectedResult)).toEqual(expectedResult) 50 | 51 | }) 52 | } 53 | 54 | }) 55 | 56 | describe('Util trimWhitespace with excess whitespace', function () { 57 | 58 | // Expected result is the same for all these test cases 59 | const partOne = 'Foo' 60 | const partTwo = 'oo' 61 | const expectedResult = partOne + ' ' + partTwo 62 | 63 | // Test this with each of the test cases provided above 64 | for (let key in whitespaceStrings) { 65 | it('should trim ' + key, function () { 66 | 67 | // Whitespace is injected in the middle 68 | expect(trimWhitespace(partOne + whitespaceStrings[key] + partTwo)).toEqual(expectedResult) 69 | 70 | }) 71 | } 72 | 73 | }) 74 | --------------------------------------------------------------------------------