├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── LICENSE ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── assets │ └── dashboard-data.json ├── dashboard.png ├── favicon.png └── index.html ├── src ├── App.vue ├── api │ └── DashboardAPI.js ├── assets │ ├── app-tree.png │ ├── dev-tools.png │ ├── vue-cli.png │ └── webpack-dev-server.png ├── components │ ├── Header.vue │ ├── LinkIcons.vue │ └── grid │ │ ├── Grid.vue │ │ ├── WidgetChartSVG.vue │ │ ├── WidgetMap3D.vue │ │ └── WidgetMetric.vue ├── demo-iframe.js ├── gl │ ├── Animation.js │ └── index.js ├── main.js ├── router │ └── index.js ├── store │ ├── index.js │ ├── ui │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations-types.js │ │ ├── mutations.js │ │ └── state.js │ └── widgets │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutations-types.js │ │ ├── mutations.js │ │ └── state.js ├── styles │ ├── base.scss │ ├── mixins.scss │ ├── reset.scss │ └── variables.scss ├── utils │ └── deep-dispose.js └── views │ ├── Demo.vue │ └── Tutorial.vue ├── tests └── unit │ ├── .eslintrc.js │ └── HelloWorld.spec.js └── vue.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | // because it wont let me add html snippets in tutorial :-/ 14 | 'vue/no-parsing-error': false 15 | }, 16 | parserOptions: { 17 | parser: 'babel-eslint' 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mika I. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.jsx?$": "babel-jest" 8 | }, 9 | moduleNameMapper: { 10 | "^@/(.*)$": "/src/$1" 11 | }, 12 | snapshotSerializers: ["jest-serializer-vue"], 13 | testMatch: [ 14 | "/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))" 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsive-dashboard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "test:unit": "vue-cli-service test:unit" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.18.1", 13 | "core-js": "^3.6.4", 14 | "lodash": "^4.17.15", 15 | "lodash-es": "^4.17.15", 16 | "three": "^0.94.0", 17 | "vue": "^2.5.16", 18 | "vue-highlightjs": "^1.3.3", 19 | "vue-router": "^3.0.1", 20 | "vuex": "^3.0.1", 21 | "vuex-router-sync": "^5.0.0" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "^4.1.2", 25 | "@vue/cli-plugin-eslint": "^3.0.0-beta.15", 26 | "@vue/cli-plugin-unit-jest": "^4.1.2", 27 | "@vue/cli-service": "^4.1.2", 28 | "@vue/test-utils": "^1.0.0-beta.16", 29 | "babel-core": "7.0.0-bridge.0", 30 | "babel-jest": "^23.0.1", 31 | "node-sass": "^4.13.1", 32 | "sass-loader": "^7.0.1", 33 | "vue-template-compiler": "^2.5.16" 34 | }, 35 | "browserslist": [ 36 | "> 1%", 37 | "last 2 versions", 38 | "not ie <= 8" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /public/assets/dashboard-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": { 3 | "transactions": "250K", 4 | "weather": "☀️", 5 | "responsiveness": "99%", 6 | "events": "28,320", 7 | "hits": "9.12K", 8 | "convertion": "69%", 9 | "jsFrameworks": { 10 | "range": [0, 10000], 11 | "values": [9892, 8932, 4253, 1990, 1600], 12 | "labels": ["vue.js", "react", "angular", "backbone", "jQuery"] 13 | }, 14 | "topWines": { 15 | "range": [0, 440000], 16 | "values": [440000, 280953, 144500, 120040], 17 | "labels": ["Haut Médoc", "Pessac", "Beaujolais", "Rioja"] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /public/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikatalk/responsive-dashboard/f28f511dbf2bf558152b3aec100ea41c15b708a9/public/dashboard.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikatalk/responsive-dashboard/f28f511dbf2bf558152b3aec100ea41c15b708a9/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Building a Responsive Dashboard with Vue.js 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 |
21 | 22 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | 36 | 76 | -------------------------------------------------------------------------------- /src/api/DashboardAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const API = { 4 | base: './assets', 5 | route: 'dashboard-data.json' 6 | } 7 | 8 | const client = axios.create({ 9 | baseURL: API.base 10 | }) 11 | 12 | export default class DashboardAPI { 13 | static loadDashboardData () { 14 | return client.get(API.route) 15 | } 16 | } -------------------------------------------------------------------------------- /src/assets/app-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikatalk/responsive-dashboard/f28f511dbf2bf558152b3aec100ea41c15b708a9/src/assets/app-tree.png -------------------------------------------------------------------------------- /src/assets/dev-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikatalk/responsive-dashboard/f28f511dbf2bf558152b3aec100ea41c15b708a9/src/assets/dev-tools.png -------------------------------------------------------------------------------- /src/assets/vue-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikatalk/responsive-dashboard/f28f511dbf2bf558152b3aec100ea41c15b708a9/src/assets/vue-cli.png -------------------------------------------------------------------------------- /src/assets/webpack-dev-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikatalk/responsive-dashboard/f28f511dbf2bf558152b3aec100ea41c15b708a9/src/assets/webpack-dev-server.png -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 59 | 60 | 159 | -------------------------------------------------------------------------------- /src/components/LinkIcons.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 87 | 88 | 89 | 103 | -------------------------------------------------------------------------------- /src/components/grid/Grid.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 111 | 112 | 113 | 142 | -------------------------------------------------------------------------------- /src/components/grid/WidgetChartSVG.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 97 | 98 | 99 | 135 | -------------------------------------------------------------------------------- /src/components/grid/WidgetMap3D.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 40 | 41 | 42 | 66 | -------------------------------------------------------------------------------- /src/components/grid/WidgetMetric.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 25 | 50 | -------------------------------------------------------------------------------- /src/demo-iframe.js: -------------------------------------------------------------------------------- 1 | import '@/styles/base.scss' 2 | import Vue from 'vue' 3 | import Grid from '@/components/grid/Grid' 4 | import store from '@/store' 5 | import { debounce } from 'lodash-es' 6 | 7 | Vue.config.productionTip = false 8 | Vue.config.devtools = process.env.NODE_ENV === 'development' 9 | 10 | const container = document.querySelector('#app') 11 | 12 | const app = new Vue({ 13 | store, 14 | render: h => h(Grid) 15 | }).$mount(container) 16 | 17 | const handleResize = debounce(() => { 18 | const {width, height} = app.$el.getBoundingClientRect() 19 | try { 20 | window.parent.postMessage({ 21 | 'event-type': 'iframe-content-resize', 22 | width, 23 | height 24 | }, 25 | document.location.origin) 26 | } catch (e) { 27 | // nothing to do here 28 | } 29 | }, 300) 30 | handleResize() 31 | window.addEventListener('resize', handleResize) 32 | -------------------------------------------------------------------------------- /src/gl/Animation.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | const noise = ` 4 | // 2D Random 5 | float random (in vec2 st) { 6 | return fract(sin(dot(st.xy, vec2(6.,15.))) + sin(dot(st.xy, vec2(2.8765,23.23)))); 7 | } 8 | // 2D Noise based on Morgan McGuire @morgan3d 9 | // https://www.shadertoy.com/view/4dS3Wd 10 | float noise (in vec2 st) { 11 | vec2 i = floor(st); 12 | vec2 f = fract(st); 13 | // Four corners in 2D of a tile 14 | float a = random(i); 15 | float b = random(i + vec2(1.0, 0.0)); 16 | float c = random(i + vec2(0.0, 1.0)); 17 | float d = random(i + vec2(1.0, 1.0)); 18 | // Smooth Interpolation 19 | // Cubic Hermine Curve. Same as SmoothStep() 20 | vec2 u = f*f*(3.0-2.0*f); 21 | // u = smoothstep(0.,1.,f) 22 | // Mix 4 coorners porcentages 23 | return mix(a, b, u.x) + 24 | (c - a)* u.y * (1.0 - u.x) + 25 | (d - b) * u.x * u.y; 26 | } 27 | ` 28 | 29 | const vs = ` 30 | precision highp float; 31 | uniform float time; 32 | uniform float sineTime; 33 | attribute vec4 color; 34 | varying vec3 vPosition; 35 | // varying vec2 vUv; 36 | ${noise} 37 | void main(){ 38 | float PI2 = 6.28; 39 | // vUv = uv; 40 | vec3 offset = vec3(0.0, 0.0, 0.0); 41 | offset.z += noise(position.xy*vec2(.01)+vec2(0.0,time*.2))* 100.0; 42 | offset.z = min(offset.z, cos(uv.x*PI2*.2)*250.0); 43 | offset.z -= 50.0; 44 | vPosition = offset + position; 45 | if ( vPosition.z < -1.0 ){ 46 | vPosition.z = -1.0; 47 | } 48 | gl_Position = projectionMatrix * modelViewMatrix * vec4( vPosition, 1.0 ); 49 | } 50 | ` 51 | 52 | const fs = ` 53 | precision highp float; 54 | 55 | uniform float time; 56 | varying vec3 vPosition; 57 | // varying vec2 vUv; 58 | void main() { 59 | float PI2 = 6.28; 60 | float v = 7.0; 61 | vec4 color = vec4(0.0, 0.0, 0.0, 0.0); 62 | if ( vPosition.z < 0.0 ) { // water level 63 | color = vec4(0.1, 0.1, 0.1, 0.2); 64 | } else if (mod(vPosition.z, v) < v/7.0 ) { 65 | color = vec4(0.11, 0.56, 0.65, 1.0) * v/7.0; 66 | } else { 67 | color = vec4(0.11, 0.56, 0.65, 0.5); 68 | } 69 | color.a = min(color.a, clamp(0.0, 1.0, 4.0*cos( min(1.0, length(vPosition.xy)/1000.0) * 3.14))); 70 | gl_FragColor = color; 71 | } 72 | ` 73 | 74 | export default class Animation { 75 | constructor () { 76 | let geometryBase = new THREE.PlaneGeometry(500, 500, 100, 100) 77 | let geometry = new THREE.BufferGeometry().fromGeometry(geometryBase) 78 | let length = geometry.attributes.position.count 79 | let barycentric = new THREE.BufferAttribute(new Float32Array(length * 3), 3) 80 | for (let i = 0, i3 = 0; i < length; i++, i3 += 3) { 81 | barycentric.setX(i3 + 0, 1) 82 | barycentric.setY(i3 + 0, 0) 83 | barycentric.setZ(i3 + 0, 0) 84 | 85 | barycentric.setX(i3 + 1, 0) 86 | barycentric.setY(i3 + 1, 1) 87 | barycentric.setZ(i3 + 1, 0) 88 | 89 | barycentric.setX(i3 + 2, 0) 90 | barycentric.setY(i3 + 2, 0) 91 | barycentric.setZ(i3 + 2, 1) 92 | } 93 | geometry.addAttribute('aBarycentric', barycentric) 94 | 95 | // material 96 | let material = new THREE.ShaderMaterial({ 97 | uniforms: { 98 | time: { value: 1.0 }, 99 | sineTime: { value: 1.0 } 100 | // mouse: { value: this.mouse }, 101 | }, 102 | vertexShader: vs, 103 | fragmentShader: fs, 104 | extensions: { 105 | derivatives: true 106 | }, 107 | wireframe: true, 108 | transparent: true 109 | }) 110 | this.mesh = new THREE.Mesh(geometry, material) 111 | } 112 | 113 | update (time) { 114 | this.mesh.material.uniforms.time.value = time 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/gl/index.js: -------------------------------------------------------------------------------- 1 | 2 | import * as THREE from 'three' 3 | import { deepDispose } from '@/utils/deep-dispose' 4 | import Animation from '@/gl/Animation' 5 | 6 | export default class GL { 7 | constructor (canvas, container) { 8 | this.canvas = canvas 9 | this.container = container 10 | 11 | const width = canvas.width 12 | const height = canvas.height 13 | 14 | this.clock = new THREE.Clock() 15 | this.scene = new THREE.Scene() 16 | this.camera = new THREE.PerspectiveCamera(35, width / height, 1, 20000) 17 | this.camera.position.set(0, -300, 65) 18 | this.camera.lookAt(this.scene.position) 19 | this.renderer = new THREE.WebGLRenderer({ 20 | canvas: canvas, 21 | antialias: true, 22 | stencil: false, 23 | alpha: !true 24 | }) 25 | this.renderer.setClearColor(0x303030, 1) 26 | this.renderer.setPixelRatio(window.devicePixelRatio || 1) 27 | this.animation = new Animation() 28 | this.scene.add(this.animation.mesh) 29 | this.running = true 30 | this.tick() 31 | this.handleResize() 32 | } 33 | 34 | destroy () { 35 | this.running = false 36 | while (this.scene.children.length > 0) { 37 | const object = this.scene.children[this.scene.children.length - 1] 38 | deepDispose(object) 39 | this.scene.remove(object) 40 | } 41 | this.renderer.dispose() 42 | this.renderer.forceContextLoss() 43 | this.renderer.context = undefined 44 | this.renderer.domElement = undefined 45 | // until next garbage collection 46 | this.canvas.width = 1 47 | this.canvas.height = 1 48 | } 49 | 50 | tick () { 51 | if (!this.running) return 52 | requestAnimationFrame(() => this.tick()) 53 | var time = performance.now() 54 | this.animation.update(time * 0.007) 55 | this.renderer.render(this.scene, this.camera) 56 | } 57 | 58 | handleResize () { 59 | const width = this.container.getBoundingClientRect().width 60 | const height = this.container.getBoundingClientRect().height 61 | this.canvas.width = width 62 | this.canvas.height = height 63 | this.renderer.setSize(width, height) 64 | this.camera.aspect = width / height 65 | this.camera.updateProjectionMatrix() 66 | } 67 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import '@/styles/base.scss' 2 | // import '@/../node_modules/highlight.js/styles/solarized-dark.css' 3 | // import '@/../node_modules/highlight.js/styles/' 4 | import '@/../node_modules/highlight.js/styles/mono-blue.css' 5 | // import '@/../node_modules/highlight.js/styles/monokai.css' 6 | // import '@/../node_modules/highlight.js/styles/monokai-sublime.css' 7 | // import '@/../node_modules/highlight.js/styles/ocean.css' 8 | // import '@/../node_modules/highlight.js/styles/pojoaque.css' 9 | 10 | import Vue from 'vue' 11 | import App from './App.vue' 12 | 13 | import router from '@/router' 14 | import store from '@/store' 15 | 16 | import { sync as syncVuexWithRouter } from 'vuex-router-sync' 17 | 18 | import VueHighlightJS from 'vue-highlightjs' 19 | 20 | Vue.use(VueHighlightJS) 21 | 22 | Vue.config.productionTip = false 23 | Vue.config.devtools = true // process.env.NODE_ENV === 'development' 24 | 25 | syncVuexWithRouter(store, router) 26 | 27 | new Vue({ 28 | router, 29 | store, 30 | render: h => h(App) 31 | }).$mount('#app') 32 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Demo from '@/views/Demo.vue' 4 | import Tutorial from '@/views/Tutorial.vue' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | // mode: 'history', 10 | // base: '/', 11 | routes: [ 12 | { 13 | path: '/demo', 14 | name: 'Demo', 15 | component: Demo 16 | }, 17 | { 18 | path: '*', 19 | name: 'Tutorial', 20 | component: Tutorial 21 | } 22 | ] 23 | }) 24 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import createLogger from 'vuex/dist/logger' 4 | import ui from './ui' 5 | import widgets from './widgets' 6 | import {throttle} from 'lodash' 7 | 8 | const debug = false // process.env.NODE_ENV === 'development' 9 | 10 | Vue.use(Vuex) 11 | 12 | const store = new Vuex.Store({ 13 | namespaced: true, 14 | state: {}, 15 | actions: {}, 16 | mutations: {}, 17 | getters: {}, 18 | modules: { 19 | ui, 20 | widgets 21 | }, 22 | strict: debug, 23 | plugins: debug ? [createLogger()] : [] 24 | }) 25 | 26 | const handleResize = throttle(() => { 27 | let size = { 28 | width: window.innerWidth, 29 | height: window.innerHeight 30 | } 31 | store.dispatch('ui/windowUpdateSize', size) 32 | // store.commit('ui/WINDOW_UPDATE_SIZE', {size}) 33 | }, 150) 34 | 35 | handleResize() 36 | window.addEventListener('resize', handleResize) 37 | 38 | export default store 39 | -------------------------------------------------------------------------------- /src/store/ui/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutations-types' 2 | 3 | export const windowUpdateSize = ({ commit }, size) => { 4 | commit(types.WINDOW_UPDATE_SIZE, { size }) 5 | } 6 | -------------------------------------------------------------------------------- /src/store/ui/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | windowWidth: state => state.window.width, 3 | windowHeight: state => state.window.height, 4 | colorA: state => state.colors.a, 5 | colorB: state => state.colors.b 6 | } 7 | -------------------------------------------------------------------------------- /src/store/ui/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import mutations from './mutations' 3 | import * as actions from './actions' 4 | import getters from './getters' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | actions, 11 | mutations 12 | } 13 | -------------------------------------------------------------------------------- /src/store/ui/mutations-types.js: -------------------------------------------------------------------------------- 1 | export const WINDOW_UPDATE_SIZE = 'WINDOW_UPDATE_SIZE' 2 | -------------------------------------------------------------------------------- /src/store/ui/mutations.js: -------------------------------------------------------------------------------- 1 | 2 | import * as types from './mutations-types' 3 | 4 | export default { 5 | [types.WINDOW_UPDATE_SIZE] (state, { size }) { 6 | state.window.width = size.width || state.window.width 7 | state.window.height = size.height || state.window.height 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/store/ui/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | window: { 3 | width: 1, 4 | height: 1 5 | }, 6 | colors: { 7 | a: { 8 | bg: '#011e2b', // '#ff36a2', 9 | fg: '#ffffff' 10 | }, 11 | b: { 12 | bg: '#ff36a2', // '#ffe803', 13 | fg: '#030303' 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/store/widgets/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutations-types' 2 | import DashboardAPI from '@/api/DashboardAPI' 3 | 4 | export const loadDashboardData = ({ commit }) => { 5 | commit(types.SET_LOADING_STATE, true) 6 | DashboardAPI.loadDashboardData().then(response => { 7 | const {data} = response 8 | commit(types.SET_DASHBOARD_DATA, data) 9 | commit(types.SET_LOADING_STATE, false) 10 | }) 11 | } -------------------------------------------------------------------------------- /src/store/widgets/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isLoading: state => state.loading, 3 | transactions: state => state.widgets.transactions, 4 | convertion: state => state.widgets.convertion, 5 | hits: state => state.widgets.hits, 6 | events: state => state.widgets.events, 7 | responsiveness: state => state.widgets.responsiveness, 8 | weather: state => state.widgets.weather, 9 | jsFrameworks: state => state.widgets.jsFrameworks, 10 | topWines: state => state.widgets.topWines, 11 | } -------------------------------------------------------------------------------- /src/store/widgets/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import mutations from './mutations' 3 | import * as actions from './actions' 4 | import getters from './getters' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | actions, 11 | mutations 12 | } 13 | -------------------------------------------------------------------------------- /src/store/widgets/mutations-types.js: -------------------------------------------------------------------------------- 1 | export const SET_LOADING_STATE = 'SET_LOADING_STATE' 2 | export const SET_DASHBOARD_DATA = 'SET_DASHBOARD_DATA' 3 | -------------------------------------------------------------------------------- /src/store/widgets/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutations-types' 2 | 3 | export default { 4 | [types.SET_LOADING_STATE] (state, value) { 5 | state.loading = value 6 | }, 7 | [types.SET_DASHBOARD_DATA] (state, {widgets}) { 8 | // Metrics 9 | state.widgets.transactions.value = widgets.transactions 10 | state.widgets.weather.value = widgets.weather 11 | state.widgets.responsiveness.value = widgets.responsiveness 12 | state.widgets.events.value = widgets.events 13 | state.widgets.hits.value = widgets.hits 14 | state.widgets.convertion.value = widgets.convertion 15 | // SVG Charts 16 | state.widgets.jsFrameworks.range = widgets.jsFrameworks.range 17 | state.widgets.jsFrameworks.values = widgets.jsFrameworks.values 18 | state.widgets.jsFrameworks.labels = widgets.jsFrameworks.labels 19 | state.widgets.topWines.range = widgets.topWines.range 20 | state.widgets.topWines.values = widgets.topWines.values 21 | state.widgets.topWines.labels = widgets.topWines.labels 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/store/widgets/state.js: -------------------------------------------------------------------------------- 1 | export default { 2 | loading: true, 3 | widgets: { 4 | transactions: { 5 | value: null 6 | }, 7 | weather: { 8 | value: null, 9 | }, 10 | responsiveness: { 11 | value: null, 12 | }, 13 | events: { 14 | value: null 15 | }, 16 | hits: { 17 | value: null 18 | }, 19 | convertion: { 20 | value: null 21 | }, 22 | jsFrameworks: { 23 | range: null, 24 | values: null, 25 | labels: null 26 | }, 27 | topWines: { 28 | range: null, 29 | values: null, 30 | labels: null 31 | }, 32 | map3D: { 33 | // not needed in this tutorial 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/base.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins.scss"; 2 | @import "./variables.scss"; 3 | @import "./reset.scss"; 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | font-size: 1em; 9 | line-height: 1.2; 10 | font-family: 'Montserrat', sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | text-align: justify; 14 | color: #2c3e50; 15 | background: #fafafa; 16 | // background: black; 17 | min-height: 100vh; 18 | .page { 19 | margin: 20px; 20 | max-width: $nav-max-width; 21 | @media (min-width: $nav-max-width) { 22 | margin: 0 auto; 23 | } 24 | article { 25 | hr { 26 | margin: 3rem 0; 27 | } 28 | a { 29 | color: inherit; 30 | } 31 | img { 32 | margin: 2rem auto; 33 | width: 100%; 34 | display: block; 35 | &.medium-size { 36 | max-width: 400px; 37 | } 38 | } 39 | .title { 40 | margin: 5px; 41 | text-align: left; 42 | font-size: 2rem; 43 | padding-top: 1rem; 44 | @media (max-width: $mobile-size) { 45 | font-size: 2rem; 46 | padding-top: 3rem; 47 | } 48 | @media (min-width: $nav-max-width) { 49 | font-size: 2.8rem; 50 | } 51 | span { 52 | font-size: 1.4rem; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | 3 | @mixin flex-size($col: 6, $gutter: 1%) { 4 | flex-basis: (100% / (6 / $col)) - $gutter * 2; 5 | } 6 | 7 | @mixin six-columns-layout($screen-type: desktop, $gutter: 1%) { 8 | .#{$screen-type}-1-col { 9 | @include flex-size(1, $gutter); 10 | } 11 | .#{$screen-type}-2-col { 12 | @include flex-size(2, $gutter); 13 | } 14 | .#{$screen-type}-3-col { 15 | @include flex-size(3, $gutter); 16 | } 17 | .#{$screen-type}-4-col { 18 | @include flex-size(4, $gutter); 19 | } 20 | .#{$screen-type}-5-col { 21 | @include flex-size(5, $gutter); 22 | } 23 | .#{$screen-type}-6-col { 24 | @include flex-size(6, $gutter); 25 | } 26 | } 27 | 28 | /** Grid Layout **/ 29 | @mixin grid-6($element-selector) { 30 | @at-root #{$element-selector + &} { 31 | display: flex; 32 | // if any margin, we want it spaced evenly 33 | justify-content: space-evenly; 34 | // we want all the widgest to have the same height when on the same line 35 | align-items: stretch; 36 | // wrap the list of widgets over multiple lines if needed 37 | flex-wrap: wrap; 38 | @media (min-width: $nav-max-width) { 39 | @include six-columns-layout(desktop); 40 | } 41 | @media (max-width: $nav-max-width) { 42 | @include six-columns-layout(tablet); 43 | } 44 | @media (max-width: $mobile-size) { 45 | @include six-columns-layout(phone); 46 | } 47 | .no-gutters { 48 | [class*='desktop-'], [class*='tablet-'], [class*='phone-'] { 49 | margin: 0 0 10px 0; 50 | } 51 | } 52 | .relaxed-gutters { 53 | [class*='desktop-'], [class*='tablet-'], [class*='phone-'] { 54 | margin: 0 1% 10px 1%; 55 | } 56 | } 57 | @media (max-width: $mobile-size) { 58 | .relaxed-gutters { 59 | @include six-columns-layout(phone, 2%); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * What follows is the result of much research on cross-browser styling. 3 | * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, 4 | * Kroc Camen, and the H5BP dev community and team. 5 | */ 6 | 7 | /* ========================================================================== 8 | Base styles: opinionated defaults 9 | ========================================================================== */ 10 | /* 11 | * Remove text-shadow in selection highlight: 12 | * https://twitter.com/miketaylr/status/12228805301 13 | * 14 | * Vendor-prefixed and regular ::selection selectors cannot be combined: 15 | * https://stackoverflow.com/a/16982510/7133471 16 | * 17 | * Customize the background color to match your design. 18 | */ 19 | 20 | ::selection { 21 | background: #b3d4fc; 22 | text-shadow: none; 23 | } 24 | 25 | /* 26 | * A better looking default horizontal rule 27 | */ 28 | 29 | hr { 30 | display: block; 31 | height: 1px; 32 | border: 0; 33 | border-top: 1px solid #ccc; 34 | margin: 1em 0; 35 | padding: 0; 36 | } 37 | 38 | /* 39 | * Remove the gap between audio, canvas, iframes, 40 | * images, videos and the bottom of their containers: 41 | * https://github.com/h5bp/html5-boilerplate/issues/440 42 | */ 43 | 44 | audio, 45 | canvas, 46 | iframe, 47 | img, 48 | svg, 49 | video { 50 | vertical-align: middle; 51 | } 52 | 53 | /* 54 | * Remove default fieldset styles. 55 | */ 56 | 57 | fieldset { 58 | border: 0; 59 | margin: 0; 60 | padding: 0; 61 | } 62 | 63 | /* 64 | * Allow only vertical resizing of textareas. 65 | */ 66 | 67 | textarea { 68 | resize: vertical; 69 | } 70 | 71 | /* ========================================================================== 72 | Browser Upgrade Prompt 73 | ========================================================================== */ 74 | 75 | .browserupgrade { 76 | margin: 0.2em 0; 77 | background: #ccc; 78 | color: #000; 79 | padding: 0.2em 0; 80 | } 81 | 82 | /* ========================================================================== 83 | Author's custom styles 84 | ========================================================================== */ 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | /* ========================================================================== 103 | Helper classes 104 | ========================================================================== */ 105 | 106 | /* 107 | * Hide visually and from screen readers 108 | */ 109 | 110 | .hidden { 111 | display: none !important; 112 | } 113 | 114 | /* 115 | * Hide only visually, but have it available for screen readers: 116 | * https://snook.ca/archives/html_and_css/hiding-content-for-accessibility 117 | * 118 | * 1. For long content, line feeds are not interpreted as spaces and small width 119 | * causes content to wrap 1 word per line: 120 | * https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe 121 | */ 122 | 123 | .visuallyhidden { 124 | border: 0; 125 | clip: rect(0 0 0 0); 126 | clip-path: inset(50%); 127 | height: 1px; 128 | margin: -1px; 129 | overflow: hidden; 130 | padding: 0; 131 | position: absolute; 132 | width: 1px; 133 | white-space: nowrap; /* 1 */ 134 | } 135 | 136 | /* 137 | * Extends the .visuallyhidden class to allow the element 138 | * to be focusable when navigated to via the keyboard: 139 | * https://www.drupal.org/node/897638 140 | */ 141 | 142 | .visuallyhidden.focusable:active, 143 | .visuallyhidden.focusable:focus { 144 | clip: auto; 145 | clip-path: none; 146 | height: auto; 147 | margin: 0; 148 | overflow: visible; 149 | position: static; 150 | width: auto; 151 | white-space: inherit; 152 | } 153 | 154 | /* 155 | * Hide visually and from screen readers, but maintain layout 156 | */ 157 | 158 | .invisible { 159 | visibility: hidden; 160 | } 161 | 162 | /* 163 | * Clearfix: contain floats 164 | * 165 | * For modern browsers 166 | * 1. The space content is one way to avoid an Opera bug when the 167 | * `contenteditable` attribute is included anywhere else in the document. 168 | * Otherwise it causes space to appear at the top and bottom of elements 169 | * that receive the `clearfix` class. 170 | * 2. The use of `table` rather than `block` is only necessary if using 171 | * `:before` to contain the top-margins of child elements. 172 | */ 173 | 174 | .clearfix:before, 175 | .clearfix:after { 176 | content: " "; /* 1 */ 177 | display: table; /* 2 */ 178 | } 179 | 180 | .clearfix:after { 181 | clear: both; 182 | } 183 | 184 | /* ========================================================================== 185 | EXAMPLE Media Queries for Responsive Design. 186 | These examples override the primary ('mobile first') styles. 187 | Modify as content requires. 188 | ========================================================================== */ 189 | 190 | @media only screen and (min-width: 35em) { 191 | /* Style adjustments for viewports that meet the condition */ 192 | } 193 | 194 | @media print, 195 | (min-resolution: 1.25dppx), 196 | (min-resolution: 120dpi) { 197 | /* Style adjustments for high resolution devices */ 198 | } 199 | 200 | /* ========================================================================== 201 | Print styles. 202 | Inlined to avoid the additional HTTP request: 203 | http://www.phpied.com/delay-loading-your-print-css/ 204 | ========================================================================== */ 205 | 206 | @media print { 207 | *, 208 | *:before, 209 | *:after { 210 | background: transparent !important; 211 | color: #000 !important; /* Black prints faster: 212 | http://www.sanbeiji.com/archives/953 */ 213 | box-shadow: none !important; 214 | text-shadow: none !important; 215 | } 216 | 217 | a, 218 | a:visited { 219 | text-decoration: underline; 220 | } 221 | 222 | a[href]:after { 223 | content: " (" attr(href) ")"; 224 | } 225 | 226 | abbr[title]:after { 227 | content: " (" attr(title) ")"; 228 | } 229 | 230 | /* 231 | * Don't show links that are fragment identifiers, 232 | * or use the `javascript:` pseudo protocol 233 | */ 234 | 235 | a[href^="#"]:after, 236 | a[href^="javascript:"]:after { 237 | content: ""; 238 | } 239 | pre { 240 | white-space: pre-wrap !important; 241 | } 242 | pre, 243 | blockquote { 244 | border: 1px solid #999; 245 | page-break-inside: avoid; 246 | } 247 | /* 248 | * Printing Tables: 249 | * http://css-discuss.incutio.com/wiki/Printing_Tables 250 | */ 251 | thead { 252 | display: table-header-group; 253 | } 254 | tr, 255 | img { 256 | page-break-inside: avoid; 257 | } 258 | p, 259 | h2, 260 | h3 { 261 | orphans: 3; 262 | widows: 3; 263 | } 264 | h2, 265 | h3 { 266 | page-break-after: avoid; 267 | } 268 | 269 | .clearfix::after { 270 | content: ""; 271 | clear: both; 272 | display: table; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $nav-max-width: 980px; 2 | $mobile-size: 600px; 3 | $tiny-size: 408px; 4 | $transition-header: all 400ms ease; 5 | 6 | $black: #4c4c4c; 7 | $yellow: #ffc608; 8 | 9 | $primary-color: #1d90a8; 10 | $accent-color: #727475; 11 | 12 | $z-index-nav: 9; 13 | $z-index-overlay: 6; 14 | $z-index-canvas: 3; 15 | // $bar-width: 4px; 16 | 17 | $nav-bg: #fafafa; // transparent; // #f1ed0d; 18 | $nav-bg-hover: transparent; // #3c3c3c; 19 | $nav-link-color: $black; 20 | $nav-link-color-hover: #2c2c2c; 21 | 22 | // $side-border-color: #f5f38f; // #f1ed0d; 23 | $color-bg: #fafafa; 24 | // $text-color: #5c5c5c; 25 | 26 | $box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, .4); 27 | -------------------------------------------------------------------------------- /src/utils/deep-dispose.js: -------------------------------------------------------------------------------- 1 | export function disposeMaterial (material) { 2 | if (material.map) { 3 | material.map.dispose() 4 | material.map = undefined 5 | } 6 | if (material.normalMap) { 7 | material.normalMap.dispose() 8 | material.normalMap = undefined 9 | } 10 | if (material.specularMap) { 11 | material.specularMap.dispose() 12 | material.specularMap = undefined 13 | } 14 | if (material.bumpMap) { 15 | material.bumpMap.dispose() 16 | material.bumpMap = undefined 17 | } 18 | material.dispose() 19 | material = undefined 20 | } 21 | 22 | export function dispose (object3D) { 23 | if (object3D.geometry) { 24 | object3D.geometry.dispose() 25 | object3D.geometry = undefined 26 | } 27 | if (object3D.material && object3D.material instanceof Array) { 28 | object3D.material.forEach(material => disposeMaterial(material)) 29 | } else if (object3D.material) { 30 | disposeMaterial(object3D.material) 31 | } 32 | } 33 | 34 | export function deepDispose (object3D) { 35 | object3D.traverse(obj => dispose(obj)) 36 | } 37 | -------------------------------------------------------------------------------- /src/views/Demo.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /src/views/Tutorial.vue: -------------------------------------------------------------------------------- 1 | 688 | 689 | 703 | 704 | 714 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | }, 5 | rules: { 6 | 'import/no-extraneous-dependencies': 'off' 7 | } 8 | } -------------------------------------------------------------------------------- /tests/unit/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import HelloWorld from "@/components/HelloWorld.vue"; 3 | 4 | describe("HelloWorld.vue", () => { 5 | it("renders props.msg when passed", () => { 6 | const msg = "new message"; 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }); 10 | expect(wrapper.text()).toMatch(msg); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: process.env.NODE_ENV === 'production' 3 | ? '/article-assets/build-a-responsive-dashboard-with-vue-js/demo/' 4 | : '/', 5 | devServer: { 6 | port: 47000 7 | }, 8 | pages: { 9 | 'main': 'src/main.js', 10 | 'demo-iframe': 'src/demo-iframe.js' 11 | } 12 | } 13 | --------------------------------------------------------------------------------