├── .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 | 
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 |
--------------------------------------------------------------------------------