├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── load-minified.js ├── service-worker-dev.js ├── service-worker-prod.js ├── 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 ├── index.html ├── package.json ├── src ├── app.vue ├── assets │ ├── images │ │ ├── backdrop.jpg │ │ ├── logo-name.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── oops.svg │ │ ├── profile.jpg │ │ └── traffic-cone.svg │ └── logo.png ├── auth │ ├── helpers.js │ ├── index.js │ └── store.js ├── components │ ├── Hello.vue │ ├── app-bar.vue │ ├── app-dialog.vue │ ├── app-footer.vue │ ├── app-sidebar.vue │ ├── app-snackbar.vue │ └── loading.vue ├── constants │ └── index.js ├── features │ ├── account │ │ ├── components │ │ │ ├── address-edit.vue │ │ │ └── countries.js │ │ ├── main.vue │ │ ├── service.js │ │ └── store.js │ ├── billing │ │ └── main.vue │ ├── dashboard │ │ ├── components │ │ │ ├── chart.vue │ │ │ └── line-chart.vue │ │ ├── main.vue │ │ ├── service.js │ │ ├── store.js │ │ └── test.css │ ├── login │ │ ├── main.vue │ │ ├── service.js │ │ └── store.js │ ├── premium │ │ └── main.vue │ ├── tutorial │ │ └── main.vue │ └── wip │ │ └── main.vue ├── http │ ├── index.js │ ├── router.js │ └── routes.js ├── layouts │ ├── default │ │ └── main.vue │ └── public │ │ └── main.vue ├── main.js ├── store │ ├── common.js │ ├── index.js │ └── plugins.js └── styles │ ├── scss │ ├── _variables.scss │ ├── _vendor.scss │ └── main.scss │ └── stylus │ ├── 1-settings │ ├── 1-settings.styl │ ├── variables.styl │ └── vendor.styl │ ├── 2-tools │ └── 2-tools.styl │ ├── 3-generic │ └── 3-generic.styl │ ├── 4-elements │ └── 4-elements.styl │ ├── 5-objects │ └── 5-objects.styl │ ├── 6-components │ ├── 6-components.styl │ └── vendor.styl │ ├── 7-utils │ └── 7-utils.styl │ └── main.styl ├── static ├── images │ └── mountains.png ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg └── manifest.json └── test └── e2e ├── custom-assertions └── elementCount.js ├── nightwatch.conf.js ├── runner.js └── specs └── test.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-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": [ "istanbul" ] 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 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint', 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | jest: true 12 | }, 13 | globals: { 14 | utils: true 15 | }, 16 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 17 | // https://github.com/vuejs/eslint-plugin-vue 18 | extends: [ 19 | 'standard', 20 | 'plugin:vue/recommended' 21 | ], 22 | // required to lint *.vue files 23 | plugins: [ 24 | 'vue' 25 | ], 26 | // add your custom rules here 27 | 'rules': { 28 | // allow paren-less arrow functions 29 | 'arrow-parens': 0, 30 | // allow async-await 31 | 'generator-star-spacing': 0, 32 | // allow debugger during development 33 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/e2e/reports 8 | selenium-debug.log 9 | package-lock.json 10 | 11 | # Editor directories and files 12 | .idea 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | "dbaeumer.vscode-eslint", 7 | "sysoev.language-stylus", 8 | "octref.vetur" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to override the default and user settings 2 | { 3 | // Settings for .js, .vue, .styl files. 4 | "[javascript]": { 5 | "editor.tabSize": 2, 6 | "editor.insertSpaces": true 7 | }, 8 | "[vue]": { 9 | "editor.tabSize": 2, 10 | "editor.insertSpaces": true 11 | }, 12 | "[stylus]": { 13 | "editor.tabSize": 2, 14 | "editor.insertSpaces": true 15 | }, 16 | 17 | // This is required for ESLint to work in Vue in VS Code. 18 | "eslint.options": { 19 | "extensions": [".html", ".js", ".vue", ".jsx"] 20 | }, 21 | "eslint.validate": [ 22 | { 23 | "language": "html", 24 | "autoFix": true 25 | }, 26 | { 27 | "language": "vue", 28 | "autoFix": true 29 | }, 30 | { 31 | "language": "javascript", 32 | "autoFix": true 33 | }, 34 | { 35 | "language": "javascriptreact", 36 | "autoFix": true 37 | } 38 | ], 39 | 40 | // When you hit ctrl+e to search, you don't want node_modules to be included. 41 | "search.exclude": { 42 | "**/.git": true, 43 | "**/node_modules": true, 44 | "**/tmp": true 45 | } 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :pizza: Vue Pizza 2 | 3 | > A tasty Vue.js starter project. Clone, remove `vue-pizza` everywhere, and add your own features. Also, read through the wiki. 4 | 5 | ## Demo 6 | 7 | [https://vue.pizza/app](https://vue.pizza/app) 8 | 9 | ## Why? 10 | 11 | A lot of starter projects or examples of Vue.js in the wild were: 12 | 13 | - Outdated. 14 | - No tutorial or wiki. 15 | - Involved unneccesary, complex, server-side-rendering/NUXT setups (this project is for authenticated, no SEO needed, static/cacheable apps that communicate to a backend server API). 16 | - Not comprehensive and didn't cover most of the elements necessary for real world apps. 17 | - This project was started from the latest official Vue-cli PWA template and built up from there. 18 | 19 | ## Wiki 20 | 21 | Visit the Github wiki tab [here](https://github.com/prograhammer/vue-pizza/wiki) to learn everything you need to know about 22 | how this starter project was built (which allows you to learn and rebuild this project from scratch if you want). 23 | 24 | ## Installation 25 | 26 | Very simple. See the Installation section in [wiki](https://github.com/prograhammer/vue-pizza/wiki). 27 | 28 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./check-versions')() 4 | 5 | process.env.NODE_ENV = 'production' 6 | 7 | const ora = require('ora') 8 | const rm = require('rimraf') 9 | const path = require('path') 10 | const chalk = require('chalk') 11 | const webpack = require('webpack') 12 | const config = require('../config') 13 | const webpackConfig = require('./webpack.prod.conf') 14 | 15 | const spinner = ora('building for production...') 16 | spinner.start() 17 | 18 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 19 | if (err) throw err 20 | webpack(webpackConfig, function (err, stats) { 21 | spinner.stop() 22 | if (err) throw err 23 | process.stdout.write(stats.toString({ 24 | colors: true, 25 | modules: false, 26 | children: false, 27 | chunks: false, 28 | chunkModules: false 29 | }) + '\n\n') 30 | 31 | console.log(chalk.cyan(' Build complete.\n')) 32 | console.log(chalk.yellow( 33 | ' Tip: built files are meant to be served over an HTTP server.\n' + 34 | ' Opening index.html over file:// won\'t work.\n' 35 | )) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const semver = require('semver') 5 | const packageConfig = require('../package.json') 6 | const shell = require('shelljs') 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 | for (let i = 0; i < versionRequirements.length; i++) { 30 | const mod = versionRequirements[i] 31 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 32 | warnings.push(mod.name + ': ' + 33 | chalk.red(mod.currentVersion) + ' should be ' + 34 | chalk.green(mod.versionRequirement) 35 | ) 36 | } 37 | } 38 | 39 | if (warnings.length) { 40 | console.log('') 41 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 42 | console.log() 43 | for (let i = 0; i < warnings.length; i++) { 44 | const warning = warnings[i] 45 | console.log(' ' + warning) 46 | } 47 | console.log() 48 | process.exit(1) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable */ 4 | require('eventsource-polyfill') 5 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 6 | 7 | hotClient.subscribe(function (event) { 8 | if (event.action === 'reload') { 9 | window.location.reload() 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./check-versions')() 4 | 5 | const config = require('../config') 6 | if (!process.env.NODE_ENV) { 7 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 8 | } 9 | 10 | const opn = require('opn') 11 | const path = require('path') 12 | const express = require('express') 13 | const webpack = require('webpack') 14 | const proxyMiddleware = require('http-proxy-middleware') 15 | const webpackConfig = process.env.NODE_ENV === 'testing' 16 | ? require('./webpack.prod.conf') 17 | : require('./webpack.dev.conf') 18 | 19 | // default port where dev server listens for incoming traffic 20 | const port = process.env.PORT || config.dev.port 21 | // automatically open browser, if not set will be false 22 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 23 | // Define HTTP proxies to your custom API backend 24 | // https://github.com/chimurai/http-proxy-middleware 25 | const proxyTable = config.dev.proxyTable 26 | 27 | const app = express() 28 | const compiler = webpack(webpackConfig) 29 | 30 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 31 | publicPath: webpackConfig.output.publicPath, 32 | quiet: true 33 | }) 34 | 35 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 36 | log: false 37 | }) 38 | // force page reload when html-webpack-plugin template changes 39 | compiler.plugin('compilation', function (compilation) { 40 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 41 | hotMiddleware.publish({ action: 'reload' }) 42 | cb() 43 | }) 44 | }) 45 | 46 | // enable hot-reload and state-preserving 47 | // compilation error display 48 | app.use(hotMiddleware) 49 | 50 | // proxy api requests 51 | Object.keys(proxyTable).forEach(function (context) { 52 | let options = proxyTable[context] 53 | if (typeof options === 'string') { 54 | options = { target: options } 55 | } 56 | app.use(proxyMiddleware(options.filter || context, options)) 57 | }) 58 | 59 | // handle fallback for HTML5 history API 60 | app.use(require('connect-history-api-fallback')()) 61 | 62 | // serve webpack bundle output 63 | app.use(devMiddleware) 64 | 65 | // serve pure static assets 66 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 67 | app.use(staticPath, express.static('./static')) 68 | 69 | const uri = 'http://localhost:' + port 70 | 71 | let _resolve 72 | const readyPromise = new Promise(resolve => { 73 | _resolve = resolve 74 | }) 75 | 76 | console.log('> Starting dev server...') 77 | devMiddleware.waitUntilValid(() => { 78 | console.log('> Listening at ' + uri + '\n') 79 | // when env is testing, don't need open it 80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 81 | opn(uri) 82 | } 83 | _resolve() 84 | }) 85 | 86 | const server = app.listen(port) 87 | 88 | module.exports = { 89 | ready: readyPromise, 90 | close: () => { 91 | server.close() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /build/load-minified.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const UglifyJS = require('uglify-es') 5 | 6 | module.exports = function(filePath) { 7 | const code = fs.readFileSync(filePath, 'utf-8') 8 | const result = UglifyJS.minify(code) 9 | if (result.error) return '' 10 | return result.code 11 | } 12 | -------------------------------------------------------------------------------- /build/service-worker-dev.js: -------------------------------------------------------------------------------- 1 | // This service worker file is effectively a 'no-op' that will reset any 2 | // previous service worker registered for the same host:port combination. 3 | // In the production build, this file is replaced with an actual service worker 4 | // file that will precache your site's local assets. 5 | // See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432 6 | 7 | self.addEventListener('install', () => self.skipWaiting()); 8 | 9 | self.addEventListener('activate', () => { 10 | self.clients.matchAll({ type: 'window' }).then(windowClients => { 11 | for (let windowClient of windowClients) { 12 | // Force open pages to refresh, so that they have a chance to load the 13 | // fresh navigation response from the local dev server. 14 | windowClient.navigate(windowClient.url); 15 | } 16 | }); 17 | }); -------------------------------------------------------------------------------- /build/service-worker-prod.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // Check to make sure service workers are supported in the current browser, 5 | // and that the current page is accessed from a secure origin. Using a 6 | // service worker from an insecure origin will trigger JS console errors. 7 | var isLocalhost = Boolean(window.location.hostname === 'localhost' || 8 | // [::1] is the IPv6 localhost address. 9 | window.location.hostname === '[::1]' || 10 | // 127.0.0.1/8 is considered localhost for IPv4. 11 | window.location.hostname.match( 12 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 13 | ) 14 | ); 15 | 16 | window.addEventListener('load', function() { 17 | if ('serviceWorker' in navigator && 18 | (window.location.protocol === 'https:' || isLocalhost)) { 19 | navigator.serviceWorker.register('service-worker.js') 20 | .then(function(registration) { 21 | // updatefound is fired if service-worker.js changes. 22 | registration.onupdatefound = function() { 23 | // updatefound is also fired the very first time the SW is installed, 24 | // and there's no need to prompt for a reload at that point. 25 | // So check here to see if the page is already controlled, 26 | // i.e. whether there's an existing service worker. 27 | if (navigator.serviceWorker.controller) { 28 | // The updatefound event implies that registration.installing is set 29 | var installingWorker = registration.installing; 30 | 31 | installingWorker.onstatechange = function() { 32 | switch (installingWorker.state) { 33 | case 'installed': 34 | // At this point, the old content will have been purged and the 35 | // fresh content will have been added to the cache. 36 | // It's the perfect time to display a "New content is 37 | // available; please refresh." message in the page's interface. 38 | break; 39 | 40 | case 'redundant': 41 | throw new Error('The installing ' + 42 | 'service worker became redundant.'); 43 | 44 | default: 45 | // Ignore 46 | } 47 | }; 48 | } 49 | }; 50 | }).catch(function(e) { 51 | console.error('Error during service worker registration:', e); 52 | }); 53 | } 54 | }); 55 | })(); 56 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const config = require('../config') 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | return path.posix.join(assetsSubDirectory, _path) 12 | } 13 | 14 | exports.cssLoaders = function (options) { 15 | options = options || {} 16 | 17 | const cssLoader = { 18 | loader: 'css-loader', 19 | options: { 20 | minimize: process.env.NODE_ENV === 'production', 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | // generate loader string to be used with extract text plugin 26 | function generateLoaders (loader, loaderOptions) { 27 | const loaders = [cssLoader] 28 | if (loader) { 29 | loaders.push({ 30 | loader: loader + '-loader', 31 | options: Object.assign({}, loaderOptions, { 32 | sourceMap: options.sourceMap 33 | }) 34 | }) 35 | } 36 | 37 | // Extract CSS when that option is specified 38 | // (which is the case during production build) 39 | if (options.extract) { 40 | return ExtractTextPlugin.extract({ 41 | use: loaders, 42 | fallback: 'vue-style-loader' 43 | }) 44 | } else { 45 | return ['vue-style-loader'].concat(loaders) 46 | } 47 | } 48 | 49 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 50 | return { 51 | css: generateLoaders(), 52 | postcss: generateLoaders(), 53 | less: generateLoaders('less'), 54 | sass: generateLoaders('sass', { indentedSyntax: true }), 55 | scss: generateLoaders('sass'), 56 | stylus: generateLoaders('stylus', { 57 | preferPathResolver: 'webpack', 58 | import: [ 59 | '~@/styles/stylus/1-settings/1-settings.styl', // <-- Load these files into every stylus file. 60 | '~@/styles/stylus/2-tools/2-tools.styl', // Only variables/functions so output CSS is not increased. 61 | ] 62 | }), 63 | styl: generateLoaders('stylus', { 64 | preferPathResolver: 'webpack', 65 | import: [ 66 | '~@/styles/stylus/1-settings/1-settings.styl', // <-- Load these files into every stylus file. 67 | '~@/styles/stylus/2-tools/2-tools.styl', // Only variables/functions so output CSS is not increased. 68 | ] 69 | }) 70 | } 71 | } 72 | 73 | // Generate loaders for standalone style files (outside of .vue) 74 | exports.styleLoaders = function (options) { 75 | const output = [] 76 | const loaders = exports.cssLoaders(options) 77 | for (const extension in loaders) { 78 | const loader = loaders[extension] 79 | output.push({ 80 | test: new RegExp('\\.' + extension + '$'), 81 | use: loader 82 | }) 83 | } 84 | return output 85 | } 86 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const isProduction = process.env.NODE_ENV === 'production' 6 | 7 | module.exports = { 8 | loaders: utils.cssLoaders({ 9 | sourceMap: isProduction 10 | ? config.build.productionSourceMap 11 | : config.dev.cssSourceMap, 12 | extract: isProduction 13 | }), 14 | transformToRequire: { 15 | video: ['src', 'poster'], 16 | source: 'src', 17 | img: 'src', 18 | image: 'xlink:href' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const utils = require('./utils') 5 | const config = require('../config') 6 | const vueLoaderConfig = require('./vue-loader.conf') 7 | 8 | function resolve (dir) { 9 | return path.join(__dirname, '..', dir) 10 | } 11 | 12 | module.exports = { 13 | entry: { 14 | app: './src/main.js' 15 | }, 16 | output: { 17 | path: config.build.assetsRoot, 18 | filename: '[name].js', 19 | publicPath: process.env.NODE_ENV === 'production' 20 | ? config.build.assetsPublicPath 21 | : config.dev.assetsPublicPath 22 | }, 23 | resolve: { 24 | extensions: ['.js', '.vue', '.json'], 25 | alias: { 26 | '@': resolve('src') 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.(js|vue)$/, 33 | loader: 'eslint-loader', 34 | enforce: 'pre', 35 | include: [resolve('src'), resolve('test')], 36 | options: { 37 | formatter: require('eslint-friendly-formatter') 38 | } 39 | }, 40 | { 41 | test: /\.vue$/, 42 | loader: 'vue-loader', 43 | options: vueLoaderConfig 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src'), resolve('test')] 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | loader: 'url-loader', 53 | options: { 54 | limit: 10000, 55 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 56 | } 57 | }, 58 | { 59 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 60 | loader: 'url-loader', 61 | options: { 62 | limit: 10000, 63 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 64 | } 65 | }, 66 | { 67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 68 | loader: 'url-loader', 69 | options: { 70 | limit: 10000, 71 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 72 | } 73 | } 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const utils = require('./utils') 6 | const webpack = require('webpack') 7 | const config = require('../config') 8 | const merge = require('webpack-merge') 9 | const baseWebpackConfig = require('./webpack.base.conf') 10 | const HtmlWebpackPlugin = require('html-webpack-plugin') 11 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 12 | 13 | // add hot-reload related code to entry chunks 14 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 15 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 16 | }) 17 | 18 | module.exports = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 21 | }, 22 | // cheap-module-eval-source-map is faster for development 23 | devtool: '#cheap-module-eval-source-map', 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | 'process.env': config.dev.env 27 | }), 28 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 29 | new webpack.HotModuleReplacementPlugin(), 30 | new webpack.NoEmitOnErrorsPlugin(), 31 | // https://github.com/ampedandwired/html-webpack-plugin 32 | new HtmlWebpackPlugin({ 33 | filename: 'index.html', 34 | template: 'index.html', 35 | inject: true, 36 | serviceWorkerLoader: `` 38 | }), 39 | new FriendlyErrorsPlugin() 40 | ] 41 | }) 42 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const utils = require('./utils') 6 | const webpack = require('webpack') 7 | const config = require('../config') 8 | const merge = require('webpack-merge') 9 | const baseWebpackConfig = require('./webpack.base.conf') 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 13 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 14 | const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin') 15 | const loadMinified = require('./load-minified') 16 | 17 | const env = process.env.NODE_ENV === 'testing' 18 | ? require('../config/test.env') 19 | : config.build.env 20 | 21 | const webpackConfig = merge(baseWebpackConfig, { 22 | module: { 23 | rules: utils.styleLoaders({ 24 | sourceMap: config.build.productionSourceMap, 25 | extract: true 26 | }) 27 | }, 28 | devtool: config.build.productionSourceMap ? '#source-map' : false, 29 | output: { 30 | path: config.build.assetsRoot, 31 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 32 | chunkFilename: utils.assetsPath('js/[name].[chunkhash].js') 33 | }, 34 | plugins: [ 35 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 36 | new webpack.DefinePlugin({ 37 | 'process.env': env 38 | }), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | compress: { 41 | warnings: false 42 | }, 43 | sourceMap: true 44 | }), 45 | // extract css into its own file 46 | new ExtractTextPlugin({ 47 | filename: utils.assetsPath('css/[name].[contenthash].css') 48 | }), 49 | // Compress extracted CSS. We are using this plugin so that possible 50 | // duplicated CSS from different components can be deduped. 51 | new OptimizeCSSPlugin({ 52 | cssProcessorOptions: { 53 | safe: true 54 | } 55 | }), 56 | // generate dist index.html with correct asset hash for caching. 57 | // you can customize output by editing /index.html 58 | // see https://github.com/ampedandwired/html-webpack-plugin 59 | new HtmlWebpackPlugin({ 60 | filename: process.env.NODE_ENV === 'testing' 61 | ? 'index.html' 62 | : config.build.index, 63 | template: 'index.html', 64 | inject: true, 65 | minify: { 66 | removeComments: true, 67 | collapseWhitespace: true, 68 | removeAttributeQuotes: true 69 | // more options: 70 | // https://github.com/kangax/html-minifier#options-quick-reference 71 | }, 72 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 73 | chunksSortMode: 'dependency', 74 | serviceWorkerLoader: `` 76 | }), 77 | // split vendor js into its own file 78 | new webpack.optimize.CommonsChunkPlugin({ 79 | name: 'vendor', 80 | minChunks: function (module, count) { 81 | // any required modules inside node_modules are extracted to vendor 82 | return ( 83 | module.resource && 84 | /\.js$/.test(module.resource) && 85 | module.resource.indexOf( 86 | path.join(__dirname, '../node_modules') 87 | ) === 0 88 | ) 89 | } 90 | }), 91 | // extract webpack runtime and module manifest to its own file in order to 92 | // prevent vendor hash from being updated whenever app bundle is updated 93 | new webpack.optimize.CommonsChunkPlugin({ 94 | name: 'manifest', 95 | chunks: ['vendor'] 96 | }), 97 | // copy custom static assets 98 | new CopyWebpackPlugin([ 99 | { 100 | from: path.resolve(__dirname, '../static'), 101 | to: config.build.assetsSubDirectory, 102 | ignore: ['.*'] 103 | } 104 | ]), 105 | // service worker caching 106 | new SWPrecacheWebpackPlugin({ 107 | cacheId: 'vue-pizza', 108 | filename: 'service-worker.js', 109 | staticFileGlobs: ['dist/**/*.{js,html,css}'], 110 | minify: true, 111 | stripPrefix: 'dist/' 112 | }) 113 | ] 114 | }) 115 | 116 | if (config.build.productionGzip) { 117 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 118 | 119 | webpackConfig.plugins.push( 120 | new CompressionWebpackPlugin({ 121 | asset: '[path].gz[query]', 122 | algorithm: 'gzip', 123 | test: new RegExp( 124 | '\\.(' + 125 | config.build.productionGzipExtensions.join('|') + 126 | ')$' 127 | ), 128 | threshold: 10240, 129 | minRatio: 0.8 130 | }) 131 | ) 132 | } 133 | 134 | if (config.build.bundleAnalyzerReport) { 135 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 136 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 137 | } 138 | 139 | module.exports = webpackConfig 140 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const merge = require('webpack-merge') 4 | const prodEnv = require('./prod.env') 5 | 6 | module.exports = merge(prodEnv, { 7 | NODE_ENV: '"development"' 8 | }) 9 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | const path = require('path') 5 | 6 | module.exports = { 7 | build: { 8 | env: require('./prod.env'), 9 | index: path.resolve(__dirname, '../dist/index.html'), 10 | assetsRoot: path.resolve(__dirname, '../dist'), 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/app/', 13 | productionSourceMap: true, 14 | // Gzip off by default as many popular static hosts such as 15 | // Surge or Netlify already gzip all static assets for you. 16 | // Before setting to `true`, make sure to: 17 | // npm install --save-dev compression-webpack-plugin 18 | productionGzip: false, 19 | productionGzipExtensions: ['js', 'css'], 20 | // Run the build command with an extra argument to 21 | // View the bundle analyzer report after build finishes: 22 | // `npm run build --report` 23 | // Set to `true` or `false` to always turn it on or off 24 | bundleAnalyzerReport: process.env.npm_config_report 25 | }, 26 | dev: { 27 | env: require('./dev.env'), 28 | port: 8080, 29 | autoOpenBrowser: true, 30 | assetsSubDirectory: 'static', 31 | assetsPublicPath: '/', 32 | proxyTable: { 33 | '/auth': { 34 | // @TODO: You need to replace this with your own backend API. 35 | // Demo OAuth2 server https://github.com/bshaffer/oauth2-demo-php. 36 | // Username: demouser Password: demopass 37 | //target: 'http://brentertainment.com/oauth2/lockdin/token', 38 | target: 'http://localhost:8081', 39 | changeOrigin: true, 40 | ws: true, 41 | pathRewrite: { 42 | '^/auth': '' 43 | }, 44 | router: { 45 | } 46 | }, 47 | '/api': { 48 | // target: 'http://brentertainment.com/oauth2', // <-- Api server. 49 | target: 'http://localhost:8081/experience', 50 | changeOrigin: true, // <-- For virtual hosted sites. 51 | ws: true, // <-- Proxy websockets. 52 | pathRewrite: { 53 | // Rewrite path localhost:8080/api to http://brentertainment.com/oauth2/lockdin. 54 | '^/api': '' 55 | }, 56 | router: { 57 | // when request.headers.host == 'dev.localhost:3000', 58 | // override target 'http://www.example.org' to 'http://localhost:8000' 59 | // 'dev.localhost:3000': 'http://localhost:8000' 60 | } 61 | } 62 | }, 63 | // CSS Sourcemaps off by default because relative paths are "buggy" 64 | // with this option, according to the CSS-Loader README 65 | // (https://github.com/webpack/css-loader#sourcemaps) 66 | // In our experience, they generally work as expected, 67 | // just be aware of this issue when enabling this option. 68 | cssSourceMap: false 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const merge = require('webpack-merge') 4 | const devEnv = require('./dev.env') 5 | 6 | module.exports = merge(devEnv, { 7 | NODE_ENV: '"testing"' 8 | }) 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue-pizza 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% for (var chunk of webpack.chunks) { 26 | for (var file of chunk.files) { 27 | if (file.match(/\.(js|css)$/)) { %> 28 | <% }}} %> 29 | 30 | 31 | 34 |
35 | 36 | <%= htmlWebpackPlugin.options.serviceWorkerLoader %> 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pizza", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "David Graham ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js", 11 | "e2e": "node test/e2e/runner.js", 12 | "test": "npm run e2e", 13 | "lint": "eslint --ext .js,.vue src test/e2e/specs", 14 | "deploy": "gh-pages-deploy" 15 | }, 16 | "gh-pages-deploy": { 17 | "staticpath": "dist" 18 | }, 19 | "dependencies": { 20 | "vue": "^2.5.2", 21 | "vue-router": "^3.0.1", 22 | "vuex-router-sync": "^5.0.0" 23 | }, 24 | "devDependencies": { 25 | "autoprefixer": "^7.1.5", 26 | "axios": "^0.18.0", 27 | "babel-core": "^6.26.0", 28 | "babel-eslint": "^8.0.1", 29 | "babel-loader": "^7.1.2", 30 | "babel-plugin-transform-runtime": "^6.23.0", 31 | "babel-preset-env": "^1.6.0", 32 | "babel-preset-stage-2": "^6.24.1", 33 | "babel-register": "^6.26.0", 34 | "chalk": "^2.1.0", 35 | "chart.js": "^2.7.2", 36 | "chromedriver": "^2.33.1", 37 | "connect-history-api-fallback": "^1.4.0", 38 | "copy-webpack-plugin": "^4.1.1", 39 | "cross-spawn": "^5.1.0", 40 | "css-loader": "^0.28.7", 41 | "cssnano": "^3.10.0", 42 | "eslint": "^4.9.0", 43 | "eslint-config-standard": "^10.2.1", 44 | "eslint-friendly-formatter": "^3.0.0", 45 | "eslint-loader": "^1.9.0", 46 | "eslint-plugin-html": "^3.2.2", 47 | "eslint-plugin-import": "^2.7.0", 48 | "eslint-plugin-node": "^5.2.0", 49 | "eslint-plugin-promise": "^3.6.0", 50 | "eslint-plugin-standard": "^3.0.1", 51 | "eslint-plugin-vue": "^4.5.0", 52 | "eventsource-polyfill": "^0.9.6", 53 | "express": "^4.16.2", 54 | "extract-text-webpack-plugin": "^3.0.0", 55 | "file-loader": "^1.1.5", 56 | "friendly-errors-webpack-plugin": "^1.6.1", 57 | "gh-pages-deploy": "^0.4.2", 58 | "html-webpack-plugin": "^2.30.1", 59 | "http-proxy-middleware": "^0.17.4", 60 | "nightwatch": "^0.9.16", 61 | "node-sass": "^4.9.0", 62 | "opn": "^5.1.0", 63 | "optimize-css-assets-webpack-plugin": "^3.2.0", 64 | "ora": "^1.3.0", 65 | "pug": "^2.0.3", 66 | "pug-loader": "^2.3.0", 67 | "rimraf": "^2.6.2", 68 | "sass-loader": "^7.0.1", 69 | "selenium-server": "^3.6.0", 70 | "semver": "^5.4.1", 71 | "shelljs": "^0.7.8", 72 | "stylus": "^0.54.5", 73 | "stylus-loader": "^3.0.2", 74 | "sw-precache-webpack-plugin": "^0.11.4", 75 | "uglify-es": "^3.1.3", 76 | "url-loader": "^0.6.2", 77 | "url-search-params": "^0.10.0", 78 | "vue-chartjs": "^3.3.1", 79 | "vue-gravatar": "^1.2.1", 80 | "vue-loader": "^13.3.0", 81 | "vue-style-loader": "^3.0.3", 82 | "vue-template-compiler": "^2.5.2", 83 | "vuetify": "^1.0.17", 84 | "vuex": "^3.0.1", 85 | "webpack": "^3.7.1", 86 | "webpack-bundle-analyzer": "^2.9.0", 87 | "webpack-dev-middleware": "^1.12.0", 88 | "webpack-hot-middleware": "^2.19.1", 89 | "webpack-merge": "^4.1.0" 90 | }, 91 | "engines": { 92 | "node": ">= 4.0.0", 93 | "npm": ">= 3.0.0" 94 | }, 95 | "browserslist": [ 96 | "> 1%", 97 | "last 2 versions", 98 | "not ie <= 8" 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | -------------------------------------------------------------------------------- /src/assets/images/backdrop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/assets/images/backdrop.jpg -------------------------------------------------------------------------------- /src/assets/images/logo-name.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 58 | 73 | 125 | 165 | 171 | PriceMatchers 190 | 195 | 200 | 205 | 210 | 215 | 220 | 225 | 230 | 231 | 232 | 237 | 238 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/oops.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /src/assets/images/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/assets/images/profile.jpg -------------------------------------------------------------------------------- /src/assets/images/traffic-cone.svg: -------------------------------------------------------------------------------- 1 | Created by Hea Poh Linfrom the Noun Project -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/assets/logo.png -------------------------------------------------------------------------------- /src/auth/helpers.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { router } from '@/http' 3 | import store from '@/store' 4 | import auth from './' 5 | 6 | const LOGIN_URL = '/auth' 7 | 8 | // const CLIENT_SECRET = 'ZGVtb2FwcDpkZW1vcGFzcw==' // Base64(client_id:client_secret) "demoapp:demopass" 9 | 10 | export default { 11 | URLSearchParams (obj) { 12 | var params = new URLSearchParams() 13 | 14 | for (var [key, value] of Object.entries(obj)) params.append(key, value) 15 | 16 | return params 17 | }, 18 | 19 | login (creds, redirect, callback) { 20 | return Vue.http({ 21 | method: 'post', 22 | url: LOGIN_URL, 23 | // headers: { 24 | // 'Authorization': 'Basic ' + CLIENT_SECRET, 25 | // 'Content-Type': 'application/x-www-form-urlencoded' 26 | // }, 27 | data: this.URLSearchParams({ 28 | grant_type: 'password', 29 | client_id: 'demoapp', 30 | client_secret: 'demopass', 31 | username: creds.username, 32 | password: creds.password 33 | }) 34 | }) 35 | .then((response) => { 36 | auth.storeToken(response) 37 | 38 | if (redirect) router.push({ name: redirect }) 39 | return response 40 | }) 41 | .catch((error) => { 42 | let errorMessage = null 43 | 44 | if (error.response) errorMessage = error.response.status 45 | else if (error.request) errorMessage = 'no response from server' 46 | else errorMessage = error.message 47 | 48 | return errorMessage 49 | }) 50 | }, 51 | 52 | logout () { 53 | store.dispatch('common/clear') 54 | router.push({ name: 'login' }) 55 | }, 56 | 57 | fakeLogin (creds, redirect) { 58 | return new Promise((resolve, reject) => { 59 | setTimeout(() => { 60 | auth.storeToken({data: { accessToken: '123456789', refreshToken: '77777777' }}) 61 | if (redirect) router.push({ name: redirect }) 62 | resolve({}) 63 | }, 500) 64 | }) 65 | }, 66 | 67 | // Standardizes errors. A place to add logging if needed. 68 | get (url, params = {}) { 69 | const config = { 70 | params: { 71 | username: store.state.auth.user.id, 72 | orgId: store.state.auth.user.orgId 73 | } 74 | } 75 | 76 | config.params = Object.assign(config.params, params) 77 | 78 | return Vue.auth.get(url, config) 79 | .then((response) => { 80 | return new Promise((resolve) => { 81 | // @TODO check for no response.data.data? 82 | resolve(response.data.data) 83 | }) 84 | }) 85 | .catch((error) => { 86 | // Standardize errors. 87 | let errorMessage = null 88 | 89 | if (error.response) { 90 | errorMessage = error.response.statusText || error.response.status 91 | } else if (error.request) { 92 | errorMessage = 'no response from server' 93 | } else { 94 | errorMessage = error.message 95 | } 96 | 97 | return new Promise((resolve, reject) => { 98 | reject(new Error(errorMessage)) 99 | }) 100 | }) 101 | }, 102 | 103 | put (url, data = {}) { 104 | const config = {} 105 | 106 | const defaultData = { 107 | username: store.state.auth.user.id, 108 | orgId: store.state.auth.user.orgId 109 | } 110 | 111 | data = Object.assign(defaultData, data) 112 | 113 | // console.log(settings.data) 114 | 115 | return Vue.auth.put(url, data, config) 116 | .then((response) => { 117 | if (response.data.errors) { 118 | return new Promise((resolve, reject) => { 119 | reject(new Error(response.data.errors[0].user_message)) 120 | }) 121 | } 122 | 123 | return new Promise((resolve) => { 124 | // @TODO check for no response.data.data? 125 | resolve(response.data.data) 126 | }) 127 | }) 128 | .catch((error) => { 129 | // Standardize errors. 130 | let errorMessage = null 131 | 132 | if (error.response) { 133 | errorMessage = error.response.statusText || error.response.status 134 | } else if (error.request) { 135 | errorMessage = 'no response from server' 136 | } else { 137 | errorMessage = error.message 138 | } 139 | 140 | return new Promise((resolve, reject) => { 141 | reject(new Error(errorMessage)) 142 | }) 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/auth/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | import axios from 'axios' 4 | import * as constants from '@/constants' 5 | 6 | // const CLIENT_SECRET = 'demopass' // Base64(client_id:client_secret) "demoapp:demopass" 7 | 8 | export default { 9 | 10 | install (Vue, options) { 11 | Vue.prototype.$auth = Vue.auth = axios.create() 12 | 13 | this.setDefaults() 14 | this.addInterceptors() 15 | }, 16 | 17 | setDefaults () { 18 | Vue.auth.defaults.baseURL = constants.API_BASE_URL 19 | }, 20 | 21 | addInterceptors () { 22 | // Watch for accessToken changes and update our common Auth header. 23 | store.watch((state) => { 24 | return state.auth.accessToken 25 | }, (accessToken) => { 26 | if (!constants.DEBUG) { 27 | Vue.auth.defaults.headers.common['Authorization'] = 'Bearer ' + accessToken 28 | Vue.auth.defaults.transformRequest = [(data, headers) => { 29 | data.access_token = accessToken 30 | return data 31 | }] 32 | } 33 | 34 | if (constants.DEBUG) { 35 | console.log('token set') 36 | } 37 | }, { 38 | deep: true 39 | }) 40 | 41 | // Intercept the response and refresh (one retry) if invalid token. 42 | Vue.auth.interceptors.response.use(function (response) { 43 | if (constants.DEBUG) return Promise.resolve(response) 44 | 45 | if (this.isInvalidToken(response)) { 46 | return this.refreshToken(response.request) 47 | } 48 | }, function (error) { 49 | return Promise.reject(error) 50 | }) 51 | }, 52 | 53 | isInvalidToken (response) { 54 | const status = response.status 55 | const error = response.data.error 56 | 57 | // Customize this to your Oauth server. 58 | return (status === 401 && (error === 'invalid_token' || error === 'expired_token')) 59 | }, 60 | 61 | refreshToken (request) { 62 | return axios({ 63 | method: 'post', 64 | url: constants.REFRESH_TOKEN_URL, 65 | // headers: {'Authorization': 'Basic ' + CLIENT_SECRET}, 66 | data: { 67 | grant_type: 'refresh_token', 68 | refresh_token: store.state.auth.refreshToken 69 | } 70 | }) 71 | .then((response) => { 72 | this.storeToken(response) 73 | return this.retry(request) 74 | }) 75 | .catch((errorResponse) => { 76 | if (this.isInvalidToken(errorResponse)) { this.logout() } 77 | return errorResponse 78 | }) 79 | }, 80 | 81 | storeToken (response) { 82 | const auth = store.state.auth 83 | 84 | auth.isLoggedIn = true 85 | auth.accessToken = response.data.accessToken 86 | auth.refreshToken = response.data.refreshToken 87 | // @TODO: get user's name from response from Oauth server. 88 | auth.user.name = 'David Graham' 89 | auth.user.id = 'e3f657cb80354820b657cb8035c8208e' 90 | 91 | store.dispatch('auth/update', auth) 92 | }, 93 | 94 | retry (request) { 95 | return Vue.auth(request) 96 | .then((response) => { return response }) 97 | .catch((response) => { return response }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/auth/store.js: -------------------------------------------------------------------------------- 1 | const defaults = { 2 | isLoggedIn: false, 3 | accessToken: null, 4 | refreshToken: null, 5 | user: { 6 | name: '', 7 | id: '' 8 | } 9 | } 10 | 11 | const auth = { 12 | namespaced: true, 13 | 14 | state: Object.assign({}, defaults), 15 | 16 | mutations: { 17 | update (state, data) { 18 | state = Object.assign({}, defaults, data) 19 | }, 20 | clear (state) { 21 | state = Object.assign(state, defaults) 22 | } 23 | }, 24 | 25 | actions: { 26 | clear ({ state, commit, rootState, dispatch }) { 27 | commit('clear') 28 | }, 29 | update ({ state, commit, rootState }, data) { 30 | commit('update', data) 31 | } 32 | } 33 | } 34 | 35 | export default auth 36 | -------------------------------------------------------------------------------- /src/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 32 | 33 | 34 | 53 | -------------------------------------------------------------------------------- /src/components/app-bar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | 44 | 66 | -------------------------------------------------------------------------------- /src/components/app-dialog.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | 36 | 39 | -------------------------------------------------------------------------------- /src/components/app-footer.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /src/components/app-sidebar.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 118 | 119 | 140 | -------------------------------------------------------------------------------- /src/components/app-snackbar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 169 | 170 | 186 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | // Testing 2 | export const DEBUG = true 3 | 4 | // Backend API endpoints 5 | export const API_BASE_URL = '/api' 6 | export const REFRESH_TOKEN_URL = '/auth' 7 | 8 | /** 9 | * Key for local storage. 10 | * 11 | * Set the key to use in local storage to hold persistant data. If logged in, 12 | * you can see this key by going to Chrome > dev tools > application tab, 13 | * then choosing "Local Storage" and "http://localhost:8080". 14 | * 15 | * @type {string} 16 | */ 17 | export const STORAGE_KEY = 'vue-pizza' 18 | -------------------------------------------------------------------------------- /src/features/account/components/address-edit.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 137 | 138 | 142 | -------------------------------------------------------------------------------- /src/features/account/components/countries.js: -------------------------------------------------------------------------------- 1 | const countries = [ 2 | { 'name': 'United States', 'code': 'US' }, 3 | { 'name': 'Afghanistan', 'code': 'AF' }, 4 | { 'name': 'Åland Islands', 'code': 'AX' }, 5 | { 'name': 'Albania', 'code': 'AL' }, 6 | { 'name': 'Algeria', 'code': 'DZ' }, 7 | { 'name': 'American Samoa', 'code': 'AS' }, 8 | { 'name': 'Andorra', 'code': 'AD' }, 9 | { 'name': 'Angola', 'code': 'AO' }, 10 | { 'name': 'Anguilla', 'code': 'AI' }, 11 | { 'name': 'Antarctica', 'code': 'AQ' }, 12 | { 'name': 'Antigua and Barbuda', 'code': 'AG' }, 13 | { 'name': 'Argentina', 'code': 'AR' }, 14 | { 'name': 'Armenia', 'code': 'AM' }, 15 | { 'name': 'Aruba', 'code': 'AW' }, 16 | { 'name': 'Australia', 'code': 'AU' }, 17 | { 'name': 'Austria', 'code': 'AT' }, 18 | { 'name': 'Azerbaijan', 'code': 'AZ' }, 19 | { 'name': 'Bahamas', 'code': 'BS' }, 20 | { 'name': 'Bahrain', 'code': 'BH' }, 21 | { 'name': 'Bangladesh', 'code': 'BD' }, 22 | { 'name': 'Barbados', 'code': 'BB' }, 23 | { 'name': 'Belarus', 'code': 'BY' }, 24 | { 'name': 'Belgium', 'code': 'BE' }, 25 | { 'name': 'Belize', 'code': 'BZ' }, 26 | { 'name': 'Benin', 'code': 'BJ' }, 27 | { 'name': 'Bermuda', 'code': 'BM' }, 28 | { 'name': 'Bhutan', 'code': 'BT' }, 29 | { 'name': 'Bolivia', 'code': 'BO' }, 30 | { 'name': 'Bosnia and Herzegovina', 'code': 'BA' }, 31 | { 'name': 'Botswana', 'code': 'BW' }, 32 | { 'name': 'Bouvet Island', 'code': 'BV' }, 33 | { 'name': 'Brazil', 'code': 'BR' }, 34 | { 'name': 'British Indian Ocean Territory', 'code': 'IO' }, 35 | { 'name': 'Brunei Darussalam', 'code': 'BN' }, 36 | { 'name': 'Bulgaria', 'code': 'BG' }, 37 | { 'name': 'Burkina Faso', 'code': 'BF' }, 38 | { 'name': 'Burundi', 'code': 'BI' }, 39 | { 'name': 'Cambodia', 'code': 'KH' }, 40 | { 'name': 'Cameroon', 'code': 'CM' }, 41 | { 'name': 'Canada', 'code': 'CA' }, 42 | { 'name': 'Cape Verde', 'code': 'CV' }, 43 | { 'name': 'Cayman Islands', 'code': 'KY' }, 44 | { 'name': 'Central African Republic', 'code': 'CF' }, 45 | { 'name': 'Chad', 'code': 'TD' }, 46 | { 'name': 'Chile', 'code': 'CL' }, 47 | { 'name': 'China', 'code': 'CN' }, 48 | { 'name': 'Christmas Island', 'code': 'CX' }, 49 | { 'name': 'Cocos (Keeling) Islands', 'code': 'CC' }, 50 | { 'name': 'Colombia', 'code': 'CO' }, 51 | { 'name': 'Comoros', 'code': 'KM' }, 52 | { 'name': 'Congo', 'code': 'CG' }, 53 | { 'name': 'Congo, The Democratic Republic of the', 'code': 'CD' }, 54 | { 'name': 'Cook Islands', 'code': 'CK' }, 55 | { 'name': 'Costa Rica', 'code': 'CR' }, 56 | { 'name': 'Cote D\'Ivoire', 'code': 'CI' }, 57 | { 'name': 'Croatia', 'code': 'HR' }, 58 | { 'name': 'Cuba', 'code': 'CU' }, 59 | { 'name': 'Cyprus', 'code': 'CY' }, 60 | { 'name': 'Czech Republic', 'code': 'CZ' }, 61 | { 'name': 'Denmark', 'code': 'DK' }, 62 | { 'name': 'Djibouti', 'code': 'DJ' }, 63 | { 'name': 'Dominica', 'code': 'DM' }, 64 | { 'name': 'Dominican Republic', 'code': 'DO' }, 65 | { 'name': 'Ecuador', 'code': 'EC' }, 66 | { 'name': 'Egypt', 'code': 'EG' }, 67 | { 'name': 'El Salvador', 'code': 'SV' }, 68 | { 'name': 'Equatorial Guinea', 'code': 'GQ' }, 69 | { 'name': 'Eritrea', 'code': 'ER' }, 70 | { 'name': 'Estonia', 'code': 'EE' }, 71 | { 'name': 'Ethiopia', 'code': 'ET' }, 72 | { 'name': 'Falkland Islands (Malvinas)', 'code': 'FK' }, 73 | { 'name': 'Faroe Islands', 'code': 'FO' }, 74 | { 'name': 'Fiji', 'code': 'FJ' }, 75 | { 'name': 'Finland', 'code': 'FI' }, 76 | { 'name': 'France', 'code': 'FR' }, 77 | { 'name': 'French Guiana', 'code': 'GF' }, 78 | { 'name': 'French Polynesia', 'code': 'PF' }, 79 | { 'name': 'French Southern Territories', 'code': 'TF' }, 80 | { 'name': 'Gabon', 'code': 'GA' }, 81 | { 'name': 'Gambia', 'code': 'GM' }, 82 | { 'name': 'Georgia', 'code': 'GE' }, 83 | { 'name': 'Germany', 'code': 'DE' }, 84 | { 'name': 'Ghana', 'code': 'GH' }, 85 | { 'name': 'Gibraltar', 'code': 'GI' }, 86 | { 'name': 'Greece', 'code': 'GR' }, 87 | { 'name': 'Greenland', 'code': 'GL' }, 88 | { 'name': 'Grenada', 'code': 'GD' }, 89 | { 'name': 'Guadeloupe', 'code': 'GP' }, 90 | { 'name': 'Guam', 'code': 'GU' }, 91 | { 'name': 'Guatemala', 'code': 'GT' }, 92 | { 'name': 'Guernsey', 'code': 'GG' }, 93 | { 'name': 'Guinea', 'code': 'GN' }, 94 | { 'name': 'Guinea-Bissau', 'code': 'GW' }, 95 | { 'name': 'Guyana', 'code': 'GY' }, 96 | { 'name': 'Haiti', 'code': 'HT' }, 97 | { 'name': 'Heard Island and Mcdonald Islands', 'code': 'HM' }, 98 | { 'name': 'Holy See (Vatican City State)', 'code': 'VA' }, 99 | { 'name': 'Honduras', 'code': 'HN' }, 100 | { 'name': 'Hong Kong', 'code': 'HK' }, 101 | { 'name': 'Hungary', 'code': 'HU' }, 102 | { 'name': 'Iceland', 'code': 'IS' }, 103 | { 'name': 'India', 'code': 'IN' }, 104 | { 'name': 'Indonesia', 'code': 'ID' }, 105 | { 'name': 'Iran, Islamic Republic Of', 'code': 'IR' }, 106 | { 'name': 'Iraq', 'code': 'IQ' }, 107 | { 'name': 'Ireland', 'code': 'IE' }, 108 | { 'name': 'Isle of Man', 'code': 'IM' }, 109 | { 'name': 'Israel', 'code': 'IL' }, 110 | { 'name': 'Italy', 'code': 'IT' }, 111 | { 'name': 'Jamaica', 'code': 'JM' }, 112 | { 'name': 'Japan', 'code': 'JP' }, 113 | { 'name': 'Jersey', 'code': 'JE' }, 114 | { 'name': 'Jordan', 'code': 'JO' }, 115 | { 'name': 'Kazakhstan', 'code': 'KZ' }, 116 | { 'name': 'Kenya', 'code': 'KE' }, 117 | { 'name': 'Kiribati', 'code': 'KI' }, 118 | { 'name': 'Democratic People\'s Republic of Korea', 'code': 'KP' }, 119 | { 'name': 'Korea, Republic of', 'code': 'KR' }, 120 | { 'name': 'Kosovo', 'code': 'XK' }, 121 | { 'name': 'Kuwait', 'code': 'KW' }, 122 | { 'name': 'Kyrgyzstan', 'code': 'KG' }, 123 | { 'name': 'Lao People\'s Democratic Republic', 'code': 'LA' }, 124 | { 'name': 'Latvia', 'code': 'LV' }, 125 | { 'name': 'Lebanon', 'code': 'LB' }, 126 | { 'name': 'Lesotho', 'code': 'LS' }, 127 | { 'name': 'Liberia', 'code': 'LR' }, 128 | { 'name': 'Libyan Arab Jamahiriya', 'code': 'LY' }, 129 | { 'name': 'Liechtenstein', 'code': 'LI' }, 130 | { 'name': 'Lithuania', 'code': 'LT' }, 131 | { 'name': 'Luxembourg', 'code': 'LU' }, 132 | { 'name': 'Macao', 'code': 'MO' }, 133 | { 'name': 'Macedonia, The Former Yugoslav Republic of', 'code': 'MK' }, 134 | { 'name': 'Madagascar', 'code': 'MG' }, 135 | { 'name': 'Malawi', 'code': 'MW' }, 136 | { 'name': 'Malaysia', 'code': 'MY' }, 137 | { 'name': 'Maldives', 'code': 'MV' }, 138 | { 'name': 'Mali', 'code': 'ML' }, 139 | { 'name': 'Malta', 'code': 'MT' }, 140 | { 'name': 'Marshall Islands', 'code': 'MH' }, 141 | { 'name': 'Martinique', 'code': 'MQ' }, 142 | { 'name': 'Mauritania', 'code': 'MR' }, 143 | { 'name': 'Mauritius', 'code': 'MU' }, 144 | { 'name': 'Mayotte', 'code': 'YT' }, 145 | { 'name': 'Mexico', 'code': 'MX' }, 146 | { 'name': 'Micronesia, Federated States of', 'code': 'FM' }, 147 | { 'name': 'Moldova, Republic of', 'code': 'MD' }, 148 | { 'name': 'Monaco', 'code': 'MC' }, 149 | { 'name': 'Mongolia', 'code': 'MN' }, 150 | { 'name': 'Montenegro', 'code': 'ME' }, 151 | { 'name': 'Montserrat', 'code': 'MS' }, 152 | { 'name': 'Morocco', 'code': 'MA' }, 153 | { 'name': 'Mozambique', 'code': 'MZ' }, 154 | { 'name': 'Myanmar', 'code': 'MM' }, 155 | { 'name': 'Namibia', 'code': 'NA' }, 156 | { 'name': 'Nauru', 'code': 'NR' }, 157 | { 'name': 'Nepal', 'code': 'NP' }, 158 | { 'name': 'Netherlands', 'code': 'NL' }, 159 | { 'name': 'Netherlands Antilles', 'code': 'AN' }, 160 | { 'name': 'New Caledonia', 'code': 'NC' }, 161 | { 'name': 'New Zealand', 'code': 'NZ' }, 162 | { 'name': 'Nicaragua', 'code': 'NI' }, 163 | { 'name': 'Niger', 'code': 'NE' }, 164 | { 'name': 'Nigeria', 'code': 'NG' }, 165 | { 'name': 'Niue', 'code': 'NU' }, 166 | { 'name': 'Norfolk Island', 'code': 'NF' }, 167 | { 'name': 'Northern Mariana Islands', 'code': 'MP' }, 168 | { 'name': 'Norway', 'code': 'NO' }, 169 | { 'name': 'Oman', 'code': 'OM' }, 170 | { 'name': 'Pakistan', 'code': 'PK' }, 171 | { 'name': 'Palau', 'code': 'PW' }, 172 | { 'name': 'Palestinian Territory, Occupied', 'code': 'PS' }, 173 | { 'name': 'Panama', 'code': 'PA' }, 174 | { 'name': 'Papua New Guinea', 'code': 'PG' }, 175 | { 'name': 'Paraguay', 'code': 'PY' }, 176 | { 'name': 'Peru', 'code': 'PE' }, 177 | { 'name': 'Philippines', 'code': 'PH' }, 178 | { 'name': 'Pitcairn', 'code': 'PN' }, 179 | { 'name': 'Poland', 'code': 'PL' }, 180 | { 'name': 'Portugal', 'code': 'PT' }, 181 | { 'name': 'Puerto Rico', 'code': 'PR' }, 182 | { 'name': 'Qatar', 'code': 'QA' }, 183 | { 'name': 'Reunion', 'code': 'RE' }, 184 | { 'name': 'Romania', 'code': 'RO' }, 185 | { 'name': 'Russian Federation', 'code': 'RU' }, 186 | { 'name': 'Rwanda', 'code': 'RW' }, 187 | { 'name': 'Saint Helena', 'code': 'SH' }, 188 | { 'name': 'Saint Kitts and Nevis', 'code': 'KN' }, 189 | { 'name': 'Saint Lucia', 'code': 'LC' }, 190 | { 'name': 'Saint Pierre and Miquelon', 'code': 'PM' }, 191 | { 'name': 'Saint Vincent and the Grenadines', 'code': 'VC' }, 192 | { 'name': 'Samoa', 'code': 'WS' }, 193 | { 'name': 'San Marino', 'code': 'SM' }, 194 | { 'name': 'Sao Tome and Principe', 'code': 'ST' }, 195 | { 'name': 'Saudi Arabia', 'code': 'SA' }, 196 | { 'name': 'Senegal', 'code': 'SN' }, 197 | { 'name': 'Serbia', 'code': 'RS' }, 198 | { 'name': 'Seychelles', 'code': 'SC' }, 199 | { 'name': 'Sierra Leone', 'code': 'SL' }, 200 | { 'name': 'Singapore', 'code': 'SG' }, 201 | { 'name': 'Slovakia', 'code': 'SK' }, 202 | { 'name': 'Slovenia', 'code': 'SI' }, 203 | { 'name': 'Solomon Islands', 'code': 'SB' }, 204 | { 'name': 'Somalia', 'code': 'SO' }, 205 | { 'name': 'South Africa', 'code': 'ZA' }, 206 | { 'name': 'South Georgia and the South Sandwich Islands', 'code': 'GS' }, 207 | { 'name': 'Spain', 'code': 'ES' }, 208 | { 'name': 'Sri Lanka', 'code': 'LK' }, 209 | { 'name': 'Sudan', 'code': 'SD' }, 210 | { 'name': 'Suriname', 'code': 'SR' }, 211 | { 'name': 'Svalbard and Jan Mayen', 'code': 'SJ' }, 212 | { 'name': 'Swaziland', 'code': 'SZ' }, 213 | { 'name': 'Sweden', 'code': 'SE' }, 214 | { 'name': 'Switzerland', 'code': 'CH' }, 215 | { 'name': 'Syrian Arab Republic', 'code': 'SY' }, 216 | { 'name': 'Taiwan', 'code': 'TW' }, 217 | { 'name': 'Tajikistan', 'code': 'TJ' }, 218 | { 'name': 'Tanzania, United Republic of', 'code': 'TZ' }, 219 | { 'name': 'Thailand', 'code': 'TH' }, 220 | { 'name': 'Timor-Leste', 'code': 'TL' }, 221 | { 'name': 'Togo', 'code': 'TG' }, 222 | { 'name': 'Tokelau', 'code': 'TK' }, 223 | { 'name': 'Tonga', 'code': 'TO' }, 224 | { 'name': 'Trinidad and Tobago', 'code': 'TT' }, 225 | { 'name': 'Tunisia', 'code': 'TN' }, 226 | { 'name': 'Turkey', 'code': 'TR' }, 227 | { 'name': 'Turkmenistan', 'code': 'TM' }, 228 | { 'name': 'Turks and Caicos Islands', 'code': 'TC' }, 229 | { 'name': 'Tuvalu', 'code': 'TV' }, 230 | { 'name': 'Uganda', 'code': 'UG' }, 231 | { 'name': 'Ukraine', 'code': 'UA' }, 232 | { 'name': 'United Arab Emirates', 'code': 'AE' }, 233 | { 'name': 'United Kingdom', 'code': 'GB' }, 234 | { 'name': 'United States', 'code': 'US' }, 235 | { 'name': 'United States Minor Outlying Islands', 'code': 'UM' }, 236 | { 'name': 'Uruguay', 'code': 'UY' }, 237 | { 'name': 'Uzbekistan', 'code': 'UZ' }, 238 | { 'name': 'Vanuatu', 'code': 'VU' }, 239 | { 'name': 'Venezuela', 'code': 'VE' }, 240 | { 'name': 'Viet Nam', 'code': 'VN' }, 241 | { 'name': 'Virgin Islands, British', 'code': 'VG' }, 242 | { 'name': 'Virgin Islands, U.S.', 'code': 'VI' }, 243 | { 'name': 'Wallis and Futuna', 'code': 'WF' }, 244 | { 'name': 'Western Sahara', 'code': 'EH' }, 245 | { 'name': 'Yemen', 'code': 'YE' }, 246 | { 'name': 'Zambia', 'code': 'ZM' }, 247 | { 'name': 'Zimbabwe', 'code': 'ZW' } 248 | ] 249 | 250 | export default countries 251 | -------------------------------------------------------------------------------- /src/features/account/main.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 198 | 199 | 216 | -------------------------------------------------------------------------------- /src/features/account/service.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | // import auth from '@/auth/helpers' 3 | 4 | export default class Service { 5 | constructor (options) { 6 | this.id = store.state.auth.user.id 7 | } 8 | 9 | getProfile () { 10 | // Mock data. 11 | // Replace this with actual call to backend server below. 12 | const parsed = { 13 | email: 'prograhammer@gmail.com', 14 | name: 'David Graham', 15 | country: 'USA', 16 | addressLine1: '1234 Some St.', 17 | addressLine2: '', 18 | state: 'Texas', 19 | zipcode: '78789' 20 | } 21 | 22 | // Simulate loading time. 23 | return new Promise((resolve) => { 24 | setTimeout(() => { resolve(parsed) }, 500) 25 | }) 26 | 27 | /* 28 | return auth.get('/account') 29 | .then((response) => { 30 | const parsed = { 31 | email: response.email, 32 | name: response.name, 33 | country: response.country, 34 | addressLine1: response.address_line1, 35 | addressLine2: response.address_line2, 36 | state: response.state, 37 | zipcode: response.zipcode 38 | } 39 | 40 | return new Promise((resolve) => { resolve(parsed) }) 41 | }) 42 | .catch((error) => { 43 | return new Promise((resolve, reject) => { reject(error) }) 44 | }) 45 | */ 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/features/account/store.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/features/account/store.js -------------------------------------------------------------------------------- /src/features/billing/main.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | 41 | 56 | -------------------------------------------------------------------------------- /src/features/dashboard/components/chart.vue: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/features/dashboard/components/line-chart.vue: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/features/dashboard/main.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 126 | 127 | 138 | -------------------------------------------------------------------------------- /src/features/dashboard/service.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/features/dashboard/service.js -------------------------------------------------------------------------------- /src/features/dashboard/store.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | store.registerModule('dashboard', { 4 | namespaced: true, 5 | 6 | // State loaded when this component is first loaded. 7 | state: { 8 | test: 0 9 | }, 10 | 11 | mutations: { 12 | updateTest (state, newVal) { 13 | state.test = newVal 14 | } 15 | }, 16 | 17 | actions: { 18 | updateTest ({ state, commit, rootState, dispatch }, newVal) { 19 | console.log(newVal) 20 | commit('updateTest', newVal) 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/features/dashboard/test.css: -------------------------------------------------------------------------------- 1 | .test { 2 | background-color: red; 3 | } -------------------------------------------------------------------------------- /src/features/login/main.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 97 | 98 | 129 | -------------------------------------------------------------------------------- /src/features/login/service.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/features/login/service.js -------------------------------------------------------------------------------- /src/features/login/store.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/features/login/store.js -------------------------------------------------------------------------------- /src/features/premium/main.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | 41 | 56 | -------------------------------------------------------------------------------- /src/features/tutorial/main.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /src/features/wip/main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 39 | -------------------------------------------------------------------------------- /src/http/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import router from './router' 3 | 4 | export const http = { 5 | install (Vue, options) { 6 | Vue.prototype.$http = Vue.http = axios.create() 7 | } 8 | } 9 | 10 | export { router } 11 | -------------------------------------------------------------------------------- /src/http/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import routes from './routes' 4 | import store from '@/store' 5 | 6 | Vue.use(Router) 7 | 8 | /** 9 | * Guard the route from unauthorized users. 10 | * 11 | * @param {Route} to The route we want to access. 12 | * @param {Route} from The route from which we are coming from. 13 | * @param {Function} next Callback for passing a route to be called next. 14 | * @return {void} 15 | */ 16 | function guardRoute (to, from, next) { 17 | // work-around to get to the Vuex store (as of Vue 2.0) 18 | const auth = router.app.$options.store.state.auth 19 | 20 | if (!auth.isLoggedIn) { 21 | next({path: '/login', query: { redirect: to.fullPath }}) 22 | } else { 23 | next() 24 | } 25 | } 26 | 27 | /** 28 | * The Router instance containing all the routes for the application. 29 | */ 30 | const router = new Router({ 31 | base: '/app', 32 | // mode: 'history', // <-- uncomment to turn on history mode (preferred) 33 | routes: routes.map(route => ({ 34 | name: route.name, 35 | path: route.path, 36 | component: route.component, 37 | beforeEnter: (to, from, next) => { 38 | // Setup some per-page stuff. 39 | document.title = route.title 40 | store.dispatch('common/updateTitle', route.title) 41 | store.dispatch('common/updateLayout', route.layout) 42 | 43 | // Auth navigation guard. 44 | if (!route.isPublic) return guardRoute(to, from, next) 45 | 46 | next() 47 | } 48 | })) 49 | }) 50 | 51 | export default router 52 | -------------------------------------------------------------------------------- /src/http/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Every route becomes a chunk, loaded only when used. 3 | * Reduces size of initial App load. 4 | */ 5 | const routes = [ 6 | { 7 | name: 'login', 8 | path: '/login', 9 | component: () => import(/* webpackChunkName: "login" */ '@/features/login/main.vue'), 10 | title: 'Login', 11 | layout: 'PublicLayout', 12 | isPublic: true 13 | }, 14 | { 15 | name: 'home', 16 | path: '/', 17 | component: () => import(/* webpackChunkName: "dashboard" */ '@/features/dashboard/main.vue'), 18 | title: 'Dashboard', 19 | layout: 'DefaultLayout', 20 | isPublic: false 21 | }, 22 | { 23 | name: 'dashboard', 24 | path: '/dashboard', 25 | component: () => import(/* webpackChunkName: "dashboard" */ '@/features/dashboard/main.vue'), 26 | title: 'Dashboard', 27 | layout: 'DefaultLayout', 28 | isPublic: false 29 | }, 30 | { 31 | name: 'account', 32 | path: '/account', 33 | component: () => import(/* webpackChunkName: "account" */ '@/features/account/main.vue'), 34 | title: 'Account', 35 | layout: 'DefaultLayout', 36 | isPublic: false 37 | }, 38 | { 39 | name: 'tutorial', 40 | path: '/examples/tutorial', 41 | component: () => import(/* webpackChunkName: "tutorial" */ '@/features/tutorial/main.vue'), 42 | title: 'Tutorial', 43 | layout: 'DefaultLayout', 44 | isPublic: false 45 | }, 46 | { 47 | name: 'wip', 48 | path: '/examples/wip', 49 | component: () => import(/* webpackChunkName: "wip" */ '@/features/wip/main.vue'), 50 | title: 'Wip', 51 | layout: 'DefaultLayout', 52 | isPublic: false 53 | } 54 | ] 55 | 56 | export default routes 57 | -------------------------------------------------------------------------------- /src/layouts/default/main.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | 31 | 33 | -------------------------------------------------------------------------------- /src/layouts/public/main.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from './store' 3 | import { sync } from 'vuex-router-sync' 4 | import { http, router } from './http' 5 | import auth from './auth' 6 | import Vuetify from 'vuetify' 7 | import URLSearchParams from 'url-search-params' 8 | import App from './app' 9 | import Loading from './components/loading' 10 | import Appbar from './components/app-bar' 11 | import Appfooter from './components/app-footer' 12 | 13 | Vue.config.productionTip = false 14 | 15 | // Polyfills 16 | global.URLSearchParams = URLSearchParams 17 | 18 | // Sync router to store, as `store.state.route`. 19 | sync(store, router) 20 | 21 | // Http and Auth plugins 22 | Vue.use(http) 23 | Vue.use(auth) 24 | 25 | // Vuetify 26 | Vue.use(Vuetify, { 27 | theme: { 28 | primary: '#21CE99', 29 | secondary: '#D81B60', 30 | accent: '#805441' 31 | } 32 | }) 33 | 34 | // Styles 35 | require('./styles/scss/main.scss') 36 | require('./styles/stylus/main.styl') 37 | 38 | // Global Components 39 | Vue.component('loading', Loading) 40 | Vue.component('Appbar', Appbar) 41 | Vue.component('Appfooter', Appfooter) 42 | 43 | /* eslint-disable no-new */ 44 | new Vue({ 45 | el: '#app', 46 | router, 47 | store, 48 | render: h => h(App) 49 | }) 50 | -------------------------------------------------------------------------------- /src/store/common.js: -------------------------------------------------------------------------------- 1 | // Common State. 2 | const defaults = { 3 | sidebar: { 4 | visible: true 5 | }, 6 | title: '', 7 | layout: 'DefaultLayout', 8 | dialog: { 9 | visible: false, 10 | text: '' 11 | }, 12 | snackbar: { 13 | visible: false, 14 | text: '', 15 | timeout: 6000, 16 | color: '' 17 | }, 18 | error: { 19 | code: null, 20 | level: null, 21 | message: '' 22 | } 23 | } 24 | 25 | // Global module loaded on first app load. 26 | export default { 27 | namespaced: true, 28 | 29 | state: Object.assign({}, defaults), 30 | 31 | mutations: { 32 | updateSidebar (state, options) { 33 | state.sidebar = Object.assign({}, defaults.sidebar, options) 34 | }, 35 | 36 | updateTitle (state, title) { 37 | state.title = title 38 | }, 39 | 40 | updateLayout (state, layout) { 41 | state.layout = layout 42 | }, 43 | 44 | updateDialog (state, options) { 45 | state.dialog = Object.assign({}, defaults.dialog, options) 46 | }, 47 | 48 | updateSnackbar (state, options) { 49 | state.snackbar = Object.assign({}, defaults.snackbar, options) 50 | }, 51 | 52 | error (state, options) { 53 | state.error = Object.assign({}, defaults.error, options) 54 | }, 55 | 56 | clear (state) { 57 | state = Object.assign({}, defaults) 58 | } 59 | }, 60 | 61 | actions: { 62 | clear ({ state, commit, rootState, dispatch }) { 63 | commit('clear') 64 | dispatch('auth/clear', {}, { root: true }) 65 | }, 66 | 67 | updateSidebar ({ commit }, options) { 68 | commit('updateSidebar', options) 69 | }, 70 | 71 | updateTitle ({ commit }, title) { 72 | commit('updateTitle', title) 73 | }, 74 | 75 | updateLayout ({ commit }, layout) { 76 | commit('updateLayout', layout) 77 | }, 78 | 79 | updateDialog ({ commit }, options) { 80 | commit('updateDialog', options) 81 | }, 82 | 83 | updateSnackbar ({ commit }, options) { 84 | commit('updateSnackbar', options) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import auth from '@/auth/store' 4 | import common from './common' 5 | import { localStoragePlugin } from './plugins' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | modules: { common, auth }, 11 | plugins: [localStoragePlugin] 12 | }) 13 | -------------------------------------------------------------------------------- /src/store/plugins.js: -------------------------------------------------------------------------------- 1 | import auth from '@/auth/store' 2 | import * as constants from '@/constants' 3 | 4 | // Sync with local storage. 5 | if (localStorage.getItem(constants.STORAGE_KEY)) { 6 | const syncedState = JSON.parse(localStorage.getItem(constants.STORAGE_KEY)) 7 | auth.state = Object.assign(auth.state, syncedState.auth) 8 | } 9 | 10 | // LocalStorage plugin. 11 | const localStoragePlugin = store => { 12 | store.subscribe((mutation, state) => { 13 | const syncedData = { auth: state.auth } 14 | 15 | localStorage.setItem(constants.STORAGE_KEY, JSON.stringify(syncedData)) 16 | 17 | if (mutation.type === 'common/clear') { 18 | localStorage.removeItem(constants.STORAGE_KEY) 19 | } 20 | }) 21 | } 22 | 23 | export { localStoragePlugin } 24 | -------------------------------------------------------------------------------- /src/styles/scss/_variables.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/styles/scss/_variables.scss -------------------------------------------------------------------------------- /src/styles/scss/_vendor.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/styles/scss/_vendor.scss -------------------------------------------------------------------------------- /src/styles/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_vendor'; -------------------------------------------------------------------------------- /src/styles/stylus/1-settings/1-settings.styl: -------------------------------------------------------------------------------- 1 | @import 'vendor' 2 | @import 'variables' 3 | -------------------------------------------------------------------------------- /src/styles/stylus/1-settings/variables.styl: -------------------------------------------------------------------------------- 1 | $app-primary = #21CE99 2 | $app-secondary = #D81B60 3 | $app-accent = #805441 4 | $app-success = #61B865 5 | $app-light-grey = #E1E2E1 6 | $app-error = #FF6666 -------------------------------------------------------------------------------- /src/styles/stylus/1-settings/vendor.styl: -------------------------------------------------------------------------------- 1 | $color-pack = false // Let's save 30k 2 | -------------------------------------------------------------------------------- /src/styles/stylus/2-tools/2-tools.styl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/styles/stylus/2-tools/2-tools.styl -------------------------------------------------------------------------------- /src/styles/stylus/3-generic/3-generic.styl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/styles/stylus/3-generic/3-generic.styl -------------------------------------------------------------------------------- /src/styles/stylus/4-elements/4-elements.styl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/styles/stylus/4-elements/4-elements.styl -------------------------------------------------------------------------------- /src/styles/stylus/5-objects/5-objects.styl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/styles/stylus/5-objects/5-objects.styl -------------------------------------------------------------------------------- /src/styles/stylus/6-components/6-components.styl: -------------------------------------------------------------------------------- 1 | @import 'vendor.styl' 2 | -------------------------------------------------------------------------------- /src/styles/stylus/6-components/vendor.styl: -------------------------------------------------------------------------------- 1 | @import '~vuetify/src/stylus/main' 2 | 3 | .application.theme--light 4 | background: #FFFFFF -------------------------------------------------------------------------------- /src/styles/stylus/7-utils/7-utils.styl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/src/styles/stylus/7-utils/7-utils.styl -------------------------------------------------------------------------------- /src/styles/stylus/main.styl: -------------------------------------------------------------------------------- 1 | // Follows ITCSS: https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/ 2 | // Note: You may need to add a /vendor folder to some of these folders for working 3 | // with vendor styles. Be careful to add them in the order you need. 4 | @import '1-settings' 5 | @import '2-tools' 6 | @import '3-generic' 7 | @import '4-elements' 8 | @import '5-objects' 9 | @import '6-components' 10 | @import '7-utils' -------------------------------------------------------------------------------- /static/images/mountains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/images/mountains.png -------------------------------------------------------------------------------- /static/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /static/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /static/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/favicon.ico -------------------------------------------------------------------------------- /static/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /static/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prograhammer/vue-pizza/6d2259e49b45a34e36999ee706838463ebe4a4f0/static/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /static/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-pizza", 3 | "short_name": "Vue Pizza", 4 | "icons": [ 5 | { 6 | "src": "/app/static/img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/app/static/img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/app/index.html", 17 | "display": "standalone", 18 | "background_color": "#000000", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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: ['test/e2e/specs'], 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 | var server = require('../../build/dev-server.js') 4 | 5 | server.ready.then(() => { 6 | // 2. run the nightwatch test suite against it 7 | // to run in additional browsers: 8 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 9 | // 2. add it to the --env flag below 10 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 11 | // For more information on Nightwatch's config file, see 12 | // http://nightwatchjs.org/guide#settings-file 13 | var opts = process.argv.slice(2) 14 | if (opts.indexOf('--config') === -1) { 15 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 16 | } 17 | if (opts.indexOf('--env') === -1) { 18 | opts = opts.concat(['--env', 'chrome']) 19 | } 20 | 21 | var spawn = require('cross-spawn') 22 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 23 | 24 | runner.on('exit', function (code) { 25 | server.close() 26 | process.exit(code) 27 | }) 28 | 29 | runner.on('error', function (err) { 30 | server.close() 31 | throw err 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js PWA') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | --------------------------------------------------------------------------------