├── static
└── .gitkeep
├── src
├── assets
│ ├── style
│ │ ├── navbar.scss
│ │ ├── _fonts.scss
│ │ ├── app.scss
│ │ └── _variables.scss
│ └── images
│ │ └── logo.png
├── store
│ ├── getters.js
│ ├── actions.js
│ ├── index.js
│ ├── plugins.js
│ ├── state.js
│ └── mutations.js
├── utils.js
├── services
│ ├── chat.js
│ ├── user.js
│ └── auth.js
├── components
│ ├── App.vue
│ ├── AppFooter.vue
│ ├── common
│ │ ├── Countries.vue
│ │ ├── Spinner.vue
│ │ └── countries.data.js
│ ├── Hello.vue
│ ├── login
│ │ └── Login.vue
│ ├── signup
│ │ └── Signup.vue
│ ├── AppNav.vue
│ └── dashboard
│ │ ├── AddressModal.vue
│ │ └── Dashboard.vue
├── main.js
└── router
│ └── index.js
├── .eslintignore
├── config
├── prod.env.js
├── test.env.js
├── dev.env.js
└── index.js
├── .gitignore
├── test
├── unit
│ ├── .eslintrc
│ ├── specs
│ │ └── Hello.spec.js
│ ├── index.js
│ └── karma.conf.js
└── e2e
│ ├── custom-assertions
│ └── elementCount.js
│ ├── specs
│ └── loginTest.js
│ ├── runner.js
│ └── nightwatch.conf.js
├── .editorconfig
├── .postcssrc.js
├── .babelrc
├── .eslintrc.js
├── index.html
├── package.json
└── README.md
/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/style/navbar.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | export const user = state => state.user
2 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Big-Silver/Vue-Chat/HEAD/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | // Here is where you can put async operations.
2 | // See the Vuex official docs for more information.
3 |
4 | // ...
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | yarn-error.log
6 | test/unit/coverage
7 | test/e2e/reports
8 | selenium-debug.log
9 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": true,
7 | "sinon": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/config/test.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var devEnv = require('./dev.env')
3 |
4 | module.exports = merge(devEnv, {
5 | NODE_ENV: '"testing"'
6 | })
7 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", { "modules": false }],
4 | "stage-2"
5 | ],
6 | "plugins": ["transform-runtime"],
7 | "comments": false,
8 | "env": {
9 | "test": {
10 | "presets": ["env", "stage-2"],
11 | "plugins": [ "istanbul" ]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/assets/style/_fonts.scss:
--------------------------------------------------------------------------------
1 | /* Font Awesome */
2 | $fa-font-path: '../../../node_modules/font-awesome/fonts';
3 | @import '../../../node_modules/font-awesome/scss/font-awesome';
4 |
5 | /* Roboto */
6 | $roboto-font-path: '../../../node_modules/roboto-fontface/fonts';
7 | @import '../../../node_modules/roboto-fontface/css/roboto/sass/roboto-fontface';
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | /**
4 | * Get the error from a response.
5 | *
6 | * @param {Response} response The Vue-resource Response that we will try to get errors from.
7 | */
8 | getError: function (response) {
9 | return response.body['error_description']
10 | ? response.body.error_description
11 | : response.statusText
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/unit/specs/Hello.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Hello from '@/components/Hello'
3 |
4 | describe('Hello.vue', () => {
5 | it('should render correct contents', () => {
6 | const Constructor = Vue.extend(Hello)
7 | const vm = new Constructor().$mount()
8 | expect(vm.$el.querySelector('.hello h1').textContent)
9 | .to.equal('Welcome to Your Vue.js App')
10 | })
11 | })
12 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import { state } from './state'
4 | import * as getters from './getters'
5 | import * as actions from './actions'
6 | import * as mutations from './mutations'
7 | import plugins from './plugins'
8 |
9 | Vue.use(Vuex)
10 |
11 | const store = new Vuex.Store({
12 | state,
13 | getters,
14 | actions,
15 | mutations,
16 | plugins
17 | })
18 |
19 | export default store
20 |
--------------------------------------------------------------------------------
/src/store/plugins.js:
--------------------------------------------------------------------------------
1 | import { STORAGE_KEY } from './state'
2 |
3 | const localStoragePlugin = store => {
4 | store.subscribe((mutation, state) => {
5 | const syncedData = { auth: state.auth, user: state.user }
6 |
7 | localStorage.setItem(STORAGE_KEY, JSON.stringify(syncedData))
8 |
9 | if (mutation.type === 'CLEAR_ALL_DATA') {
10 | localStorage.removeItem(STORAGE_KEY)
11 | }
12 | })
13 | }
14 |
15 | // TODO: setup env
16 | // export default process.env.NODE_ENV !== 'production' ? [localStoragePlugin] : [localStoragePlugin]
17 | export default [localStoragePlugin]
18 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://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 | // required for eslint-config-vue
13 | extends: 'vue',
14 | // required to lint *.vue files
15 | plugins: [
16 | 'html'
17 | ],
18 | globals: {
19 | '$': true,
20 | '_': true,
21 | 'utils': true
22 | },
23 | // add your custom rules here
24 | 'rules': {
25 | // allow debugger during development
26 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/services/chat.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | const API_URL = 'http://localhost:3000'
4 |
5 | export default {
6 |
7 | install (Vue, options) {
8 | Vue.http.interceptors.push((request, next) => {
9 | request.headers.set('Content-Type', 'application/x-www-form-urlencoded')
10 | request.headers.set('Accept', 'application/json')
11 | })
12 |
13 | Vue.prototype.$chat = Vue.chat = this
14 | },
15 |
16 | initMessage () {
17 | return Vue.http.get(API_URL + '/message')
18 | .then((response) => {
19 | return response
20 | })
21 | .catch((errorResponse) => {
22 | return errorResponse
23 | })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | Vue.config.productionTip = false
3 |
4 | // Polyfill fn.bind() for PhantomJS
5 | /* eslint-disable no-extend-native */
6 | Function.prototype.bind = require('function-bind')
7 |
8 | // require all test files (files that ends with .spec.js)
9 | const testsContext = require.context('./specs', true, /\.spec$/)
10 | testsContext.keys().forEach(testsContext)
11 |
12 | // require all src files except main.js for coverage.
13 | // you can also change this to match only the subset of files that
14 | // you want coverage for.
15 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
16 | srcContext.keys().forEach(srcContext)
17 |
--------------------------------------------------------------------------------
/src/components/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
24 |
25 |
38 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Vue Project
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/services/user.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import * as QueryString from 'querystring'
4 |
5 | const API_URL = 'http://localhost:3000'
6 |
7 | export default {
8 |
9 | install (Vue, options) {
10 | Vue.http.interceptors.push((request, next) => {
11 | request.headers.set('Content-Type', 'application/x-www-form-urlencoded')
12 | request.headers.set('Accept', 'application/json')
13 | })
14 |
15 | Vue.prototype.$user = Vue.user = this
16 | },
17 |
18 | getUsers (origin) {
19 | const data = QueryString.stringify({
20 | 'workspaceId': origin.workspaceId
21 | })
22 | return Vue.http.post(API_URL + '/users', data)
23 | .then((response) => {
24 | return response
25 | })
26 | .catch((err) => {
27 | return err
28 | })
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | // Set the key we'll use in local storage.
2 | // Go to Chrome dev tools, application tab, click "Local Storage" and "http://localhost:8080"
3 | // and you'll see this key set below (if logged in):
4 | export const STORAGE_KEY = 'example-vue-project'
5 |
6 | let syncedData = {
7 | auth: {
8 | isLoggedIn: false,
9 | accessToken: null,
10 | refreshToken: null
11 | },
12 | user: {
13 | name: null
14 | }
15 | }
16 |
17 | const notSyncedData = {
18 | appnav: {
19 | searchText: '',
20 | searchTimestamp: null
21 | }
22 | }
23 |
24 | // Sync with local storage.
25 | if (localStorage.getItem(STORAGE_KEY)) {
26 | syncedData = JSON.parse(localStorage.getItem(STORAGE_KEY))
27 | }
28 |
29 | // Merge data and export it.
30 | export const state = Object.assign(syncedData, notSyncedData)
31 |
--------------------------------------------------------------------------------
/src/assets/style/app.scss:
--------------------------------------------------------------------------------
1 | @import 'fonts';
2 | @import 'variables';
3 | @import '../../../node_modules/bootstrap/dist/css/bootstrap.css';
4 | @import '../../../node_modules/bootstrap-vue/dist/bootstrap-vue.css';
5 | @import '../../../node_modules/bootstrap/scss/bootstrap';
6 | @import '../../../node_modules/vue-multiselect/dist/vue-multiselect.min.css';
7 |
8 | // Some quick styling to match the multiselct with Bootstrap.
9 | #app .multiselect__tags {
10 | border: $input-btn-border-width solid $input-border-color;
11 | }
12 |
13 | #app .multiselect, .multiselect__input, .multiselect__single {
14 | font-size: $font-size-base;
15 | }
16 |
17 | #app .multiselect--active .multiselect__tags {
18 | color: $input-color-focus;
19 | background-color: $input-bg-focus;
20 | border-color: $input-border-focus;
21 | outline: none;
22 | }
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | export const UPDATE_AUTH = (state, auth) => {
2 | state.auth = auth
3 | }
4 |
5 | export const UPDATE_USER = (state, user) => {
6 | state.user = user
7 | }
8 |
9 | export const APPNAV_SEARCH = (state, searchData) => {
10 | state.appnav = searchData
11 | }
12 |
13 | /**
14 | * Clear each property, one by one, so reactivity still works.
15 | *
16 | * (ie. clear out state.auth.isLoggedIn so Navbar component automatically reacts to logged out state,
17 | * and the Navbar menu adjusts accordingly)
18 | *
19 | * TODO: use a common import of default state to reset these values with.
20 | */
21 | export const CLEAR_ALL_DATA = (state) => {
22 | // Auth
23 | state.auth.isLoggedIn = false
24 | state.auth.accessToken = null
25 | state.auth.refreshToken = null
26 |
27 | // User
28 | state.user.name = ''
29 | }
30 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/AppFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
25 |
26 |
41 |
--------------------------------------------------------------------------------
/test/e2e/specs/loginTest.js:
--------------------------------------------------------------------------------
1 | // For authoring Nightwatch tests, see
2 | // http://nightwatchjs.org/guide#usage
3 |
4 | /**
5 | * Test that user can login and see dashboard.
6 | */
7 | module.exports = {
8 | 'default e2e tests': function (browser) {
9 | // automatically uses dev Server port from /config.index.js
10 | // default: http://localhost:8080
11 | // see nightwatch.conf.js
12 | const devServer = browser.globals.devServerURL
13 |
14 | browser
15 | .url(devServer)
16 | .waitForElementVisible('#app', 5000)
17 |
18 | // Assert that user can see login.
19 | .assert.elementPresent('.ev-login')
20 | .setValue('.js-login__username', 'demouser')
21 | .setValue('.js-login__password', 'testpass')
22 | .click('.js-login__submit')
23 | .pause(3000)
24 |
25 | // Assert that user can see dashboard.
26 | .assert.containsText('.ev-dashboard__heading', 'This is the dashboard')
27 | .end()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/common/Countries.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
39 |
40 |
43 |
--------------------------------------------------------------------------------
/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 | // 2. run the nightwatch test suite against it
6 | // to run in additional browsers:
7 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
8 | // 2. add it to the --env flag below
9 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
10 | // For more information on Nightwatch's config file, see
11 | // http://nightwatchjs.org/guide#settings-file
12 | var opts = process.argv.slice(2)
13 | if (opts.indexOf('--config') === -1) {
14 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
15 | }
16 | if (opts.indexOf('--env') === -1) {
17 | opts = opts.concat(['--env', 'chrome'])
18 | }
19 |
20 | var spawn = require('cross-spawn')
21 | var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
22 |
23 | runner.on('exit', function (code) {
24 | server.close()
25 | process.exit(code)
26 | })
27 |
28 | runner.on('error', function (err) {
29 | server.close()
30 | throw err
31 | })
32 |
--------------------------------------------------------------------------------
/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | // This is a karma config file. For more details see
2 | // http://karma-runner.github.io/0.13/config/configuration-file.html
3 | // we are also using it with karma-webpack
4 | // https://github.com/webpack/karma-webpack
5 |
6 | var webpackConfig = require('../../build/webpack.test.conf')
7 |
8 | module.exports = function (config) {
9 | config.set({
10 | // to run in additional browsers:
11 | // 1. install corresponding karma launcher
12 | // http://karma-runner.github.io/0.13/config/browsers.html
13 | // 2. add it to the `browsers` array below.
14 | browsers: ['PhantomJS'],
15 | frameworks: ['mocha', 'sinon-chai'],
16 | reporters: ['spec', 'coverage'],
17 | files: [
18 | '../../node_modules/babel-polyfill/dist/polyfill.js',
19 | './index.js'
20 | ],
21 | preprocessors: {
22 | './index.js': ['webpack', 'sourcemap']
23 | },
24 | webpack: webpackConfig,
25 | webpackMiddleware: {
26 | noInfo: true
27 | },
28 | coverageReporter: {
29 | dir: './coverage',
30 | reporters: [
31 | { type: 'lcov', subdir: '.' },
32 | { type: 'text-summary' }
33 | ]
34 | }
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/test/e2e/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | require('babel-register')
2 | var config = require('../../config')
3 |
4 | // http://nightwatchjs.org/getingstarted#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 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | /* Twitter Bootstrap JS (this could also be handled in an app.js file) */
2 | require('bootstrap')
3 |
4 | /* Vue */
5 | import Vue from 'vue'
6 | import router from './router'
7 | import store from './store'
8 | import VueResource from 'vue-resource'
9 | import BootstrapVue from 'bootstrap-vue'
10 | import socketio from 'socket.io-client'
11 | import VueSocketio from 'vue-socket.io'
12 | import VueSession from 'vue-session'
13 | import VueChatScroll from 'vue-chat-scroll'
14 | import Autoscroll from 'vue-autoscroll'
15 |
16 | Vue.use(BootstrapVue)
17 | Vue.use(VueResource)
18 | Vue.use(VueSocketio, socketio('http://localhost:3000'))
19 | Vue.use(VueSession)
20 | Vue.use(VueChatScroll)
21 | Vue.use(Autoscroll)
22 |
23 | Vue.config.productionTip = false
24 |
25 | /* App sass */
26 | import './assets/style/app.scss'
27 |
28 | /* App component */
29 | import App from './components/App'
30 |
31 | /* Auth plugin */
32 | import Auth from './services/auth'
33 | import Chat from './services/chat'
34 | import User from './services/user'
35 | Vue.use(Auth)
36 | Vue.use(Chat)
37 | Vue.use(User)
38 |
39 | /* eslint-disable no-new */
40 | new Vue({
41 | el: '#app',
42 | // Attach the Vue instance to the window,
43 | // so it's available globally.
44 | created: function () {
45 | window.Vue = this
46 | },
47 | router,
48 | store,
49 | render: h => h(App)
50 | })
51 |
--------------------------------------------------------------------------------
/src/components/Hello.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
Essential Links
5 |
13 |
Ecosystem
14 |
20 |
21 |
22 |
23 |
33 |
34 |
35 |
54 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 |
4 | Vue.use(Router)
5 |
6 | const router = new Router({
7 | mode: 'history',
8 | routes: [
9 | // Each of these routes are loaded asynchronously, when a user first navigates to each corresponding endpoint.
10 | // The route will load once into memory, the first time it's called, and no more on future calls.
11 | // This behavior can be observed on the network tab of your browser dev tools.
12 | {
13 | path: '/',
14 | name: 'login',
15 | component: function (resolve) {
16 | require(['@/components/login/Login.vue'], resolve)
17 | },
18 | beforeEnter: authRoute
19 | },
20 | {
21 | path: '/login',
22 | name: 'login1',
23 | component: function (resolve) {
24 | require(['@/components/login/Login.vue'], resolve)
25 | },
26 | beforeEnter: authRoute
27 | },
28 | {
29 | path: '/signup',
30 | name: 'signup',
31 | component: function (resolve) {
32 | require(['@/components/signup/Signup.vue'], resolve)
33 | }
34 | },
35 | {
36 | path: '/home',
37 | name: 'dashboard',
38 | component: function (resolve) {
39 | require(['@/components/dashboard/Dashboard.vue'], resolve)
40 | },
41 | beforeEnter: guardRoute
42 | },
43 | {
44 | path: '/*',
45 | redirect: '/login'
46 | }
47 | ]
48 | })
49 |
50 | function guardRoute (to, from, next) {
51 | // work-around to get to the Vuex store (as of Vue 2.0)
52 | const auth = router.app.$options.store.state.auth
53 |
54 | if (!auth.isLoggedIn) {
55 | next({
56 | path: '/login',
57 | query: { redirect: to.fullPath }
58 | })
59 | } else {
60 | next()
61 | }
62 | }
63 |
64 | function authRoute (to, from, next) {
65 | console.log('authRoute')
66 | const auth = router.app.$options.store.state.auth
67 |
68 | if (auth.isLoggedIn) {
69 | next({
70 | path: '/home'
71 | })
72 | } else {
73 | next()
74 | }
75 | next()
76 | }
77 |
78 | export default router
79 |
--------------------------------------------------------------------------------
/src/components/login/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
15 |
16 |
17 |
23 |
24 |
31 |
32 | Don’t have an account?
Sign up here.
33 |
34 |
35 |
36 |
37 |
75 |
76 |
82 |
--------------------------------------------------------------------------------
/src/components/common/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
{{ message }}
9 |
10 |
11 |
12 |
13 |
37 |
38 |
93 |
--------------------------------------------------------------------------------
/src/components/signup/Signup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
15 |
16 |
17 |
24 |
25 |
26 |
32 |
33 |
40 |
41 | If you already have any account
Sign in here.
42 |
43 |
44 |
45 |
46 |
81 |
82 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | env: require('./prod.env'),
7 | index: path.resolve(__dirname, '../dist/index.html'),
8 | assetsRoot: path.resolve(__dirname, '../dist'),
9 | assetsSubDirectory: 'static',
10 | assetsPublicPath: '/',
11 | productionSourceMap: true,
12 | // Gzip off by default as many popular static hosts such as
13 | // Surge or Netlify already gzip all static assets for you.
14 | // Before setting to `true`, make sure to:
15 | // npm install --save-dev compression-webpack-plugin
16 | productionGzip: false,
17 | productionGzipExtensions: ['js', 'css'],
18 | // Run the build command with an extra argument to
19 | // View the bundle analyzer report after build finishes:
20 | // `npm run build --report`
21 | // Set to `true` or `false` to always turn it on or off
22 | bundleAnalyzerReport: process.env.npm_config_report
23 | },
24 | dev: {
25 | env: require('./dev.env'),
26 | port: 8080,
27 | autoOpenBrowser: true,
28 | assetsSubDirectory: 'static',
29 | assetsPublicPath: '/',
30 | proxyTable: {
31 | '/auth': {
32 | // TODO: Update to use node express oauth2 server for better example.
33 | target: 'http://brentertainment.com/oauth2/lockdin/token', // <-- demo oauth2 server, https://github.com/bshaffer/oauth2-demo-php
34 | changeOrigin: true,
35 | ws: true,
36 | pathRewrite: {
37 | '^/auth': ''
38 | },
39 | router: {
40 | }
41 | },
42 | '/api': {
43 | target: 'http://brentertainment.com/oauth2', // api server
44 | changeOrigin: true, // needed for virtual hosted sites
45 | ws: true, // proxy websockets
46 | pathRewrite: {
47 | '^/api': '/lockdin' // rewrite path localhost:8080/api to http://brentertainment.com/oauth2/lockdin
48 | },
49 | router: {
50 | // when request.headers.host == 'dev.localhost:3000',
51 | // override target 'http://www.example.org' to 'http://localhost:8000'
52 | // 'dev.localhost:3000': 'http://localhost:8000'
53 | }
54 | }
55 | },
56 | // CSS Sourcemaps off by default because relative paths are "buggy"
57 | // with this option, according to the CSS-Loader README
58 | // (https://github.com/webpack/css-loader#sourcemaps)
59 | // In our experience, they generally work as expected,
60 | // just be aware of this issue when enabling this option.
61 | cssSourceMap: false
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/AppNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
18 |
25 |
26 |
27 |
28 |
29 |
30 | -
31 | Login (current)
32 |
33 | -
34 | Signup (current)
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
69 |
70 |
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-vue-project",
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 | "build": "node build/build.js",
10 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
11 | "e2e": "node test/e2e/runner.js",
12 | "test": "npm run unit && npm run e2e",
13 | "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs"
14 | },
15 | "dependencies": {
16 | "bootstrap-vue": "^2.0.0-rc.8",
17 | "popper.js": "^1.14.3",
18 | "query-string": "^6.0.0",
19 | "socket.io-client": "^2.1.0",
20 | "vue": "^2.2.2",
21 | "vue-autoscroll": "^1.1.1",
22 | "vue-chat-scroll": "^1.2.1",
23 | "vue-resource": "^1.2.1",
24 | "vue-router": "^2.2.0",
25 | "vue-session": "^1.0.0",
26 | "vue-socket.io": "^2.1.1-b",
27 | "vuex": "^2.2.1"
28 | },
29 | "devDependencies": {
30 | "autoprefixer": "^6.7.2",
31 | "babel-core": "^6.22.1",
32 | "babel-eslint": "^7.1.1",
33 | "babel-loader": "^6.2.10",
34 | "babel-plugin-istanbul": "^3.1.2",
35 | "babel-plugin-transform-runtime": "^6.22.0",
36 | "babel-polyfill": "^6.23.0",
37 | "babel-preset-env": "^1.2.1",
38 | "babel-preset-stage-2": "^6.22.0",
39 | "babel-register": "^6.22.0",
40 | "bootstrap": "^4.0.0-alpha.6",
41 | "chai": "^3.5.0",
42 | "chalk": "^1.1.3",
43 | "chromedriver": "^2.27.2",
44 | "connect-history-api-fallback": "^1.3.0",
45 | "copy-webpack-plugin": "^4.0.1",
46 | "cross-env": "^3.1.4",
47 | "cross-spawn": "^5.0.1",
48 | "css-loader": "^0.26.1",
49 | "eslint": "^3.14.1",
50 | "eslint-config-vue": "^2.0.2",
51 | "eslint-friendly-formatter": "^2.0.7",
52 | "eslint-loader": "^1.6.1",
53 | "eslint-plugin-html": "^2.0.0",
54 | "eslint-plugin-vue": "^2.0.1",
55 | "eventsource-polyfill": "^0.9.6",
56 | "express": "^4.14.1",
57 | "extract-text-webpack-plugin": "^2.0.0",
58 | "file-loader": "^0.10.0",
59 | "font-awesome": "^4.7.0",
60 | "friendly-errors-webpack-plugin": "^1.1.3",
61 | "function-bind": "^1.1.0",
62 | "html-webpack-plugin": "^2.28.0",
63 | "http-proxy-middleware": "^0.17.3",
64 | "inject-loader": "^2.0.1",
65 | "jquery": "^3.2.0",
66 | "karma": "^1.4.1",
67 | "karma-coverage": "^1.1.1",
68 | "karma-mocha": "^1.3.0",
69 | "karma-phantomjs-launcher": "^1.0.2",
70 | "karma-sinon-chai": "^1.2.4",
71 | "karma-sourcemap-loader": "^0.3.7",
72 | "karma-spec-reporter": "0.0.26",
73 | "karma-webpack": "^2.0.2",
74 | "lodash": "^4.17.4",
75 | "lolex": "^1.5.2",
76 | "mocha": "^3.2.0",
77 | "nightwatch": "^0.9.12",
78 | "node-sass": "^4.5.0",
79 | "opn": "^4.0.2",
80 | "optimize-css-assets-webpack-plugin": "^1.3.0",
81 | "ora": "^1.1.0",
82 | "phantomjs-prebuilt": "^2.1.14",
83 | "rimraf": "^2.6.0",
84 | "roboto-fontface": "^0.7.0",
85 | "sass-loader": "^6.0.3",
86 | "selenium-server": "^3.0.1",
87 | "semver": "^5.3.0",
88 | "sinon": "^1.17.7",
89 | "sinon-chai": "^2.8.0",
90 | "stylus": "^0.54.5",
91 | "stylus-loader": "^3.0.1",
92 | "tether": "^1.4.0",
93 | "url-loader": "^0.5.7",
94 | "vue-loader": "^11.1.4",
95 | "vue-multiselect": "^2.0.0-beta.15",
96 | "vue-style-loader": "^2.0.0",
97 | "vue-template-compiler": "^2.2.4",
98 | "webpack": "^2.2.1",
99 | "webpack-bundle-analyzer": "^2.2.1",
100 | "webpack-dev-middleware": "^1.10.0",
101 | "webpack-hot-middleware": "^2.16.1",
102 | "webpack-merge": "^2.6.1"
103 | },
104 | "engines": {
105 | "node": ">= 4.0.0",
106 | "npm": ">= 3.0.0"
107 | },
108 | "browserslist": [
109 | "> 1%",
110 | "last 2 versions",
111 | "not ie <= 8"
112 | ]
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/dashboard/AddressModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | >
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
208 |
209 |
212 |
--------------------------------------------------------------------------------
/src/components/dashboard/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | USERS
8 |
9 | {{user.name}} typing...
10 |
11 |
12 |
13 |
14 |
15 |
16 | General Platform
17 |
18 |
19 | {{message.user}}
20 | {{message.date}}
21 |
22 |
23 | {{message.message}}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Send
35 |
36 |
37 |
38 |
39 |
46 |
47 |
48 |
49 |
50 |
51 |
171 |
172 |
225 |
--------------------------------------------------------------------------------
/src/services/auth.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import router from '../router'
3 | import store from '../store'
4 |
5 | import * as QueryString from 'querystring'
6 |
7 | /**
8 | * @var{string} LOGIN_URL The endpoint for logging in. This endpoint should be proxied by Webpack dev server
9 | * and maybe nginx in production (cleaner calls and avoids CORS issues).
10 | */
11 | const API_URL = 'http://localhost:3000'
12 |
13 | /**
14 | * @var{string} REFRESH_TOKEN_URL The endpoint for refreshing an access_token. This endpoint should be proxied
15 | * by Webpack dev server and maybe nginx in production (cleaner calls and avoids CORS issues).
16 | */
17 | const REFRESH_TOKEN_URL = '/auth'
18 |
19 | /**
20 | * TODO: This is here to demonstrate what an OAuth server will want. Ultimately you don't want to
21 | * expose a client_secret here. You want your real project backend to take a username/password
22 | * request and add the client secret on the server-side and forward that request
23 | * onto an OAuth server. Your backend acts as a middle-man in the process, which is better, for
24 | * example in situations like DDoS attacks.
25 | *
26 | * @var{Object} AUTH_BASIC_HEADERS The options to pass into a Vue-resource http call. Includes
27 | * the headers used for login and token refresh and emulateJSON flag since we are hitting an
28 | * OAuth server directly that can't handle application/json.
29 | */
30 | const AUTH_BASIC_HEADERS = {
31 | headers: {
32 | 'Authorization': 'Basic ZGVtb2FwcDpkZW1vcGFzcw==' // Base64(client_id:client_secret) "demoapp:demopass"
33 | },
34 | emulateJSON: true
35 | }
36 |
37 | /**
38 | * Auth Plugin
39 | *
40 | * (see https://vuejs.org/v2/guide/plugins.html for more info on Vue.js plugins)
41 | *
42 | * Handles login and token authentication using OAuth2.
43 | */
44 | export default {
45 |
46 | /**
47 | * Install the Auth class.
48 | *
49 | * Creates a Vue-resource http interceptor to handle automatically adding auth headers
50 | * and refreshing tokens. Then attaches this object to the global Vue (as Vue.auth).
51 | *
52 | * @param {Object} Vue The global Vue.
53 | * @param {Object} options Any options we want to have in our plugin.
54 | * @return {void}
55 | */
56 | install (Vue, options) {
57 | /*
58 | ************* Token **************
59 |
60 | Vue.http.interceptors.push((request, next) => {
61 | const token = store.state.auth.accessToken
62 | const hasAuthHeader = request.headers.has('Authorization')
63 | console.log(store.state)
64 | if (token && !hasAuthHeader) {
65 | this.setAuthHeader(request)
66 | }
67 |
68 | next((response) => {
69 | if (this._isInvalidToken(response)) {
70 | return this._refreshToken(request)
71 | }
72 | })
73 | })
74 | */
75 |
76 | Vue.http.interceptors.push((request, next) => {
77 | request.headers.set('Content-Type', 'application/x-www-form-urlencoded')
78 | request.headers.set('Accept', 'application/json')
79 | })
80 |
81 | Vue.prototype.$auth = Vue.auth = this
82 | },
83 |
84 | /**
85 | * Login
86 | *
87 | * @param {Object.} creds The username and password for logging in.
88 | * @param {string|null} redirect The name of the Route to redirect to.
89 | * @return {Promise}
90 | */
91 | login (creds, redirect) {
92 | // const params = { 'grant_type': 'password', 'email': creds.username, 'password': creds.password }
93 | const data = QueryString.stringify({
94 | 'workspaceId': creds.workspaceId,
95 | 'email': creds.email,
96 | 'password': creds.password
97 | })
98 | return Vue.http.post(API_URL + '/login', data)
99 | .then((response) => {
100 | this._storeToken(response)
101 | if (redirect) {
102 | router.push({ name: redirect })
103 | }
104 |
105 | return response
106 | })
107 | .catch((errorResponse) => {
108 | return errorResponse
109 | })
110 | },
111 |
112 | register (creds, redirect) {
113 | const data = QueryString.stringify({
114 | 'name': creds.name,
115 | 'workspace': creds.workspace,
116 | 'email': creds.email,
117 | 'password': creds.password
118 | })
119 | return Vue.http.post(API_URL + '/register', data)
120 | .then((response) => {
121 | this._storeToken(response)
122 | if (redirect) {
123 | router.push({ name: redirect })
124 | }
125 | })
126 | .catch((err) => {
127 | return err
128 | })
129 | },
130 | /**
131 | * Logout
132 | *
133 | * Clear all data in our Vuex store (which resets logged-in status) and redirect back
134 | * to login form.
135 | *
136 | * @return {void}
137 | */
138 | logout () {
139 | store.commit('CLEAR_ALL_DATA')
140 | router.push({ name: 'login' })
141 | },
142 |
143 | /**
144 | * Set the Authorization header on a Vue-resource Request.
145 | *
146 | * @param {Request} request The Vue-Resource Request instance to set the header on.
147 | * @return {void}
148 | */
149 | setAuthHeader (request) {
150 | request.headers.set('Authorization', 'Bearer ' + store.state.auth.accessToken)
151 | // The demo Oauth2 server we are using requires this param, but normally you only set the header.
152 | /* eslint-disable camelcase */
153 | request.params.access_token = store.state.auth.accessToken
154 | },
155 |
156 | /**
157 | * Retry the original request.
158 | *
159 | * Let's retry the user's original target request that had recieved a invalid token response
160 | * (which we fixed with a token refresh).
161 | *
162 | * @param {Request} request The Vue-resource Request instance to use to repeat an http call.
163 | * @return {Promise}
164 | */
165 | _retry (request) {
166 | this.setAuthHeader(request)
167 |
168 | return Vue.http(request)
169 | .then((response) => {
170 | return response
171 | })
172 | .catch((response) => {
173 | return response
174 | })
175 | },
176 |
177 | /**
178 | * Refresh the access token
179 | *
180 | * Make an ajax call to the OAuth2 server to refresh the access token (using our refresh token).
181 | *
182 | * @private
183 | * @param {Request} request Vue-resource Request instance, the original request that we'll retry.
184 | * @return {Promise}
185 | */
186 | _refreshToken (request) {
187 | const params = { 'grant_type': 'refresh_token', 'refresh_token': store.state.auth.refreshToken }
188 |
189 | return Vue.http.post(REFRESH_TOKEN_URL, params, AUTH_BASIC_HEADERS)
190 | .then((result) => {
191 | this._storeToken(result)
192 | return this._retry(request)
193 | })
194 | .catch((errorResponse) => {
195 | if (this._isInvalidToken(errorResponse)) {
196 | this.logout()
197 | }
198 | return errorResponse
199 | })
200 | },
201 |
202 | /**
203 | * Store tokens
204 | *
205 | * Update the Vuex store with the access/refresh tokens received from the response from
206 | * the Oauth2 server.
207 | *
208 | * @private
209 | * @param {Response} response Vue-resource Response instance from an OAuth2 server.
210 | * that contains our tokens.
211 | * @return {void}
212 | */
213 | _storeToken (response) {
214 | const auth = store.state.auth
215 | const user = store.state.user
216 |
217 | auth.isLoggedIn = true
218 | auth.accessToken = response.body.access_token
219 | auth.refreshToken = response.body.refresh_token
220 | // TODO: get user's name from response from Oauth server.
221 | user.name = 'John Smith'
222 |
223 | store.commit('UPDATE_AUTH', auth)
224 | store.commit('UPDATE_USER', user)
225 | },
226 |
227 | /**
228 | * Check if the Vue-resource Response is an invalid token response.
229 | *
230 | * @private
231 | * @param {Response} response The Vue-resource Response instance received from an http call.
232 | * @return {boolean}
233 | */
234 | _isInvalidToken (response) {
235 | const status = response.status
236 | const error = response.data.error
237 |
238 | return (status === 401 && (error === 'invalid_token' || error === 'expired_token'))
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/components/common/countries.data.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | { value: '', text: 'Select a country' },
3 | { value: 'Afghanistan', text: 'Afghanistan' },
4 | { value: 'Åland Islands', text: 'Åland Islands' },
5 | { value: 'Albania', text: 'Albania' },
6 | { value: 'Algeria', text: 'Algeria' },
7 | { value: 'American Samoa', text: 'American Samoa' },
8 | { value: 'Andorra', text: 'Andorra' },
9 | { value: 'Angola', text: 'Angola' },
10 | { value: 'Anguilla', text: 'Anguilla' },
11 | { value: 'Antarctica', text: 'Antarctica' },
12 | { value: 'Antigua and Barbuda', text: 'Antigua and Barbuda' },
13 | { value: 'Argentina', text: 'Argentina' },
14 | { value: 'Armenia', text: 'Armenia' },
15 | { value: 'Aruba', text: 'Aruba' },
16 | { value: 'Australia', text: 'Australia' },
17 | { value: 'Austria', text: 'Austria' },
18 | { value: 'Azerbaijan', text: 'Azerbaijan' },
19 | { value: 'Bahamas', text: 'Bahamas' },
20 | { value: 'Bahrain', text: 'Bahrain' },
21 | { value: 'Bangladesh', text: 'Bangladesh' },
22 | { value: 'Barbados', text: 'Barbados' },
23 | { value: 'Belarus', text: 'Belarus' },
24 | { value: 'Belgium', text: 'Belgium' },
25 | { value: 'Belize', text: 'Belize' },
26 | { value: 'Benin', text: 'Benin' },
27 | { value: 'Bermuda', text: 'Bermuda' },
28 | { value: 'Bhutan', text: 'Bhutan' },
29 | { value: 'Bolivia', text: 'Bolivia' },
30 | { value: 'Bosnia and Herzegovina', text: 'Bosnia and Herzegovina' },
31 | { value: 'Botswana', text: 'Botswana' },
32 | { value: 'Bouvet Island', text: 'Bouvet Island' },
33 | { value: 'Brazil', text: 'Brazil' },
34 | { value: 'British Indian Ocean Territory', text: 'British Indian Ocean Territory' },
35 | { value: 'Brunei Darussalam', text: 'Brunei Darussalam' },
36 | { value: 'Bulgaria', text: 'Bulgaria' },
37 | { value: 'Burkina Faso', text: 'Burkina Faso' },
38 | { value: 'Burundi', text: 'Burundi' },
39 | { value: 'Cambodia', text: 'Cambodia' },
40 | { value: 'Cameroon', text: 'Cameroon' },
41 | { value: 'Canada', text: 'Canada' },
42 | { value: 'Cape Verde', text: 'Cape Verde' },
43 | { value: 'Cayman Islands', text: 'Cayman Islands' },
44 | { value: 'Central African Republic', text: 'Central African Republic' },
45 | { value: 'Chad', text: 'Chad' },
46 | { value: 'Chile', text: 'Chile' },
47 | { value: 'China', text: 'China' },
48 | { value: 'Christmas Island', text: 'Christmas Island' },
49 | { value: 'Cocos (Keeling) Islands', text: 'Cocos (Keeling) Islands' },
50 | { value: 'Colombia', text: 'Colombia' },
51 | { value: 'Comoros', text: 'Comoros' },
52 | { value: 'Congo', text: 'Congo' },
53 | { value: 'Congo, The Democratic Republic of The', text: 'Congo, The Democratic Republic of The' },
54 | { value: 'Cook Islands', text: 'Cook Islands' },
55 | { value: 'Costa Rica', text: 'Costa Rica' },
56 | { value: 'Cote D\'ivoire', text: 'Cote D\'ivoire' },
57 | { value: 'Croatia', text: 'Croatia' },
58 | { value: 'Cuba', text: 'Cuba' },
59 | { value: 'Cyprus', text: 'Cyprus' },
60 | { value: 'Czech Republic', text: 'Czech Republic' },
61 | { value: 'Denmark', text: 'Denmark' },
62 | { value: 'Djibouti', text: 'Djibouti' },
63 | { value: 'Dominica', text: 'Dominica' },
64 | { value: 'Dominican Republic', text: 'Dominican Republic' },
65 | { value: 'Ecuador', text: 'Ecuador' },
66 | { value: 'Egypt', text: 'Egypt' },
67 | { value: 'El Salvador', text: 'El Salvador' },
68 | { value: 'Equatorial Guinea', text: 'Equatorial Guinea' },
69 | { value: 'Eritrea', text: 'Eritrea' },
70 | { value: 'Estonia', text: 'Estonia' },
71 | { value: 'Ethiopia', text: 'Ethiopia' },
72 | { value: 'Falkland Islands (Malvinas)', text: 'Falkland Islands (Malvinas)' },
73 | { value: 'Faroe Islands', text: 'Faroe Islands' },
74 | { value: 'Fiji', text: 'Fiji' },
75 | { value: 'Finland', text: 'Finland' },
76 | { value: 'France', text: 'France' },
77 | { value: 'French Guiana', text: 'French Guiana' },
78 | { value: 'French Polynesia', text: 'French Polynesia' },
79 | { value: 'French Southern Territories', text: 'French Southern Territories' },
80 | { value: 'Gabon', text: 'Gabon' },
81 | { value: 'Gambia', text: 'Gambia' },
82 | { value: 'Georgia', text: 'Georgia' },
83 | { value: 'Germany', text: 'Germany' },
84 | { value: 'Ghana', text: 'Ghana' },
85 | { value: 'Gibraltar', text: 'Gibraltar' },
86 | { value: 'Greece', text: 'Greece' },
87 | { value: 'Greenland', text: 'Greenland' },
88 | { value: 'Grenada', text: 'Grenada' },
89 | { value: 'Guadeloupe', text: 'Guadeloupe' },
90 | { value: 'Guam', text: 'Guam' },
91 | { value: 'Guatemala', text: 'Guatemala' },
92 | { value: 'Guernsey', text: 'Guernsey' },
93 | { value: 'Guinea', text: 'Guinea' },
94 | { value: 'Guinea-bissau', text: 'Guinea-bissau' },
95 | { value: 'Guyana', text: 'Guyana' },
96 | { value: 'Haiti', text: 'Haiti' },
97 | { value: 'Heard Island and Mcdonald Islands', text: 'Heard Island and Mcdonald Islands' },
98 | { value: 'Holy See (Vatican City State)', text: 'Holy See (Vatican City State)' },
99 | { value: 'Honduras', text: 'Honduras' },
100 | { value: 'Hong Kong', text: 'Hong Kong' },
101 | { value: 'Hungary', text: 'Hungary' },
102 | { value: 'Iceland', text: 'Iceland' },
103 | { value: 'India', text: 'India' },
104 | { value: 'Indonesia', text: 'Indonesia' },
105 | { value: 'Iran, Islamic Republic of', text: 'Iran, Islamic Republic of' },
106 | { value: 'Iraq', text: 'Iraq' },
107 | { value: 'Ireland', text: 'Ireland' },
108 | { value: 'Isle of Man', text: 'Isle of Man' },
109 | { value: 'Israel', text: 'Israel' },
110 | { value: 'Italy', text: 'Italy' },
111 | { value: 'Jamaica', text: 'Jamaica' },
112 | { value: 'Japan', text: 'Japan' },
113 | { value: 'Jersey', text: 'Jersey' },
114 | { value: 'Jordan', text: 'Jordan' },
115 | { value: 'Kazakhstan', text: 'Kazakhstan' },
116 | { value: 'Kenya', text: 'Kenya' },
117 | { value: 'Kiribati', text: 'Kiribati' },
118 | { value: 'Korea, Democratic People\'s Republic of', text: 'Korea, Democratic People\'s Republic of' },
119 | { value: 'Korea, Republic of', text: 'Korea, Republic of' },
120 | { value: 'Kuwait', text: 'Kuwait' },
121 | { value: 'Kyrgyzstan', text: 'Kyrgyzstan' },
122 | { value: 'Lao People\'s Democratic Republic', text: 'Lao People\'s Democratic Republic' },
123 | { value: 'Latvia', text: 'Latvia' },
124 | { value: 'Lebanon', text: 'Lebanon' },
125 | { value: 'Lesotho', text: 'Lesotho' },
126 | { value: 'Liberia', text: 'Liberia' },
127 | { value: 'Libyan Arab Jamahiriya', text: 'Libyan Arab Jamahiriya' },
128 | { value: 'Liechtenstein', text: 'Liechtenstein' },
129 | { value: 'Lithuania', text: 'Lithuania' },
130 | { value: 'Luxembourg', text: 'Luxembourg' },
131 | { value: 'Macao', text: 'Macao' },
132 | { value: 'Macedonia, The Former Yugoslav Republic of', text: 'Macedonia, The Former Yugoslav Republic of' },
133 | { value: 'Madagascar', text: 'Madagascar' },
134 | { value: 'Malawi', text: 'Malawi' },
135 | { value: 'Malaysia', text: 'Malaysia' },
136 | { value: 'Maldives', text: 'Maldives' },
137 | { value: 'Mali', text: 'Mali' },
138 | { value: 'Malta', text: 'Malta' },
139 | { value: 'Marshall Islands', text: 'Marshall Islands' },
140 | { value: 'Martinique', text: 'Martinique' },
141 | { value: 'Mauritania', text: 'Mauritania' },
142 | { value: 'Mauritius', text: 'Mauritius' },
143 | { value: 'Mayotte', text: 'Mayotte' },
144 | { value: 'Mexico', text: 'Mexico' },
145 | { value: 'Micronesia, Federated States of', text: 'Micronesia, Federated States of' },
146 | { value: 'Moldova, Republic of', text: 'Moldova, Republic of' },
147 | { value: 'Monaco', text: 'Monaco' },
148 | { value: 'Mongolia', text: 'Mongolia' },
149 | { value: 'Montenegro', text: 'Montenegro' },
150 | { value: 'Montserrat', text: 'Montserrat' },
151 | { value: 'Morocco', text: 'Morocco' },
152 | { value: 'Mozambique', text: 'Mozambique' },
153 | { value: 'Myanmar', text: 'Myanmar' },
154 | { value: 'Namibia', text: 'Namibia' },
155 | { value: 'Nauru', text: 'Nauru' },
156 | { value: 'Nepal', text: 'Nepal' },
157 | { value: 'Netherlands', text: 'Netherlands' },
158 | { value: 'Netherlands Antilles', text: 'Netherlands Antilles' },
159 | { value: 'New Caledonia', text: 'New Caledonia' },
160 | { value: 'New Zealand', text: 'New Zealand' },
161 | { value: 'Nicaragua', text: 'Nicaragua' },
162 | { value: 'Niger', text: 'Niger' },
163 | { value: 'Nigeria', text: 'Nigeria' },
164 | { value: 'Niue', text: 'Niue' },
165 | { value: 'Norfolk Island', text: 'Norfolk Island' },
166 | { value: 'Northern Mariana Islands', text: 'Northern Mariana Islands' },
167 | { value: 'Norway', text: 'Norway' },
168 | { value: 'Oman', text: 'Oman' },
169 | { value: 'Pakistan', text: 'Pakistan' },
170 | { value: 'Palau', text: 'Palau' },
171 | { value: 'Palestinian Territory, Occupied', text: 'Palestinian Territory, Occupied' },
172 | { value: 'Panama', text: 'Panama' },
173 | { value: 'Papua New Guinea', text: 'Papua New Guinea' },
174 | { value: 'Paraguay', text: 'Paraguay' },
175 | { value: 'Peru', text: 'Peru' },
176 | { value: 'Philippines', text: 'Philippines' },
177 | { value: 'Pitcairn', text: 'Pitcairn' },
178 | { value: 'Poland', text: 'Poland' },
179 | { value: 'Portugal', text: 'Portugal' },
180 | { value: 'Puerto Rico', text: 'Puerto Rico' },
181 | { value: 'Qatar', text: 'Qatar' },
182 | { value: 'Reunion', text: 'Reunion' },
183 | { value: 'Romania', text: 'Romania' },
184 | { value: 'Russian Federation', text: 'Russian Federation' },
185 | { value: 'Rwanda', text: 'Rwanda' },
186 | { value: 'Saint Helena', text: 'Saint Helena' },
187 | { value: 'Saint Kitts and Nevis', text: 'Saint Kitts and Nevis' },
188 | { value: 'Saint Lucia', text: 'Saint Lucia' },
189 | { value: 'Saint Pierre and Miquelon', text: 'Saint Pierre and Miquelon' },
190 | { value: 'Saint Vincent and The Grenadines', text: 'Saint Vincent and The Grenadines' },
191 | { value: 'Samoa', text: 'Samoa' },
192 | { value: 'San Marino', text: 'San Marino' },
193 | { value: 'Sao Tome and Principe', text: 'Sao Tome and Principe' },
194 | { value: 'Saudi Arabia', text: 'Saudi Arabia' },
195 | { value: 'Senegal', text: 'Senegal' },
196 | { value: 'Serbia', text: 'Serbia' },
197 | { value: 'Seychelles', text: 'Seychelles' },
198 | { value: 'Sierra Leone', text: 'Sierra Leone' },
199 | { value: 'Singapore', text: 'Singapore' },
200 | { value: 'Slovakia', text: 'Slovakia' },
201 | { value: 'Slovenia', text: 'Slovenia' },
202 | { value: 'Solomon Islands', text: 'Solomon Islands' },
203 | { value: 'Somalia', text: 'Somalia' },
204 | { value: 'South Africa', text: 'South Africa' },
205 | { value: 'South Georgia and The South Sandwich Islands', text: 'South Georgia and The South Sandwich Islands' },
206 | { value: 'Spain', text: 'Spain' },
207 | { value: 'Sri Lanka', text: 'Sri Lanka' },
208 | { value: 'Sudan', text: 'Sudan' },
209 | { value: 'Suriname', text: 'Suriname' },
210 | { value: 'Svalbard and Jan Mayen', text: 'Svalbard and Jan Mayen' },
211 | { value: 'Swaziland', text: 'Swaziland' },
212 | { value: 'Sweden', text: 'Sweden' },
213 | { value: 'Switzerland', text: 'Switzerland' },
214 | { value: 'Syrian Arab Republic', text: 'Syrian Arab Republic' },
215 | { value: 'Taiwan, Province of China', text: 'Taiwan, Province of China' },
216 | { value: 'Tajikistan', text: 'Tajikistan' },
217 | { value: 'Tanzania, United Republic of', text: 'Tanzania, United Republic of' },
218 | { value: 'Thailand', text: 'Thailand' },
219 | { value: 'Timor-leste', text: 'Timor-leste' },
220 | { value: 'Togo', text: 'Togo' },
221 | { value: 'Tokelau', text: 'Tokelau' },
222 | { value: 'Tonga', text: 'Tonga' },
223 | { value: 'Trinidad and Tobago', text: 'Trinidad and Tobago' },
224 | { value: 'Tunisia', text: 'Tunisia' },
225 | { value: 'Turkey', text: 'Turkey' },
226 | { value: 'Turkmenistan', text: 'Turkmenistan' },
227 | { value: 'Turks and Caicos Islands', text: 'Turks and Caicos Islands' },
228 | { value: 'Tuvalu', text: 'Tuvalu' },
229 | { value: 'Uganda', text: 'Uganda' },
230 | { value: 'Ukraine', text: 'Ukraine' },
231 | { value: 'United Arab Emirates', text: 'United Arab Emirates' },
232 | { value: 'United Kingdom', text: 'United Kingdom' },
233 | { value: 'United States', text: 'United States (USA)' },
234 | { value: 'United States Minor Outlying Islands', text: 'United States Minor Outlying Islands' },
235 | { value: 'Uruguay', text: 'Uruguay' },
236 | { value: 'Uzbekistan', text: 'Uzbekistan' },
237 | { value: 'Vanuatu', text: 'Vanuatu' },
238 | { value: 'Venezuela', text: 'Venezuela' },
239 | { value: 'Viet Nam', text: 'Viet Nam' },
240 | { value: 'Virgin Islands, British', text: 'Virgin Islands, British' },
241 | { value: 'Virgin Islands, U.S.', text: 'Virgin Islands, U.S.' },
242 | { value: 'Wallis and Futuna', text: 'Wallis and Futuna' },
243 | { value: 'Western Sahara', text: 'Western Sahara' },
244 | { value: 'Yemen', text: 'Yemen' },
245 | { value: 'Zambia', text: 'Zambia' },
246 | { value: 'Zimbabwe', text: 'Zimbabwe' }
247 | ]
248 |
--------------------------------------------------------------------------------
/src/assets/style/_variables.scss:
--------------------------------------------------------------------------------
1 | // Variables
2 | //
3 | // Copy settings from this file into the provided `_custom.scss` to override
4 | // the Bootstrap defaults without modifying key, versioned files.
5 |
6 |
7 | // Table of Contents
8 | //
9 | // Colors
10 | // Options
11 | // Spacing
12 | // Body
13 | // Links
14 | // Grid breakpoints
15 | // Grid containers
16 | // Grid columns
17 | // Fonts
18 | // Components
19 | // Tables
20 | // Buttons
21 | // Forms
22 | // Dropdowns
23 | // Z-index master list
24 | // Navbar
25 | // Navs
26 | // Pagination
27 | // Jumbotron
28 | // Form states and alerts
29 | // Cards
30 | // Tooltips
31 | // Popovers
32 | // Badges
33 | // Modals
34 | // Alerts
35 | // Progress bars
36 | // List group
37 | // Image thumbnails
38 | // Figures
39 | // Breadcrumbs
40 | // Carousel
41 | // Close
42 | // Code
43 |
44 | @mixin _assert-ascending($map, $map-name) {
45 | $prev-key: null;
46 | $prev-num: null;
47 | @each $key, $num in $map {
48 | @if $prev-num == null {
49 | // Do nothing
50 | } @else if not comparable($prev-num, $num) {
51 | @warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !";
52 | } @else if $prev-num >= $num {
53 | @warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !";
54 | }
55 | $prev-key: $key;
56 | $prev-num: $num;
57 | }
58 | }
59 |
60 | // Replace `$search` with `$replace` in `$string`
61 | // @author Hugo Giraudel
62 | // @param {String} $string - Initial string
63 | // @param {String} $search - Substring to replace
64 | // @param {String} $replace ('') - New value
65 | // @return {String} - Updated string
66 | @function str-replace($string, $search, $replace: "") {
67 | $index: str-index($string, $search);
68 |
69 | @if $index {
70 | @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
71 | }
72 |
73 | @return $string;
74 | }
75 |
76 | @mixin _assert-starts-at-zero($map) {
77 | $values: map-values($map);
78 | $first-value: nth($values, 1);
79 | @if $first-value != 0 {
80 | @warn "First breakpoint in `$grid-breakpoints` must start at 0, but starts at #{$first-value}.";
81 | }
82 | }
83 |
84 |
85 | // General variable structure
86 | //
87 | // Variable format should follow the `$component-modifier-state-property` order.
88 |
89 |
90 | // Colors
91 | //
92 | // Grayscale and brand colors for use across Bootstrap.
93 |
94 | // Start with assigning color names to specific hex values.
95 | $white: #fff !default;
96 | $black: #000 !default;
97 | $red: #d9534f !default;
98 | $orange: #f0ad4e !default;
99 | $yellow: #ffd500 !default;
100 | $green: #5cb85c !default;
101 | $blue: #0275d8 !default;
102 | $teal: #5bc0de !default;
103 | $pink: #ff5b77 !default;
104 | $purple: #613d7c !default;
105 |
106 | // Create grayscale
107 | $gray-dark: #292b2c !default;
108 | $gray: #464a4c !default;
109 | $gray-light: #636c72 !default;
110 | $gray-lighter: #eceeef !default;
111 | $gray-lightest: #f7f7f9 !default;
112 |
113 | // Reassign color vars to semantic color scheme
114 | $brand-primary: $blue !default;
115 | $brand-success: $green !default;
116 | $brand-info: $teal !default;
117 | $brand-warning: $orange !default;
118 | $brand-danger: $red !default;
119 | $brand-inverse: $gray-dark !default;
120 |
121 |
122 | // Options
123 | //
124 | // Quickly modify global styling by enabling or disabling optional features.
125 |
126 | $enable-rounded: true !default;
127 | $enable-shadows: false !default;
128 | $enable-gradients: false !default;
129 | $enable-transitions: true !default;
130 | $enable-hover-media-query: false !default;
131 | $enable-grid-classes: true !default;
132 | $enable-print-styles: true !default;
133 |
134 |
135 | // Spacing
136 | //
137 | // Control the default styling of most Bootstrap elements by modifying these
138 | // variables. Mostly focused on spacing.
139 | // You can add more entries to the $spacers map, should you need more variation.
140 |
141 | $spacer: 1rem !default;
142 | $spacer-x: $spacer !default;
143 | $spacer-y: $spacer !default;
144 | /*$spacers: (
145 | 0: (
146 | x: 0,
147 | y: 0
148 | ),
149 | 1: (
150 | x: ($spacer-x * .25),
151 | y: ($spacer-y * .25)
152 | ),
153 | 2: (
154 | x: ($spacer-x * .5),
155 | y: ($spacer-y * .5)
156 | ),
157 | 3: (
158 | x: $spacer-x,
159 | y: $spacer-y
160 | ),
161 | 4: (
162 | x: ($spacer-x * 1.5),
163 | y: ($spacer-y * 1.5)
164 | ),
165 | 5: (
166 | x: ($spacer-x * 3),
167 | y: ($spacer-y * 3)
168 | )
169 | ) !default;*/
170 | $border-width: 1px !default;
171 |
172 | // This variable affects the `.h-*` and `.w-*` classes.
173 | $sizes: (
174 | 25: 25%,
175 | 50: 50%,
176 | 75: 75%,
177 | 100: 100%
178 | ) !default;
179 |
180 | // Body
181 | //
182 | // Settings for the `` element.
183 |
184 | $body-bg: $white !default;
185 | $body-color: $gray-dark !default;
186 | $inverse-bg: $gray-dark !default;
187 | $inverse-color: $gray-lighter !default;
188 |
189 |
190 | // Links
191 | //
192 | // Style anchor elements.
193 |
194 | $link-color: $brand-primary !default;
195 | $link-decoration: none !default;
196 | $link-hover-color: darken($link-color, 15%) !default;
197 | $link-hover-decoration: underline !default;
198 |
199 |
200 | // Grid breakpoints
201 | //
202 | // Define the minimum dimensions at which your layout will change,
203 | // adapting to different screen sizes, for use in media queries.
204 |
205 | $grid-breakpoints: (
206 | xs: 0,
207 | sm: 576px,
208 | md: 768px,
209 | lg: 992px,
210 | xl: 1200px
211 | ) !default;
212 | @include _assert-ascending($grid-breakpoints, "$grid-breakpoints");
213 | @include _assert-starts-at-zero($grid-breakpoints);
214 |
215 |
216 | // Grid containers
217 | //
218 | // Define the maximum width of `.container` for different screen sizes.
219 |
220 | $container-max-widths: (
221 | sm: 540px,
222 | md: 720px,
223 | lg: 960px,
224 | xl: 1140px
225 | ) !default;
226 | @include _assert-ascending($container-max-widths, "$container-max-widths");
227 |
228 |
229 | // Grid columns
230 | //
231 | // Set the number of columns and specify the width of the gutters.
232 |
233 | $grid-columns: 12 !default;
234 | $grid-gutter-width-base: 30px !default;
235 | $grid-gutter-widths: (
236 | xs: $grid-gutter-width-base,
237 | sm: $grid-gutter-width-base,
238 | md: $grid-gutter-width-base,
239 | lg: $grid-gutter-width-base,
240 | xl: $grid-gutter-width-base
241 | ) !default;
242 |
243 | // Fonts
244 | //
245 | // Font, line-height, and color for body text, headings, and more.
246 |
247 | $font-family-sans-serif: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default;
248 | $font-family-serif: Georgia, "Times New Roman", Times, serif !default;
249 | $font-family-monospace: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default;
250 | $font-family-base: $font-family-sans-serif !default;
251 |
252 | $font-size-base: 1rem !default; // Assumes the browser default, typically `16px`
253 | $font-size-lg: 1.25rem !default;
254 | $font-size-sm: .875rem !default;
255 | $font-size-xs: .75rem !default;
256 |
257 | $font-weight-normal: normal !default;
258 | $font-weight-bold: bold !default;
259 |
260 | $font-weight-base: $font-weight-normal !default;
261 | $line-height-base: 1.5 !default;
262 |
263 | $font-size-h1: 2.5rem !default;
264 | $font-size-h2: 2rem !default;
265 | $font-size-h3: 1.75rem !default;
266 | $font-size-h4: 1.5rem !default;
267 | $font-size-h5: 1.25rem !default;
268 | $font-size-h6: 1rem !default;
269 |
270 | $headings-margin-bottom: ($spacer / 2) !default;
271 | $headings-font-family: inherit !default;
272 | $headings-font-weight: 500 !default;
273 | $headings-line-height: 1.1 !default;
274 | $headings-color: inherit !default;
275 |
276 | $display1-size: 6rem !default;
277 | $display2-size: 5.5rem !default;
278 | $display3-size: 4.5rem !default;
279 | $display4-size: 3.5rem !default;
280 |
281 | $display1-weight: 300 !default;
282 | $display2-weight: 300 !default;
283 | $display3-weight: 300 !default;
284 | $display4-weight: 300 !default;
285 | $display-line-height: $headings-line-height !default;
286 |
287 | $lead-font-size: 1.25rem !default;
288 | $lead-font-weight: 300 !default;
289 |
290 | $small-font-size: 80% !default;
291 |
292 | $text-muted: $gray-light !default;
293 |
294 | $abbr-border-color: $gray-light !default;
295 |
296 | $blockquote-small-color: $gray-light !default;
297 | $blockquote-font-size: ($font-size-base * 1.25) !default;
298 | $blockquote-border-color: $gray-lighter !default;
299 | $blockquote-border-width: .25rem !default;
300 |
301 | $hr-border-color: rgba($black,.1) !default;
302 | $hr-border-width: $border-width !default;
303 |
304 | $mark-padding: .2em !default;
305 |
306 | $dt-font-weight: $font-weight-bold !default;
307 |
308 | $kbd-box-shadow: inset 0 -.1rem 0 rgba($black,.25) !default;
309 | $nested-kbd-font-weight: $font-weight-bold !default;
310 |
311 | $list-inline-padding: 5px !default;
312 |
313 |
314 | // Components
315 | //
316 | // Define common padding and border radius sizes and more.
317 |
318 | $line-height-lg: (4 / 3) !default;
319 | $line-height-sm: 1.5 !default;
320 |
321 | $border-radius: .25rem !default;
322 | $border-radius-lg: .3rem !default;
323 | $border-radius-sm: .2rem !default;
324 |
325 | $component-active-color: $white !default;
326 | $component-active-bg: $brand-primary !default;
327 |
328 | $caret-width: .3em !default;
329 |
330 | $transition-base: all .2s ease-in-out !default;
331 | $transition-fade: opacity .15s linear !default;
332 | $transition-collapse: height .35s ease !default;
333 |
334 |
335 | // Tables
336 | //
337 | // Customizes the `.table` component with basic values, each used across all table variations.
338 |
339 | $table-cell-padding: .75rem !default;
340 | $table-sm-cell-padding: .3rem !default;
341 |
342 | $table-bg: transparent !default;
343 |
344 | $table-inverse-bg: $gray-dark !default;
345 | $table-inverse-color: $body-bg !default;
346 |
347 | $table-bg-accent: rgba($black,.05) !default;
348 | $table-bg-hover: rgba($black,.075) !default;
349 | $table-bg-active: $table-bg-hover !default;
350 |
351 | $table-head-bg: $gray-lighter !default;
352 | $table-head-color: $gray !default;
353 |
354 | $table-border-width: $border-width !default;
355 | $table-border-color: $gray-lighter !default;
356 |
357 |
358 | // Buttons
359 | //
360 | // For each of Bootstrap's buttons, define text, background and border color.
361 |
362 | $btn-padding-x: 1rem !default;
363 | $btn-padding-y: .5rem !default;
364 | $btn-line-height: 1.25 !default;
365 | $btn-font-weight: $font-weight-normal !default;
366 | $btn-box-shadow: inset 0 1px 0 rgba($white,.15), 0 1px 1px rgba($black,.075) !default;
367 | $btn-focus-box-shadow: 0 0 0 2px rgba($brand-primary, .25) !default;
368 | $btn-active-box-shadow: inset 0 3px 5px rgba($black,.125) !default;
369 |
370 | $btn-primary-color: $white !default;
371 | $btn-primary-bg: $brand-primary !default;
372 | $btn-primary-border: $btn-primary-bg !default;
373 |
374 | $btn-secondary-color: $gray-dark !default;
375 | $btn-secondary-bg: $white !default;
376 | $btn-secondary-border: #ccc !default;
377 |
378 | $btn-info-color: $white !default;
379 | $btn-info-bg: $brand-info !default;
380 | $btn-info-border: $btn-info-bg !default;
381 |
382 | $btn-success-color: $white !default;
383 | $btn-success-bg: $brand-success !default;
384 | $btn-success-border: $btn-success-bg !default;
385 |
386 | $btn-warning-color: $white !default;
387 | $btn-warning-bg: $brand-warning !default;
388 | $btn-warning-border: $btn-warning-bg !default;
389 |
390 | $btn-danger-color: $white !default;
391 | $btn-danger-bg: $brand-danger !default;
392 | $btn-danger-border: $btn-danger-bg !default;
393 |
394 | $btn-link-disabled-color: $gray-light !default;
395 |
396 | $btn-padding-x-sm: .5rem !default;
397 | $btn-padding-y-sm: .25rem !default;
398 |
399 | $btn-padding-x-lg: 1.5rem !default;
400 | $btn-padding-y-lg: .75rem !default;
401 |
402 | $btn-block-spacing-y: .5rem !default;
403 | $btn-toolbar-margin: .5rem !default;
404 |
405 | // Allows for customizing button radius independently from global border radius
406 | $btn-border-radius: $border-radius !default;
407 | $btn-border-radius-lg: $border-radius-lg !default;
408 | $btn-border-radius-sm: $border-radius-sm !default;
409 |
410 | $btn-transition: all .2s ease-in-out !default;
411 |
412 |
413 | // Forms
414 |
415 | $input-padding-x: .75rem !default;
416 | $input-padding-y: .5rem !default;
417 | $input-line-height: 1.25 !default;
418 |
419 | $input-bg: $white !default;
420 | $input-bg-disabled: $gray-lighter !default;
421 |
422 | $input-color: $gray !default;
423 | $input-border-color: rgba($black,.15) !default;
424 | $input-btn-border-width: $border-width !default; // For form controls and buttons
425 | $input-box-shadow: inset 0 1px 1px rgba($black,.075) !default;
426 |
427 | $input-border-radius: $border-radius !default;
428 | $input-border-radius-lg: $border-radius-lg !default;
429 | $input-border-radius-sm: $border-radius-sm !default;
430 |
431 | $input-bg-focus: $input-bg !default;
432 | $input-border-focus: lighten($brand-primary, 25%) !default;
433 | $input-box-shadow-focus: $input-box-shadow, rgba($input-border-focus, .6) !default;
434 | $input-color-focus: $input-color !default;
435 |
436 | $input-color-placeholder: $gray-light !default;
437 |
438 | $input-padding-x-sm: .5rem !default;
439 | $input-padding-y-sm: .25rem !default;
440 |
441 | $input-padding-x-lg: 1.5rem !default;
442 | $input-padding-y-lg: .75rem !default;
443 |
444 | $input-height: (($font-size-base * $input-line-height) + ($input-padding-y * 2)) !default;
445 | $input-height-lg: (($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)) !default;
446 | $input-height-sm: (($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)) !default;
447 |
448 | $input-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s !default;
449 |
450 | $form-text-margin-top: .25rem !default;
451 | $form-feedback-margin-top: $form-text-margin-top !default;
452 |
453 | $form-check-margin-bottom: .5rem !default;
454 | $form-check-input-gutter: 1.25rem !default;
455 | $form-check-input-margin-y: .25rem !default;
456 | $form-check-input-margin-x: .25rem !default;
457 |
458 | $form-check-inline-margin-x: .75rem !default;
459 |
460 | $form-group-margin-bottom: $spacer-y !default;
461 |
462 | $input-group-addon-bg: $gray-lighter !default;
463 | $input-group-addon-border-color: $input-border-color !default;
464 |
465 | $cursor-disabled: not-allowed !default;
466 |
467 | $custom-control-gutter: 1.5rem !default;
468 | $custom-control-spacer-x: 1rem !default;
469 | $custom-control-spacer-y: .25rem !default;
470 |
471 | $custom-control-indicator-size: 1rem !default;
472 | $custom-control-indicator-margin-y: (($line-height-base * 1rem) - $custom-control-indicator-size) / -2 !default;
473 | $custom-control-indicator-bg: #ddd !default;
474 | $custom-control-indicator-bg-size: 50% 50% !default;
475 | $custom-control-indicator-box-shadow: inset 0 .25rem .25rem rgba($black,.1) !default;
476 |
477 | $custom-control-disabled-cursor: $cursor-disabled !default;
478 | $custom-control-disabled-indicator-bg: $gray-lighter !default;
479 | $custom-control-disabled-description-color: $gray-light !default;
480 |
481 | $custom-control-checked-indicator-color: $white !default;
482 | $custom-control-checked-indicator-bg: $brand-primary !default;
483 | $custom-control-checked-indicator-box-shadow: none !default;
484 |
485 | $custom-control-focus-indicator-box-shadow: 0 0 0 1px $body-bg, 0 0 0 3px $brand-primary !default;
486 |
487 | $custom-control-active-indicator-color: $white !default;
488 | $custom-control-active-indicator-bg: lighten($brand-primary, 35%) !default;
489 | $custom-control-active-indicator-box-shadow: none !default;
490 |
491 | $custom-checkbox-radius: $border-radius !default;
492 | $custom-checkbox-checked-icon: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='#{$custom-control-checked-indicator-color}' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"), "#", "%23") !default;
493 |
494 | $custom-checkbox-indeterminate-bg: $brand-primary !default;
495 | $custom-checkbox-indeterminate-indicator-color: $custom-control-checked-indicator-color !default;
496 | $custom-checkbox-indeterminate-icon: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='#{$custom-checkbox-indeterminate-indicator-color}' d='M0 2h4'/%3E%3C/svg%3E"), "#", "%23") !default;
497 | $custom-checkbox-indeterminate-box-shadow: none !default;
498 |
499 | $custom-radio-radius: 50% !default;
500 | $custom-radio-checked-icon: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$custom-control-checked-indicator-color}'/%3E%3C/svg%3E"), "#", "%23") !default;
501 |
502 | $custom-select-padding-x: .75rem !default;
503 | $custom-select-padding-y: .375rem !default;
504 | $custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator
505 | $custom-select-line-height: $input-line-height !default;
506 | $custom-select-color: $input-color !default;
507 | $custom-select-disabled-color: $gray-light !default;
508 | $custom-select-bg: $white !default;
509 | $custom-select-disabled-bg: $gray-lighter !default;
510 | $custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions
511 | $custom-select-indicator-color: #333 !default;
512 | $custom-select-indicator: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='#{$custom-select-indicator-color}' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E"), "#", "%23") !default;
513 | $custom-select-border-width: $input-btn-border-width !default;
514 | $custom-select-border-color: $input-border-color !default;
515 | $custom-select-border-radius: $border-radius !default;
516 |
517 | $custom-select-focus-border-color: lighten($brand-primary, 25%) !default;
518 | $custom-select-focus-box-shadow: inset 0 1px 2px rgba($black, .075), 0 0 5px rgba($custom-select-focus-border-color, .5) !default;
519 |
520 | $custom-select-sm-padding-y: .2rem !default;
521 | $custom-select-sm-font-size: 75% !default;
522 |
523 | $custom-file-height: 2.5rem !default;
524 | $custom-file-width: 14rem !default;
525 | $custom-file-focus-box-shadow: 0 0 0 .075rem $white, 0 0 0 .2rem $brand-primary !default;
526 |
527 | $custom-file-padding-x: .5rem !default;
528 | $custom-file-padding-y: 1rem !default;
529 | $custom-file-line-height: 1.5 !default;
530 | $custom-file-color: $gray !default;
531 | $custom-file-bg: $white !default;
532 | $custom-file-border-width: $border-width !default;
533 | $custom-file-border-color: $input-border-color !default;
534 | $custom-file-border-radius: $border-radius !default;
535 | $custom-file-box-shadow: inset 0 .2rem .4rem rgba($black,.05) !default;
536 | $custom-file-button-color: $custom-file-color !default;
537 | $custom-file-button-bg: $gray-lighter !default;
538 | /*$custom-file-text: (
539 | placeholder: (
540 | en: "Choose file..."
541 | ),
542 | button-label: (
543 | en: "Browse"
544 | )
545 | ) !default;*/
546 |
547 |
548 | // Form validation icons
549 | $form-icon-success-color: $brand-success !default;
550 | $form-icon-success: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='#{$form-icon-success-color}' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E"), "#", "%23") !default;
551 |
552 | $form-icon-warning-color: $brand-warning !default;
553 | $form-icon-warning: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='#{$form-icon-warning-color}' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E"), "#", "%23") !default;
554 |
555 | $form-icon-danger-color: $brand-danger !default;
556 | $form-icon-danger: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$form-icon-danger-color}' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E"), "#", "%23") !default;
557 |
558 |
559 | // Dropdowns
560 | //
561 | // Dropdown menu container and contents.
562 |
563 | $dropdown-min-width: 10rem !default;
564 | $dropdown-padding-y: .5rem !default;
565 | $dropdown-margin-top: .125rem !default;
566 | $dropdown-bg: $white !default;
567 | $dropdown-border-color: rgba($black,.15) !default;
568 | $dropdown-border-width: $border-width !default;
569 | $dropdown-divider-bg: $gray-lighter !default;
570 | $dropdown-box-shadow: 0 .5rem 1rem rgba($black,.175) !default;
571 |
572 | $dropdown-link-color: $gray-dark !default;
573 | $dropdown-link-hover-color: darken($gray-dark, 5%) !default;
574 | $dropdown-link-hover-bg: $gray-lightest !default;
575 |
576 | $dropdown-link-active-color: $component-active-color !default;
577 | $dropdown-link-active-bg: $component-active-bg !default;
578 |
579 | $dropdown-link-disabled-color: $gray-light !default;
580 |
581 | $dropdown-item-padding-x: 1.5rem !default;
582 |
583 | $dropdown-header-color: $gray-light !default;
584 |
585 |
586 | // Z-index master list
587 | //
588 | // Warning: Avoid customizing these values. They're used for a bird's eye view
589 | // of components dependent on the z-axis and are designed to all work together.
590 |
591 | $zindex-dropdown-backdrop: 990 !default;
592 | $zindex-navbar: 1000 !default;
593 | $zindex-dropdown: 1000 !default;
594 | $zindex-fixed: 1030 !default;
595 | $zindex-sticky: 1030 !default;
596 | $zindex-modal-backdrop: 1040 !default;
597 | $zindex-modal: 1050 !default;
598 | $zindex-popover: 1060 !default;
599 | $zindex-tooltip: 1070 !default;
600 |
601 |
602 | // Navbar
603 |
604 | $navbar-border-radius: $border-radius !default;
605 | $navbar-padding-x: $spacer !default;
606 | $navbar-padding-y: ($spacer / 2) !default;
607 |
608 | $navbar-brand-padding-y: .25rem !default;
609 |
610 | $navbar-toggler-padding-x: .75rem !default;
611 | $navbar-toggler-padding-y: .25rem !default;
612 | $navbar-toggler-font-size: $font-size-lg !default;
613 | $navbar-toggler-border-radius: $btn-border-radius !default;
614 |
615 | $navbar-inverse-color: rgba($white,.5) !default;
616 | $navbar-inverse-hover-color: rgba($white,.75) !default;
617 | $navbar-inverse-active-color: rgba($white,1) !default;
618 | $navbar-inverse-disabled-color: rgba($white,.25) !default;
619 | $navbar-inverse-toggler-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-inverse-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E"), "#", "%23") !default;
620 | $navbar-inverse-toggler-border: rgba($white,.1) !default;
621 |
622 | $navbar-light-color: rgba($black,.5) !default;
623 | $navbar-light-hover-color: rgba($black,.7) !default;
624 | $navbar-light-active-color: rgba($black,.9) !default;
625 | $navbar-light-disabled-color: rgba($black,.3) !default;
626 | $navbar-light-toggler-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-light-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E"), "#", "%23") !default;
627 | $navbar-light-toggler-border: rgba($black,.1) !default;
628 |
629 | // Navs
630 |
631 | $nav-item-margin: .2rem !default;
632 | $nav-item-inline-spacer: 1rem !default;
633 | $nav-link-padding: .5em 1em !default;
634 | $nav-link-hover-bg: $gray-lighter !default;
635 | $nav-disabled-link-color: $gray-light !default;
636 |
637 | $nav-tabs-border-color: #ddd !default;
638 | $nav-tabs-border-width: $border-width !default;
639 | $nav-tabs-border-radius: $border-radius !default;
640 | $nav-tabs-link-hover-border-color: $gray-lighter !default;
641 | $nav-tabs-active-link-hover-color: $gray !default;
642 | $nav-tabs-active-link-hover-bg: $body-bg !default;
643 | $nav-tabs-active-link-hover-border-color: #ddd !default;
644 | $nav-tabs-justified-link-border-color: #ddd !default;
645 | $nav-tabs-justified-active-link-border-color: $body-bg !default;
646 |
647 | $nav-pills-border-radius: $border-radius !default;
648 | $nav-pills-active-link-color: $component-active-color !default;
649 | $nav-pills-active-link-bg: $component-active-bg !default;
650 |
651 |
652 | // Pagination
653 |
654 | $pagination-padding-x: .75rem !default;
655 | $pagination-padding-y: .5rem !default;
656 | $pagination-padding-x-sm: .5rem !default;
657 | $pagination-padding-y-sm: .25rem !default;
658 | $pagination-padding-x-lg: 1.5rem !default;
659 | $pagination-padding-y-lg: .75rem !default;
660 | $pagination-line-height: 1.25 !default;
661 |
662 | $pagination-color: $link-color !default;
663 | $pagination-bg: $white !default;
664 | $pagination-border-width: $border-width !default;
665 | $pagination-border-color: #ddd !default;
666 |
667 | $pagination-hover-color: $link-hover-color !default;
668 | $pagination-hover-bg: $gray-lighter !default;
669 | $pagination-hover-border: #ddd !default;
670 |
671 | $pagination-active-color: $white !default;
672 | $pagination-active-bg: $brand-primary !default;
673 | $pagination-active-border: $brand-primary !default;
674 |
675 | $pagination-disabled-color: $gray-light !default;
676 | $pagination-disabled-bg: $white !default;
677 | $pagination-disabled-border: #ddd !default;
678 |
679 |
680 | // Jumbotron
681 |
682 | $jumbotron-padding: 2rem !default;
683 | $jumbotron-bg: $gray-lighter !default;
684 |
685 |
686 | // Form states and alerts
687 | //
688 | // Define colors for form feedback states and, by default, alerts.
689 |
690 | $state-success-text: #3c763d !default;
691 | $state-success-bg: #dff0d8 !default;
692 | $state-success-border: darken($state-success-bg, 5%) !default;
693 |
694 | $state-info-text: #31708f !default;
695 | $state-info-bg: #d9edf7 !default;
696 | $state-info-border: darken($state-info-bg, 7%) !default;
697 |
698 | $state-warning-text: #8a6d3b !default;
699 | $state-warning-bg: #fcf8e3 !default;
700 | $mark-bg: $state-warning-bg !default;
701 | $state-warning-border: darken($state-warning-bg, 5%) !default;
702 |
703 | $state-danger-text: #a94442 !default;
704 | $state-danger-bg: #f2dede !default;
705 | $state-danger-border: darken($state-danger-bg, 5%) !default;
706 |
707 |
708 | // Cards
709 |
710 | $card-spacer-x: 1.25rem !default;
711 | $card-spacer-y: .75rem !default;
712 | $card-border-width: 1px !default;
713 | $card-border-radius: $border-radius !default;
714 | $card-border-color: rgba($black,.125) !default;
715 | $card-border-radius-inner: calc(#{$card-border-radius} - #{$card-border-width}) !default;
716 | $card-cap-bg: $gray-lightest !default;
717 | $card-bg: $white !default;
718 |
719 | $card-link-hover-color: $white !default;
720 |
721 | $card-img-overlay-padding: 1.25rem !default;
722 |
723 | $card-deck-margin: ($grid-gutter-width-base / 2) !default;
724 |
725 | $card-columns-count: 3 !default;
726 | $card-columns-gap: 1.25rem !default;
727 | $card-columns-margin: $card-spacer-y !default;
728 |
729 |
730 | // Tooltips
731 |
732 | $tooltip-max-width: 200px !default;
733 | $tooltip-color: $white !default;
734 | $tooltip-bg: $black !default;
735 | $tooltip-opacity: .9 !default;
736 | $tooltip-padding-y: 3px !default;
737 | $tooltip-padding-x: 8px !default;
738 | $tooltip-margin: 3px !default;
739 |
740 | $tooltip-arrow-width: 5px !default;
741 | $tooltip-arrow-color: $tooltip-bg !default;
742 |
743 |
744 | // Popovers
745 |
746 | $popover-inner-padding: 1px !default;
747 | $popover-bg: $white !default;
748 | $popover-max-width: 276px !default;
749 | $popover-border-width: $border-width !default;
750 | $popover-border-color: rgba($black,.2) !default;
751 | $popover-box-shadow: 0 5px 10px rgba($black,.2) !default;
752 |
753 | $popover-title-bg: darken($popover-bg, 3%) !default;
754 | $popover-title-padding-x: 14px !default;
755 | $popover-title-padding-y: 8px !default;
756 |
757 | $popover-content-padding-x: 14px !default;
758 | $popover-content-padding-y: 9px !default;
759 |
760 | $popover-arrow-width: 10px !default;
761 | $popover-arrow-color: $popover-bg !default;
762 |
763 | $popover-arrow-outer-width: ($popover-arrow-width + 1px) !default;
764 | $popover-arrow-outer-color: fade-in($popover-border-color, .05) !default;
765 |
766 |
767 | // Badges
768 |
769 | $badge-default-bg: $gray-light !default;
770 | $badge-primary-bg: $brand-primary !default;
771 | $badge-success-bg: $brand-success !default;
772 | $badge-info-bg: $brand-info !default;
773 | $badge-warning-bg: $brand-warning !default;
774 | $badge-danger-bg: $brand-danger !default;
775 |
776 | $badge-color: $white !default;
777 | $badge-link-hover-color: $white !default;
778 | $badge-font-size: 75% !default;
779 | $badge-font-weight: $font-weight-bold !default;
780 | $badge-padding-x: .4em !default;
781 | $badge-padding-y: .25em !default;
782 |
783 | $badge-pill-padding-x: .6em !default;
784 | // Use a higher than normal value to ensure completely rounded edges when
785 | // customizing padding or font-size on labels.
786 | $badge-pill-border-radius: 10rem !default;
787 |
788 |
789 | // Modals
790 |
791 | // Padding applied to the modal body
792 | $modal-inner-padding: 15px !default;
793 |
794 | $modal-dialog-margin: 10px !default;
795 | $modal-dialog-sm-up-margin-y: 30px !default;
796 |
797 | $modal-title-line-height: $line-height-base !default;
798 |
799 | $modal-content-bg: $white !default;
800 | $modal-content-border-color: rgba($black,.2) !default;
801 | $modal-content-border-width: $border-width !default;
802 | $modal-content-xs-box-shadow: 0 3px 9px rgba($black,.5) !default;
803 | $modal-content-sm-up-box-shadow: 0 5px 15px rgba($black,.5) !default;
804 |
805 | $modal-backdrop-bg: $black !default;
806 | $modal-backdrop-opacity: .5 !default;
807 | $modal-header-border-color: $gray-lighter !default;
808 | $modal-footer-border-color: $modal-header-border-color !default;
809 | $modal-header-border-width: $modal-content-border-width !default;
810 | $modal-footer-border-width: $modal-header-border-width !default;
811 | $modal-header-padding: 15px !default;
812 |
813 | $modal-lg: 800px !default;
814 | $modal-md: 500px !default;
815 | $modal-sm: 300px !default;
816 |
817 | $modal-transition: transform .3s ease-out !default;
818 |
819 |
820 | // Alerts
821 | //
822 | // Define alert colors, border radius, and padding.
823 |
824 | $alert-padding-x: 1.25rem !default;
825 | $alert-padding-y: .75rem !default;
826 | $alert-margin-bottom: $spacer-y !default;
827 | $alert-border-radius: $border-radius !default;
828 | $alert-link-font-weight: $font-weight-bold !default;
829 | $alert-border-width: $border-width !default;
830 |
831 | $alert-success-bg: $state-success-bg !default;
832 | $alert-success-text: $state-success-text !default;
833 | $alert-success-border: $state-success-border !default;
834 |
835 | $alert-info-bg: $state-info-bg !default;
836 | $alert-info-text: $state-info-text !default;
837 | $alert-info-border: $state-info-border !default;
838 |
839 | $alert-warning-bg: $state-warning-bg !default;
840 | $alert-warning-text: $state-warning-text !default;
841 | $alert-warning-border: $state-warning-border !default;
842 |
843 | $alert-danger-bg: $state-danger-bg !default;
844 | $alert-danger-text: $state-danger-text !default;
845 | $alert-danger-border: $state-danger-border !default;
846 |
847 |
848 | // Progress bars
849 |
850 | $progress-height: 1rem !default;
851 | $progress-font-size: .75rem !default;
852 | $progress-bg: $gray-lighter !default;
853 | $progress-border-radius: $border-radius !default;
854 | $progress-box-shadow: inset 0 .1rem .1rem rgba($black,.1) !default;
855 | $progress-bar-color: $white !default;
856 | $progress-bar-bg: $brand-primary !default;
857 | $progress-bar-animation-timing: 1s linear infinite !default;
858 |
859 | // List group
860 |
861 | $list-group-color: $body-color !default;
862 | $list-group-bg: $white !default;
863 | $list-group-border-color: rgba($black,.125) !default;
864 | $list-group-border-width: $border-width !default;
865 | $list-group-border-radius: $border-radius !default;
866 |
867 | $list-group-item-padding-x: 1.25rem !default;
868 | $list-group-item-padding-y: .75rem !default;
869 |
870 | $list-group-hover-bg: $gray-lightest !default;
871 | $list-group-active-color: $component-active-color !default;
872 | $list-group-active-bg: $component-active-bg !default;
873 | $list-group-active-border: $list-group-active-bg !default;
874 | $list-group-active-text-color: lighten($list-group-active-bg, 50%) !default;
875 |
876 | $list-group-disabled-color: $gray-light !default;
877 | $list-group-disabled-bg: $list-group-bg !default;
878 | $list-group-disabled-text-color: $list-group-disabled-color !default;
879 |
880 | $list-group-link-color: $gray !default;
881 | $list-group-link-heading-color: $gray-dark !default;
882 | $list-group-link-hover-color: $list-group-link-color !default;
883 |
884 | $list-group-link-active-color: $list-group-color !default;
885 | $list-group-link-active-bg: $gray-lighter !default;
886 |
887 |
888 | // Image thumbnails
889 |
890 | $thumbnail-padding: .25rem !default;
891 | $thumbnail-bg: $body-bg !default;
892 | $thumbnail-border-width: $border-width !default;
893 | $thumbnail-border-color: #ddd !default;
894 | $thumbnail-border-radius: $border-radius !default;
895 | $thumbnail-box-shadow: 0 1px 2px rgba($black,.075) !default;
896 | $thumbnail-transition: all .2s ease-in-out !default;
897 |
898 |
899 | // Figures
900 |
901 | $figure-caption-font-size: 90% !default;
902 | $figure-caption-color: $gray-light !default;
903 |
904 |
905 | // Breadcrumbs
906 |
907 | $breadcrumb-padding-y: .75rem !default;
908 | $breadcrumb-padding-x: 1rem !default;
909 | $breadcrumb-item-padding: .5rem !default;
910 |
911 | $breadcrumb-bg: $gray-lighter !default;
912 | $breadcrumb-divider-color: $gray-light !default;
913 | $breadcrumb-active-color: $gray-light !default;
914 | $breadcrumb-divider: "/" !default;
915 |
916 |
917 | // Carousel
918 |
919 | $carousel-control-color: $white !default;
920 | $carousel-control-width: 15% !default;
921 | $carousel-control-opacity: .5 !default;
922 |
923 | $carousel-indicator-width: 30px !default;
924 | $carousel-indicator-height: 3px !default;
925 | $carousel-indicator-spacer: 3px !default;
926 | $carousel-indicator-active-bg: $white !default;
927 |
928 | $carousel-caption-width: 70% !default;
929 | $carousel-caption-color: $white !default;
930 |
931 | $carousel-control-icon-width: 20px !default;
932 |
933 | $carousel-control-prev-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E"), "#", "%23") !default;
934 | $carousel-control-next-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E"), "#", "%23") !default;
935 |
936 | $carousel-transition: transform .6s ease-in-out !default;
937 |
938 |
939 | // Close
940 |
941 | $close-font-size: $font-size-base * 1.5 !default;
942 | $close-font-weight: $font-weight-bold !default;
943 | $close-color: $black !default;
944 | $close-text-shadow: 0 1px 0 $white !default;
945 |
946 |
947 | // Code
948 |
949 | $code-font-size: 90% !default;
950 | $code-padding-x: .4rem !default;
951 | $code-padding-y: .2rem !default;
952 | $code-color: #bd4147 !default;
953 | $code-bg: $gray-lightest !default;
954 |
955 | $kbd-color: $white !default;
956 | $kbd-bg: $gray-dark !default;
957 |
958 | $pre-bg: $gray-lightest !default;
959 | $pre-color: $gray-dark !default;
960 | $pre-border-color: #ccc !default;
961 | $pre-scrollable-max-height: 340px !default;
962 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # VueJS 2 Example Project (and Tutorial)
3 |
4 | A scalable Single Page Application (SPA) example. This example uses Vue-cli, VueRouter, Vuex, VueResource and more. Clone the repo, do `npm install`, and use right away or read through this tutorial below to get an idea of how to build the project from scratch and setup Sublime Text.
5 |
6 | ## Table of Contents
7 | 1. [Todo](#todo)
8 | 2. [Install Node](#install-node)
9 | 3. [Install Vue-CLI](#install-vue-cli)
10 | 4. [Add Dependencies](#add-dependencies)
11 | 5. [Configure JQuery and Lodash and Tether](#configure-jquery-and-lodash-and-tether)
12 | 6. [Global Utilities](#global-utilities)
13 | 7. [Configure Sublime Text 3](#configure-sublime-text-3)
14 | 8. [Configure ESLint](#configure-eslint)
15 | 9. [Setup Main and Routes](#setup-main-and-routes)
16 | 10. [Setup Authentication (OAuth2), User Profile, and Vuex](#setup-authentication-user-profile-and-vuex)
17 | 11. [Proxy Api Calls in Webpack Dev Server](#proxy-api-calls-in-webpack-dev-server)
18 | 12. [Components](#components)
19 | 13. [Twitter Bootstrap 4 Configuration](#twitter-bootstrap-4-configuration)
20 | 14. [Fonts and Font-Awesome](#fonts-and-font-awesome)
21 | 15. [Images and Other Assets](#images-and-other-assets)
22 | 16. [App.scss](#app-scss)
23 | 17. [Unit Testing and End-to-End Testing](#unit-testing-and-end-to-end-testing)
24 | 18. [Run the Dev Server](#run-the-dev-server)
25 | 19. [Vue Dev Tools](#vue-dev-tools)
26 | 20. [Create & Publish a Component/Library for Other Developers to Use](#create-and-publish-a-library-for-other-developers-to-use)
27 | 21. [Quick Learning Webpack Resources](#quick-learning-webpack-resources)
28 |
29 | ## Todo
30 |
31 | - Currently, remote calls are made to an online example OAuth2 demo server [here](http://brentertainment.com/oauth2/) by Brent Shaffer. We can remove this and instead setup up a Node.js Express OAuth2.
32 | - Add a section in this tutorial about working in a production environment.
33 | - File splitting (Webpack's CommonChunksPlugin and etc.) and improving page load times.
34 |
35 | ## Install Node
36 |
37 | #### Install Node and NPM (Using PPA to get latest version)
38 |
39 | Get the setup script:
40 |
41 | ```shell
42 | $ cd ~
43 | $ curl -sL https://deb.nodesource.com/setup_6.x -o nodesource_setup.sh
44 | ```
45 |
46 | Inspect that you have the script, then run with `sudo`:
47 |
48 | ```shell
49 | $ vim nodesource_setup.sh
50 | $ sudo bash nodesource_setup.sh
51 | ```
52 |
53 | Now install Nodejs:
54 |
55 | ```shell
56 | $ sudo apt-get install nodejs
57 | ```
58 |
59 | The nodejs package contains the nodejs binary as well as npm, so you don't need to install npm separately. However, in order for some npm packages to work (such as those that require compiling code from source), you will need to install the build-essential package:
60 |
61 | ```shell
62 | $ sudo apt-get install build-essential
63 | ```
64 |
65 | ## Install Vue-CLI
66 |
67 | Change directory to the directory where you want this example project to reside:
68 |
69 | ```shell
70 | # an example folder will be created here on the next step...
71 | $ cd ~
72 | ```
73 |
74 | Install Vue-cli with webpack:
75 |
76 | ```shell
77 | $ sudo npm install -g vue-cli
78 | $ vue init webpack example-vue-project
79 | ```
80 |
81 | (Note: If you've already installed the cli before and when you init a new project you get the message: `A newer version of vue-cli is available`, then ctrl+c at the prompt and then: `sudo npm install vue-cli -g` to update (re-install) vue-cli to the latest version.)
82 |
83 | Now you'll get some output like this:
84 |
85 | ```
86 | ? Project name: example-vue-project
87 | ? Project description: A Vue.js project
88 | ? Author: Your Name
89 | ? Vue build: Runtime-only # saves you 6kb
90 | ? Install vue-router? Y
91 | ? Use ESLint to lint your code? Y
92 | ? Pick an ESLint preset: none # we'll use a vue specific preset based on Standard
93 | ? Setup unit tests with Karma + Mocha? Y
94 | ? Setup e2e tests with Nightwatch? Y
95 |
96 | vue-cli Generated "example-vue-project"
97 | ```
98 |
99 | Install dependencies in `package.json`:
100 |
101 | ```shell
102 | $ cd example-vue-project
103 | $ npm install # do this first before you add more dependencies (to avoid peer warns)
104 | ```
105 |
106 | ## Add Dependencies
107 |
108 | (Note: We are installing most dependencies into the devDependencies section of package.json using `--save-dev`. The production server will not need those dependencies. Most of these dependencies are used to build a set of files into your `dist` folder for your production server to use.)
109 |
110 | Install Vuex and Vue Resource (Vue Router was installed from vue-cli earlier)
111 |
112 | ```shell
113 | $ npm install vuex vue-resource --save
114 | ```
115 |
116 | Install jQuery, Tether (required by Boostrap), Bootstrap, Font-Awesome, Roboto and Lodash
117 |
118 | ```shell
119 | $ npm install jquery tether bootstrap@next font-awesome roboto-fontface lodash --save-dev
120 | ```
121 |
122 | Install Vue Multiselect (a vendor component used in an example)
123 |
124 | ```shell
125 | $ npm install vue-multiselect@next --save-dev
126 | ```
127 |
128 | Install Vue ESLint plugin
129 |
130 | ```shell
131 | $ npm install eslint-config-vue eslint-plugin-vue --save-dev
132 | ```
133 |
134 | Install babel-polyfill (for example, transpiling es6 promises, so that tests will work in testing browsers...see section on "Unit Testing and End-to-End Testing" further down).
135 |
136 | ```shell
137 | npm install babel-polyfill --save-dev
138 | ```
139 |
140 | Install sass builders:
141 |
142 | ```shell
143 | $ npm install sass-loader node-sass --save-dev
144 | ```
145 |
146 | Install stylus (optional):
147 |
148 | ```shell
149 | $ npm install stylus -g # install stylus globally for Sublime
150 | $ stylus -V # this confirms that stylus has been added to your path, if not, you need to do so for it to work correctly with Sublime
151 | $ npm install stylus stylus-loader --save-dev # also install locally
152 | ```
153 |
154 | See Sublime Text 3 section further down for installing the Stylus package for it.
155 |
156 | *(This concludes all extra dependencies, however feel free to check the `package.json` in the Github repo)*
157 |
158 | ## Configure JQuery and Lodash and Tether
159 |
160 | #### Option #1: Use ProvidePlugin
161 |
162 | Add the [ProvidePlugin](https://webpack.github.io/docs/list-of-plugins.html#provideplugin) to the plugins array in both `build/webpack.dev.conf.js` and `build/webpack.prod.conf.js` so that jQuery and Lodash become globally available to all your modules (and also Tether for Bootstrap):
163 |
164 | #### build/webpack.dev.conf.js, build/webpack.prod.conf.js
165 |
166 | ```js
167 | plugins: [
168 |
169 | // ...
170 |
171 | new webpack.ProvidePlugin({
172 | $: 'jquery',
173 | jquery: 'jquery',
174 | 'window.jQuery': 'jquery',
175 | jQuery: 'jquery',
176 | '_': 'lodash',
177 | 'Tether': 'tether',
178 | utils: 'utils'
179 | })
180 | ]
181 | ```
182 |
183 | *Note: The `utils` property is for a set of utility functions we want global to all modules. See the section [Global Utilities](#global-utilities) for more information on how this is set up.*
184 |
185 | #### Option #2: Use Expose Loader module for webpack
186 |
187 | Alternatively you can add the [Expose Loader](https://www.npmjs.com/package/expose-loader) package:
188 |
189 | ```shell
190 | npm install expose-loader --save-dev
191 | ```
192 |
193 | Use in your entry point `main.js` like this:
194 |
195 | ```shell
196 | import 'expose?$!expose?jQuery!jquery'
197 |
198 | // ...
199 | ```
200 | ## Global Utilities
201 |
202 | Using the `ProvidePlugin` in the previous section, we were able to include jQuery and Lodash in all modules that used it. But these were from node_modules. What if we want to do this with one of our own modules from our project (so we don't have to directly require it each time we need it). In the previous section you can see we added `utils` to the ProvidePlugin. Now let's actually create a module (in the Node form) in our `src/` directory for keeping these utilities we want globally:
203 |
204 | #### src/utils.js
205 |
206 | ```js
207 |
208 | module.exports = {
209 |
210 | /**
211 | * Get the error from a response.
212 | *
213 | * @param {Response} response The Vue-resource Response that we will try to get errors from.
214 | */
215 | getError: function (response) {
216 | return response.body['error_description']
217 | ? response.body.error_description
218 | : response.statusText
219 | }
220 | }
221 |
222 | ```
223 |
224 | In the section [Configure ESLint](#configure-eslint) you will notice we have added **utils** to the globals so that the linter will not complain when we use it.
225 |
226 | For the `utils` to work in the ProvidePlugin you could just require it directly, but it gives a warning and build fails (because it's an expression). So let's work around this by adding it to the set of aliases in `webpack.base.conf.js`:
227 |
228 | #### build/webpack.base.conf.js
229 |
230 | ```js
231 |
232 | module.exports = {
233 |
234 | // ...
235 |
236 | resolve: {
237 | extensions: ['.js', '.vue', '.json'],
238 | alias: {
239 | '@': resolve('src'),
240 | 'utils': resolve('src/utils')
241 | }
242 | },
243 |
244 | // ...
245 |
246 | }
247 |
248 | ```
249 |
250 | So we use the `utils` alias in the plugin:
251 |
252 | #### build/webpack.dev.conf.js, build/webpack.prod.conf.js
253 |
254 | ```js
255 | plugins: [
256 |
257 | // ...
258 |
259 | new webpack.ProvidePlugin({
260 | $: 'jquery',
261 | jquery: 'jquery',
262 | 'window.jQuery': 'jquery',
263 | jQuery: 'jquery',
264 | '_': 'lodash',
265 | 'Tether': 'tether',
266 | utils: 'utils'
267 | })
268 | ]
269 | ```
270 |
271 | Take a look in the `Login.vue` component to see how we use this utility to display an error message (when login credentials are invalid). Note: A more scalable way to handle responses/errors in your app would be to standardize them in your backend API. For example, when an error happens, the backend API can still return a 200, but include an error property with detail in the returned response JSON.
272 |
273 | ## Configure Sublime Text 3
274 |
275 | #### Install Package Control
276 | https://packagecontrol.io/installation
277 |
278 | #### Install Babel syntax definitions for ES6 JavaScript
279 |
280 | * Go to `Preferences > Package Control > Install Package` or press `ctrl+shift+p` (Win, Linux) or `cmd+shift+p` (OS X) and search for "Package Control: Install Package".
281 | * Search for the package "Babel" and install it.
282 | * Open any .js file in Sublime. Then go to `View > Syntax > Open all with current extension as... > Babel > Javascript (Babel)`.
283 |
284 | #### Install a theme that works well with Babel.
285 | For example, here's how you can install the **Oceanic Next** theme:
286 |
287 | * Try the Oceanic Next theme: `Open Package Control -> Install Package` and search for Oceanic Next color theme.
288 | * Go to `Preferences > Oceanic Next Color theme > Oceanic next`.
289 |
290 | #### Setup soft tabs and 2 space indention
291 |
292 | * Open any .js file. Go to `Preferences > Settings - More > Syntax Specific - User`.
293 | * It should open a file like `JavaScript (Babel).sublime-settings`
294 | * Add these parameters to the file:
295 |
296 | ```
297 | {
298 | "extensions":
299 | [
300 | "js"
301 | ],
302 | "tab_size": 2,
303 | "translate_tabs_to_spaces": true
304 | }
305 |
306 | ```
307 | * Open any .vue file and repeat this process.
308 |
309 | #### Install Stylus package for Sublime
310 |
311 | * Open `Package Control: Install Package` and search for `Stylus` and install it (should be billymoon/Stylus package).
312 | * Restart Sublime.
313 |
314 | #### Install Sublime-linter and ESLinter
315 |
316 | * Open `Package Control: Install Package` and search for `SublimeLinter` and install it.
317 | * Search for `SublimeLinter-contrib-eslint` and install it as well.
318 | * Restart Sublime.
319 |
320 | *Note: In the next section you'll configure eslint. If you install eslint into the same directory you are modifying Sublime files from (and same machine), then Sublimelinter will have no problem using it. Or you can also install eslint (and all the other eslint-* packages) globally on the same machine as Sublime. But if you are using a server or a virtual machine (Vagrant/Virtualbox) configuration, then you need to tell sublimelinter where eslint is. You can change the path with `Sublime Text -> Prefences -> Package Settings -> SublimeLinter -> Settings-User`.*
321 |
322 | ## Configure ESLint
323 |
324 | Let's add some more things to eslint from the default given. You'll need to restart Sublime each time you makes changes to this file. One thing to point out is the `env` and `globals` properties. These are necessary so eslint doesn't complain about use of these globals in our JS files (and so we don't have to add something like `/* globals localStorage */` to the top of those files to suppress the errors). See other sections in this tutorial, [Configure JQuery](#configure-jquery) and [Global Helpers](#global-helpers) for information about working in a global context in Webpack.
325 |
326 | Make sure you installed the additional eslint dependencies:
327 |
328 | ```shell
329 | $ npm install eslint-config-vue eslint-plugin-vue --save-dev
330 | ```
331 |
332 | Now open up your eslintrc.js file and make the following changes:
333 |
334 | #### eslintrc.js
335 |
336 | ```js
337 | module.exports = {
338 | root: true,
339 | parser: 'babel-eslint',
340 | parserOptions: {
341 | sourceType: 'module'
342 | },
343 | // required for eslint-config-vue
344 | extends: 'vue',
345 | // required to lint *.vue files
346 | plugins: [
347 | 'html'
348 | ],
349 | env: {
350 | browser: true
351 | },
352 | globals: {
353 | '$': true,
354 | '_': true,
355 | 'utils': true
356 | },
357 | // add your custom rules here
358 | 'rules': {
359 | // allow debugger during development
360 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
361 | }
362 | }
363 | ```
364 |
365 | #### .eslintignore
366 |
367 | You can tell ESLint to [ignore specific files and directories](http://eslint.org/docs/user-guide/configuring.html#ignoring-files-and-directories) by using an `.eslintignore` file in your project’s root directory:
368 |
369 | **.eslintignore**
370 |
371 | build/*.js
372 | config/*.js
373 |
374 | The ignore patterns behave according to the `.gitignore` specification.
375 | (Don't forget to restart your editor, ie. SublimeText3)
376 |
377 | ## Setup Main and Routes
378 |
379 | #### src/main.js
380 |
381 | ```js
382 | /* Twitter Bootstrap JS (this could also be handled in an app.js file) */
383 | require('bootstrap')
384 |
385 | /* Vue */
386 | import Vue from 'vue'
387 | import router from './router'
388 | import store from './store'
389 | import VueResource from 'vue-resource'
390 |
391 | Vue.use(VueResource)
392 | Vue.config.productionTip = false
393 |
394 | /* App sass */
395 | import './assets/style/app.scss'
396 |
397 | /* App component */
398 | import App from './components/App.vue'
399 |
400 | /* Auth plugin */
401 | import Auth from './auth'
402 | Vue.use(Auth)
403 |
404 | /* eslint-disable no-new */
405 | new Vue({
406 | el: '#app',
407 | // Attach the Vue instance to the window,
408 | // so it's available globally.
409 | created: function () {
410 | window.Vue = this
411 | },
412 | router,
413 | store,
414 | render: h => h(App)
415 | })
416 |
417 | ```
418 | #### src/router/index.js
419 |
420 | ```js
421 | import Vue from 'vue'
422 | import Router from 'vue-router'
423 |
424 | Vue.use(Router)
425 |
426 | const router = new Router({
427 | mode: 'history',
428 | routes: [
429 | // Each of these routes are loaded asynchronously, when a user first navigates to each corresponding endpoint.
430 | // The route will load once into memory, the first time it's called, and no more on future calls.
431 | // This behavior can be observed on the network tab of your browser dev tools.
432 | {
433 | path: '/login',
434 | name: 'login',
435 | component: function (resolve) {
436 | require(['@/components/login/Login.vue'], resolve)
437 | }
438 | },
439 | {
440 | path: '/signup',
441 | name: 'signup',
442 | component: function (resolve) {
443 | require(['@/components/signup/Signup.vue'], resolve)
444 | }
445 | },
446 | {
447 | path: '/',
448 | name: 'dashboard',
449 | component: function (resolve) {
450 | require(['@/components/dashboard/Dashboard.vue'], resolve)
451 | },
452 | beforeEnter: guardRoute
453 | }
454 | ]
455 | })
456 |
457 | function guardRoute (to, from, next) {
458 | // work-around to get to the Vuex store (as of Vue 2.0)
459 | const auth = router.app.$options.store.state.auth
460 |
461 | if (!auth.isLoggedIn) {
462 | next({
463 | path: '/login',
464 | query: { redirect: to.fullPath }
465 | })
466 | } else {
467 | next()
468 | }
469 | }
470 |
471 | export default router
472 |
473 | ```
474 |
475 | ## Setup Authentication, User Profile, and Vuex
476 |
477 | Create a folder called `store` in the `src` directory:
478 |
479 | ```shell
480 | $ mkdir store
481 | ```
482 |
483 | Now let's create the following files that will comprise our central Vuex storage.
484 |
485 | ### Vuex State
486 |
487 | Let's setup the state of our central data storage. We'll want some state to be available accross browser tabs (and when the app is closed/reopened) so let's sync this state with LocalStorage. When the app bootstraps, we want to first check in the browser's localStorage and retrieve all of our previously stored data. We'll also have other state we can use for to make component-to-component communication easier (for situations where you don't have a simple parent-child communication, but more complex sibling-to-sibling or other component relationships). Let's just add a property for storing the search text and button press on the navbar for demonstration purposes.
488 |
489 | #### src/store/state.js
490 |
491 | ```js
492 | // Set the key we'll use in local storage.
493 | // Go to Chrome dev tools, application tab, click "Local Storage" and "http://localhost:8080"
494 | // and you'll see this key set below (if logged in):
495 | export const STORAGE_KEY = 'example-vue-project'
496 |
497 | let initialState = {}
498 |
499 | // Local storage sync state
500 | if (localStorage.getItem(STORAGE_KEY)) {
501 | initialState = JSON.parse(localStorage.getItem(STORAGE_KEY))
502 | } else {
503 | initialState = {
504 | auth: {
505 | isLoggedIn: false,
506 | accessToken: null,
507 | refreshToken: null
508 | },
509 | user: {
510 | name: null
511 | }
512 | }
513 | }
514 |
515 | // Other state (not synced in local storage)
516 | initialState.appnav = {
517 | searchText: '',
518 | searchTimestamp: null
519 | }
520 |
521 | export const state = initialState
522 |
523 | ```
524 |
525 | ### Vuex Mutations, Getters, and Actions
526 |
527 | Now create a file to hold all the methods that will change the state in our Vuex store:
528 |
529 | #### src/store/mutations.js
530 |
531 | ```js
532 | export const UPDATE_AUTH = (state, auth) => {
533 | state.auth = auth
534 | }
535 |
536 | export const UPDATE_USER = (state, user) => {
537 | state.user = user
538 | }
539 |
540 | export const APPNAV_SEARCH = (state, searchData) => {
541 | state.appnav = searchData
542 | }
543 |
544 | /**
545 | * Clear each property, one by one, so reactivity still works.
546 | *
547 | * (ie. clear out state.auth.isLoggedIn so Navbar component automatically reacts to logged out state,
548 | * and the Navbar menu adjusts accordingly)
549 | *
550 | * TODO: use a common import of default state to reset these values with.
551 | */
552 | export const CLEAR_ALL_DATA = (state) => {
553 | // Auth
554 | state.auth.isLoggedIn = false
555 | state.auth.accessToken = null
556 | state.auth.refreshToken = null
557 |
558 | // User
559 | state.user.name = ''
560 | }
561 |
562 |
563 | ```
564 |
565 | And some getters (although you can accesss the Vuex state directly as we'll see shortly):
566 |
567 | #### src/store/getters.js
568 |
569 | ```js
570 | export const user = state => state.user
571 | ```
572 |
573 | We'll also go ahead and add an actions file (but leave it empty for this project since we don't need it):
574 |
575 | #### src/store/actions.js
576 |
577 | ```js
578 | // Here is where you can put async operations.
579 | // See the Vuex official docs for more information.
580 |
581 | // ...
582 |
583 | ```
584 |
585 | ### Vuex Plugins
586 | Plugins offer a nice approach to hook into mutations and do things like logging or syncing with another store such as `localStorage` or `websockets`:
587 |
588 | #### src/store/plugins.js
589 |
590 | ```js
591 | import { STORAGE_KEY } from './state'
592 |
593 | const localStoragePlugin = store => {
594 | store.subscribe((mutation, state) => {
595 | const syncedData = { auth: state.auth, user: state.user }
596 |
597 | localStorage.setItem(STORAGE_KEY, JSON.stringify(syncedData))
598 |
599 | if (mutation.type === 'CLEAR_ALL_DATA') {
600 | localStorage.removeItem(STORAGE_KEY)
601 | }
602 | })
603 | }
604 |
605 | // TODO: setup env
606 | // export default process.env.NODE_ENV !== 'production' ? [localStoragePlugin] : [localStoragePlugin]
607 | export default [localStoragePlugin]
608 |
609 | ```
610 |
611 | ### Vuex index.js
612 |
613 | And bring it all together in the index.js file:
614 |
615 | #### src/store/index.js
616 |
617 | ```js
618 | import Vue from 'vue'
619 | import Vuex from 'vuex'
620 | import { state } from './state'
621 | import * as getters from './getters'
622 | import * as actions from './actions'
623 | import * as mutations from './mutations'
624 | import plugins from './plugins'
625 |
626 | Vue.use(Vuex)
627 |
628 | const store = new Vuex.Store({
629 | state,
630 | getters,
631 | actions,
632 | mutations,
633 | plugins
634 | })
635 |
636 | export default store
637 | ```
638 | ### Auth Script
639 |
640 | Now let's add our auth script. Here we handle getting **OAuth2** access_tokens and automatically refreshing them.
641 |
642 | #### src/auth.js
643 |
644 | ```js
645 | import Vue from 'vue'
646 | import router from './router'
647 | import store from './store'
648 |
649 | /**
650 | * @var{string} LOGIN_URL The endpoint for logging in. This endpoint should be proxied by Webpack dev server
651 | * and maybe nginx in production (cleaner calls and avoids CORS issues).
652 | */
653 | const LOGIN_URL = '/auth'
654 |
655 | /**
656 | * @var{string} REFRESH_TOKEN_URL The endpoint for refreshing an access_token. This endpoint should be proxied
657 | * by Webpack dev server and maybe nginx in production (cleaner calls and avoids CORS issues).
658 | */
659 | const REFRESH_TOKEN_URL = '/auth'
660 |
661 | /**
662 | * TODO: This is here to demonstrate what an OAuth server will want. Ultimately you don't want to
663 | * expose a client_secret here. You want your real project backend to take a username/password
664 | * request and add the client secret on the server-side and forward that request
665 | * onto an OAuth server. Your backend acts as a middle-man in the process, which is better, for
666 | * example in situations like DDoS attacks.
667 | *
668 | * @var{Object} AUTH_BASIC_HEADERS The options to pass into a Vue-resource http call. Includes
669 | * the headers used for login and token refresh and emulateJSON flag since we are hitting an
670 | * OAuth server directly that can't handle application/json.
671 | */
672 | const AUTH_BASIC_HEADERS = {
673 | headers: {
674 | 'Authorization': 'Basic ZGVtb2FwcDpkZW1vcGFzcw==' // Base64(client_id:client_secret) "demoapp:demopass"
675 | },
676 | emulateJSON: true
677 | }
678 |
679 | /**
680 | * Auth Plugin
681 | *
682 | * (see https://vuejs.org/v2/guide/plugins.html for more info on Vue.js plugins)
683 | *
684 | * Handles login and token authentication using OAuth2.
685 | */
686 | export default {
687 |
688 | /**
689 | * Install the Auth class.
690 | *
691 | * Creates a Vue-resource http interceptor to handle automatically adding auth headers
692 | * and refreshing tokens. Then attaches this object to the global Vue (as Vue.auth).
693 | *
694 | * @param {Object} Vue The global Vue.
695 | * @param {Object} options Any options we want to have in our plugin.
696 | * @return {void}
697 | */
698 | install (Vue, options) {
699 | Vue.http.interceptors.push((request, next) => {
700 | const token = store.state.auth.accessToken
701 | const hasAuthHeader = request.headers.has('Authorization')
702 |
703 | if (token && !hasAuthHeader) {
704 | this.setAuthHeader(request)
705 | }
706 |
707 | next((response) => {
708 | if (this._isInvalidToken(response)) {
709 | return this._refreshToken(request)
710 | }
711 | })
712 | })
713 |
714 | Vue.prototype.$auth = Vue.auth = this
715 | },
716 |
717 | /**
718 | * Login
719 | *
720 | * @param {Object.} creds The username and password for logging in.
721 | * @param {string|null} redirect The name of the Route to redirect to.
722 | * @return {Promise}
723 | */
724 | login (creds, redirect) {
725 | const params = { 'grant_type': 'password', 'username': creds.username, 'password': creds.password }
726 |
727 | return Vue.http.post(LOGIN_URL, params, AUTH_BASIC_HEADERS)
728 | .then((response) => {
729 | this._storeToken(response)
730 |
731 | if (redirect) {
732 | router.push({ name: redirect })
733 | }
734 |
735 | return response
736 | })
737 | .catch((errorResponse) => {
738 | return errorResponse
739 | })
740 | },
741 |
742 | /**
743 | * Logout
744 | *
745 | * Clear all data in our Vuex store (which resets logged-in status) and redirect back
746 | * to login form.
747 | *
748 | * @return {void}
749 | */
750 | logout () {
751 | store.commit('CLEAR_ALL_DATA')
752 | router.push({ name: 'login' })
753 | },
754 |
755 | /**
756 | * Set the Authorization header on a Vue-resource Request.
757 | *
758 | * @param {Request} request The Vue-Resource Request instance to set the header on.
759 | * @return {void}
760 | */
761 | setAuthHeader (request) {
762 | request.headers.set('Authorization', 'Bearer ' + store.state.auth.accessToken)
763 | // The demo Oauth2 server we are using requires this param, but normally you only set the header.
764 | /* eslint-disable camelcase */
765 | request.params.access_token = store.state.auth.accessToken
766 | },
767 |
768 | /**
769 | * Retry the original request.
770 | *
771 | * Let's retry the user's original target request that had recieved a invalid token response
772 | * (which we fixed with a token refresh).
773 | *
774 | * @param {Request} request The Vue-resource Request instance to use to repeat an http call.
775 | * @return {Promise}
776 | */
777 | _retry (request) {
778 | this.setAuthHeader(request)
779 |
780 | return Vue.http(request)
781 | .then((response) => {
782 | return response
783 | })
784 | .catch((response) => {
785 | return response
786 | })
787 | },
788 |
789 | /**
790 | * Refresh the access token
791 | *
792 | * Make an ajax call to the OAuth2 server to refresh the access token (using our refresh token).
793 | *
794 | * @private
795 | * @param {Request} request Vue-resource Request instance, the original request that we'll retry.
796 | * @return {Promise}
797 | */
798 | _refreshToken (request) {
799 | const params = { 'grant_type': 'refresh_token', 'refresh_token': store.state.auth.refreshToken }
800 |
801 | return Vue.http.post(REFRESH_TOKEN_URL, params, AUTH_BASIC_HEADERS)
802 | .then((result) => {
803 | this._storeToken(result)
804 | return this._retry(request)
805 | })
806 | .catch((errorResponse) => {
807 | if (this._isInvalidToken(errorResponse)) {
808 | this.logout()
809 | }
810 | return errorResponse
811 | })
812 | },
813 |
814 | /**
815 | * Store tokens
816 | *
817 | * Update the Vuex store with the access/refresh tokens received from the response from
818 | * the Oauth2 server.
819 | *
820 | * @private
821 | * @param {Response} response Vue-resource Response instance from an OAuth2 server.
822 | * that contains our tokens.
823 | * @return {void}
824 | */
825 | _storeToken (response) {
826 | const auth = store.state.auth
827 | const user = store.state.user
828 |
829 | auth.isLoggedIn = true
830 | auth.accessToken = response.body.access_token
831 | auth.refreshToken = response.body.refresh_token
832 | // TODO: get user's name from response from Oauth server.
833 | user.name = 'John Smith'
834 |
835 | store.commit('UPDATE_AUTH', auth)
836 | store.commit('UPDATE_USER', user)
837 | },
838 |
839 | /**
840 | * Check if the Vue-resource Response is an invalid token response.
841 | *
842 | * @private
843 | * @param {Response} response The Vue-resource Response instance received from an http call.
844 | * @return {boolean}
845 | */
846 | _isInvalidToken (response) {
847 | const status = response.status
848 | const error = response.data.error
849 |
850 | return (status === 401 && (error === 'invalid_token' || error === 'expired_token'))
851 | }
852 | }
853 |
854 | ```
855 |
856 | Checkout out `Login.vue` component to see how we use `Auth`. Also take a look at `Dashboard.vue` component, you can see the Vue-resource http interceptors let us not worry about including authorization headers in our AJAX calls. The interceptors also take care of refreshing tokens behind the scenes. See the comments marked "TODO" for some caveats with this demo and your own project. I hope to update this demo using a Node Express OAuth2 server for better demonstration of Auth flow.
857 |
858 | ## Proxy Api Calls in Webpack Dev Server
859 |
860 | When using Webpack for Hot Reloading, we'll need to tell the webpack dev server that `/api` calls need to be reverse proxied to another server (ie. running on node express, nginx, or some embedded server in your backend IDE). For production you would just use nginx to do the proxying. The big advantage is we don't have to worry about CORS and also we don't expose the true API endpoints to the client.
861 |
862 | Notice in `build/dev-server.js` this line:
863 |
864 | ```js
865 | // proxy api requests
866 | Object.keys(proxyTable).forEach(function (context) {
867 | var options = proxyTable[context]
868 | if (typeof options === 'string') {
869 | options = { target: options }
870 | }
871 | app.use(proxyMiddleware(context, options))
872 | })
873 | ```
874 |
875 | In this setup we are using: https://github.com/chimurai/http-proxy-middleware (you can see examples there). So let's add options to our config to make this work:
876 |
877 | In `config/index.js`, update the *proxyTable* object to look like this:
878 |
879 | ```js
880 | dev: {
881 |
882 | // ...
883 |
884 | proxyTable: {
885 | '/auth': {
886 | // TODO: Update to use node express oauth2 server for better example.
887 | target: 'http://brentertainment.com/oauth2/lockdin/token', // <-- demo oauth2 server, https://github.com/bshaffer/oauth2-demo-php
888 | changeOrigin: true,
889 | ws: true,
890 | pathRewrite: {
891 | '^/auth': ''
892 | },
893 | router: {
894 | }
895 | },
896 | '/api': {
897 | target: 'http://brentertainment.com/oauth2', // api server
898 | changeOrigin: true, // needed for virtual hosted sites
899 | ws: true, // proxy websockets
900 | pathRewrite: {
901 | '^/api': '/lockdin' // rewrite path localhost:8080/api to http://brentertainment.com/oauth2/lockdin
902 | },
903 | router: {
904 | // when request.headers.host == 'dev.localhost:3000',
905 | // override target 'http://www.example.org' to 'http://localhost:8000'
906 | // 'dev.localhost:3000': 'http://localhost:8000'
907 | }
908 | }
909 | },
910 |
911 | // ...
912 | }
913 | ```
914 |
915 | ## Components
916 |
917 | Delete the `App.vue` file located in /src folder:
918 |
919 | ```shell
920 | $ rm App.vue
921 | ```
922 |
923 | In the `/src/components` folder create the following folders and .Vue files (just copy these directly from this repo):
924 |
925 | ```
926 | /src
927 | /components
928 | - App.vue
929 | - AppFooter.vue
930 | - AppNav.vue
931 | - Hello.vue
932 | /common
933 | - Countries.vue
934 | - Spinner.vue
935 | - countries.data.js
936 | /dashboard
937 | - Dashboard.vue
938 | - AddressModal.vue
939 | /login
940 | - Login.vue
941 | /signup
942 | - Signup.vue
943 | /users
944 |
945 | ```
946 |
947 | Here we use a folder for each "page" in our SPA. This allows us to represent "pages" with more than a single .Vue file. We can ad other supporting .Vue components, .js files, or data files. There's also a `common` folder to put any components we feel don't necessarily belong to a page parent. If over time you feel there are too many folders, you can further group/consolidate pages into folders ("page group folders").
948 |
949 |
950 | ## Twitter Bootstrap 4 Configuration
951 |
952 | - Install Bootstrap 4 and Tether.js, see section: [Add Dependencies](#add-dependencies).
953 | - Add Tether to providePlugin, see section: [Configure JQuery and Lodash and Tether](#configure-jquery-and-lodash-and-tether)
954 | - Require in main.js: see section: [Setup Main and Routes](#setup-main-and-routes)
955 | - Add a folder `style` (if you haven't already) to your `/assets` directory and create the following file:
956 |
957 | #### src/assets/style/_variables.scss
958 |
959 | ```scss
960 | // copy and paste here everything from the node_modules/bootstrap/scss/_variables.scss
961 | // Then make adjustments to variables for your specific app.
962 |
963 | ```
964 | - Import into your app.scss, see section: [App scss](#app-scss).
965 |
966 | ## Fonts and Font-Awesome
967 |
968 | Install packages (if you haven't already from earlier section ):
969 |
970 | ```
971 | npm install font-awesome roboto-fontface --save-dev
972 | ```
973 |
974 | Then add `_fonts.scss` stylesheet. We'll setup your fonts and also `font-awesome` here:
975 |
976 | #### src/assets/style/_fonts.scss
977 |
978 | ```scss
979 | /* Font Awesome */
980 | $fa-font-path: '../../../node_modules/font-awesome/fonts';
981 | @import '../../../node_modules/font-awesome/scss/font-awesome';
982 |
983 | /* Roboto */
984 | $roboto-font-path: '../../../node_modules/roboto-fontface/fonts';
985 | @import '../../../node_modules/roboto-fontface/css/roboto/sass/roboto-fontface';
986 |
987 | ```
988 |
989 | ## Images and Other Assets
990 |
991 | Create an images folder at `src/assets/images` then cut an paste the Vue `logo.png` file that resides in the assets folder by default. The Navbar component uses a relative link to this image, which Webpack will resolve for us automatically.
992 |
993 | You can read more about static assets here: https://vuejs-templates.github.io/webpack/static.html
994 |
995 | ## App scss
996 |
997 | Bring everything to together into an `app.scss` file that we import in our main entry:
998 |
999 | #### src/assets/style/app.scss
1000 |
1001 | ```
1002 | @import 'fonts';
1003 | @import 'variables';
1004 | @import '../../../node_modules/bootstrap/scss/bootstrap';
1005 | @import '../../../node_modules/vue-multiselect/dist/vue-multiselect.min.css';
1006 |
1007 | ```
1008 |
1009 | Of course if this file gets too big, you can break it up into different supporting files: `_forms.scss`, `_blah-blah.scss`, etc.
1010 |
1011 | ## Unit Testing and End-to-End Testing
1012 |
1013 | Make sure you installed `babel-polyfill` earlier in this tutorial or es6 promises won't work in PhantomJS. If you didn't, you can install it with:
1014 |
1015 | ```shell
1016 | npm install babel-polyfill --save-dev
1017 | ```
1018 |
1019 | Then update your `test/unit/karma.conf.js` file to include the polyfill:
1020 |
1021 |
1022 | #### test/unit/karma.conf.js
1023 |
1024 | ```js
1025 |
1026 | //...
1027 |
1028 | files: [
1029 | '../../node_modules/babel-polyfill/dist/polyfill.js',
1030 | './index.js'
1031 | ],
1032 | ```
1033 |
1034 | A unit test is included from the Webpack template already. It's a simple example that tests the content outputted from the Hello vue component:
1035 |
1036 | #### test/unit/specs/Hello.spec.js
1037 |
1038 | ```js
1039 | import Vue from 'vue'
1040 | import Hello from 'src/components/Hello'
1041 |
1042 | describe('Hello.vue', () => {
1043 | it('should render correct contents', () => {
1044 | const vm = new Vue({
1045 | el: document.createElement('div'),
1046 | render: (h) => h(Hello)
1047 | })
1048 | expect(vm.$el.querySelector('.hello h1').textContent)
1049 | .to.equal('Welcome to Your Vue.js App')
1050 | })
1051 | })
1052 |
1053 | ```
1054 |
1055 | #### End-to-End Testing with Nightwatch.js and Selenium server
1056 |
1057 | I find End-to-End testing and Integration testing even more beneficial. Vue-cli has put together a nice setup that includes Nightwatch.js (which uses Selenium and a Chrome driver) for e2e testing right out of the box. Let's remove the existing test located at `test/e2e/specs/test.js` since it will no longer work with the changes we have made. Let's add a new test that tests that our login form works and that we can reach the dashboard:
1058 |
1059 | #### test/e2e/specs/loginTest.js
1060 |
1061 | ```js
1062 | // For authoring Nightwatch tests, see
1063 | // http://nightwatchjs.org/guide#usage
1064 |
1065 | /**
1066 | * Test that user can login and see dashboard.
1067 | */
1068 | module.exports = {
1069 | 'default e2e tests': function (browser) {
1070 | // automatically uses dev Server port from /config.index.js
1071 | // default: http://localhost:8080
1072 | // see nightwatch.conf.js
1073 | const devServer = browser.globals.devServerURL
1074 |
1075 | browser
1076 | .url(devServer)
1077 | .waitForElementVisible('#app', 5000)
1078 |
1079 | // Assert that user can see login.
1080 | .assert.elementPresent('.login')
1081 | .setValue('.js-login__username', 'demouser')
1082 | .setValue('.js-login__password', 'testpass')
1083 | .click('.js-login__submit')
1084 | .pause(1000)
1085 |
1086 | // Assert that user can see dashboard.
1087 | .assert.containsText('.ev-dashboard__heading h1', 'This is the dashboard')
1088 | .pause(2000)
1089 | .end()
1090 | }
1091 | }
1092 |
1093 | ```
1094 |
1095 | *Note: You may wish to add another `assert` that asserts the dashboard is unreachable when a user is logged out.*
1096 |
1097 | #### Running the Tests
1098 |
1099 | Now let's run both the unit test and the e2e test. Make sure you are in your project directory, then:
1100 |
1101 | ```shell
1102 | npm run test
1103 | ```
1104 |
1105 | You should see some output initially showing the results of each unit test ran:
1106 |
1107 | > Hello.vue
1108 | ✓ should render correct contents
1109 | ...
1110 | PhantomJS 2.1.1 (Linux 0.0.0): Executed 1 of 1 SUCCESS (0.018 secs / 0.004 secs)
1111 | TOTAL: 1 SUCCESS
1112 |
1113 | Then the Selenium server will fire up Chrome browser and run the e2e tests to see if those pass:
1114 |
1115 | > ✔ Element <#app> was visible after 65 milliseconds.
1116 | ✔ Testing if element <.ev-login> is present.
1117 | ✔ Testing if element <.ev-dashboard__heading> contains text: "This is the dashboard".
1118 | ...
1119 | OK. 3 assertions passed. (18.522s)
1120 |
1121 | You can of course run unit tests and e2e tests seperately with: `npm run unit` and `npm run e2e`.
1122 |
1123 | ## Run the Dev Server
1124 |
1125 | Run the dev server:
1126 |
1127 | ```shell
1128 | $ cd ~/example-vue-project
1129 | $ npm run dev
1130 | ```
1131 |
1132 | Open your browser and visit http://localhost:8080 . You should see something like this:
1133 |
1134 |
1135 |
1136 |
1137 | ## Vue Dev Tools
1138 |
1139 | Visit the Chrome Web Store to get the [Vue Dev Tools extension](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) for helping debug Vue.js applications.
1140 |
1141 | Once installed, Open Chrome dev tools and go to the "Vue" tab.
1142 |
1143 | #### Vuex Tab
1144 | If you click on the "Vuex" tab, you can see all data from the store in the right pane. Click the `export button` to copy the data to the clipboard. Click the `import button` and paste the clipboard data there.
1145 |
1146 | For example, you can alter the *accessToken* to something invalid (to simulate an expired *oauth access_token* without waiting on actual expiration) in the pasted data. Then click the `import button` again and the Vuex store will live update. Now you can confirm that the automatic refreshToken interceptor works.
1147 |
1148 | ## Create and Publish a Library for Other Developers to Use
1149 |
1150 | So now you want to go further and develop a component that others can `npm install` and import into their own project?
1151 | Here you go: https://github.com/prograhammer/vue-library-template
1152 |
1153 | ## Quick Learning Webpack Resources
1154 |
1155 | - SurviveJs: https://survivejs.com/webpack/introduction/
1156 | - Official Webpack Tutorial: https://webpack.js.org/guides/get-started/
1157 |
--------------------------------------------------------------------------------