diff --git a/LICENSE b/LICENSE
index a67e4da21..efbb72870 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2013-2020, Daniel Roy Greenfeld
+Copyright (c) 2013-2020, Daniel Roy Greenfeld, Mike Hoolehan
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
diff --git a/README.rst b/README.rst
index 47f2bfb7d..45d2b7dff 100644
--- a/README.rst
+++ b/README.rst
@@ -1,323 +1,100 @@
-Cookiecutter Django
+Cookiecutter Vue Django
=======================
-.. image:: https://travis-ci.org/pydanny/cookiecutter-django.svg?branch=master
- :target: https://travis-ci.org/pydanny/cookiecutter-django?branch=master
+.. image:: https://travis-ci.com/ilikerobots/cookiecutter-vue-django.svg?branch=master
+ :target: https://travis-ci.com/ilikerobots/cookiecutter-vue-django?branch=master
:alt: Build Status
-.. image:: https://readthedocs.org/projects/cookiecutter-django/badge/?version=latest
- :target: https://cookiecutter-django.readthedocs.io/en/latest/?badge=latest
+.. image:: https://readthedocs.org/projects/cookiecutter-vue-django/badge/?version=latest
+ :target: https://cookiecutter-vue-django.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
-.. image:: https://pyup.io/repos/github/pydanny/cookiecutter-django/shield.svg
- :target: https://pyup.io/repos/github/pydanny/cookiecutter-django/
+.. image:: https://pyup.io/repos/github/ilikerobots/cookiecutter-vue-django/shield.svg
+ :target: https://pyup.io/repos/github/ilikerobots/cookiecutter-vue-django/
:alt: Updates
-.. image:: https://img.shields.io/badge/cookiecutter-Join%20on%20Slack-green?style=flat&logo=slack
- :target: https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U
+Vue + Django with no compromise.
-.. image:: https://www.codetriage.com/pydanny/cookiecutter-django/badges/users.svg
- :target: https://www.codetriage.com/pydanny/cookiecutter-django
- :alt: Code Helpers Badge
+Cookiecutter Vue Django is a framework for jumpstarting production-ready Django + Vue projects quickly. Expanding on the the wonderful Cookiecutter Django, this project template allows the intermingling of both Django Templates and Vue, even on the same page, without compromising the full power of either.
-.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
- :target: https://github.com/ambv/black
- :alt: Code style: black
+Typical solutions to integrating Django and Vue forgo much of the strengths of one lieu of the other. For example, a common approach is using Django Rest Framework as backend and writing the entire front end in Vue, making it difficult to utilize Django templates in places it could be expedient. A second approach is to use Vue within Django templates using browser `
+
+{% endblock %}{% endraw %}
diff --git a/{{cookiecutter.project_slug}}/fruit/tests.py b/{{cookiecutter.project_slug}}/fruit/tests.py
new file mode 100644
index 000000000..a39b155ac
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/fruit/tests.py
@@ -0,0 +1 @@
+# Create your tests here.
diff --git a/{{cookiecutter.project_slug}}/fruit/urls.py b/{{cookiecutter.project_slug}}/fruit/urls.py
new file mode 100644
index 000000000..b786a24ad
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/fruit/urls.py
@@ -0,0 +1,25 @@
+from django.urls import path
+from django.views.generic import TemplateView
+{%- if cookiecutter.use_drf == "y" %}
+from fruit.views import FruitList
+{%- endif %}
+
+app_name = "fruit"
+
+urlpatterns = [
+ {%- if cookiecutter.use_drf == "y" %}
+ path(
+ "fruits/",
+ FruitList.as_view(extra_context={"title": "Fruit Inspector"}),
+ name="list",
+ ),
+ {% endif -%}
+ path(
+ "fruit-counter/",
+ TemplateView.as_view(
+ template_name="fruit/counter.html",
+ extra_context={"counter_message": "How many fruits could you eat?"},
+ ),
+ name="counter",
+ ),
+]
diff --git a/{{cookiecutter.project_slug}}/fruit/views.py b/{{cookiecutter.project_slug}}/fruit/views.py
new file mode 100644
index 000000000..6a38b3ec8
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/fruit/views.py
@@ -0,0 +1,6 @@
+from django.views.generic import ListView
+from fruit.models import Fruit
+
+
+class FruitList(ListView):
+ model = Fruit
diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt
index ba62bae08..9daeea66f 100644
--- a/{{cookiecutter.project_slug}}/requirements/base.txt
+++ b/{{cookiecutter.project_slug}}/requirements/base.txt
@@ -42,3 +42,6 @@ django-redis==4.12.1 # https://github.com/jazzband/django-redis
# Django REST Framework
djangorestframework==3.11.0 # https://github.com/encode/django-rest-framework
{%- endif %}
+{%- if cookiecutter.use_vue == "y" %}
+django-webpack-loader==0.6.0 # https://github.com/owais/django-webpack-loader
+{%- endif %}
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.browserslistrc b/{{cookiecutter.project_slug}}/vue_frontend/.browserslistrc
new file mode 100644
index 000000000..214388fe4
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/.browserslistrc
@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.env b/{{cookiecutter.project_slug}}/vue_frontend/.env
new file mode 100644
index 000000000..8b0726f30
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/.env
@@ -0,0 +1,3 @@
+VUE_APP_STATIC_ROOT=http://localhost:8000/static
+VUE_APP_API_ROOT=http://localhost:8000/api
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.eslintrc.js b/{{cookiecutter.project_slug}}/vue_frontend/.eslintrc.js
new file mode 100644
index 000000000..8ad90db46
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/.eslintrc.js
@@ -0,0 +1,17 @@
+module.exports = {
+ root: true,
+ env: {
+ node: true
+ },
+ 'extends': [
+ 'plugin:vue/essential',
+ 'eslint:recommended'
+ ],
+ parserOptions: {
+ parser: 'babel-eslint'
+ },
+ rules: {
+ 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/.gitignore b/{{cookiecutter.project_slug}}/vue_frontend/.gitignore
new file mode 100644
index 000000000..fc546a5b9
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/.gitignore
@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+# local env files
+.env.local
+.env.*.local
+!.env
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/README.md b/{{cookiecutter.project_slug}}/vue_frontend/README.md
new file mode 100644
index 000000000..fd2a3556a
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/README.md
@@ -0,0 +1,24 @@
+# vue_frontend
+
+## Project setup
+`yarn install` or `npm install`
+
+### Compiles and hot-reloads for development
+`yarn serve` or `npm run serve`
+
+{% if cookiecutter.use_pycharm == 'y' -%}
+Or use the PyCharm run configuration *Vue serve*.
+{%- endif %}
+
+### Compiles and minifies for production
+`yarn build` or `npm run build`
+
+{% if cookiecutter.use_pycharm == 'y' -%}
+Or use the PyCharm run configuration *Vue build*.
+{%- endif %}
+
+### Lints and fixes files
+`yarn lint` or `npm run lint`
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/babel.config.js b/{{cookiecutter.project_slug}}/vue_frontend/babel.config.js
new file mode 100644
index 000000000..e9558405f
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/babel.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ presets: [
+ '@vue/cli-plugin-babel/preset'
+ ]
+}
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/package.json b/{{cookiecutter.project_slug}}/vue_frontend/package.json
new file mode 100644
index 000000000..2622edb3c
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "vue_frontend",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "serve": "vue-cli-service serve",
+ "build": "vue-cli-service build",
+ "lint": "vue-cli-service lint"
+ },
+ "dependencies": {
+ "axios": "^0.19.2",
+ "core-js": "^3.6.5",
+ "vue": "^2.6.11"{% if cookiecutter.use_vuex == 'y' -%},
+ "vuex": "^3.4.0",
+ "vuex-persistedstate": "^3.0.1"
+ {%- endif %}
+},
+ "devDependencies": {
+ "@vue/cli-plugin-babel": "~4.4.0",
+ "@vue/cli-plugin-eslint": "~4.4.0",
+ "@vue/cli-plugin-vuex": "~4.4.0",
+ "@vue/cli-service": "~4.4.0",
+ "babel-eslint": "^10.1.0",
+ "eslint": "^6.7.2",
+ "eslint-plugin-vue": "^6.2.2",
+ "node-sass": "^4.12.0",
+ "sass-loader": "^8.0.2",
+ "vue-template-compiler": "^2.6.11",
+ "webpack-bundle-tracker": "^0.4.3"
+ }
+}
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/Counter.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/Counter.vue
new file mode 100644
index 000000000..56df8292f
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/Counter.vue
@@ -0,0 +1,56 @@
+
+
+
{% raw %}{{ msg }}{% endraw %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterAdjustor.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterAdjustor.vue
new file mode 100644
index 000000000..09ae9d2a7
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterAdjustor.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterBanner.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterBanner.vue
new file mode 100644
index 000000000..3ff84010c
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterBanner.vue
@@ -0,0 +1,34 @@
+
+
+
{% raw %}{{ message }}{% endraw %}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterDisplay.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterDisplay.vue
new file mode 100644
index 000000000..d368e1415
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/CounterDisplay.vue
@@ -0,0 +1,43 @@
+
+
+
+
{% raw %}{{ count }}{% endraw %}
+
+
+
+
+
+
+
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/FruitInspector.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/FruitInspector.vue
new file mode 100644
index 000000000..be14b05f5
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/components/FruitInspector.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_counter.js b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_counter.js
new file mode 100644
index 000000000..c9301a7ae
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_counter.js
@@ -0,0 +1,34 @@
+import Vue from "vue/dist/vue.js";
+import storePlugin from "../../../../vue_frontend/src/store/vuex_store_as_plugin";
+import createPersistedState from "vuex-persistedstate";
+import FruitModule from "../store/module_fruit"
+const Counter = () => import( /* webpackChunkName: "chunk-counter" */ "../components/Counter.vue");
+const CounterBanner = () => import( /* webpackChunkName: "chunk-counter-banner" */ "../components/CounterBanner.vue");
+
+Vue.config.productionTip = false
+
+// Vuex state will be used in this entry point
+Vue.use(storePlugin);
+
+// Include Vuex modules as needed for this entry point
+Vue.prototype.$store.registerModule('fruit', FruitModule);
+
+// Designate what state should persist across page loads
+createPersistedState({
+ paths: [
+ "fruit.count",
+ "fruit.activeFruit",
+ ]
+ }
+)(Vue.prototype.$store);
+
+// Mount top level components
+new Vue({
+ el: "#app",
+ components: {Counter}
+});
+
+new Vue({
+ el: "#counter_banner",
+ components: {CounterBanner}
+});
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_list.js b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_list.js
new file mode 100644
index 000000000..0d286e9b0
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/entry/fruit_list.js
@@ -0,0 +1,30 @@
+import Vue from "vue/dist/vue.js";
+import storePlugin from "../../store/vuex_store_as_plugin";
+import createPersistedState from "vuex-persistedstate";
+import FruitModule from "../store/module_fruit"
+const FruitInspector = () => import( /* webpackChunkName: "chunk-fruit-inspector" */ "../components/FruitInspector.vue");
+
+Vue.config.productionTip = false
+
+// Vuex state will be used in this entry point
+Vue.use(storePlugin);
+
+// Include Vuex modules as needed for this entry point
+Vue.prototype.$store.registerModule('fruit', FruitModule);
+
+
+// Designate what state should persist across page loads
+createPersistedState({
+ paths: [
+ "fruit.count",
+ "fruit.activeFruit",
+ ]
+ }
+)(Vue.prototype.$store);
+
+// Mount top level components
+new Vue({
+ el: "#app",
+ components: {FruitInspector}
+});
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/store/module_fruit.js b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/store/module_fruit.js
new file mode 100644
index 000000000..1728f03b5
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/fruit/store/module_fruit.js
@@ -0,0 +1,77 @@
+{%- if cookiecutter.use_drf == 'y' -%}
+import api from "../../rest/rest";
+
+{%- endif -%}
+export const MAX_COUNT = 42
+export const MIN_COUNT = 0
+
+{%- if cookiecutter.use_drf == 'y' %}
+export const ACTION_GET_FRUITS = 'ACT_GET_FRUITS'
+export const ACTION_SET_ACTIVE_FRUIT = 'ACT_SET_ACTIVE_FRUIT'
+{%- endif %}
+export const ACTION_INCREMENT_COUNTER = 'ACT_INC_COUNT'
+export const ACTION_DECREMENT_COUNTER = 'ACT_DEC_COUNT'
+
+{%- if cookiecutter.use_drf == 'y' %}
+const MUTATION_SET_FRUITS = 'MUT_SET_FRUITS'
+const MUTATION_SET_ACTIVE_FRUIT = 'MUT_SET_ACTIVE_FRUIT'
+{%- endif %}
+const MUTATION_INCREMENT_COUNTER = 'MUT_INC_COUNT'
+const MUTATION_DECREMENT_COUNTER = 'MUT_DEC_COUNT'
+
+export default {
+ state: {
+ count: 0,
+ {%- if cookiecutter.use_drf == 'y' %}
+ fruits: [],
+ activeFruit: null,
+ {%- endif %}
+ },
+ mutations: {
+ {%- if cookiecutter.use_drf == 'y' %}
+ [MUTATION_SET_FRUITS](state, fruitList) {
+ state.fruits = fruitList;
+ },
+ [MUTATION_SET_ACTIVE_FRUIT](state, fruit) {
+ state.activeFruit = fruit;
+ },
+ {%- endif %}
+ [MUTATION_INCREMENT_COUNTER]: state => state.count++,
+ [MUTATION_DECREMENT_COUNTER]: state => state.count--
+ },
+ actions: {
+ {%- if cookiecutter.use_drf == 'y' %}
+ [ACTION_GET_FRUITS](context) {
+ api.getFruits()
+ .then(function (response) {
+ context.commit(MUTATION_SET_FRUITS, response.data);
+ })
+ .catch(function (error) {
+ // handle error
+ console.log(error);
+ });
+ },
+ [ACTION_SET_ACTIVE_FRUIT](context, fruitId) {
+ api.getFruit(fruitId)
+ .then(function (response) {
+ context.commit(MUTATION_SET_ACTIVE_FRUIT, response.data);
+ })
+ .catch(function (error) {
+ // handle error
+ console.log(error);
+ });
+ },
+ {%- endif %}
+ [ACTION_INCREMENT_COUNTER](context) {
+ if (context.state.count < MAX_COUNT) {
+ context.commit(MUTATION_INCREMENT_COUNTER);
+ }
+ },
+ [ACTION_DECREMENT_COUNTER](context) {
+ if (context.state.count > MIN_COUNT) {
+ context.commit(MUTATION_DECREMENT_COUNTER);
+ }
+ }
+
+ },
+}
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/rest/rest.js b/{{cookiecutter.project_slug}}/vue_frontend/src/rest/rest.js
new file mode 100644
index 000000000..9ebe64912
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/rest/rest.js
@@ -0,0 +1,23 @@
+import axios from "axios";
+
+// axios settings
+axios.defaults.baseURL = process.env.VUE_APP_API_ROOT;
+axios.defaults.xsrfHeaderName = "X-CSRFToken";
+axios.defaults.xsrfCookieName = 'csrftoken';
+
+
+const api = axios.create({});
+
+
+export default {
+{%- if cookiecutter.use_fruit_demo == "y" %}
+ getFruits: function () {
+ return api.get('/fruits/')
+ },
+ getFruit: function (id) {
+ return api.get('/fruits/' + id + '/')
+ }
+{%- endif %}
+ /* Include additional API calls here */
+}
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/store/vuex_store_as_plugin.js b/{{cookiecutter.project_slug}}/vue_frontend/src/store/vuex_store_as_plugin.js
new file mode 100644
index 000000000..c51c5ac95
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/store/vuex_store_as_plugin.js
@@ -0,0 +1,17 @@
+import Vue from "vue/dist/vue.js";
+import Vuex from "vuex";
+
+Vue.use(Vuex);
+
+let store = new Vuex.Store({
+ modules: {
+ },
+ strict: process.env.NODE_ENV !== "production",
+});
+
+export default {
+ store,
+ install(Vue) { //resetting the default store to use this plugin store
+ Vue.prototype.$store = store;
+ }
+}
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/assets/logo.png b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/assets/logo.png
new file mode 100644
index 000000000..f3d2503fc
Binary files /dev/null and b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/assets/logo.png differ
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/components/HelloWorld.vue b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/components/HelloWorld.vue
new file mode 100644
index 000000000..b17d4ad26
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/components/HelloWorld.vue
@@ -0,0 +1,116 @@
+
+
+
+

+
+
+
![Django logo]()
+
+
{% raw %}{{ msg }}{% endraw %}
+
+ This cookiecutter implements the techniques discussed in the series of articles "Vue + Django: The Best of Both Frontends."
+
+
+
+
+ In addition to the base options provided by the wonderful Cookiecutter Django, this cookiecutter providees the following options:
+
+
+
+
+ -
+ use_vue -- provides Vue integration using django-webpack-loader, featuring:
+
+ - Harmonious coexistence of Django templates and Vue components
+ - Vue Single File Components (SFCs)
+ - Multi-page App (MPA) layout
+ - Vue Loader Hot Reload
+ - Property passing from Django -> Vue
+ - Sass/SCSS pre-compilation
+ - Vue DevTools support
+ - Chunked resource loading
+ - Deferred loading of Vue and/or Vue components
+
+
+
+ -
+ use_vuex -- provides Vuex integration, featuring:
+
+ - Shared state across components on the same page
+ - Persistent state across pages
+ - Rest support via Axios -> DRF (if use_drf enabled)
+
+
+ -
+ use_fruit_demo -- provides a sample app, demonstrating:
+
+ - All basic features from selected options above
+ - Deferred loading
+ - Static asset loading from Django or Vue
+ - If use_drf enabled, a sample API integration
+ - If custom_bootstrap_compilation enabled, SCSS partials import in SFCs
+ - Useless information about fruit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/entry/main.js b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/entry/main.js
new file mode 100644
index 000000000..6ba1a968e
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/src/{{cookiecutter.project_slug}}/entry/main.js
@@ -0,0 +1,10 @@
+import Vue from "vue/dist/vue.js";
+const HelloWorld = () => import( /* webpackChunkName: "chunk-hello-world" */ "../components/HelloWorld.vue");
+
+Vue.config.productionTip = false
+
+// Mount top level components
+new Vue({
+ el: "#app",
+ components: {HelloWorld}
+});
diff --git a/{{cookiecutter.project_slug}}/vue_frontend/vue.config.js b/{{cookiecutter.project_slug}}/vue_frontend/vue.config.js
new file mode 100644
index 000000000..c90b0b941
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/vue_frontend/vue.config.js
@@ -0,0 +1,88 @@
+const BundleTracker = require("webpack-bundle-tracker");
+const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
+
+
+const pages = {
+ 'main': {
+ entry: './src/{{cookiecutter.project_slug}}/entry/main.js',
+ chunks: ['chunk-common']
+ },
+ {%- if cookiecutter.use_fruit_demo == 'y' %}
+ 'fruit-counter': {
+ entry: './src/fruit/entry/fruit_counter.js',
+ chunks: ['chunk-common', 'chunk-state']
+ },
+ {%- endif %}{%- if cookiecutter.use_fruit_demo == 'y' and cookiecutter.use_drf == 'y' %}
+ 'fruit-list': {
+ entry: './src/fruit/entry/fruit_list.js',
+ chunks: ['chunk-common', 'chunk-state']
+ },
+ {%- endif %}
+}
+
+module.exports = {
+ pages: pages,
+ filenameHashing: true,
+ productionSourceMap: false,
+ publicPath: process.env.NODE_ENV === 'production'
+ ? '/static/vue'
+ : 'http://localhost:8080/',
+ outputDir: '../{{cookiecutter.project_slug}}/static/vue/',
+
+ chainWebpack: config => {
+
+ config.optimization
+ .splitChunks({
+ cacheGroups: {
+ state: {
+ /* As vuex state is not needed in all our entry points, we isolate it
+ * in a separate chunk to be loaded only where needed.
+ */
+ test: /[\\/]node_modules[\\/](vuex|vuex-persisted-state)/,
+ name: "chunk-state",
+ chunks: "all",
+ priority: 5
+ },
+ vendor: {
+ /* This chunk contains modules that may be used in all entry points,
+ * including Vue itself
+ */
+ test: /[\\/]node_modules[\\/]/,
+ name: "chunk-common",
+ chunks: "all",
+ priority: 1
+ },
+ },
+ });
+
+ Object.keys(pages).forEach(page => {
+ config.plugins.delete(`html-${page}`);
+ config.plugins.delete(`preload-${page}`);
+ config.plugins.delete(`prefetch-${page}`);
+ })
+
+ config
+ .plugin('BundleTracker')
+ .use(BundleTracker, [{
+ path: '../vue_frontend/',
+ filename: 'webpack-stats.json'
+ }]);
+
+ // Uncomment below to analyze bundle sizes
+ // config.plugin("BundleAnalyzerPlugin").use(BundleAnalyzerPlugin);
+
+ // config.resolve.alias
+ // .set('__STATIC__', 'static')
+
+ config.devServer
+ .public('http://localhost:8080')
+ .host('localhost')
+ .port(8080)
+ .hotOnly(true)
+ .watchOptions({poll: 1000})
+ .https(false)
+ .headers({"Access-Control-Allow-Origin": ["*"]})
+
+ }
+};
+
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/images/django_logo.png b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/images/django_logo.png
new file mode 100644
index 000000000..68706a474
Binary files /dev/null and b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/images/django_logo.png differ
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/js/project.js b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/js/project.js
index d26d23b9b..d5094b2eb 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/js/project.js
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/js/project.js
@@ -1 +1,19 @@
+{%- if cookiecutter.use_vue == 'y' %}
+function load_bundle_file(url, filetype) {
+ let fileref;
+ if (filetype === "js") { // build js script tag
+ fileref = document.createElement('script');
+ fileref.setAttribute("type", "text/javascript");
+ fileref.setAttribute("src", url);
+ } else if (filetype === "css") { // build css link tag
+ fileref = document.createElement("link");
+ fileref.setAttribute("rel", "stylesheet");
+ fileref.setAttribute("type", "text/css");
+ fileref.setAttribute("href", url);
+ }
+ if (typeof fileref != "undefined")
+ document.getElementsByTagName("head")[0].appendChild(fileref);
+}
+
+{% endif -%}
/* Project specific Javascript goes here. */
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/custom_bootstrap_vars.scss b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/custom_bootstrap_vars.scss
index e69de29bb..bb61cf415 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/custom_bootstrap_vars.scss
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/static/sass/custom_bootstrap_vars.scss
@@ -0,0 +1,4 @@
+{%- if cookiecutter.use_vue == 'y' -%}
+$vue-blue: #34495E;
+$vue-green: #41B883;
+{% endif -%}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html
index cba1b7cf7..39530042f 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html
@@ -50,7 +50,13 @@
About
-
+ {% endraw %}{% if cookiecutter.use_fruit_demo == "y" %}{% raw %}
+
+ Fruit Counter
+ {% endraw %}{% endif %}{% if cookiecutter.use_fruit_demo == "y" and cookiecutter.use_drf == "y"%}{% raw %}
+
+ Fruit List
+ {% endraw %}{% endif %}{% raw %}
{% if request.user.is_authenticated %}
{# URL provided by django-allauth/account/urls.py #}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/pages/home.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/pages/home.html
index 94beff9c8..1a361d137 100644
--- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/pages/home.html
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/pages/home.html
@@ -1 +1,14 @@
-{% raw %}{% extends "base.html" %}{% endraw %}
\ No newline at end of file
+{% raw %}{% extends "base.html" %}{% endraw %}{%- if cookiecutter.use_vue == 'y' %}{% raw %}
+{% load render_bundle from webpack_loader %}
+
+
+{% block content %}
+
+
+
+
+
+{% render_bundle 'chunk-common' %}
+{% render_bundle 'main' %}
+
+{% endblock content %}{% endraw %}{% endif -%}
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/webpack_bundle/lazy_render_bundle.html b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/webpack_bundle/lazy_render_bundle.html
new file mode 100644
index 000000000..0fb968706
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/webpack_bundle/lazy_render_bundle.html
@@ -0,0 +1,10 @@
+{% raw %}{% load get_files from webpack_loader %}
+
+{% get_files bundle as bundle_files %}
+{% for f in bundle_files %}
+ {% if f.url|default:""|slice:"-2:" == "js" %}
+ load_bundle_file("{{ f.url }}", "js");
+ {% else %}
+ load_bundle_file("{{ f.url }}", "css");
+ {% endif %}
+{% endfor %}{% endraw %}
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/apps.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/apps.py
new file mode 100644
index 000000000..bb6e0a263
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class WebpackBundleConfig(AppConfig):
+ name = "{{ cookiecutter.project_slug }}.webpack_bundle"
+ verbose_name = _("Webpack Bundle")
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/templatetags/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/templatetags/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/templatetags/webpack_bundle.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/templatetags/webpack_bundle.py
new file mode 100644
index 000000000..732131d49
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/webpack_bundle/templatetags/webpack_bundle.py
@@ -0,0 +1,10 @@
+from typing import Dict
+
+from django import template
+
+register = template.Library()
+
+
+@register.inclusion_tag("webpack_bundle/lazy_render_bundle.html", takes_context=False)
+def lazy_render_bundle(bundle: Dict[str, str]) -> Dict[str, Dict[str, str]]:
+ return {"bundle": bundle}