├── .env_sample ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── scripts └── webpack-config-output.js ├── server.js ├── src ├── assets │ ├── Muli-Light.ttf │ ├── Muli-Regular.ttf │ └── logo.png ├── style.scss └── web.entry.js ├── webpack-dev-middleware.js ├── webpack-server-compiler.js └── webpack.config.js /.env_sample: -------------------------------------------------------------------------------- 1 | PORT=9000 2 | NODE_ENV=development 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | .idea 5 | .env 6 | dist 7 | tmp 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue.js Calendar 2 | 3 | Source code for the [Build A Professional Vue App with Vuex & Server-Side Rendering](https://courses.vuejsdevelopers.com/p/build-vue-vuex-app-ssr?utm_source=github-vjd). 4 | 5 | #### Demo 6 | 7 | See the completed project here: [http://vuejs-calendar.vuejsdevelopers.com/](http://vuejs-calendar.vuejsdevelopers.com/) 8 | 9 | #### Pre-installation 10 | 11 | - Ensure [Node.js >=5.10](https://nodejs.org/en/download/), [NPM](https://docs.npmjs.com) and [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) are installed on your system 12 | 13 | #### Installation 14 | 15 | 1. Install this code on your local system 16 | 17 | 1. Fork this repository (click 'Fork' button in top right corner) 18 | 2. Clone the forked repository on your local file system 19 | 20 | ``` 21 | cd /path/to/install/location 22 | 23 | git clone https://github.com/[your_username]/vuejs-calendar.git 24 | ``` 25 | 26 | 2. Change into directory 27 | 28 | ``` 29 | cd vuejs-calendar 30 | ``` 31 | 32 | 3. Install dependencies 33 | 34 | ``` 35 | cd vuejs-calendar 36 | npm install 37 | ``` 38 | 39 | 4. Create a `.env` file by copying the sample 40 | 41 | ``` 42 | cp .env_sample .env 43 | ``` 44 | 45 | Edit the .env file and replace any variables if needed 46 | 47 | 5. Start project 48 | 49 | ``` 50 | npm run start 51 | ``` 52 | 53 | Your site will be available at *localhost:[PORT]* where `PORT` is whatever value is set in your `.env` file. 54 | 55 | ## Sponsors 56 | 57 | Vue SchoolSnipcart 58 | 59 | [Support Vue.js Developers](https://www.patreon.com/anthonygore?utm-source=github-vjd&utm-medium=link&utm_campaign=sponsors) to get your logo here. 60 | 61 | ## Lecture branches 62 | 63 | Each branch of of the repo shows the state of the code at the end of any particular video e.g. `video/06` shows the state at the end of video 6. 64 | 65 | If you want the initial state of the code, use the `master` branch. 66 | 67 | If you're doing the *Vue.js Essentials - 3 Course Bundle* course on Udemy, you'll need the following conversion table to match the branch to the lecture number. 68 | 69 | | Lecture # | Branch name | 70 | | - | - | 71 | | 121 | video/02 | 72 | | 125 | video/06 | 73 | | 126 | video/07 | 74 | | 128 | video/09 | 75 | | 129 | video/10 | 76 | | 134 | video/15 | 77 | | 136 | video/17 | 78 | | 137 | video/18 | 79 | | 138 | video/19 | 80 | | 139 | video/20 | 81 | | 141 | video/22 | 82 | | 142 | video/23 | 83 | | 144 | video/25 | 84 | | 148 | video/29 | 85 | | 149 | video/30 | 86 | | 150 | video/31 | 87 | | 151 | video/32 | 88 | | 152 | video/33 | 89 | | 155 | video/36 | 90 | | 157 | video/38 | 91 | | 158 | video/39 | 92 | | 162 | video/43 | 93 | | 164 | video/45 | 94 | | 167 | video/48 | 95 | | 169 | video/50 | 96 | | 172 | video/53 | 97 | | 173 | video/54 | 98 | | 175 | video/56 | 99 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue.js Calendar 7 | 8 | 9 | 10 | 11 |
{{ msg }}
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-calendar", 3 | "version": "1.1.0", 4 | "description": "Ultimate Vue.js Developers Course - Vue.js Calendar", 5 | "main": "server.js", 6 | "author": "Anthony Gore ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/vuejsdevelopers/vuejs-calendar" 10 | }, 11 | "license": "UNLICENSED", 12 | "scripts": { 13 | "start": "nodemon ./server.js --ignore src/ -e js,html,css", 14 | "build": "rimraf dist && cross-env NODE_ENV=production webpack --config webpack.config.js --progress --hide-modules", 15 | "webpack-config-output": "rimraf tmp && mkdir tmp && cross-env NODE_ENV=production node ./scripts/webpack-config-output.js && cross-env NODE_ENV=development node ./scripts/webpack-config-output.js" 16 | }, 17 | "engines": { 18 | "node": ">=5.10" 19 | }, 20 | "dependencies": { 21 | "body-parser": "~1.18.3", 22 | "cross-env": "~3.1.3", 23 | "dotenv": "~2.0.0", 24 | "express": "~4.16.4", 25 | "moment": "~2.23.0", 26 | "moment-timezone": "~0.5.11", 27 | "nodemon": "~1.18.9", 28 | "reload": "~2.4.0", 29 | "request": "~2.88.0", 30 | "serialize-javascript": "~1.3.0", 31 | "vue-server-renderer": "~2.1.8" 32 | }, 33 | "devDependencies": { 34 | "axios": "^0.15.3", 35 | "babel-core": "~6.26.0", 36 | "babel-loader": "~6.4.1", 37 | "babel-plugin-es6-promise": "~1.1.1", 38 | "babel-plugin-transform-es2015-destructuring": "~6.23.0", 39 | "babel-plugin-transform-runtime": "~6.23.0", 40 | "babel-preset-env": "~1.6.1", 41 | "css-loader": "~0.25.0", 42 | "es6-promise": "~4.2.4", 43 | "extract-text-webpack-plugin": "~2.1.2", 44 | "file-loader": "~0.9.0", 45 | "js-object-pretty-print": "~0.2.0", 46 | "node-sass": "~4.9.3", 47 | "opn": "~5.4.0", 48 | "sass-loader": "~4.1.1", 49 | "style-loader": "~0.13.1", 50 | "uglify-js": "~3.0.28", 51 | "vue": "~2.1.0", 52 | "vue-loader": "~10.0.0", 53 | "vue-resource": "~1.0.3", 54 | "vue-style-loader": "~1.0.0", 55 | "vue-template-compiler": "~2.1.0", 56 | "vuex": "~2.1.1", 57 | "webpack": "~2.7.0", 58 | "webpack-dev-middleware": "~1.9.0", 59 | "webpack-hot-middleware": "~2.14.0", 60 | "webpack-merge": "~2.3.1", 61 | "webpack-module-hot-accept": "~1.0.4" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejsdevelopers/vuejs-calendar/0145cdb8b1d838957c5ae0adc98aa5a34a1ac5fe/public/favicon.ico -------------------------------------------------------------------------------- /scripts/webpack-config-output.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var pretty = require('js-object-pretty-print').pretty; 3 | 4 | require('../webpack.config.js').forEach(target => { 5 | let fileName = `./tmp/${target.target}.${process.env.NODE_ENV === 'production' ? 'prod' : 'dev' }.js`; 6 | fs.writeFile( 7 | fileName, 8 | `module.export = ${pretty(target)};`, 9 | function(err) { 10 | if(err) { 11 | return console.log(err); 12 | } 13 | console.log(`${fileName} successfully saved.`); 14 | } 15 | ); 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ silent: true }); 2 | 3 | const express = require('express'); 4 | const app = express(); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const http = require('http'); 8 | 9 | app.use('/public', express.static(path.join(__dirname, 'public'))); 10 | 11 | app.get('/', (req, res) => { 12 | let template = fs.readFileSync(path.resolve('./index.html'), 'utf-8'); 13 | res.send(template); 14 | 15 | }); 16 | 17 | const server = http.createServer(app); 18 | 19 | if (process.env.NODE_ENV === 'development') { 20 | const reload = require('reload'); 21 | const reloadServer = reload(app); 22 | require('./webpack-dev-middleware').init(app); 23 | } 24 | 25 | server.listen(process.env.PORT, function () { 26 | console.log(`Example app listening on port ${process.env.PORT}!`); 27 | if (process.env.NODE_ENV === 'development') { 28 | require("opn")(`http://localhost:${process.env.PORT}`); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/assets/Muli-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejsdevelopers/vuejs-calendar/0145cdb8b1d838957c5ae0adc98aa5a34a1ac5fe/src/assets/Muli-Light.ttf -------------------------------------------------------------------------------- /src/assets/Muli-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejsdevelopers/vuejs-calendar/0145cdb8b1d838957c5ae0adc98aa5a34a1ac5fe/src/assets/Muli-Regular.ttf -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuejsdevelopers/vuejs-calendar/0145cdb8b1d838957c5ae0adc98aa5a34a1ac5fe/src/assets/logo.png -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | 3 | $dusty-gray: #999; 4 | $gallery: #eeeeee; 5 | $alto: #ddd; 6 | $bittersweet: #ff6f69; 7 | $buttermilk: rgba(255, 238, 173, 0.4); 8 | $vista-blue: #42b983; 9 | $pink: rgba(255, 182, 193, 0.3); 10 | $pickled-bluewood: #35495e; 11 | 12 | $calendar-border: 1px solid $alto; 13 | 14 | @mixin calendar-row { 15 | display: flex; 16 | justify-content: flex-start; 17 | width: 100%; 18 | } 19 | 20 | @mixin calendar-cell { 21 | width: 100%; 22 | padding: 0.5rem; 23 | } 24 | 25 | @font-face { 26 | font-family: 'Muli'; 27 | src: url('~./assets/Muli-Light.ttf') format('truetype'); 28 | font-weight: normal; 29 | font-style: normal; 30 | } 31 | 32 | @font-face { 33 | font-family: 'Muli'; 34 | src: url('~./assets/Muli-Regular.ttf') format('truetype'); 35 | font-weight: bold; 36 | font-style: normal; 37 | } 38 | 39 | body { 40 | font-family: 'Muli', Helvetica, Arial, sans-serif; 41 | color: $pickled-bluewood; 42 | padding-bottom: 2rem; 43 | 44 | display: flex; 45 | justify-content: center; 46 | 47 | 48 | // Test 49 | background-color: $gallery; 50 | 51 | #app { 52 | display: block !important; 53 | flex-basis: 1000px; 54 | 55 | #header { 56 | display: flex; 57 | flex-direction: row; 58 | justify-content: space-between; 59 | align-items: flex-end; 60 | 61 | div:first-child { 62 | display: flex; 63 | flex-direction: row; 64 | align-items: center; 65 | margin: 1rem 0; 66 | img { 67 | width: 50px; 68 | height: 50px; 69 | } 70 | h1 { 71 | margin: 0 0 0 0.5rem; 72 | color: $pickled-bluewood; 73 | padding-top: 7px; 74 | } 75 | } 76 | 77 | div:last-child { 78 | font-weight: bold; 79 | button { 80 | color: $dusty-gray; 81 | font-size: 1rem; 82 | padding: 0; 83 | width: 20px; 84 | background-color: white; 85 | user-select: none; 86 | border: 1px solid $alto; 87 | border-radius: 2px; 88 | margin-left: 0.25rem; 89 | &:focus { 90 | outline: none; 91 | } 92 | &:hover { 93 | color: $pickled-bluewood; 94 | box-shadow: 0 2px 2px $alto; 95 | } 96 | } 97 | } 98 | } 99 | 100 | #day-bar { 101 | @include calendar-row; 102 | div { 103 | @include calendar-cell; 104 | } 105 | } 106 | 107 | #calendar { 108 | background-color: white; 109 | .calendar-week { 110 | @include calendar-row; 111 | .day { 112 | overflow: hidden; 113 | font-weight: bold; 114 | @include calendar-cell; 115 | height: 100px; 116 | user-select: none; 117 | cursor: default; 118 | border-left: $calendar-border; 119 | border-top: $calendar-border; 120 | 121 | &:last-child { 122 | border-right: $calendar-border; 123 | } 124 | 125 | &.past { 126 | opacity: 0.6; 127 | } 128 | 129 | &.not-current-month { 130 | color: $alto; 131 | } 132 | 133 | &.today { 134 | background-color: $buttermilk; 135 | } 136 | 137 | &.active { 138 | background-color: $pink; 139 | } 140 | 141 | .event-list { 142 | font-size: 0.8rem; 143 | color: $vista-blue; 144 | font-weight: bold; 145 | list-style: none; 146 | padding: 0; 147 | margin: 0.5rem 0 0.5rem 0; 148 | 149 | li { 150 | white-space: nowrap; 151 | } 152 | } 153 | } 154 | 155 | 156 | .day { 157 | 158 | 159 | } 160 | &:last-child { 161 | .day { 162 | border-bottom: $calendar-border; 163 | } 164 | } 165 | } 166 | } 167 | 168 | $event-form-width: 300px; 169 | 170 | #event-form { 171 | 172 | display: none; 173 | box-shadow: 0 2px 4px $alto; 174 | position: fixed; 175 | width: $event-form-width; 176 | transform: translate(-50%, -100%); 177 | z-index: 10; 178 | padding: 1rem; 179 | background-color: white; 180 | border: 1px $alto solid; 181 | 182 | &.active { 183 | display: flex; 184 | } 185 | 186 | flex-direction: column; 187 | align-content: space-between; 188 | 189 | 190 | h4 { 191 | margin: 0 0 0.75rem 0; 192 | color: $dusty-gray; 193 | font-weight: normal; 194 | font-size: 1.15rem; 195 | } 196 | 197 | p { 198 | font-size: 0.85rem; 199 | margin: 0 0 0.85rem 0; 200 | } 201 | 202 | 203 | & > * { 204 | width: 100% 205 | } 206 | .text { 207 | input[type='text'] { 208 | width: calc(100% - 0.75rem); 209 | padding: 0.25rem; 210 | font-size: 0.75rem; 211 | } 212 | margin-bottom: 0.75rem; 213 | } 214 | .buttons { 215 | text-align: right; 216 | button { 217 | $button-col: $pickled-bluewood; 218 | padding: 0.5rem; 219 | background-color: $button-col; 220 | border: 1px solid darken($button-col, 5%); 221 | font-weight: bold; 222 | border-radius: 2px; 223 | color: white; 224 | &:focus { 225 | outline: none; 226 | } 227 | &:hover { 228 | background-color: lighten($button-col, 4%); 229 | border: 1px solid $button-col; 230 | } 231 | } 232 | } 233 | #close-button { 234 | margin: 0; 235 | padding: 0; 236 | font-size: 1.25rem; 237 | background-color: white; 238 | position: absolute; 239 | border: none; 240 | width: 20px; 241 | font-weight: bold; 242 | color: #666; 243 | right: 0.6rem; 244 | top: 0.6rem; 245 | cursor: pointer; 246 | &:focus { 247 | outline: none; 248 | } 249 | } 250 | } 251 | 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /src/web.entry.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | new Vue({ 4 | el: '#app', 5 | data: { 6 | msg: 'Hello World' 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /webpack-dev-middleware.js: -------------------------------------------------------------------------------- 1 | const webpackDevMiddleware = require("webpack-dev-middleware"); 2 | const webpack = require("webpack"); 3 | const webpackConfig = require('./webpack.config.js').find(item => item.target === 'web'); 4 | const compiler = webpack(webpackConfig); 5 | 6 | module.exports = { 7 | init(app) { 8 | app.use(webpackDevMiddleware(compiler, { 9 | hot: true, 10 | stats: { 11 | colors: true 12 | }, 13 | historyApiFallback: true, 14 | publicPath: webpackConfig.output.publicPath, 15 | filename: webpackConfig.output.filename, 16 | })); 17 | 18 | app.use(require("webpack-hot-middleware")(compiler, { 19 | log: console.log, 20 | path: '/__webpack_hmr', 21 | heartbeat: 10 * 1000, 22 | })); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /webpack-server-compiler.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const webpackConfig = require('./webpack.config').find(item => item.target === 'node'); 3 | const path = require('path'); 4 | const MFS = require('memory-fs'); 5 | 6 | module.exports = { 7 | init(bundleUpdated) { 8 | const compiler = webpack(webpackConfig); 9 | const mfs = new MFS(); 10 | const outputPath = path.join(webpackConfig.output.path, webpackConfig.output.filename); 11 | compiler.outputFileSystem = mfs; 12 | compiler.watch({}, (err, stats) => { 13 | if (err) { 14 | throw err; 15 | } 16 | console.log(stats.toString({ colors: true })); 17 | bundleUpdated(mfs.readFileSync(outputPath, 'utf-8')); 18 | }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | var webpackMerge = require('webpack-merge'); 6 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 7 | 8 | var baseConfig = { 9 | output: { 10 | path: path.resolve(__dirname, './dist'), 11 | publicPath: '/dist/', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | use: [{ 18 | loader: 'babel-loader', 19 | options: { 20 | "presets": [ [ "env" ] ], 21 | "plugins": [ "transform-es2015-destructuring", "transform-runtime", "es6-promise" ] 22 | } 23 | }], 24 | exclude: /node_modules/ 25 | }, 26 | { 27 | test: /\.scss$/, 28 | loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader!sass-loader' }) 29 | }, 30 | { 31 | test: /\.(png|jpg|gif|svg|ttf)$/, 32 | loader: 'file-loader', 33 | options: { 34 | name: '[name].[ext]?[hash]', 35 | publicPath: process.env.CDN_URL && process.env.NODE_ENV === 'production' ? `${process.env.CDN_URL}/dist/` : false 36 | } 37 | }, 38 | { 39 | test: /\.vue$/, 40 | loader: 'vue-loader', 41 | options: { 42 | loaders: { 43 | 'scss': 'vue-style-loader!css-loader!sass-loader', 44 | 'js': 'babel-loader?presets[]=env' 45 | } 46 | } 47 | } 48 | ] 49 | }, 50 | resolve: { 51 | alias: { 52 | 'vue$': 'vue/dist/vue.common.js', 53 | } 54 | }, 55 | devServer: { 56 | historyApiFallback: true, 57 | noInfo: true 58 | }, 59 | performance: { 60 | hints: false 61 | } 62 | }; 63 | 64 | let targets = [ 'web', 'node' ].map((target) => { 65 | let obj = webpackMerge(baseConfig, { 66 | target: target, 67 | entry: { 68 | app: target === 'web' 69 | ? process.env.NODE_ENV === 'development' 70 | ? [ `./src/${target}.entry.js`, 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000' ] 71 | : [ `./src/${target}.entry.js` ] 72 | : [ `./src/${target}.entry.js` ] 73 | , 74 | }, 75 | output: { 76 | filename: `${target}.bundle.js`, 77 | libraryTarget: target === 'web' ? 'var' : 'commonjs2' 78 | }, 79 | module: { 80 | rules: [ 81 | 82 | ] 83 | }, 84 | plugins: target === 'web' 85 | ? process.env.NODE_ENV === 'development' 86 | ? [ 87 | new webpack.HotModuleReplacementPlugin(), 88 | new ExtractTextPlugin("style.css") 89 | ] 90 | : [ 91 | new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }), 92 | new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false } }), 93 | new webpack.LoaderOptionsPlugin({ minimize: true }), 94 | new ExtractTextPlugin("style.css") 95 | ] 96 | : [] 97 | , 98 | devtool: target === 'web' 99 | ? process.env.NODE_ENV === 'development' 100 | ? '#eval-source-map' 101 | : '#source-map' 102 | : false 103 | }); 104 | if (process.env.NODE_ENV === 'development' && target === 'web') { 105 | obj.module.rules[0].use.push({ loader: 'webpack-module-hot-accept' }); 106 | } 107 | return obj; 108 | }); 109 | 110 | module.exports = targets; 111 | --------------------------------------------------------------------------------