├── .gitignore ├── config ├── environments │ ├── production.js │ └── development.js └── index.js ├── public ├── favicon.ico └── fonts │ ├── aileron-thin-webfont.woff │ ├── aileron-thin-webfont.woff2 │ ├── aileron-ultralight-webfont.woff │ ├── aileron-ultralight-webfont.woff2 │ └── aileron.css ├── webpackConfig ├── common.config.js ├── webpack.server.babel.js ├── webpack.base.js ├── webpack.server.prod.babel.js ├── webpack.client.prod.babel.js ├── webpack.server.dev.babel.js └── webpack.hot.js ├── .eslintignore ├── .eslintrc ├── src ├── vuex │ ├── getters.js │ ├── store.js │ └── actions.js ├── clientEntry.js ├── containers │ ├── App.vue │ ├── Landing │ │ ├── Landing.vue │ │ └── Landing.js │ ├── ServerApp.js │ ├── ClientApp.js │ └── sharedAttr.js ├── scss │ ├── mixins.scss │ ├── gradients.scss │ ├── main.scss │ ├── transitions.scss │ ├── helpers.scss │ ├── normalize.css │ └── grid12.scss ├── components │ ├── LoadingScreen │ │ ├── LoadingScreen.js │ │ └── LoadingScreen.vue │ └── Todos │ │ ├── TodoInput │ │ ├── TodoInput.js │ │ └── TodoInput.vue │ │ ├── Todos.js │ │ ├── TodoItem │ │ ├── EditTodo │ │ │ ├── EditTodo.js │ │ │ └── EditTodo.vue │ │ ├── TodoItem.vue │ │ └── TodoItem.js │ │ └── Todos.vue ├── routes │ ├── serverRoutes.js │ └── clientRoutes.js └── serverEntry.js ├── .babelrc ├── serverRoutes ├── catchAll.js └── index.js ├── data └── todos.js ├── views ├── helpers │ ├── generateInitialState.js │ ├── getProductionBundle.js │ ├── getManifest.js │ ├── generateClosingTags.js │ ├── getDevBundle.js │ └── generateHead.js └── index.js ├── server.js ├── README.md ├── package.json └── controllers └── todos.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/dist/ 3 | dist/ 4 | *.swp 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /config/environments/production.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'port': '8080', 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slegrib/vue-todo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /webpackConfig/common.config.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'vue', 3 | 'vue-router', 4 | ]; 5 | -------------------------------------------------------------------------------- /config/environments/development.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'port': '8080', 3 | 'hot': '3000', 4 | }; 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.base.js 2 | webpack.client.prod.js 3 | webpack.server.dev.js 4 | webpack.server.prod.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/vuex/getters.js: -------------------------------------------------------------------------------- 1 | // get the current state of the todos 2 | export const getTodos = state => state.todos; 3 | -------------------------------------------------------------------------------- /public/fonts/aileron-thin-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slegrib/vue-todo/HEAD/public/fonts/aileron-thin-webfont.woff -------------------------------------------------------------------------------- /public/fonts/aileron-thin-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slegrib/vue-todo/HEAD/public/fonts/aileron-thin-webfont.woff2 -------------------------------------------------------------------------------- /public/fonts/aileron-ultralight-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slegrib/vue-todo/HEAD/public/fonts/aileron-ultralight-webfont.woff -------------------------------------------------------------------------------- /public/fonts/aileron-ultralight-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slegrib/vue-todo/HEAD/public/fonts/aileron-ultralight-webfont.woff2 -------------------------------------------------------------------------------- /src/clientEntry.js: -------------------------------------------------------------------------------- 1 | import App from './containers/ClientApp'; 2 | import css from './scss/main.scss'; 3 | 4 | 5 | App.$mount('#root'); 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false 8 | } 9 | -------------------------------------------------------------------------------- /src/containers/App.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /serverRoutes/catchAll.js: -------------------------------------------------------------------------------- 1 | // catch all route for the single page application 2 | import renderApp from '../views'; 3 | 4 | export default (server) => { 5 | server.get('*', renderApp); 6 | }; 7 | -------------------------------------------------------------------------------- /data/todos.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { id: 1, done: true, description: 'Create the best todo app ever' }, 3 | { id: 2, done: false, description: 'Get Interview' }, 4 | { id: 3, done: false, description: 'Get Hired' }, 5 | ]; 6 | -------------------------------------------------------------------------------- /views/helpers/generateInitialState.js: -------------------------------------------------------------------------------- 1 | export default (context) => { 2 | const initialState = context.initialState || 'initial state'; 3 | return ``; 4 | }; 5 | -------------------------------------------------------------------------------- /src/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin transform($transforms) { 2 | -moz-transform: $transforms; 3 | -o-transform: $transforms; 4 | -ms-transform: $transforms; 5 | -webkit-transform: $transforms; 6 | transform: $transforms; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/LoadingScreen/LoadingScreen.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is a loading screen for the app. When the app finishes mounting then the 3 | top level component sets its state to loading = false. Then unmounts this 4 | component. 5 | */ 6 | export default { 7 | props: { 8 | loading: Boolean, 9 | required: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/LoadingScreen/LoadingScreen.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import development from './environments/development'; 2 | import production from './environments/production'; 3 | 4 | const config = { 5 | 'development': development, 6 | 'production': production, 7 | }; 8 | 9 | // export the right config settings depending on the node environment 10 | export default config[process.env.NODE_ENV]; 11 | -------------------------------------------------------------------------------- /src/components/Todos/TodoInput/TodoInput.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data() { 3 | return { 4 | value: '', 5 | }; 6 | }, 7 | methods: { 8 | handleSubmit() { 9 | if (this.value.length > 0) { 10 | this.$store.dispatch('createTodo', { description: this.value, component: this }); 11 | } 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/containers/Landing/Landing.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/Todos/Todos.js: -------------------------------------------------------------------------------- 1 | import { mapGetters } from 'vuex'; 2 | import TodoInput from './TodoInput/TodoInput.vue'; 3 | import TodoItem from './TodoItem/TodoItem.vue'; 4 | 5 | export default { 6 | components: { 7 | 'todo-input': TodoInput, 8 | 'todo-item': TodoItem, 9 | }, 10 | computed: { 11 | // map the todos from vuex 12 | ...mapGetters({ 13 | todos: 'getTodos', 14 | }), 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /views/helpers/getProductionBundle.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { createBundleRenderer } from 'vue-server-renderer'; 4 | 5 | export default ()=> { 6 | const projectRoot = path.resolve(__dirname, '../../'); 7 | const filePath = path.join(projectRoot, 'dist/bundle.server.js'); 8 | const code = fs.readFileSync(filePath, 'utf8'); 9 | 10 | const bundleRenderer = createBundleRenderer(code); 11 | return bundleRenderer; 12 | }; 13 | -------------------------------------------------------------------------------- /views/helpers/getManifest.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | // root of the project folder 4 | const projectRoot = path.resolve(__dirname, '../../'); 5 | const manifestPath = path.join(projectRoot, 'public/dist/manifest.json'); 6 | // read the file using node's built in file system 7 | const manifestJSON = fs.readFileSync(manifestPath, 'utf8'); 8 | // parse the manifest to use when importing client side javascript files 9 | const manifest = JSON.parse(manifestJSON); 10 | 11 | export default manifest; 12 | -------------------------------------------------------------------------------- /views/helpers/generateClosingTags.js: -------------------------------------------------------------------------------- 1 | import manifest from './getManifest.js'; 2 | import config from '../../config'; 3 | const hotPort = config.hot; 4 | const NODE_ENV = process.env.NODE_ENV; 5 | const isProd = NODE_ENV === 'production'; 6 | 7 | const bundlePath = isProd ? manifest['/dist/bundle.js'] : `http://localhost:${hotPort}/dist/bundle.js`; 8 | 9 | const closingTags = 10 | ` 11 | 12 | 13 | 14 | `; 15 | 16 | export default (res, context)=> { 17 | res.end(closingTags); 18 | }; 19 | -------------------------------------------------------------------------------- /src/containers/Landing/Landing.js: -------------------------------------------------------------------------------- 1 | import Todos from '../../components/Todos/Todos.vue'; 2 | 3 | // this function would normally be used if 4 | const fetchInitialData = store => store.dispatch('getTodos'); 5 | 6 | export default { 7 | // if we had data stored on a database and we were not server side rendering 8 | // we would run this prefetch function in the mounted() function 9 | // we could also detect this prefetch request in the serverEntry file durring 10 | // the server side render 11 | prefetch: fetchInitialData, 12 | components: { 13 | todos: Todos, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/containers/ServerApp.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from '../routes/serverRoutes'; 4 | import sharedAttr from './sharedAttr'; 5 | import store from '../vuex/store'; 6 | 7 | // The application component that contains everything 8 | const ServerApp = new Vue({ 9 | name: 'ServerApp', 10 | router, 11 | store, 12 | ...App, 13 | metaInfo: sharedAttr.metaInfo, 14 | components: sharedAttr.components, 15 | data() { 16 | return { 17 | loading: true, 18 | }; 19 | }, 20 | }); 21 | 22 | export { ServerApp, router, store }; 23 | -------------------------------------------------------------------------------- /src/routes/serverRoutes.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Meta from 'vue-meta'; 3 | import VueRouter from 'vue-router'; 4 | import Landing from '../containers/Landing/Landing.vue'; 5 | 6 | // Prepare vue plugins 7 | Vue.use(VueRouter); 8 | Vue.use(Meta); 9 | 10 | // define the routes in your application 11 | const routes = [ 12 | { path: '/', component: Landing }, 13 | ]; 14 | 15 | 16 | // Create the vue router for the server 17 | export default new VueRouter({ 18 | mode: 'abstract', // for server side rendering we need to set this to abstract 19 | routes, // set the routes 20 | }); 21 | -------------------------------------------------------------------------------- /public/fonts/aileron.css: -------------------------------------------------------------------------------- 1 | /* Generated by Font Squirrel (https://www.fontsquirrel.com) on March 26, 2017 */ 2 | @font-face { 3 | font-family: 'aileronthin'; 4 | src: url('aileron-thin-webfont.woff2') format('woff2'), 5 | url('aileron-thin-webfont.woff') format('woff'); 6 | font-weight: normal; 7 | font-style: normal; 8 | 9 | } 10 | 11 | @font-face { 12 | font-family: 'aileronultralight'; 13 | src: url('aileron-ultralight-webfont.woff2') format('woff2'), 14 | url('aileron-ultralight-webfont.woff') format('woff'); 15 | font-weight: normal; 16 | font-style: normal; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/clientRoutes.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import Meta from 'vue-meta'; 4 | import Landing from '../containers/Landing/Landing.vue'; 5 | 6 | // Prepare vue plugins 7 | Vue.use(VueRouter); 8 | Vue.use(Meta); 9 | 10 | // define the routes in your application 11 | const routes = [ 12 | { path: '/', component: Landing }, 13 | ]; 14 | 15 | export default new VueRouter({ 16 | mode: 'history', 17 | /* 18 | set the scroll behavior of the router to scroll to the top when the route 19 | changes 20 | */ 21 | scrollBehavior(to, from, scrollPosition) { 22 | if (scrollPosition) { 23 | return scrollPosition; 24 | } 25 | return { 26 | y: 0, 27 | }; 28 | }, 29 | routes, // short for routes: routes es6 syntax 30 | }); 31 | -------------------------------------------------------------------------------- /src/scss/gradients.scss: -------------------------------------------------------------------------------- 1 | // found on uigradients.com 2 | .endless-river { 3 | background: #43cea2; /* fallback for old browsers */ 4 | background: -webkit-linear-gradient(-45deg, rgba(67, 207, 162, 1) , rgba(24, 90, 157, 1)); /* Chrome 10-25, Safari 5.1-6 */ 5 | background: linear-gradient(-45deg, rgba(67, 207, 162, 1) , rgba(24, 90, 157, 1)); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 6 | } 7 | 8 | .endless-river-modal { 9 | background: #43cea2; /* fallback for old browsers */ 10 | background: -webkit-linear-gradient(-45deg, rgba(67, 207, 162, 0.93) , rgba(24, 90, 157, 0.93)); /* Chrome 10-25, Safari 5.1-6 */ 11 | background: linear-gradient(-45deg, rgba(67, 207, 162, 0.93) , rgba(24, 90, 157, 0.93)); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 12 | } 13 | -------------------------------------------------------------------------------- /src/containers/ClientApp.js: -------------------------------------------------------------------------------- 1 | import { sync } from 'vuex-router-sync'; 2 | import Vue from 'vue'; 3 | import store from '../vuex/store'; 4 | import App from './App.vue'; 5 | import router from '../routes/clientRoutes'; 6 | import sharedAttr from './sharedAttr'; 7 | 8 | // sync the store to vue router 9 | sync(store, router); 10 | 11 | // The application component that will be mounted to the div with id "root" 12 | export default new Vue({ 13 | name: 'ClientApp', 14 | router, 15 | store, 16 | ...App, 17 | metaInfo: sharedAttr.metaInfo, 18 | components: sharedAttr.components, 19 | data() { 20 | return { 21 | // set this to false to remove the loading screen 22 | loading: true, 23 | }; 24 | }, 25 | mounted() { 26 | // once the app has rendered turn off the loading screen 27 | this.loading = false; 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* 2 | Todo app built in vue js 3 | Author: Scott Birgel 4 | */ 5 | // main imports 6 | import express from 'express'; 7 | import bodyParser from 'body-parser'; 8 | 9 | // import variables and routes 10 | import config from './config'; 11 | import { serverRoutes } from './serverRoutes'; 12 | 13 | // create the express server 14 | const server = express(); 15 | 16 | // point requests for static assets to the correct folder 17 | server.use(express.static('public')); 18 | 19 | // apply middleware to the express server 20 | server.use(bodyParser.json()); 21 | serverRoutes(server); 22 | 23 | // get the port the app will run on from the serverConfig 24 | const port = config.port; 25 | 26 | // start the app 27 | server.listen(config.port, (err) => { 28 | if (err) { 29 | console.log(err); 30 | } 31 | console.info('🚀 SERVER ONLINE PORT %s 🚀', port); 32 | }); 33 | -------------------------------------------------------------------------------- /src/containers/sharedAttr.js: -------------------------------------------------------------------------------- 1 | import LoadingScreen from '../components/LoadingScreen/LoadingScreen.vue'; 2 | 3 | /* 4 | These are properties that are used on both the client and server side 5 | application. Place tags in metaInfo to be used by vue meta. 6 | */ 7 | export default { 8 | // all titles will be injected into this template 9 | metaInfo: { 10 | titleTemplate: 'Todo App', 11 | htmlAttrs: { 12 | lang: 'en', 13 | }, 14 | meta: [ 15 | { charset: 'utf-8' }, 16 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 17 | ], 18 | link: [ 19 | { rel: 'stylesheet', href: '/fonts/aileron.css' }, 20 | { rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }, 21 | { rel: 'favicon', src: 'favicon.ico' }, 22 | ], 23 | }, 24 | components: { 25 | 'loading-screen': LoadingScreen, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /views/helpers/getDevBundle.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import webpackConfig from '../../webpackConfig/webpack.server.dev.babel.js'; 4 | import { createBundleRenderer } from 'vue-server-renderer'; 5 | 6 | const serverBundleCompiler = webpack(webpackConfig); 7 | const MFS = require('memory-fs'); 8 | const mfs = new MFS(); 9 | 10 | let bundleRenderer; 11 | serverBundleCompiler.outputFileSystem = mfs; 12 | serverBundleCompiler.watch({}, (err, stats)=> { 13 | if (err) {throw err;} 14 | console.log(''); 15 | console.log('🔥 recompiling server bundle 🔥'); 16 | console.log(''); 17 | const webpackStats = stats.toJson(); 18 | webpackStats.errors.forEach(error => console.error(error)); 19 | webpackStats.warnings.forEach(error => console.warn(error)); 20 | const bundlePath = path.resolve(process.cwd(), 'dist/bundle.server.js'); 21 | bundleRenderer = createBundleRenderer(mfs.readFileSync(bundlePath, 'utf-8')); 22 | }); 23 | module.export = bundleRenderer; 24 | -------------------------------------------------------------------------------- /serverRoutes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Apply all routes to the express server object here 3 | */ 4 | import catchAll from './catchAll'; 5 | import todoData from '../data/todos'; 6 | import todos from '../controllers/todos'; 7 | 8 | const data = { 9 | todos: todoData, 10 | }; 11 | // create the controller for the todos API 12 | const controllers = { 13 | todos: todos(data.todos), 14 | }; 15 | 16 | export const serverRoutes = (server) => { 17 | // Todo API 18 | server.get('/api/todos', controllers.todos.getAll); 19 | server.get('/api/todos/:id', controllers.todos.getOne); 20 | server.post('/api/todos', controllers.todos.create); 21 | server.put('/api/todos/:id', controllers.todos.update); 22 | server.delete('/api/todos/:id', controllers.todos.delete); 23 | /* 24 | route: /* 25 | Should be placed at the end of this function. This is the catch all route 26 | that is used to server side render the vue js SPA 27 | */ 28 | catchAll(server); 29 | }; 30 | 31 | export const dataRef = data; 32 | -------------------------------------------------------------------------------- /webpackConfig/webpack.server.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint no-var: 0, no-console: 0 */ 2 | 3 | import webpack from 'webpack'; 4 | import WebpackDevServer from 'webpack-dev-server'; 5 | 6 | import webpackConfig from './webpack.hot.js'; 7 | import serverConfig from '../config'; 8 | 9 | const hotPort = serverConfig.hot; 10 | const compiler = webpack(webpackConfig); 11 | 12 | const devServer = new WebpackDevServer(compiler, { 13 | contentBase: `http://localhost:${hotPort}`, 14 | publicPath: webpackConfig.output.publicPath, 15 | hot: true, 16 | inline: true, 17 | historyApiFallback: true, 18 | quiet: false, 19 | noInfo: false, 20 | lazy: false, 21 | stats: { 22 | colors: true, 23 | hash: false, 24 | version: false, 25 | chunks: false, 26 | children: false, 27 | }, 28 | }); 29 | 30 | devServer.listen(hotPort, 'localhost', err => { 31 | if (err) console.error(err); 32 | console.log( 33 | `=> 🔥 Webpack development server is running on port ${hotPort}` 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; // normalize the browser to override defaults 2 | @import 'grid12.scss'; // simple bootstrap grid system 3 | @import 'mixins.scss'; 4 | @import 'helpers.scss'; // shorthand helper css 5 | @import 'transitions.scss'; // transitions used with vue js transition component 6 | @import 'gradients.scss'; // gradients used ont he app 7 | 8 | // more specific css used throught the webapp 9 | * { 10 | outline: none; 11 | box-sizing: border-box; 12 | } 13 | body { 14 | background: #f8f8f8; 15 | color: black; 16 | font-family: 'aileronultralight'; 17 | } 18 | 19 | #todo-item { 20 | padding: 24px 24px 24px 75px; 21 | background: white; 22 | border-radius: 3px; 23 | font-family: 'aileronthin'; 24 | font-size: 1.1em; 25 | margin: 12px 0px; 26 | } 27 | 28 | #edit-textarea { 29 | min-height: 30px; 30 | width: 500px; 31 | background: none; 32 | border: none; 33 | border-bottom: 1px solid white; 34 | resize: none; 35 | color: white; 36 | } 37 | -------------------------------------------------------------------------------- /src/serverEntry.js: -------------------------------------------------------------------------------- 1 | // server-entry.js 2 | // import Vue from 'vue'; 3 | import { ServerApp, router, store } from './containers/ServerApp'; 4 | // import data from '../data/todos'; 5 | 6 | const meta = ServerApp.$meta(); 7 | // which will receive the context of the render call when rendering on the 8 | // the server 9 | export default (context) => { 10 | // data pre-fetching 11 | router.push(context.url); 12 | // pass in the meta data passed on the route 13 | context.meta = meta; 14 | return Promise.all(router.getMatchedComponents().map((component) => { 15 | if (component.prefetch) { 16 | // the intial data is passed into context at the initial render 17 | return store.commit('TODOS_LIST', context.initialState); 18 | // return component.prefetch(store); 19 | } 20 | })).then(() => { 21 | // set initial store on context 22 | // the request handler will inline the state in the HTML response. 23 | context.initialState = store.state; 24 | return ServerApp; 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/scss/transitions.scss: -------------------------------------------------------------------------------- 1 | // transition used on most items throughout the app 2 | .cubic-bezier { 3 | -webkit-transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1); 4 | -moz-transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1); 5 | transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1); 6 | } 7 | .cubic-bezier-color { 8 | -webkit-transition: color 450ms cubic-bezier(0.23, 1, 0.32, 1), border 450ms cubic-bezier(0.23, 1, 0.32, 1); 9 | -moz-transition: color 450ms cubic-bezier(0.23, 1, 0.32, 1), border 450ms cubic-bezier(0.23, 1, 0.32, 1); 10 | transition: color 450ms cubic-bezier(0.23, 1, 0.32, 1), border 450ms cubic-bezier(0.23, 1, 0.32, 1); 11 | } 12 | // simple fade 13 | .fade-enter-active, .fade-leave-active { 14 | transition: opacity .5s 15 | } 16 | .fade-enter, .fade-leave-to /* .fade-leave-active in <2.1.8 */ { 17 | opacity: 0 18 | } 19 | 20 | // simple fade 21 | .todo-enter-active, .todo-leave-active { 22 | transition: all .5s; 23 | } 24 | .todo-enter, .todo-leave-to /* .fade-leave-active in <2.1.8 */ { 25 | opacity: 0; 26 | @include transform(translateX(100%)); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Todos/TodoItem/EditTodo/EditTodo.js: -------------------------------------------------------------------------------- 1 | import autosize from 'autosize'; 2 | 3 | export default { 4 | props: { 5 | edit: { 6 | type: Boolean, 7 | required: true, 8 | }, 9 | closeEdit: { 10 | type: Function, 11 | required: true, 12 | }, 13 | updateDescription: { 14 | type: Function, 15 | required: true, 16 | }, 17 | description: { 18 | type: String, 19 | required: true, 20 | }, 21 | }, 22 | data() { 23 | return { 24 | value: this.description, 25 | originalValue: this.description, 26 | }; 27 | }, 28 | mounted() { 29 | autosize(this.$refs.textarea); 30 | this.$refs.textarea.focus(); 31 | }, 32 | methods: { 33 | attemptUpdate() { 34 | if (this.value !== this.originalValue && this.value !== '') { 35 | this.updateDescription(this.value); 36 | this.closeEdit(); 37 | } 38 | }, 39 | }, 40 | computed: { 41 | addBtn() { 42 | return { 43 | width: '25px', 44 | height: '25px', 45 | background: '#43cea2', 46 | }; 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/Todos/Todos.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 41 | -------------------------------------------------------------------------------- /views/helpers/generateHead.js: -------------------------------------------------------------------------------- 1 | import generateInitialState from './generateInitialState'; 2 | import manifest from './getManifest'; 3 | // import config from '../../config'; 4 | // const hotPort = config.hot; 5 | const NODE_ENV = process.env.NODE_ENV; 6 | const isProd = NODE_ENV === 'production'; 7 | 8 | const cssBundle = isProd ? `` : ''; 9 | const commonBundle = isProd ? `` : ''; 10 | 11 | export default (res, context) => { 12 | // get the seo tag attributes from vue-meta 13 | const { 14 | title, htmlAttrs, bodyAttrs, link, style, script, noscript, meta, 15 | } = context.meta.inject(); 16 | 17 | // console.log(`incoming request ${context.url}`); 18 | // generate the Html head tags ans ish 19 | const htmlHead = 20 | ` 21 | 22 | 23 | ${cssBundle} 24 | ${commonBundle} 25 | ${title.text()} 26 | ${meta.text()} 27 | ${link.text()} 28 | ${style.text()} 29 | ${script.text()} 30 | ${noscript.text()} 31 | ${generateInitialState(context)} 32 | 33 | 34 | `; 35 | res.write(htmlHead); 36 | }; 37 | -------------------------------------------------------------------------------- /src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import _ from 'underscore'; 4 | import * as actions from './actions'; 5 | import * as getters from './getters'; 6 | 7 | Vue.use(Vuex); 8 | 9 | const defaultState = { 10 | todos: [], 11 | }; 12 | 13 | const inBrowser = typeof window !== 'undefined'; 14 | 15 | const state = (inBrowser && window.__INITIAL_STATE__) || defaultState; 16 | 17 | const updateTodoKeys = (index, storeState, todo) => { 18 | Object.keys(todo).forEach((key) => { 19 | storeState.todos[index][key] = todo[key]; 20 | }); 21 | }; 22 | 23 | const mutations = { 24 | TODOS_LIST: (storeState, todos) => { 25 | storeState.todos = todos; 26 | }, 27 | UPDATE_TODO: (storeState, todo) => { 28 | const index = _.findIndex(storeState.todos, { id: todo.id }); 29 | updateTodoKeys(index, storeState, todo); 30 | }, 31 | DELETE_TODO: (storeState, index) => { 32 | storeState.todos.splice(index, 1); 33 | }, 34 | CREATE_TODO: (storeState, commitInfo) => { 35 | // put the new todo at the top of the list 36 | storeState.todos.unshift(commitInfo.todo); 37 | commitInfo.component.value = ''; 38 | }, 39 | }; 40 | 41 | export default new Vuex.Store({ 42 | state, 43 | actions, 44 | mutations, 45 | getters, 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/Todos/TodoInput/TodoInput.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-todo 2 | A todo app built in vue js to demonstrate server side rendering and client side hydration 3 | 4 | ![Alt Vue js todo app](http://i.imgur.com/CWI3nxW.png) 5 | 6 | ## Installation 7 | This application was built using `node v7.9.0` `npm v4.5.0` `vue v2.2.6`. 8 | 9 | 1. Install the dependencies 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | 2. Run the application in production mode 16 | 17 | ``` 18 | npm run prod 19 | ``` 20 | 21 | 3. Point the browser to: 22 | 23 | ``` 24 | http://localhost:8080/ 25 | ``` 26 | 27 | ## Development Environment 28 | All commands can also be found in the package.json scripts 29 | 30 | 1. Open a shell and run: 31 | 32 | ``` 33 | npm run dev:server 34 | ``` 35 | 36 | 2. Open another shell and run: 37 | 38 | ``` 39 | npm run hot 40 | ``` 41 | 42 | 3. Point the browser to: 43 | 44 | ``` 45 | http://localhost:8080/ 46 | ``` 47 | 48 | Now you have hot module replacement for both the server side bundle and client bundle. 49 | **Both of these must be running in order to run the app.** 50 | 51 | ## About 52 | This todo app was an exercise in order to understand how to create a single page server side rendered vue js application. 53 | It also demonstrates how to properly use vuex, vue-router, and vue transitions. The todos are stored on the server in memory and are also prefetched before the initial page render. The client side then hydrates the data and puts it in a vuex store. Feel free to let me know how it can be improved. 54 | -------------------------------------------------------------------------------- /src/components/Todos/TodoItem/EditTodo/EditTodo.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /webpackConfig/webpack.base.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import ManifestPlugin from 'webpack-manifest-plugin'; 4 | 5 | const projectRoot = path.resolve(__dirname, '..'); 6 | const appSrc = path.join(projectRoot, 'src'); 7 | const NODE_ENV = process.env.NODE_ENV; 8 | 9 | export default () => { return { 10 | context: appSrc, 11 | /* 12 | * resolve lets Webpack now in advance what file extensions you plan on 13 | * "require"ing into the web application, and allows you to drop them 14 | * in your code. 15 | */ 16 | resolve: { 17 | root: appSrc, 18 | extensions: ['', '.vue', '.js', '.json'], 19 | alias: { 20 | vue: 'vue/dist/vue.common.js', 21 | }, 22 | }, 23 | /* 24 | For SSR we need to externalize all modules listed under "dependencies" 25 | in the package.json file 26 | */ 27 | // externals: Object.keys(require('./package.json').dependencies), 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.json$/, 32 | loader: 'json-loader', 33 | }, 34 | { 35 | test: /\.vue$/, 36 | loader: 'vue', 37 | }, 38 | { 39 | test: /\.js$/, 40 | loader: 'babel', 41 | // include: projectRoot, 42 | exclude: /node_modules/, 43 | }, 44 | ], 45 | }, 46 | plugins: [ 47 | new webpack.DefinePlugin({ 48 | 'process.env': { 49 | NODE_ENV: JSON.stringify(NODE_ENV), 50 | }, 51 | // TRACE_TURBOLINKS: devBuild, 52 | }), 53 | ], 54 | }}; 55 | -------------------------------------------------------------------------------- /webpackConfig/webpack.server.prod.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | const path = require('path'); 3 | // root of the project folder 4 | const projectRoot = path.resolve(__dirname, '..'); 5 | // path in which webpack will put the build 6 | const outputPath = path.join(projectRoot, 'dist'); 7 | // import the base webpack config 8 | import webpackBase from './webpack.base.js'; 9 | const config = webpackBase(); 10 | 11 | config.target = 'node'; 12 | config.entry = { 13 | 'skadii': [path.join(projectRoot, 'src/serverEntry.js')], 14 | }; 15 | 16 | config.output = { 17 | libraryTarget: 'commonjs2', // !different 18 | path: outputPath, 19 | filename: 'bundle.server.js', 20 | }; 21 | 22 | // add plugins to minize code and to optomize 23 | config.plugins.push( 24 | new webpack.optimize.OccurenceOrderPlugin(), 25 | new webpack.optimize.DedupePlugin(), 26 | new webpack.optimize.UglifyJsPlugin({ 27 | compress: { 28 | warnings: false, 29 | }, 30 | }), 31 | ); 32 | 33 | export default config; 34 | // module.exports = { 35 | // target: 'node', // !different 36 | // entry: path.join(projectRoot, 'src/serverEntry.js'), 37 | // output: { 38 | // libraryTarget: 'commonjs2', // !different 39 | // path: path.join(projectRoot, 'dist'), 40 | // filename: 'bundle.server.js', 41 | // }, 42 | // module: { 43 | // loaders: [ 44 | // { 45 | // test: /\.vue$/, 46 | // loader: 'vue', 47 | // }, 48 | // { 49 | // test: /\.js$/, 50 | // loader: 'babel', 51 | // include: projectRoot, 52 | // exclude: /node_modules/, 53 | // }, 54 | // ], 55 | // }, 56 | // }; 57 | -------------------------------------------------------------------------------- /webpackConfig/webpack.client.prod.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ManifestPlugin from 'webpack-manifest-plugin'; 3 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 4 | import WebpackMd5Hash from 'webpack-md5-hash'; 5 | // the packages that will be bundled into the vendor file 6 | import commonConfig from './common.config.js'; 7 | 8 | const path = require('path'); 9 | // get the root of the project 10 | const projectRoot = path.resolve(__dirname, '..'); 11 | 12 | // import the base webpack config 13 | import webpackBase from './webpack.base.js'; 14 | const config = webpackBase(); 15 | 16 | // entrypoint for webpack to start bundling 17 | config.entry = { 18 | 'bundle': [path.join(projectRoot, 'src/clientEntry.js')], 19 | 'common': commonConfig, 20 | }; 21 | 22 | // destination for webpack bundle 23 | config.output = { 24 | path: path.join(projectRoot, 'public/dist'), 25 | filename: 'bundle.[chunkhash].js', 26 | }; 27 | 28 | config.module.loaders.push({ 29 | test: /\.scss$/, 30 | // loaders: ['style-loader', 'css-loader', 'sass-loader'], 31 | loader: ExtractTextPlugin.extract( 32 | 'style', 33 | 'css!sass'), 34 | }); 35 | 36 | // add plugins to minize code and to optimize 37 | config.plugins.push( 38 | new WebpackMd5Hash(), 39 | new ManifestPlugin({ 40 | fileName: 'manifest.json', 41 | basePath: '/dist/', 42 | }), 43 | new ExtractTextPlugin('bundle.[chunkhash].css'), 44 | new webpack.optimize.DedupePlugin(), 45 | new webpack.optimize.CommonsChunkPlugin({ 46 | name: 'common', 47 | minChunks: Infinity, 48 | }), 49 | new webpack.optimize.UglifyJsPlugin({ 50 | compress: { 51 | warnings: false, 52 | }, 53 | }), 54 | new webpack.optimize.OccurenceOrderPlugin(), 55 | ); 56 | 57 | // export the config 58 | export default config; 59 | -------------------------------------------------------------------------------- /views/index.js: -------------------------------------------------------------------------------- 1 | // import fs from 'fs'; 2 | import path from 'path'; 3 | import { createBundleRenderer } from 'vue-server-renderer'; 4 | import generateHead from './helpers/generateHead'; 5 | import generateClosingTags from './helpers/generateClosingTags'; 6 | import getProductionBundle from './helpers/getProductionBundle'; 7 | import webpackConfig from '../webpackConfig/webpack.server.dev.babel'; 8 | import { dataRef } from '../serverRoutes/'; 9 | // import getDevBundle from './helpers/getDevBundle.js'; 10 | 11 | const NODE_ENV = process.env.NODE_ENV; 12 | // this is the Vue bundleRenderer that is set depending on the node env 13 | let bundleRenderer; 14 | if (NODE_ENV === 'production') { 15 | bundleRenderer = getProductionBundle(); 16 | } else { 17 | const webpack = require('webpack'); 18 | const serverBundleCompiler = webpack(webpackConfig); 19 | const MFS = require('memory-fs'); 20 | const mfs = new MFS(); 21 | serverBundleCompiler.outputFileSystem = mfs; 22 | serverBundleCompiler.watch({}, (err, stats) => { 23 | if (err) { throw err; } 24 | console.log(''); 25 | console.log('🔥 recompiling server bundle 🔥'); 26 | console.log(''); 27 | const webpackStats = stats.toJson(); 28 | webpackStats.errors.forEach(error => console.error(error)); 29 | webpackStats.warnings.forEach(error => console.warn(error)); 30 | const bundlePath = path.resolve(process.cwd(), 'dist/bundle.server.js'); 31 | bundleRenderer = createBundleRenderer(mfs.readFileSync(bundlePath, 'utf-8')); 32 | }); 33 | } 34 | // const bundleRenderer = require('vue-server-renderer').createBundleRenderer(code) 35 | 36 | export default (req, res) => { 37 | const context = { url: req.url }; 38 | context.initialState = dataRef.todos; 39 | const stream = bundleRenderer.renderToStream(context); 40 | 41 | stream.once('data', () => { 42 | generateHead(res, context); 43 | }); 44 | // write the ServerApp chunks 45 | stream.on('data', (chunk) => { 46 | res.write(chunk); 47 | }); 48 | 49 | stream.on('end', () => { 50 | generateClosingTags(res, context); 51 | }); 52 | 53 | // incase of a stream error 54 | stream.on('error', error => res.status(500).end(`
${error.stack}
`)); 55 | // res.send('it worked'); 56 | }; 57 | -------------------------------------------------------------------------------- /src/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import request from 'axios'; 2 | 3 | /* 4 | About actions.js 5 | The actions used by vuex in order to communicate with the server. I am using 6 | axios as our promise based HTTP client of choice. One should only commit 7 | changes to the client side vuex store when the server sends a response of 200. 8 | Never update the vuex store without using the commit function. 9 | */ 10 | 11 | // the name space of the todos api 12 | const api = '/api/todos/'; 13 | 14 | /* 15 | Get all the todos from the server then commit them to the vuex store. Mainly 16 | used to get the initial todos from the server when the app loads. Since we are 17 | hydrating this data already this may not be nessesary to use. 18 | */ 19 | export const getTodos = function (commit) { 20 | return request.get('/api/todos').then((response) => { 21 | if (response.statusText === 'OK') { 22 | commit('TODOS_LIST', response.data); 23 | } 24 | }).catch((error) => { 25 | console.log(error); 26 | }); 27 | }; 28 | 29 | /* 30 | Updates a todo on the server. Must provide the entire new state of the updated 31 | todo. It will replace the todo given an id. 32 | */ 33 | export const updateTodo = function ({ commit, state }, todo) { 34 | const requestUrl = `${api}${todo.id}`; 35 | return request.put(requestUrl, todo).then((response) => { 36 | if (response.status === 200) { 37 | commit('UPDATE_TODO', response.data); 38 | } 39 | }).catch((error) => { 40 | console.log(error); 41 | }); 42 | }; 43 | 44 | /* 45 | Provided an id removes the todo from the list after the server responds 46 | */ 47 | export const deleteTodo = function ({ commit, state }, deleteInfo) { 48 | const requestUrl = `${api}${deleteInfo.id}`; 49 | return request.delete(requestUrl).then((response) => { 50 | if (response.status === 200) { 51 | commit('DELETE_TODO', deleteInfo.index); 52 | } 53 | }).catch((error) => { 54 | console.log(error); 55 | }); 56 | }; 57 | 58 | /* 59 | create a new todo item and push it to the client side vuex store 60 | */ 61 | export const createTodo = function ({ commit, state }, createInfo) { 62 | return request.post(api, { description: createInfo.description }).then((response) => { 63 | if (response.status === 200) { 64 | console.log(response.data.id); 65 | const commitInfo = { 66 | todo: response.data, 67 | component: createInfo.component, 68 | }; 69 | commit('CREATE_TODO', commitInfo); 70 | } 71 | }).catch((error) => { 72 | console.log(error); 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/Todos/TodoItem/TodoItem.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 39 | 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-todo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "author": "Scott Birgel", 6 | "engines": { 7 | "node": "6" 8 | }, 9 | "scripts": { 10 | "start": "NODE_ENV=development babel-node server.js", 11 | "dev:server": "NODE_ENV=development babel-node server.js", 12 | "hot": "NODE_ENV=development babel-node ./webpackConfig/webpack.server.babel.js", 13 | "prod:build:client": "npm run clobber:client && NODE_ENV=production webpack --config ./webpackConfig/webpack.client.prod.babel.js", 14 | "prod:build:server": "npm run clobber:server && NODE_ENV=production webpack --config ./webpackConfig/webpack.server.prod.babel.js", 15 | "prod:build:both": "npm run prod:build:client && npm run prod:build:server", 16 | "clobber:client": "rm -rf public/dist", 17 | "clobber:server": "rm -rf dist", 18 | "prod": "npm run prod:build:both && NODE_ENV=production babel-node server.js" 19 | }, 20 | "dependencies": { 21 | "autosize": "^3.0.20", 22 | "axios": "^0.15.3", 23 | "babel-cli": "^6.24.1", 24 | "babel-eslint": "^7.2.2", 25 | "babel-loader": "^6.4.1", 26 | "babel-plugin-transform-runtime": "^6.23.0", 27 | "babel-preset-es2015": "^6.24.1", 28 | "babel-preset-stage-0": "^6.24.1", 29 | "body-parser": "^1.17.1", 30 | "css-loader": "^0.26.4", 31 | "eslint": "^3.19.0", 32 | "eslint-plugin-import": "^2.2.0", 33 | "eslint-plugin-jsx-a11y": "^4.0.0", 34 | "eslint-plugin-react": "^6.10.3", 35 | "express": "^4.15.2", 36 | "node-sass": "^4.5.2", 37 | "style-loader": "^0.13.2", 38 | "underscore": "^1.8.3", 39 | "vue": "^2.2.6", 40 | "vue-loader": "^10.3.0", 41 | "vue-meta": "^0.5.6", 42 | "vue-router": "^2.4.0", 43 | "vue-server-renderer": "^2.2.6", 44 | "vue-template-compiler": "^2.2.6", 45 | "vuex": "^2.3.0", 46 | "vuex-router-sync": "^4.1.2", 47 | "webpack": "^1.15.0", 48 | "webpack-dev-server": "^1.16.3" 49 | }, 50 | "devDependencies": { 51 | "babel-cli": "^6.24.1", 52 | "babel-loader": "^6.4.1", 53 | "babel-plugin-transform-runtime": "^6.23.0", 54 | "babel-preset-es2015": "^6.24.1", 55 | "babel-preset-stage-0": "^6.24.1", 56 | "eslint": "^3.18.0", 57 | "eslint-config-airbnb": "^14.1.0", 58 | "eslint-plugin-import": "^2.2.0", 59 | "eslint-plugin-jsx-a11y": "^4.0.0", 60 | "eslint-plugin-react": "^6.10.3", 61 | "extract-text-webpack-plugin": "^1.0.1", 62 | "json-loader": "^0.5.4", 63 | "memory-fs": "^0.4.1", 64 | "node-sass": "^4.3.0", 65 | "sass-loader": "^4.1.1", 66 | "vue-loader": "^10.0.2", 67 | "vue-template-compiler": "^2.1.10", 68 | "webpack-dev-server": "^1.16.2", 69 | "webpack-manifest-plugin": "^1.1.0", 70 | "webpack-md5-hash": "0.0.5" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Todos/TodoItem/TodoItem.js: -------------------------------------------------------------------------------- 1 | import EditTodo from './EditTodo/EditTodo.vue'; 2 | 3 | /* 4 | The todo item component that represents the todo items. 5 | */ 6 | export default { 7 | props: { 8 | // the object that contains the information about the todo 9 | data: { 10 | type: Object, 11 | required: true, 12 | }, 13 | // index of the todo in the list 14 | index: { 15 | type: Number, 16 | required: true, 17 | }, 18 | }, 19 | data() { 20 | return { 21 | edit: false, // is the todo item being edited? 22 | hoverComplete: false, // is the user hovering over the checkmark? 23 | destroyHover: false, // is the user hovering over the destroy todo btn? 24 | }; 25 | }, 26 | methods: { 27 | /* 28 | Handles when the user checks or unchecks a todo item and dispatches 29 | the event to the api. The vuex store is updated when we recieve a response 30 | from the server. See src/vuex/actions.js 31 | */ 32 | handleDone() { 33 | // create the updated todo object and set the done key to the opposite 34 | const updatedTodo = { 35 | id: this.data.id, 36 | done: !this.data.done, 37 | description: this.data.description, 38 | }; 39 | // updatedTodo.done = !updatedTodo.done; 40 | this.$store.dispatch('updateTodo', updatedTodo); 41 | }, 42 | /* 43 | Handles when a user clicks the destroy todo button on the bottom right 44 | hand corner of the todo item. 45 | */ 46 | handleDestroy() { 47 | const deleteInfo = { 48 | id: this.data.id, 49 | index: this.index, 50 | }; 51 | this.$store.dispatch('deleteTodo', deleteInfo); 52 | }, 53 | /* 54 | Close the edit modal. This function is passed down to the edit-todo 55 | component in order to communicate with the state of this component. 56 | */ 57 | closeEdit(e) { 58 | // if there is no event that means we called this function internally. 59 | // i.e. after a user submits an edit to the todo we want to close the modal 60 | if (!e) { 61 | this.edit = false; 62 | } else if (e.target.getAttribute('data-removeedit') === 'true') { 63 | this.edit = false; 64 | } 65 | }, 66 | /* 67 | Update the description of the todo by creating a new object that represents 68 | the new state of the todo and send it to the todo api 69 | */ 70 | updateDescription(newDescription) { 71 | const updatedTodo = { 72 | id: this.data.id, 73 | done: this.data.done, 74 | description: newDescription, 75 | }; 76 | this.$store.dispatch('updateTodo', updatedTodo); 77 | }, 78 | }, 79 | computed: { 80 | // computing the style of the checkmark is a little bit too much for css to handle 81 | checkmark() { 82 | let color = this.hoverComplete ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0)'; 83 | // color = this.destroyHover && !this.data.done ? 'rgba(0,0,0,0)' : color; 84 | color = this.data.done ? '#43cea2' : color; 85 | return { 86 | fontSize: '1.7em', 87 | color, 88 | }; 89 | }, 90 | }, 91 | components: { 92 | // the modal component used for editing the text of a todo 93 | 'edit-todo': EditTodo, 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /webpackConfig/webpack.server.dev.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import baseConfig from './webpack.base.js'; 4 | const projectRoot = path.resolve(__dirname, '..'); 5 | 6 | const config = baseConfig(); 7 | // import serverConfig from './config/serverConfig'; 8 | // const projectRoot = path.resolve(__dirname, '..'); 9 | // const outputPath = path.join(projectRoot, 'dist'); 10 | config.target = 'node'; 11 | config.entry = { 12 | 'skadii': ['../src/serverEntry.js'], 13 | }; 14 | /* 15 | * The combination of path and filename tells Webpack what name to give to 16 | * the final bundled JavaScript file and where to store this file. 17 | */ 18 | config.output = { 19 | // libraryTarget: 'commonjs2', 20 | libraryTarget: 'commonjs2', 21 | path: path.join(projectRoot, 'dist'), 22 | filename: 'bundle.server.js', 23 | }; 24 | 25 | // push hot reload plugins 26 | config.plugins.push( 27 | new webpack.optimize.OccurenceOrderPlugin(), 28 | new webpack.optimize.DedupePlugin() 29 | ); 30 | 31 | config.module.loaders.push( 32 | { 33 | test: /\.scss$/, 34 | loaders: ['style', 'css', 'sass'], 35 | } 36 | ); 37 | 38 | // for the dev server we should externalize modules for performance 39 | config.externals = Object.keys(require('../package.json').dependencies); 40 | 41 | export default config; 42 | 43 | // var ClosureCompilerPlugin = require('webpack-closure-compiler'); 44 | // export default { 45 | // /* 46 | // * app.ts represents the entry point to your web application. Webpack will 47 | // * recursively go through every "require" statement in app.ts and 48 | // * efficiently build out the application's dependency tree. 49 | // */ 50 | // entry: ['./src/serverEntry.js'], 51 | // target: 'node', 52 | // /* 53 | // * The combination of path and filename tells Webpack what name to give to 54 | // * the final bundled JavaScript file and where to store this file. 55 | // */ 56 | // output: { 57 | // libraryTarget: 'commonjs2', 58 | // path: path.resolve(__dirname, 'serverAssets'), 59 | // filename: 'ssr-bundle.js', 60 | // }, 61 | // // this will externalize all modules listed under "dependencies" 62 | // // in your package.json 63 | // externals: Object.keys(require('./package.json').dependencies), 64 | // /* 65 | // * resolve lets Webpack now in advance what file extensions you plan on 66 | // * "require"ing into the web application, and allows you to drop them 67 | // * in your code. 68 | // */ 69 | // resolve: { 70 | // extensions: ['', '.vue', '.js'], 71 | // alias: { 72 | // vue: 'vue/dist/vue.js', 73 | // }, 74 | // }, 75 | // /* 76 | // For SSR we need to externalize all modules listed under "dependencies" 77 | // in the package.json file 78 | // */ 79 | // // externals: Object.keys(require('./package.json').dependencies), 80 | // module: { 81 | // /* 82 | // * Each loader needs an associated Regex test that goes through each 83 | // * of the files you've included (or in this case, all files but the 84 | // * ones in the excluded directories) and finds all files that pass 85 | // * the test. Then it will apply the loader to that file. I haven't 86 | // * installed ts-loader yet, but will do that shortly. 87 | // */ 88 | // loaders: [ 89 | // { 90 | // test: /\.js$/, 91 | // loader: 'babel', 92 | // exclude: /node_modules/, 93 | // }, 94 | // { 95 | // test: /\.vue$/, 96 | // loader: 'vue', 97 | // exclude: /node_modules/, 98 | // }, 99 | // ], 100 | // }, 101 | // }; 102 | -------------------------------------------------------------------------------- /src/scss/helpers.scss: -------------------------------------------------------------------------------- 1 | /* 2 | These helper methods are designed for quick developement by me. They are 3 | abbreviations that I myself am used to. It would be best not to use this in 4 | larger projects because they can get confuing to others. When im working alone 5 | I like to use my set of helpers 6 | */ 7 | .light-font { 8 | font-weight: 300; 9 | } 10 | .white { 11 | color: white; 12 | } 13 | // dimensions 14 | .f-w { 15 | width: 100%; 16 | } 17 | .f-h { 18 | height: 100%; 19 | } 20 | // stands for full-area 21 | .f-a { 22 | width: 100%; 23 | height: 100%; 24 | } 25 | 26 | .p-r { 27 | position: relative; 28 | } 29 | .p-a { 30 | position: absolute; 31 | } 32 | .p-f { 33 | position: fixed; 34 | } 35 | 36 | .t-a-c { 37 | text-align: center; 38 | } 39 | .t-a-l { 40 | text-align: left; 41 | } 42 | .t-a-r { 43 | text-align: right; 44 | } 45 | 46 | .t { 47 | top: 0; 48 | } 49 | .b { 50 | bottom: 0; 51 | } 52 | .l { 53 | left: 0; 54 | } 55 | .r { 56 | right: 0; 57 | } 58 | 59 | .d-i-b { 60 | display: inline-block; 61 | } 62 | .d-b { 63 | display: block; 64 | } 65 | .c-p { 66 | cursor: pointer; 67 | } 68 | 69 | .o-h { 70 | overflow: hidden; 71 | } 72 | .v-a-m { 73 | vertical-align: middle; 74 | display: inline-block; 75 | } 76 | .center { 77 | position: absolute; 78 | top: 50%; 79 | left: 50%; 80 | -webkit-transform: translate(-50%, -50%); 81 | -moz-transform: translate(-50%, -50%); 82 | -ms-transform: translate(-50%, -50%); 83 | transform: translate(-50%, -50%); 84 | } 85 | .center-t { 86 | position: absolute; 87 | top: 0%; 88 | left: 50%; 89 | -webkit-transform: translate(-50%, 0%); 90 | -moz-transform: translate(-50%, 0%); 91 | -ms-transform: translate(-50%, 0%); 92 | transform: translate(-50%, 0%); 93 | } 94 | .center-l { 95 | position: absolute; 96 | top: 50%; 97 | left: 0%; 98 | -webkit-transform: translate(-0%, -50%); 99 | -moz-transform: translate(-0%, -50%); 100 | -ms-transform: translate(-0%, -50%); 101 | transform: translate(-0%, -50%); 102 | } 103 | .center-r { 104 | position: absolute; 105 | top: 50%; 106 | right: 0%; 107 | -webkit-transform: translate(-0%, -50%); 108 | -moz-transform: translate(-0%, -50%); 109 | -ms-transform: translate(-0%, -50%); 110 | transform: translate(-0%, -50%); 111 | } 112 | .center-b { 113 | position: absolute; 114 | bottom: 0%; 115 | left: 50%; 116 | -webkit-transform: translate(-50%, 0%); 117 | -moz-transform: translate(-50%, 0%); 118 | -ms-transform: translate(-50%, 0%); 119 | transform: translate(-50%, 0%); 120 | } 121 | 122 | .center-margins { 123 | margin: 0 auto; 124 | } 125 | 126 | .circle { 127 | border-radius: 50%; 128 | } 129 | 130 | .uncollapse-margins:before, 131 | .uncollapse-margins:after 132 | { 133 | content: "\00a0"; /* No-break space character */ 134 | display: block; 135 | overflow: hidden; 136 | height: 0; 137 | } 138 | 139 | .f-bg { 140 | background-size: cover; 141 | background-repeat: no-repeat; 142 | background-position: 50% 50%; 143 | } 144 | .bold { 145 | font-weight: bold; 146 | } 147 | /* 148 | material design shadows 149 | */ 150 | .z-1 { 151 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 152 | } 153 | .z-hover:hover { 154 | box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); 155 | } 156 | .z-2 { 157 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 158 | } 159 | .z-3 { 160 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); 161 | } 162 | .z-4 { 163 | box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); 164 | } 165 | .z-5 { 166 | box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22); 167 | } 168 | -------------------------------------------------------------------------------- /controllers/todos.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | 3 | module.exports = (todos) => { 4 | const todoAPI = { 5 | 6 | // Gets a list of all the todo items 7 | getAll: (req, res) => { 8 | try { 9 | res.status(200).json(todos); 10 | } catch (e) { 11 | res.status(500).send('An error occured while attempting to get all of the todo items'); 12 | } 13 | }, 14 | 15 | // Gets a specific todo item, given a supplied ID 16 | getOne: (req, res) => { 17 | let statusCode = 200; 18 | try { 19 | if (!req.params.id) { 20 | statusCode = 500; 21 | throw new Error('An ID was not supplied'); 22 | } 23 | const id = parseInt(req.params.id, 10); 24 | const todo = _.findWhere(todos, { id }); 25 | if (!todo) { 26 | statusCode = 404; 27 | throw new Error('A todo with the given ID was not found'); 28 | } else { 29 | res.status(statusCode).json(todo); 30 | } 31 | } catch (e) { 32 | res.status(statusCode).send(e); 33 | } 34 | }, 35 | 36 | // Create a new todo object and add it to the list 37 | create: (req, res) => { 38 | let statusCode = 200; 39 | try { 40 | /* 41 | get a new ID (would be handled by proepr DB automatically) 42 | this id will equal infinity if there are no todo items. 43 | to avoid errors I check if the todos array is empty and restart 44 | the id count to 1 45 | */ 46 | let id = Math.max.apply(null, _.pluck(todos, 'id')) + 1; 47 | if (todos.length === 0) { 48 | id = 1; 49 | } 50 | const todo = { 51 | id, 52 | done: req.body.done || false, 53 | description: req.body.description, 54 | }; 55 | 56 | if (!req.body.description || req.body.description === '') { 57 | statusCode = 400; 58 | throw new Error('A valid description must be given'); 59 | } 60 | // I changes this to unshift to put the todo at the top of the list. I think it looks better 61 | todos.unshift(todo); 62 | res.status(statusCode).json(todo); 63 | } catch (e) { 64 | res.status(statusCode).send(e); 65 | } 66 | }, 67 | 68 | // Update a todo, given a supplied ID 69 | update: (req, res) => { 70 | let statusCode = 200; 71 | try { 72 | if (!req.params.id) { 73 | statusCode = 500; 74 | throw new Error('An ID was not supplied'); 75 | } 76 | const id = parseInt(req.params.id, 10); 77 | let todo = _.findWhere(todos, { id }); 78 | if (!todo) { 79 | statusCode = 404; 80 | throw new Error('A todo with the given ID was not found'); 81 | } else { 82 | _.map(todos, (t) => { 83 | if (t === todo) { 84 | todo = _.extendOwn(t, req.body); 85 | return todo; 86 | } 87 | }); 88 | res.status(statusCode).json(todo); 89 | } 90 | } catch (e) { 91 | res.status(statusCode).send(e); 92 | } 93 | }, 94 | 95 | // Delete a todo, given a supplied ID 96 | delete: (req, res) => { 97 | let statusCode = 200; 98 | try { 99 | if (!req.params.id) { 100 | statusCode = 500; 101 | throw new Error('An ID was not supplied'); 102 | } 103 | const id = parseInt(req.params.id, 10); 104 | const todo = _.findWhere(todos, { id }); 105 | if (!todo) { 106 | statusCode = 404; 107 | throw new Error('A todo with the given ID was not found'); 108 | } else { 109 | todos.splice(todos.indexOf(todo), 1); 110 | res.status(statusCode).send(true); 111 | } 112 | } catch (e) { 113 | res.status(statusCode).send(e); 114 | } 115 | }, 116 | 117 | }; 118 | 119 | return todoAPI; 120 | }; 121 | -------------------------------------------------------------------------------- /webpackConfig/webpack.hot.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | // the packages that will be bundled into the vendor file 4 | import commonConfig from './common.config.js'; 5 | import serverConfig from '../config'; 6 | 7 | const hotPort = serverConfig.hot; 8 | 9 | const path = require('path'); 10 | // get the root of the project 11 | const projectRoot = path.resolve(__dirname, '..'); 12 | 13 | // import the base webpack config 14 | import webpackBase from './webpack.base.js'; 15 | const config = webpackBase(); 16 | 17 | // entrypoint for webpack to start bundling 18 | config.entry = { 19 | // 'common': commonConfig, 20 | 'skadii': [ 21 | '../src/clientEntry.js', 22 | `webpack-dev-server/client?http://localhost:${hotPort}`, 23 | 'webpack/hot/only-dev-server', 24 | ], 25 | }; 26 | 27 | // destination for webpack bundle 28 | config.output = { 29 | path: path.resolve(__dirname, 'public/dist'), 30 | filename: 'bundle.js', 31 | publicPath: `http://localhost:${hotPort}/dist`, 32 | }; 33 | 34 | config.module.loaders.push({ 35 | test: /\.scss$/, 36 | loaders: ['style', 'css?sourceMap', 'sass?sourceMap'], 37 | // loader: ExtractTextPlugin.extract( 38 | // 'style', 39 | // 'css!sass'), 40 | }); 41 | 42 | // add plugins to minize code and to optimize 43 | config.plugins.push( 44 | new webpack.optimize.DedupePlugin(), 45 | new webpack.HotModuleReplacementPlugin(), 46 | // new webpack.optimize.CommonsChunkPlugin({ 47 | // name: 'common', 48 | // minChunks: Infinity, 49 | // }), 50 | new webpack.optimize.OccurenceOrderPlugin(), 51 | ); 52 | 53 | config.devtool = 'eval-source-map'; 54 | 55 | // export the config 56 | export default config; 57 | 58 | 59 | // import path from 'path'; 60 | // import webpack from 'webpack'; 61 | // import serverConfig from './config/serverConfig'; 62 | // const hotPort = serverConfig.hotPort; 63 | // const sharedConfig = require('./shared.config.js'); 64 | // import baseConfig from './webpack.base.js'; 65 | // const config = baseConfig(); 66 | // const ExtractTextPlugin = require('extract-text-webpack-plugin'); 67 | // /* 68 | // * The combination of path and filename tells Webpack what name to give to 69 | // * the final bundled JavaScript file and where to store this file. 70 | // */ 71 | // config.output = { 72 | // // libraryTarget: 'commonjs2', 73 | // filename: '[name].js', 74 | // path: path.resolve(__dirname, 'public/assets'), 75 | // chunkFilename: '[id]-split.js', 76 | // publicPath: `http://localhost:${hotPort}/assets/`, // public paths for the chunks that are created 77 | // }; 78 | // 79 | // config.entry = { 80 | // 'shared': sharedConfig, 81 | // 'gamii-retail': [ 82 | // './src/clientEntry.js', 83 | // `webpack-dev-server/client?http://localhost:${hotPort}`, 84 | // 'webpack/hot/only-dev-server', 85 | // ], 86 | // }; 87 | // 88 | // config.module.loaders.push( 89 | // /* 90 | // * Each loader needs an associated Regex test that goes through each 91 | // * of the files you've included (or in this case, all files but the 92 | // * ones in the excluded directories) and finds all files that pass 93 | // * the test. Then it will apply the loader to that file. I haven't 94 | // * installed ts-loader yet, but will do that shortly. 95 | // */ 96 | // { 97 | // test: /\.js$/, 98 | // loaders: ['babel'], 99 | // exclude: /node_modules/, 100 | // }, 101 | // { 102 | // test: /\.vue$/, 103 | // loader: 'vue-loader', 104 | // exclude: /node_modules/, 105 | // }, 106 | // { 107 | // test: /\.scss$/, 108 | // // loader: ExtractTextPlugin.extract('css?sourceMap!sass?sourceMap'), 109 | // loaders: ['style', 'css?sourceMap', 'sass?sourceMap'], 110 | // } 111 | // ); 112 | // 113 | // // push hot reload plugins 114 | // config.plugins.push( 115 | // new webpack.optimize.DedupePlugin(), 116 | // new webpack.optimize.OccurenceOrderPlugin(), 117 | // new webpack.HotModuleReplacementPlugin(), 118 | // new webpack.NoErrorsPlugin(), 119 | // // new ExtractTextPlugin('gamii.css', { allChunks: false }), 120 | // new webpack.optimize.CommonsChunkPlugin({ 121 | // // This name 'vendor' ties into the entry definition 122 | // name: 'shared', 123 | // // We don't want the default vendor.js name 124 | // filename: 'shared.js', 125 | // // Passing Infinity just creates the commons chunk, but moves no modules into it. 126 | // // In other words, we only put what's in the vendor entry definition in vendor-bundle.js 127 | // minChunks: Infinity, 128 | // }) 129 | // ); 130 | // 131 | // config.devtool = 'eval-source-map'; 132 | // 133 | // export default config; 134 | -------------------------------------------------------------------------------- /src/scss/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Correct the line height in all browsers. 6 | * 3. Prevent adjustments of font size after orientation changes in 7 | * IE on Windows Phone and in iOS. 8 | */ 9 | 10 | /* Document 11 | ========================================================================== */ 12 | 13 | html { 14 | font-family: sans-serif; /* 1 */ 15 | line-height: 1.15; /* 2 */ 16 | -ms-text-size-adjust: 100%; /* 3 */ 17 | -webkit-text-size-adjust: 100%; /* 3 */ 18 | } 19 | 20 | /* Sections 21 | ========================================================================== */ 22 | 23 | /** 24 | * Remove the margin in all browsers (opinionated). 25 | */ 26 | 27 | body { 28 | margin: 0; 29 | } 30 | 31 | /** 32 | * Add the correct display in IE 9-. 33 | */ 34 | 35 | article, 36 | aside, 37 | footer, 38 | header, 39 | nav, 40 | section { 41 | display: block; 42 | } 43 | 44 | /** 45 | * Correct the font size and margin on `h1` elements within `section` and 46 | * `article` contexts in Chrome, Firefox, and Safari. 47 | */ 48 | 49 | h1 { 50 | font-size: 2em; 51 | margin: 0.67em 0; 52 | } 53 | 54 | /* Grouping content 55 | ========================================================================== */ 56 | 57 | /** 58 | * Add the correct display in IE 9-. 59 | * 1. Add the correct display in IE. 60 | */ 61 | 62 | figcaption, 63 | figure, 64 | main { /* 1 */ 65 | display: block; 66 | } 67 | 68 | /** 69 | * Add the correct margin in IE 8. 70 | */ 71 | 72 | figure { 73 | margin: 1em 40px; 74 | } 75 | 76 | /** 77 | * 1. Add the correct box sizing in Firefox. 78 | * 2. Show the overflow in Edge and IE. 79 | */ 80 | 81 | hr { 82 | box-sizing: content-box; /* 1 */ 83 | height: 0; /* 1 */ 84 | overflow: visible; /* 2 */ 85 | } 86 | 87 | /** 88 | * 1. Correct the inheritance and scaling of font size in all browsers. 89 | * 2. Correct the odd `em` font sizing in all browsers. 90 | */ 91 | 92 | pre { 93 | font-family: monospace, monospace; /* 1 */ 94 | font-size: 1em; /* 2 */ 95 | } 96 | 97 | /* Text-level semantics 98 | ========================================================================== */ 99 | 100 | /** 101 | * 1. Remove the gray background on active links in IE 10. 102 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 103 | */ 104 | 105 | a { 106 | background-color: transparent; /* 1 */ 107 | -webkit-text-decoration-skip: objects; /* 2 */ 108 | } 109 | 110 | /** 111 | * Remove the outline on focused links when they are also active or hovered 112 | * in all browsers (opinionated). 113 | */ 114 | 115 | a:active, 116 | a:hover { 117 | outline-width: 0; 118 | } 119 | 120 | /** 121 | * 1. Remove the bottom border in Firefox 39-. 122 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 123 | */ 124 | 125 | abbr[title] { 126 | border-bottom: none; /* 1 */ 127 | text-decoration: underline; /* 2 */ 128 | text-decoration: underline dotted; /* 2 */ 129 | } 130 | 131 | /** 132 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 133 | */ 134 | 135 | b, 136 | strong { 137 | font-weight: inherit; 138 | } 139 | 140 | /** 141 | * Add the correct font weight in Chrome, Edge, and Safari. 142 | */ 143 | 144 | b, 145 | strong { 146 | font-weight: bolder; 147 | } 148 | 149 | /** 150 | * 1. Correct the inheritance and scaling of font size in all browsers. 151 | * 2. Correct the odd `em` font sizing in all browsers. 152 | */ 153 | 154 | code, 155 | kbd, 156 | samp { 157 | font-family: monospace, monospace; /* 1 */ 158 | font-size: 1em; /* 2 */ 159 | } 160 | 161 | /** 162 | * Add the correct font style in Android 4.3-. 163 | */ 164 | 165 | dfn { 166 | font-style: italic; 167 | } 168 | 169 | /** 170 | * Add the correct background and color in IE 9-. 171 | */ 172 | 173 | mark { 174 | background-color: #ff0; 175 | color: #000; 176 | } 177 | 178 | /** 179 | * Add the correct font size in all browsers. 180 | */ 181 | 182 | small { 183 | font-size: 80%; 184 | } 185 | 186 | /** 187 | * Prevent `sub` and `sup` elements from affecting the line height in 188 | * all browsers. 189 | */ 190 | 191 | sub, 192 | sup { 193 | font-size: 75%; 194 | line-height: 0; 195 | position: relative; 196 | vertical-align: baseline; 197 | } 198 | 199 | sub { 200 | bottom: -0.25em; 201 | } 202 | 203 | sup { 204 | top: -0.5em; 205 | } 206 | 207 | /* Embedded content 208 | ========================================================================== */ 209 | 210 | /** 211 | * Add the correct display in IE 9-. 212 | */ 213 | 214 | audio, 215 | video { 216 | display: inline-block; 217 | } 218 | 219 | /** 220 | * Add the correct display in iOS 4-7. 221 | */ 222 | 223 | audio:not([controls]) { 224 | display: none; 225 | height: 0; 226 | } 227 | 228 | /** 229 | * Remove the border on images inside links in IE 10-. 230 | */ 231 | 232 | img { 233 | border-style: none; 234 | } 235 | 236 | /** 237 | * Hide the overflow in IE. 238 | */ 239 | 240 | svg:not(:root) { 241 | overflow: hidden; 242 | } 243 | 244 | /* Forms 245 | ========================================================================== */ 246 | 247 | /** 248 | * 1. Change the font styles in all browsers (opinionated). 249 | * 2. Remove the margin in Firefox and Safari. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | font-family: sans-serif; /* 1 */ 258 | font-size: 100%; /* 1 */ 259 | line-height: 1.15; /* 1 */ 260 | margin: 0; /* 2 */ 261 | } 262 | 263 | /** 264 | * Show the overflow in IE. 265 | * 1. Show the overflow in Edge. 266 | */ 267 | 268 | button, 269 | input { /* 1 */ 270 | overflow: visible; 271 | } 272 | 273 | /** 274 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 275 | * 1. Remove the inheritance of text transform in Firefox. 276 | */ 277 | 278 | button, 279 | select { /* 1 */ 280 | text-transform: none; 281 | } 282 | 283 | /** 284 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 285 | * controls in Android 4. 286 | * 2. Correct the inability to style clickable types in iOS and Safari. 287 | */ 288 | 289 | button, 290 | html [type="button"], /* 1 */ 291 | [type="reset"], 292 | [type="submit"] { 293 | -webkit-appearance: button; /* 2 */ 294 | } 295 | 296 | /** 297 | * Remove the inner border and padding in Firefox. 298 | */ 299 | 300 | button::-moz-focus-inner, 301 | [type="button"]::-moz-focus-inner, 302 | [type="reset"]::-moz-focus-inner, 303 | [type="submit"]::-moz-focus-inner { 304 | border-style: none; 305 | padding: 0; 306 | } 307 | 308 | /** 309 | * Restore the focus styles unset by the previous rule. 310 | */ 311 | 312 | button:-moz-focusring, 313 | [type="button"]:-moz-focusring, 314 | [type="reset"]:-moz-focusring, 315 | [type="submit"]:-moz-focusring { 316 | outline: 1px dotted ButtonText; 317 | } 318 | 319 | /** 320 | * Change the border, margin, and padding in all browsers (opinionated). 321 | */ 322 | 323 | fieldset { 324 | border: 1px solid #c0c0c0; 325 | margin: 0 2px; 326 | padding: 0.35em 0.625em 0.75em; 327 | } 328 | 329 | /** 330 | * 1. Correct the text wrapping in Edge and IE. 331 | * 2. Correct the color inheritance from `fieldset` elements in IE. 332 | * 3. Remove the padding so developers are not caught out when they zero out 333 | * `fieldset` elements in all browsers. 334 | */ 335 | 336 | legend { 337 | box-sizing: border-box; /* 1 */ 338 | color: inherit; /* 2 */ 339 | display: table; /* 1 */ 340 | max-width: 100%; /* 1 */ 341 | padding: 0; /* 3 */ 342 | white-space: normal; /* 1 */ 343 | } 344 | 345 | /** 346 | * 1. Add the correct display in IE 9-. 347 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 348 | */ 349 | 350 | progress { 351 | display: inline-block; /* 1 */ 352 | vertical-align: baseline; /* 2 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * 1. Correct the inability to style clickable types in iOS and Safari. 404 | * 2. Change font properties to `inherit` in Safari. 405 | */ 406 | 407 | ::-webkit-file-upload-button { 408 | -webkit-appearance: button; /* 1 */ 409 | font: inherit; /* 2 */ 410 | } 411 | 412 | /* Interactive 413 | ========================================================================== */ 414 | 415 | /* 416 | * Add the correct display in IE 9-. 417 | * 1. Add the correct display in Edge, IE, and Firefox. 418 | */ 419 | 420 | details, /* 1 */ 421 | menu { 422 | display: block; 423 | } 424 | 425 | /* 426 | * Add the correct display in all browsers. 427 | */ 428 | 429 | summary { 430 | display: list-item; 431 | } 432 | 433 | /* Scripting 434 | ========================================================================== */ 435 | 436 | /** 437 | * Add the correct display in IE 9-. 438 | */ 439 | 440 | canvas { 441 | display: inline-block; 442 | } 443 | 444 | /** 445 | * Add the correct display in IE. 446 | */ 447 | 448 | template { 449 | display: none; 450 | } 451 | 452 | /* Hidden 453 | ========================================================================== */ 454 | 455 | /** 456 | * Add the correct display in IE 10-. 457 | */ 458 | 459 | [hidden] { 460 | display: none; 461 | } 462 | -------------------------------------------------------------------------------- /src/scss/grid12.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap Grid System 2 | @-ms-viewport { 3 | width: device-width; 4 | } 5 | .visible-xs, 6 | .visible-sm, 7 | .visible-md, 8 | .visible-lg { 9 | display: none !important; 10 | } 11 | .visible-xs-block, 12 | .visible-xs-inline, 13 | .visible-xs-inline-block, 14 | .visible-sm-block, 15 | .visible-sm-inline, 16 | .visible-sm-inline-block, 17 | .visible-md-block, 18 | .visible-md-inline, 19 | .visible-md-inline-block, 20 | .visible-lg-block, 21 | .visible-lg-inline, 22 | .visible-lg-inline-block { 23 | display: none !important; 24 | } 25 | @media (max-width: 767px) { 26 | .visible-xs { 27 | display: block !important; 28 | } 29 | table.visible-xs { 30 | display: table; 31 | } 32 | tr.visible-xs { 33 | display: table-row !important; 34 | } 35 | th.visible-xs, 36 | td.visible-xs { 37 | display: table-cell !important; 38 | } 39 | } 40 | @media (max-width: 767px) { 41 | .visible-xs-block { 42 | display: block !important; 43 | } 44 | } 45 | @media (max-width: 767px) { 46 | .visible-xs-inline { 47 | display: inline !important; 48 | } 49 | } 50 | @media (max-width: 767px) { 51 | .visible-xs-inline-block { 52 | display: inline-block !important; 53 | } 54 | } 55 | @media (min-width: 768px) and (max-width: 991px) { 56 | .visible-sm { 57 | display: block !important; 58 | } 59 | table.visible-sm { 60 | display: table; 61 | } 62 | tr.visible-sm { 63 | display: table-row !important; 64 | } 65 | th.visible-sm, 66 | td.visible-sm { 67 | display: table-cell !important; 68 | } 69 | } 70 | @media (min-width: 768px) and (max-width: 991px) { 71 | .visible-sm-block { 72 | display: block !important; 73 | } 74 | } 75 | @media (min-width: 768px) and (max-width: 991px) { 76 | .visible-sm-inline { 77 | display: inline !important; 78 | } 79 | } 80 | @media (min-width: 768px) and (max-width: 991px) { 81 | .visible-sm-inline-block { 82 | display: inline-block !important; 83 | } 84 | } 85 | @media (min-width: 992px) and (max-width: 1199px) { 86 | .visible-md { 87 | display: block !important; 88 | } 89 | table.visible-md { 90 | display: table; 91 | } 92 | tr.visible-md { 93 | display: table-row !important; 94 | } 95 | th.visible-md, 96 | td.visible-md { 97 | display: table-cell !important; 98 | } 99 | } 100 | @media (min-width: 992px) and (max-width: 1199px) { 101 | .visible-md-block { 102 | display: block !important; 103 | } 104 | } 105 | @media (min-width: 992px) and (max-width: 1199px) { 106 | .visible-md-inline { 107 | display: inline !important; 108 | } 109 | } 110 | @media (min-width: 992px) and (max-width: 1199px) { 111 | .visible-md-inline-block { 112 | display: inline-block !important; 113 | } 114 | } 115 | @media (min-width: 1200px) { 116 | .visible-lg { 117 | display: block !important; 118 | } 119 | table.visible-lg { 120 | display: table; 121 | } 122 | tr.visible-lg { 123 | display: table-row !important; 124 | } 125 | th.visible-lg, 126 | td.visible-lg { 127 | display: table-cell !important; 128 | } 129 | } 130 | @media (min-width: 1200px) { 131 | .visible-lg-block { 132 | display: block !important; 133 | } 134 | } 135 | @media (min-width: 1200px) { 136 | .visible-lg-inline { 137 | display: inline !important; 138 | } 139 | } 140 | @media (min-width: 1200px) { 141 | .visible-lg-inline-block { 142 | display: inline-block !important; 143 | } 144 | } 145 | @media (max-width: 767px) { 146 | .hidden-xs { 147 | display: none !important; 148 | } 149 | } 150 | @media (min-width: 768px) and (max-width: 991px) { 151 | .hidden-sm { 152 | display: none !important; 153 | } 154 | } 155 | @media (min-width: 992px) and (max-width: 1199px) { 156 | .hidden-md { 157 | display: none !important; 158 | } 159 | } 160 | @media (min-width: 1200px) { 161 | .hidden-lg { 162 | display: none !important; 163 | } 164 | } 165 | .visible-print { 166 | display: none !important; 167 | } 168 | @media print { 169 | .visible-print { 170 | display: block !important; 171 | } 172 | table.visible-print { 173 | display: table; 174 | } 175 | tr.visible-print { 176 | display: table-row !important; 177 | } 178 | th.visible-print, 179 | td.visible-print { 180 | display: table-cell !important; 181 | } 182 | } 183 | .visible-print-block { 184 | display: none !important; 185 | } 186 | @media print { 187 | .visible-print-block { 188 | display: block !important; 189 | } 190 | } 191 | .visible-print-inline { 192 | display: none !important; 193 | } 194 | @media print { 195 | .visible-print-inline { 196 | display: inline !important; 197 | } 198 | } 199 | .visible-print-inline-block { 200 | display: none !important; 201 | } 202 | @media print { 203 | .visible-print-inline-block { 204 | display: inline-block !important; 205 | } 206 | } 207 | @media print { 208 | .hidden-print { 209 | display: none !important; 210 | } 211 | } 212 | .container { 213 | margin-right: auto; 214 | margin-left: auto; 215 | padding-left: 15px; 216 | padding-right: 15px; 217 | } 218 | @media (min-width: 768px) { 219 | .container { 220 | width: 750px; 221 | } 222 | } 223 | @media (min-width: 992px) { 224 | .container { 225 | width: 970px; 226 | } 227 | } 228 | @media (min-width: 1200px) { 229 | .container { 230 | width: 1170px; 231 | } 232 | } 233 | .container-fluid { 234 | margin-right: auto; 235 | margin-left: auto; 236 | padding-left: 15px; 237 | padding-right: 15px; 238 | } 239 | .row { 240 | margin-left: -15px; 241 | margin-right: -15px; 242 | } 243 | .col, .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { 244 | position: relative; 245 | min-height: 1px; 246 | padding-left: 15px; 247 | padding-right: 15px; 248 | } 249 | .col, .col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { 250 | float: left; 251 | } 252 | .col-xs-12 { 253 | width: 100%; 254 | } 255 | .col-xs-11 { 256 | width: 91.66666667%; 257 | } 258 | .col-xs-10 { 259 | width: 83.33333333%; 260 | } 261 | .col-xs-9 { 262 | width: 75%; 263 | } 264 | .col-xs-8 { 265 | width: 66.66666667%; 266 | } 267 | .col-xs-7 { 268 | width: 58.33333333%; 269 | } 270 | .col-xs-6 { 271 | width: 50%; 272 | } 273 | .col-xs-5 { 274 | width: 41.66666667%; 275 | } 276 | .col-xs-4 { 277 | width: 33.33333333%; 278 | } 279 | .col-xs-3 { 280 | width: 25%; 281 | } 282 | .col-xs-2 { 283 | width: 16.66666667%; 284 | } 285 | .col-xs-1 { 286 | width: 8.33333333%; 287 | } 288 | .col-xs-pull-12 { 289 | right: 100%; 290 | } 291 | .col-xs-pull-11 { 292 | right: 91.66666667%; 293 | } 294 | .col-xs-pull-10 { 295 | right: 83.33333333%; 296 | } 297 | .col-xs-pull-9 { 298 | right: 75%; 299 | } 300 | .col-xs-pull-8 { 301 | right: 66.66666667%; 302 | } 303 | .col-xs-pull-7 { 304 | right: 58.33333333%; 305 | } 306 | .col-xs-pull-6 { 307 | right: 50%; 308 | } 309 | .col-xs-pull-5 { 310 | right: 41.66666667%; 311 | } 312 | .col-xs-pull-4 { 313 | right: 33.33333333%; 314 | } 315 | .col-xs-pull-3 { 316 | right: 25%; 317 | } 318 | .col-xs-pull-2 { 319 | right: 16.66666667%; 320 | } 321 | .col-xs-pull-1 { 322 | right: 8.33333333%; 323 | } 324 | .col-xs-pull-0 { 325 | right: auto; 326 | } 327 | .col-xs-push-12 { 328 | left: 100%; 329 | } 330 | .col-xs-push-11 { 331 | left: 91.66666667%; 332 | } 333 | .col-xs-push-10 { 334 | left: 83.33333333%; 335 | } 336 | .col-xs-push-9 { 337 | left: 75%; 338 | } 339 | .col-xs-push-8 { 340 | left: 66.66666667%; 341 | } 342 | .col-xs-push-7 { 343 | left: 58.33333333%; 344 | } 345 | .col-xs-push-6 { 346 | left: 50%; 347 | } 348 | .col-xs-push-5 { 349 | left: 41.66666667%; 350 | } 351 | .col-xs-push-4 { 352 | left: 33.33333333%; 353 | } 354 | .col-xs-push-3 { 355 | left: 25%; 356 | } 357 | .col-xs-push-2 { 358 | left: 16.66666667%; 359 | } 360 | .col-xs-push-1 { 361 | left: 8.33333333%; 362 | } 363 | .col-xs-push-0 { 364 | left: auto; 365 | } 366 | .col-xs-offset-12 { 367 | margin-left: 100%; 368 | } 369 | .col-xs-offset-11 { 370 | margin-left: 91.66666667%; 371 | } 372 | .col-xs-offset-10 { 373 | margin-left: 83.33333333%; 374 | } 375 | .col-xs-offset-9 { 376 | margin-left: 75%; 377 | } 378 | .col-xs-offset-8 { 379 | margin-left: 66.66666667%; 380 | } 381 | .col-xs-offset-7 { 382 | margin-left: 58.33333333%; 383 | } 384 | .col-xs-offset-6 { 385 | margin-left: 50%; 386 | } 387 | .col-xs-offset-5 { 388 | margin-left: 41.66666667%; 389 | } 390 | .col-xs-offset-4 { 391 | margin-left: 33.33333333%; 392 | } 393 | .col-xs-offset-3 { 394 | margin-left: 25%; 395 | } 396 | .col-xs-offset-2 { 397 | margin-left: 16.66666667%; 398 | } 399 | .col-xs-offset-1 { 400 | margin-left: 8.33333333%; 401 | } 402 | .col-xs-offset-0 { 403 | margin-left: 0%; 404 | } 405 | @media (min-width: 768px) { 406 | .col, .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { 407 | float: left; 408 | } 409 | .col-sm-12 { 410 | width: 100%; 411 | } 412 | .col-sm-11 { 413 | width: 91.66666667%; 414 | } 415 | .col-sm-10 { 416 | width: 83.33333333%; 417 | } 418 | .col-sm-9 { 419 | width: 75%; 420 | } 421 | .col-sm-8 { 422 | width: 66.66666667%; 423 | } 424 | .col-sm-7 { 425 | width: 58.33333333%; 426 | } 427 | .col-sm-6 { 428 | width: 50%; 429 | } 430 | .col-sm-5 { 431 | width: 41.66666667%; 432 | } 433 | .col-sm-4 { 434 | width: 33.33333333%; 435 | } 436 | .col-sm-3 { 437 | width: 25%; 438 | } 439 | .col-sm-2 { 440 | width: 16.66666667%; 441 | } 442 | .col-sm-1 { 443 | width: 8.33333333%; 444 | } 445 | .col-sm-pull-12 { 446 | right: 100%; 447 | } 448 | .col-sm-pull-11 { 449 | right: 91.66666667%; 450 | } 451 | .col-sm-pull-10 { 452 | right: 83.33333333%; 453 | } 454 | .col-sm-pull-9 { 455 | right: 75%; 456 | } 457 | .col-sm-pull-8 { 458 | right: 66.66666667%; 459 | } 460 | .col-sm-pull-7 { 461 | right: 58.33333333%; 462 | } 463 | .col-sm-pull-6 { 464 | right: 50%; 465 | } 466 | .col-sm-pull-5 { 467 | right: 41.66666667%; 468 | } 469 | .col-sm-pull-4 { 470 | right: 33.33333333%; 471 | } 472 | .col-sm-pull-3 { 473 | right: 25%; 474 | } 475 | .col-sm-pull-2 { 476 | right: 16.66666667%; 477 | } 478 | .col-sm-pull-1 { 479 | right: 8.33333333%; 480 | } 481 | .col-sm-pull-0 { 482 | right: auto; 483 | } 484 | .col-sm-push-12 { 485 | left: 100%; 486 | } 487 | .col-sm-push-11 { 488 | left: 91.66666667%; 489 | } 490 | .col-sm-push-10 { 491 | left: 83.33333333%; 492 | } 493 | .col-sm-push-9 { 494 | left: 75%; 495 | } 496 | .col-sm-push-8 { 497 | left: 66.66666667%; 498 | } 499 | .col-sm-push-7 { 500 | left: 58.33333333%; 501 | } 502 | .col-sm-push-6 { 503 | left: 50%; 504 | } 505 | .col-sm-push-5 { 506 | left: 41.66666667%; 507 | } 508 | .col-sm-push-4 { 509 | left: 33.33333333%; 510 | } 511 | .col-sm-push-3 { 512 | left: 25%; 513 | } 514 | .col-sm-push-2 { 515 | left: 16.66666667%; 516 | } 517 | .col-sm-push-1 { 518 | left: 8.33333333%; 519 | } 520 | .col-sm-push-0 { 521 | left: auto; 522 | } 523 | .col-sm-offset-12 { 524 | margin-left: 100%; 525 | } 526 | .col-sm-offset-11 { 527 | margin-left: 91.66666667%; 528 | } 529 | .col-sm-offset-10 { 530 | margin-left: 83.33333333%; 531 | } 532 | .col-sm-offset-9 { 533 | margin-left: 75%; 534 | } 535 | .col-sm-offset-8 { 536 | margin-left: 66.66666667%; 537 | } 538 | .col-sm-offset-7 { 539 | margin-left: 58.33333333%; 540 | } 541 | .col-sm-offset-6 { 542 | margin-left: 50%; 543 | } 544 | .col-sm-offset-5 { 545 | margin-left: 41.66666667%; 546 | } 547 | .col-sm-offset-4 { 548 | margin-left: 33.33333333%; 549 | } 550 | .col-sm-offset-3 { 551 | margin-left: 25%; 552 | } 553 | .col-sm-offset-2 { 554 | margin-left: 16.66666667%; 555 | } 556 | .col-sm-offset-1 { 557 | margin-left: 8.33333333%; 558 | } 559 | .col-sm-offset-0 { 560 | margin-left: 0%; 561 | } 562 | } 563 | @media (min-width: 992px) { 564 | .col, .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { 565 | float: left; 566 | } 567 | .col-md-12 { 568 | width: 100%; 569 | } 570 | .col-md-11 { 571 | width: 91.66666667%; 572 | } 573 | .col-md-10 { 574 | width: 83.33333333%; 575 | } 576 | .col-md-9 { 577 | width: 75%; 578 | } 579 | .col-md-8 { 580 | width: 66.66666667%; 581 | } 582 | .col-md-7 { 583 | width: 58.33333333%; 584 | } 585 | .col-md-6 { 586 | width: 50%; 587 | } 588 | .col-md-5 { 589 | width: 41.66666667%; 590 | } 591 | .col-md-4 { 592 | width: 33.33333333%; 593 | } 594 | .col-md-3 { 595 | width: 25%; 596 | } 597 | .col-md-2 { 598 | width: 16.66666667%; 599 | } 600 | .col-md-1 { 601 | width: 8.33333333%; 602 | } 603 | .col-md-pull-12 { 604 | right: 100%; 605 | } 606 | .col-md-pull-11 { 607 | right: 91.66666667%; 608 | } 609 | .col-md-pull-10 { 610 | right: 83.33333333%; 611 | } 612 | .col-md-pull-9 { 613 | right: 75%; 614 | } 615 | .col-md-pull-8 { 616 | right: 66.66666667%; 617 | } 618 | .col-md-pull-7 { 619 | right: 58.33333333%; 620 | } 621 | .col-md-pull-6 { 622 | right: 50%; 623 | } 624 | .col-md-pull-5 { 625 | right: 41.66666667%; 626 | } 627 | .col-md-pull-4 { 628 | right: 33.33333333%; 629 | } 630 | .col-md-pull-3 { 631 | right: 25%; 632 | } 633 | .col-md-pull-2 { 634 | right: 16.66666667%; 635 | } 636 | .col-md-pull-1 { 637 | right: 8.33333333%; 638 | } 639 | .col-md-pull-0 { 640 | right: auto; 641 | } 642 | .col-md-push-12 { 643 | left: 100%; 644 | } 645 | .col-md-push-11 { 646 | left: 91.66666667%; 647 | } 648 | .col-md-push-10 { 649 | left: 83.33333333%; 650 | } 651 | .col-md-push-9 { 652 | left: 75%; 653 | } 654 | .col-md-push-8 { 655 | left: 66.66666667%; 656 | } 657 | .col-md-push-7 { 658 | left: 58.33333333%; 659 | } 660 | .col-md-push-6 { 661 | left: 50%; 662 | } 663 | .col-md-push-5 { 664 | left: 41.66666667%; 665 | } 666 | .col-md-push-4 { 667 | left: 33.33333333%; 668 | } 669 | .col-md-push-3 { 670 | left: 25%; 671 | } 672 | .col-md-push-2 { 673 | left: 16.66666667%; 674 | } 675 | .col-md-push-1 { 676 | left: 8.33333333%; 677 | } 678 | .col-md-push-0 { 679 | left: auto; 680 | } 681 | .col-md-offset-12 { 682 | margin-left: 100%; 683 | } 684 | .col-md-offset-11 { 685 | margin-left: 91.66666667%; 686 | } 687 | .col-md-offset-10 { 688 | margin-left: 83.33333333%; 689 | } 690 | .col-md-offset-9 { 691 | margin-left: 75%; 692 | } 693 | .col-md-offset-8 { 694 | margin-left: 66.66666667%; 695 | } 696 | .col-md-offset-7 { 697 | margin-left: 58.33333333%; 698 | } 699 | .col-md-offset-6 { 700 | margin-left: 50%; 701 | } 702 | .col-md-offset-5 { 703 | margin-left: 41.66666667%; 704 | } 705 | .col-md-offset-4 { 706 | margin-left: 33.33333333%; 707 | } 708 | .col-md-offset-3 { 709 | margin-left: 25%; 710 | } 711 | .col-md-offset-2 { 712 | margin-left: 16.66666667%; 713 | } 714 | .col-md-offset-1 { 715 | margin-left: 8.33333333%; 716 | } 717 | .col-md-offset-0 { 718 | margin-left: 0%; 719 | } 720 | } 721 | @media (min-width: 1200px) { 722 | .col, .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { 723 | float: left; 724 | } 725 | .col-lg-12 { 726 | width: 100%; 727 | } 728 | .col-lg-11 { 729 | width: 91.66666667%; 730 | } 731 | .col-lg-10 { 732 | width: 83.33333333%; 733 | } 734 | .col-lg-9 { 735 | width: 75%; 736 | } 737 | .col-lg-8 { 738 | width: 66.66666667%; 739 | } 740 | .col-lg-7 { 741 | width: 58.33333333%; 742 | } 743 | .col-lg-6 { 744 | width: 50%; 745 | } 746 | .col-lg-5 { 747 | width: 41.66666667%; 748 | } 749 | .col-lg-4 { 750 | width: 33.33333333%; 751 | } 752 | .col-lg-3 { 753 | width: 25%; 754 | } 755 | .col-lg-2 { 756 | width: 16.66666667%; 757 | } 758 | .col-lg-1 { 759 | width: 8.33333333%; 760 | } 761 | .col-lg-pull-12 { 762 | right: 100%; 763 | } 764 | .col-lg-pull-11 { 765 | right: 91.66666667%; 766 | } 767 | .col-lg-pull-10 { 768 | right: 83.33333333%; 769 | } 770 | .col-lg-pull-9 { 771 | right: 75%; 772 | } 773 | .col-lg-pull-8 { 774 | right: 66.66666667%; 775 | } 776 | .col-lg-pull-7 { 777 | right: 58.33333333%; 778 | } 779 | .col-lg-pull-6 { 780 | right: 50%; 781 | } 782 | .col-lg-pull-5 { 783 | right: 41.66666667%; 784 | } 785 | .col-lg-pull-4 { 786 | right: 33.33333333%; 787 | } 788 | .col-lg-pull-3 { 789 | right: 25%; 790 | } 791 | .col-lg-pull-2 { 792 | right: 16.66666667%; 793 | } 794 | .col-lg-pull-1 { 795 | right: 8.33333333%; 796 | } 797 | .col-lg-pull-0 { 798 | right: auto; 799 | } 800 | .col-lg-push-12 { 801 | left: 100%; 802 | } 803 | .col-lg-push-11 { 804 | left: 91.66666667%; 805 | } 806 | .col-lg-push-10 { 807 | left: 83.33333333%; 808 | } 809 | .col-lg-push-9 { 810 | left: 75%; 811 | } 812 | .col-lg-push-8 { 813 | left: 66.66666667%; 814 | } 815 | .col-lg-push-7 { 816 | left: 58.33333333%; 817 | } 818 | .col-lg-push-6 { 819 | left: 50%; 820 | } 821 | .col-lg-push-5 { 822 | left: 41.66666667%; 823 | } 824 | .col-lg-push-4 { 825 | left: 33.33333333%; 826 | } 827 | .col-lg-push-3 { 828 | left: 25%; 829 | } 830 | .col-lg-push-2 { 831 | left: 16.66666667%; 832 | } 833 | .col-lg-push-1 { 834 | left: 8.33333333%; 835 | } 836 | .col-lg-push-0 { 837 | left: auto; 838 | } 839 | .col-lg-offset-12 { 840 | margin-left: 100%; 841 | } 842 | .col-lg-offset-11 { 843 | margin-left: 91.66666667%; 844 | } 845 | .col-lg-offset-10 { 846 | margin-left: 83.33333333%; 847 | } 848 | .col-lg-offset-9 { 849 | margin-left: 75%; 850 | } 851 | .col-lg-offset-8 { 852 | margin-left: 66.66666667%; 853 | } 854 | .col-lg-offset-7 { 855 | margin-left: 58.33333333%; 856 | } 857 | .col-lg-offset-6 { 858 | margin-left: 50%; 859 | } 860 | .col-lg-offset-5 { 861 | margin-left: 41.66666667%; 862 | } 863 | .col-lg-offset-4 { 864 | margin-left: 33.33333333%; 865 | } 866 | .col-lg-offset-3 { 867 | margin-left: 25%; 868 | } 869 | .col-lg-offset-2 { 870 | margin-left: 16.66666667%; 871 | } 872 | .col-lg-offset-1 { 873 | margin-left: 8.33333333%; 874 | } 875 | .col-lg-offset-0 { 876 | margin-left: 0%; 877 | } 878 | } 879 | .clearfix, 880 | .clearfix:before, 881 | .clearfix:after, 882 | .container:before, 883 | .container:after, 884 | .container-fluid:before, 885 | .container-fluid:after, 886 | .row:before, 887 | .row:after { 888 | content: " "; 889 | display: table; 890 | } 891 | .clearfix:after, 892 | .container:after, 893 | .container-fluid:after, 894 | .row:after { 895 | clear: both; 896 | } 897 | .center-block { 898 | display: block; 899 | margin-left: auto; 900 | margin-right: auto; 901 | } 902 | .pull-right { 903 | float: right !important; 904 | } 905 | .pull-left { 906 | float: left !important; 907 | } 908 | *, 909 | *:before, 910 | *:after { 911 | -webkit-box-sizing: border-box; 912 | -moz-box-sizing: border-box; 913 | box-sizing: border-box; 914 | } 915 | --------------------------------------------------------------------------------