├── .babelrc
├── .editorconfig
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── src
├── App.html
├── App.vue
├── components
│ ├── Asset.vue
│ └── Modal.vue
├── directives
│ └── index.js
├── filters
│ ├── capitalize.js
│ └── money.js
├── main.js
├── modules
│ ├── app
│ │ ├── actions.js
│ │ ├── components
│ │ │ ├── End.vue
│ │ │ ├── Goal.vue
│ │ │ ├── HelpModal.vue
│ │ │ ├── Home.vue
│ │ │ └── Navigation.vue
│ │ ├── index.js
│ │ └── mutations.js
│ ├── persistence
│ │ ├── actions.js
│ │ ├── api.js
│ │ ├── components
│ │ │ ├── LoadModal.vue
│ │ │ ├── PersistenceNavItem.vue
│ │ │ └── SaveModal.vue
│ │ ├── index.js
│ │ └── mutations.js
│ ├── portfolio
│ │ ├── components
│ │ │ └── Portfolio.vue
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
│ └── stock
│ │ ├── actions.js
│ │ ├── components
│ │ └── Stocks.vue
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
├── routes.js
├── store.js
└── utils
│ └── index.js
├── vercel.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", { "modules": false }],
4 | "stage-3"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | yarn-error.log
6 |
7 | # Editor directories and files
8 | .idea
9 | *.suo
10 | *.ntvs*
11 | *.njsproj
12 | *.sln
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue.js Stock Trader Game
2 |
3 | A simple game created for learning the Vue.js ecosystem. This project makes use of following:
4 |
5 | ### Modules:
6 |
7 | * [Vue.js](https://github.com/vuejs/vue) - Framework
8 | * [Vue Router](https://github.com/vuejs/vue-router) - Routing
9 | * [Vuex](https://github.com/vuejs/vuex) - State Management
10 | * [Vuelidate](https://github.com/monterail/vuelidate) - Form validation
11 |
12 | ### Features
13 |
14 | * [Mixins](https://vuejs.org/v2/guide/mixins.html)
15 | * [Filters](https://vuejs.org/v2/guide/filters.html)
16 | * [Custom directives](https://vuejs.org/v2/guide/custom-directive.html)
17 | * [Single file components](https://vuejs.org/v2/guide/single-file-components.html)
18 | * [Component scoped CSS](https://vue-loader.vuejs.org/en/features/scoped-css.html)
19 | * [Enter/Leave transitions](https://vuejs.org/v2/guide/transitions.html)
20 |
21 | The project was made while trying to follow the best practices found on the web. Although many patterns were created right here.
22 |
23 | ## How to run
24 |
25 | ```bash
26 | # Install dependencies
27 | npm install
28 |
29 | # Serve with hot reload at localhost:8080
30 | npm run dev
31 |
32 | # Build for production with minification
33 | npm run build
34 | ```
35 |
36 | ## Things to add
37 |
38 | * Testing
39 | * State transitions
40 |
41 | ## Contribution
42 |
43 | Feel free to contribute to this project in order to introduce better practices building `Vue.js` apps. Create an issue with a question/bug to discuss certain problems or just introduce a pull request right away!
44 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Development - Stock Trader
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "training-vuejs",
3 | "description": "A Vue.js project",
4 | "version": "1.0.0",
5 | "author": "Jakub Sarnowski ",
6 | "license": "MIT",
7 | "private": true,
8 | "scripts": {
9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
10 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
11 | },
12 | "dependencies": {
13 | "lodash": "^4.17.4",
14 | "random-words": "0.0.1",
15 | "vue": "^2.5.11",
16 | "vue-router": "^3.0.1",
17 | "vuelidate": "^0.6.1",
18 | "vuex": "^3.0.1"
19 | },
20 | "browserslist": [
21 | "> 1%",
22 | "last 2 versions",
23 | "not ie <= 8"
24 | ],
25 | "devDependencies": {
26 | "babel-core": "^6.26.0",
27 | "babel-loader": "^7.1.2",
28 | "babel-preset-env": "^1.6.0",
29 | "babel-preset-stage-3": "^6.24.1",
30 | "copy-webpack-plugin": "^4.3.1",
31 | "cross-env": "^5.0.5",
32 | "css-loader": "^0.28.7",
33 | "file-loader": "^1.1.4",
34 | "html-webpack-include-assets-plugin": "^1.0.2",
35 | "html-webpack-plugin": "^2.30.1",
36 | "pug": "^2.0.0-rc.4",
37 | "vue-loader": "^13.0.5",
38 | "vue-template-compiler": "^2.4.4",
39 | "webpack": "^3.6.0",
40 | "webpack-dev-server": "^2.9.1"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/App.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue.js - Stock Trader
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 | div
3 | div(v-if="!finished")
4 | navigation
5 | goal
6 | transition(name="fade" mode="out-in")
7 | router-view
8 | save-modal
9 | load-modal
10 | help-modal
11 | div(v-if="finished")
12 | end
13 |
14 |
15 |
16 |
34 |
35 |
48 |
--------------------------------------------------------------------------------
/src/components/Asset.vue:
--------------------------------------------------------------------------------
1 |
2 | div.card
3 | div.card-content
4 | div.level
5 | div.level-left
6 | div.level-item
7 | h3.title.is-4 {{ name }}
8 | div.level-item(v-if="quantity")
9 | div.tag.is-info.is-large {{ quantity }}
10 | div.level-item
11 | div.tag.is-success.is-large ${{ price }}
12 | div.card-footer
13 | div.card-footer-item
14 | div.control
15 | input.input(
16 | type="number",
17 | placeholder="Quantity",
18 | v-model="quantityForAction",
19 | @input="$v.quantityForAction.$touch",
20 | :class="quantityForActionClasses"
21 | @keydown.enter="runAction"
22 | tabindex="1"
23 | v-focus="true"
24 | )
25 | div.card-footer-item
26 | strong(:class="calculatedPriceClasses") ${{ calculatedPrice | money }}
27 | div.card-footer-item
28 | div.buttons
29 | button.button(@click="maxQuantity") Max
30 | button.button.is-info(
31 | tabindex="2"
32 | @click="runAction"
33 | :disabled="$v.$invalid"
34 | ) {{ action | capitalize }}
35 |
36 |
37 |
145 |
--------------------------------------------------------------------------------
/src/components/Modal.vue:
--------------------------------------------------------------------------------
1 |
2 | div.modal(
3 | :class="modalClasses",
4 | @keydown.esc="onClose",
5 | v-focus="active"
6 | tabindex="1"
7 | )
8 | div.modal-background(@click="onClose")
9 | div.modal-content
10 | slot
11 | button.modal-close.is-large(@click="onClose")
12 |
13 |
14 |
26 |
27 |
--------------------------------------------------------------------------------
/src/directives/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | const isActiveElementAnInput = () => document.activeElement.tagName === 'INPUT';
4 |
5 | Vue.directive('focus', {
6 | inserted(el) {
7 | if (!isActiveElementAnInput()) {
8 | el.focus();
9 | }
10 | },
11 | update(el) {
12 | if (!isActiveElementAnInput()) {
13 | Vue.nextTick(() => el.focus());
14 | }
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/src/filters/capitalize.js:
--------------------------------------------------------------------------------
1 | import capitalize from 'lodash/capitalize';
2 |
3 | export default value => capitalize(value);
4 |
--------------------------------------------------------------------------------
/src/filters/money.js:
--------------------------------------------------------------------------------
1 | export default value => value.toLocaleString();
2 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 |
4 | import './directives';
5 | import routes from './routes';
6 | import store from './store';
7 |
8 | import App from './App.vue';
9 | import utils from './utils';
10 |
11 | Vue.use(VueRouter);
12 |
13 | const router = new VueRouter({
14 | routes,
15 | mode: 'history'
16 | });
17 |
18 | new Vue({
19 | el: '#app',
20 | router,
21 | store,
22 | render: h => h(App),
23 | mounted() {
24 | const vm = this;
25 | utils.attachKeyboardShortcuts(vm);
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/src/modules/app/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | buy: ({ state, commit }, { price, quantity }) => {
3 | commit('decreaseFunds', price * quantity);
4 | commit('updateProgress');
5 |
6 | if (state.progress >= 100) {
7 | commit('endgame');
8 | }
9 | },
10 | sell: ({ state, commit }, { price, quantity }) => {
11 | commit('increaseFunds', price * quantity);
12 | commit('updateProgress');
13 |
14 | if (state.progress >= 100) {
15 | commit('endgame');
16 | }
17 | },
18 | endday: ({ commit }) => {
19 | commit('endday');
20 | commit('stock/endday', null, { root: true });
21 | },
22 | invokeHelpModal: ({ commit }) => commit('invokeHelpModal'),
23 | revokeHelpModal: ({ commit }) => commit('revokeHelpModal')
24 | };
25 |
--------------------------------------------------------------------------------
/src/modules/app/components/End.vue:
--------------------------------------------------------------------------------
1 |
2 | transition(name="fade" mode="out-in")
3 | section.section
4 | div.container.has-text-centered
5 | h1.title Woohoo!
6 | p Congratulations! You have
7 | strong won!
8 | p Refresh the page to start again.
9 |
--------------------------------------------------------------------------------
/src/modules/app/components/Goal.vue:
--------------------------------------------------------------------------------
1 |
2 | div.hero.is-light
3 | div.hero-body
4 | div.container
5 | div.title.has-text-centered Goal: ${{ goal | money }}
6 | progress.progress(:value="funds", :max="goal", :class="progressClasses")
7 |
8 |
9 |
29 |
--------------------------------------------------------------------------------
/src/modules/app/components/HelpModal.vue:
--------------------------------------------------------------------------------
1 |
2 | transition(name="fade" mode="out-in")
3 | modal(v-if="showHelpModal", :onClose="revokeHelpModal", :active="showHelpModal")
4 | div.box
5 | div.content
6 | h4 Keyboard shortcuts
7 | table.table.is-striped.is-fullwidth
8 | thead
9 | tr
10 | th Key
11 | th Action
12 | tbody
13 | tr
14 | td p
15 | td Go to Portfolio
16 | tr
17 | td s
18 | td Go to Stocks
19 | tr
20 | td d
21 | td End day
22 |
23 |
24 |
36 |
--------------------------------------------------------------------------------
/src/modules/app/components/Home.vue:
--------------------------------------------------------------------------------
1 |
2 | div.section
3 | div.container
4 | h2.title Home
5 | div.content
6 | h3 Introduction
7 | p Welcome to the Stock Trader! Here you can play around on the stock market and get a little bit of knowledge about what brokers are doing every day!
8 | h3 How to play
9 | p
10 | | This is really easy. Just buy some stocks on the
11 | router-link(to="/stocks") market
12 | | (of course if they're priced well), then end the day, check the prices again
13 | | and decide if you would be profitable
14 | router-link(to="/portfolio") to sell them right now!
15 | | So it all ends up in few simple steps:
16 | ol
17 | li Buy/sell stocks
18 | li End the day
19 | li Check the prices
20 | li Profit & Repeat
21 |
--------------------------------------------------------------------------------
/src/modules/app/components/Navigation.vue:
--------------------------------------------------------------------------------
1 |
2 | nav.navbar.is-dark.is-active
3 | div.container
4 | div.navbar-brand
5 | router-link(to="/" class="navbar-item is-text-centered")
6 | h1.title.is-4.has-text-white Stock Trader
7 | div.navbar-menu.is-active
8 | div.navbar-start
9 | router-link(to="/portfolio" class="navbar-item") Portfolio
10 | router-link(to="/stocks" class="navbar-item") Stocks
11 | div.navbar-end
12 | persistence
13 | div.navbar-item Day:
14 | |
15 | strong.has-text-white {{ day }}
16 | div.navbar-item
17 | button.button(@click="endday") End Day
18 | div.navbar-item Funds:
19 | |
20 | strong.has-text-white ${{ funds | money }}
21 | div.navbar-item(@click="invokeHelpModal")
22 | span.icon.has-text-white
23 | i.fa.fa-question-circle.fa-lg
24 |
25 |
26 |
27 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/src/modules/app/index.js:
--------------------------------------------------------------------------------
1 | import actions from './actions';
2 | import mutations from './mutations';
3 |
4 | import Home from './components/Home';
5 | import End from './components/End';
6 | import Navigation from './components/Navigation';
7 | import Goal from './components/Goal';
8 | import HelpModal from './components/HelpModal';
9 |
10 | const defaultState = {
11 | day: 1,
12 | funds: 10000,
13 | goal: 1000000,
14 | progress: 1,
15 | finished: false,
16 | showHelpModal: false
17 | };
18 |
19 | const module = {
20 | namespaced: true,
21 | state: defaultState,
22 | mutations,
23 | actions
24 | };
25 |
26 | export { module as default, Navigation, Home, Goal, End, HelpModal };
27 |
--------------------------------------------------------------------------------
/src/modules/app/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | state(state, newState) {
3 | state = Object.assign(state, newState);
4 | },
5 | decreaseFunds(state, amount) {
6 | state.funds -= amount;
7 | },
8 | increaseFunds(state, amount) {
9 | state.funds += amount;
10 | },
11 | updateProgress(state) {
12 | state.progress = state.funds / state.goal * 100;
13 | },
14 | endgame(state) {
15 | state.finished = true;
16 | },
17 | endday(state) {
18 | state.day++;
19 | },
20 | invokeHelpModal(state) {
21 | state.showHelpModal = true;
22 | },
23 | revokeHelpModal(state) {
24 | state.showHelpModal = false;
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/modules/persistence/actions.js:
--------------------------------------------------------------------------------
1 | import pick from 'lodash/pick';
2 | import api from './api';
3 |
4 | export default {
5 | invokeSaveModal: ({ commit }) => commit('invokeSaveModal'),
6 | save: ({ rootState, commit }, name) => {
7 | commit('saveStart');
8 |
9 | const stateToSave = {
10 | name,
11 | timestamp: Date.now(),
12 | state: pick(rootState, ['app', 'stock', 'portfolio'])
13 | };
14 |
15 | return api
16 | .save(stateToSave)
17 | .then(response => response.json())
18 | .then(() => {
19 | commit('saveEnd');
20 | commit('revokeSaveModal');
21 | })
22 | .catch(() => commit('saveError'));
23 | },
24 | cancelSave: ({ commit }) => {
25 | commit('revokeSaveModal');
26 | },
27 | invokeLoadModal: ({ commit }) => {
28 | commit('loadStart');
29 |
30 | return api
31 | .load()
32 | .then(response => response.json())
33 | .then(data => {
34 | commit('loadEnd', Object.values(data));
35 | commit('invokeLoadModal');
36 | })
37 | .catch(() => commit('loadError'));
38 | },
39 | load: ({ commit }, { state }) => {
40 | Object.keys(state).forEach(key => {
41 | commit(`${key}/state`, state[key], { root: true });
42 | });
43 | commit('revokeLoadModal');
44 | },
45 | cancelLoad: ({ commit }) => {
46 | commit('revokeLoadModal');
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/modules/persistence/api.js:
--------------------------------------------------------------------------------
1 | const apiUrl = 'https://vuejs-stock-trader-23217.firebaseio.com';
2 |
3 | const resource = {
4 | save: state =>
5 | fetch(`${apiUrl}/state.json`, {
6 | method: 'POST',
7 | body: JSON.stringify(state)
8 | }),
9 | load: () =>
10 | fetch(`${apiUrl}/state.json`, {
11 | method: 'GET'
12 | })
13 | };
14 |
15 | export default {
16 | save(state) {
17 | return resource.save(state);
18 | },
19 | load() {
20 | return resource.load();
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/modules/persistence/components/LoadModal.vue:
--------------------------------------------------------------------------------
1 |
2 | transition(name="fade" mode="out-in")
3 | modal(v-if="show", :onClose="cancelLoad", :active="show")
4 | div.box
5 | h2.title.is-3 Load
6 | h3.subtitle.is-6 Choose a save from the table below. Just click on the desired row to go back to selected game state.
7 | table.table.is-striped.is-hoverable.is-fullwidth
8 | thead
9 | tr
10 | th Date
11 | th Name
12 | tbody
13 | tr(v-for="item in items" @click="load(item)")
14 | td {{ item.timestamp }}
15 | td {{ item.name }}
16 |
17 |
18 |
34 |
35 |
40 |
--------------------------------------------------------------------------------
/src/modules/persistence/components/PersistenceNavItem.vue:
--------------------------------------------------------------------------------
1 |
2 | div.navbar-item
3 | div.buttons
4 | button.button.is-info(@click="invokeSaveModal") Save
5 | button.button.is-info(@click="invokeLoadModal" :class="{ 'is-loading': inProgress }") Load
6 |
7 |
8 |
18 |
--------------------------------------------------------------------------------
/src/modules/persistence/components/SaveModal.vue:
--------------------------------------------------------------------------------
1 |
2 | transition(name="fade" mode="out-in")
3 | modal(v-if="show", :onClose="cancelSave", :active="show")
4 | div.box
5 | div.field
6 | label.label Save name
7 | div.control
8 | input.input(v-model="saveName", v-focus="show", @keydown.enter="save(saveName)")
9 | div.field.is-grouped.is-grouped-right
10 | div.control
11 | button.button(@click="cancelSave") Cancel
12 | div.control
13 | button.button.is-primary(:class="submitButtonClasses", @click="save(saveName)") Save
14 |
15 |
16 |
43 |
--------------------------------------------------------------------------------
/src/modules/persistence/index.js:
--------------------------------------------------------------------------------
1 | import mutations from './mutations';
2 | import actions from './actions';
3 | import './api';
4 |
5 | import PersistenceNavItem from './components/PersistenceNavItem';
6 | import SaveModal from './components/SaveModal';
7 | import LoadModal from './components/LoadModal';
8 |
9 | const defaultState = {
10 | load: {
11 | items: [],
12 | inProgress: false,
13 | showModal: false
14 | },
15 | save: {
16 | inProgress: false,
17 | showModal: false
18 | }
19 | };
20 |
21 | const module = {
22 | namespaced: true,
23 | state: defaultState,
24 | mutations,
25 | actions
26 | };
27 |
28 | export { module as default, PersistenceNavItem, SaveModal, LoadModal };
29 |
--------------------------------------------------------------------------------
/src/modules/persistence/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | state(state, newState) {
3 | state = Object.assign(state, newState);
4 | },
5 | invokeSaveModal(state) {
6 | state.save.showModal = true;
7 | },
8 | revokeSaveModal(state) {
9 | state.save.showModal = false;
10 | },
11 | saveStart(state) {
12 | state.save.inProgress = true;
13 | },
14 | saveEnd(state) {
15 | state.save.inProgress = false;
16 | },
17 | saveError(state) {
18 | state.save.inProgress = false;
19 | },
20 | invokeLoadModal(state) {
21 | state.load.showModal = true;
22 | },
23 | revokeLoadModal(state) {
24 | state.load.showModal = false;
25 | },
26 | loadStart(state) {
27 | state.load.inProgress = true;
28 | },
29 | loadEnd(state, data) {
30 | state.load.items = data
31 | .map(item => ({
32 | ...item,
33 | timestamp: new Date(item.timestamp).toLocaleString()
34 | }))
35 | .reverse();
36 |
37 | state.load.inProgress = false;
38 | },
39 | loadError(state) {
40 | state.load.inProgress = false;
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/modules/portfolio/components/Portfolio.vue:
--------------------------------------------------------------------------------
1 |
2 | div.section
3 | div.container
4 | h2.title Portfolio
5 | div(v-if="items.length === 0")
6 | div.content
7 | p
8 | | You have no shares right now. Buy some on the
9 | router-link(to="/stocks") market.
10 | div(v-else)
11 | div.columns.is-multiline
12 | div.column.is-half(v-for="asset in items")
13 | asset(
14 | :name="asset.name",
15 | :quantity="asset.quantity",
16 | :price="asset.price",
17 | action="SELL"
18 | :onAction="sell"
19 | )
20 |
21 |
22 |
35 |
--------------------------------------------------------------------------------
/src/modules/portfolio/getters.js:
--------------------------------------------------------------------------------
1 | export default {
2 | items(state, getters, rootState, rootGetters) {
3 | return state.items.map(item => ({
4 | ...item,
5 | price: rootGetters['stock/getPriceByName'](item.name)
6 | }));
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/modules/portfolio/index.js:
--------------------------------------------------------------------------------
1 | import getters from './getters';
2 | import mutations from './mutations';
3 |
4 | import Portfolio from './components/Portfolio';
5 |
6 | const defaultState = {
7 | items: []
8 | };
9 |
10 | const module = {
11 | namespaced: true,
12 | state: defaultState,
13 | getters,
14 | mutations
15 | };
16 |
17 | export {
18 | module as default,
19 | Portfolio
20 | }
21 |
--------------------------------------------------------------------------------
/src/modules/portfolio/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | state(state, newState) {
3 | state = Object.assign(state, newState);
4 | },
5 | buy(state, { name, quantity, price }) {
6 | const asset = state.items.find(asset => asset.name === name);
7 |
8 | if (asset) {
9 | asset.quantity += quantity;
10 | } else {
11 | state.items.push({ name, quantity });
12 | }
13 | },
14 | sell(state, { name, quantity, price }) {
15 | const asset = state.items.find(asset => asset.name === name);
16 | const assetQuantity = asset.quantity - quantity;
17 |
18 | if (assetQuantity === 0) {
19 | const assetIndex = state.items.indexOf(asset);
20 | state.items.splice(assetIndex, 1);
21 | } else {
22 | asset.quantity = assetQuantity;
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/modules/stock/actions.js:
--------------------------------------------------------------------------------
1 | export default {
2 | generateData: ({ commit }) => commit('generate'),
3 | buy: ({ commit, dispatch, getters }, { name, quantity }) => {
4 | const payload = {
5 | ...getters.getByName(name),
6 | quantity
7 | };
8 |
9 | dispatch('app/buy', payload, { root: true });
10 | commit('portfolio/buy', payload, { root: true });
11 | },
12 | sell: ({ commit, dispatch, getters }, { name, quantity }) => {
13 | const payload = {
14 | ...getters.getByName(name),
15 | quantity
16 | };
17 |
18 | dispatch('app/sell', payload, { root: true });
19 | commit('portfolio/sell', payload, { root: true });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/src/modules/stock/components/Stocks.vue:
--------------------------------------------------------------------------------
1 |
2 | div.section
3 | div.container
4 | h2.title Stocks
5 | div.columns.is-multiline
6 | div.column.is-half(v-for="asset in items")
7 | asset(
8 | :name="asset.name",
9 | :price="asset.price",
10 | :funds="funds"
11 | action="BUY"
12 | :onAction="buy"
13 | )
14 |
15 |
16 |
37 |
--------------------------------------------------------------------------------
/src/modules/stock/getters.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getByName: state => name => {
3 | return state.items.find(item => item.name === name);
4 | },
5 | getPriceByName: (state, getters) => name => {
6 | return getters.getByName(name).price;
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/src/modules/stock/index.js:
--------------------------------------------------------------------------------
1 | import actions from './actions';
2 | import mutations from './mutations';
3 | import getters from './getters';
4 |
5 | import Stocks from './components/Stocks';
6 |
7 | const defaultState = {
8 | initialized: false,
9 | items: [
10 | { name: 'Apple', price: 25 },
11 | { name: 'Microsoft', price: 15 },
12 | { name: 'Oracle', price: 50 }
13 | ]
14 | };
15 |
16 | const module = {
17 | namespaced: true,
18 | state: defaultState,
19 | mutations,
20 | getters,
21 | actions
22 | };
23 |
24 | export { module as default, Stocks };
25 |
--------------------------------------------------------------------------------
/src/modules/stock/mutations.js:
--------------------------------------------------------------------------------
1 | import random from 'lodash/random';
2 | import capitalize from 'lodash/capitalize';
3 | import randomWords from 'random-words';
4 |
5 | export default {
6 | state(state, newState) {
7 | state = Object.assign(state, newState);
8 | },
9 | generate(state) {
10 | state.items = randomWords(10).map(name => ({
11 | name: capitalize(name),
12 | price: random(15, 100)
13 | }));
14 | state.initialized = true;
15 | },
16 | endday(state) {
17 | state.items = state.items.map(stock => ({
18 | ...stock,
19 | price: random(15, 100)
20 | }));
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import { Home } from './modules/app';
2 | import { Portfolio } from './modules/portfolio';
3 | import { Stocks } from './modules/stock';
4 |
5 | export default [
6 | { path: '/', component: Home },
7 | { path: '/portfolio', component: Portfolio },
8 | { path: '/stocks', component: Stocks }
9 | ];
10 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 |
4 | import app from './modules/app';
5 | import portfolio from './modules/portfolio';
6 | import stock from './modules/stock';
7 | import persistence from './modules/persistence';
8 |
9 | Vue.use(Vuex);
10 |
11 | export default new Vuex.Store({
12 | modules: {
13 | app,
14 | portfolio,
15 | stock,
16 | persistence
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | attachKeyboardShortcuts: vm => {
3 | window.addEventListener('keydown', event => {
4 | switch (event.key) {
5 | case 'p':
6 | vm.$router.push('/portfolio');
7 | event.preventDefault();
8 | break;
9 | case 's':
10 | vm.$router.push('/stocks');
11 | event.preventDefault();
12 | break;
13 | case 'd':
14 | vm.$store.dispatch('app/endday');
15 | event.preventDefault();
16 | break;
17 | }
18 | });
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/(.*)",
5 | "destination": "/"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var HtmlWebpackIncludeAssetsPlugin = require('html-webpack-include-assets-plugin');
5 | var CopyWebpackPlugin = require('copy-webpack-plugin');
6 |
7 | module.exports = {
8 | entry: './src/main.js',
9 | output: {
10 | path: path.resolve(__dirname, './dist'),
11 | publicPath: '/dist/',
12 | filename: 'build.js'
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.css$/,
18 | use: ['vue-style-loader', 'css-loader']
19 | },
20 | {
21 | test: /\.vue$/,
22 | loader: 'vue-loader'
23 | },
24 | {
25 | test: /\.js$/,
26 | loader: 'babel-loader',
27 | exclude: /node_modules/
28 | },
29 | {
30 | test: /\.(png|jpg|gif|svg)$/,
31 | loader: 'file-loader',
32 | options: {
33 | name: '[name].[ext]?[hash]'
34 | }
35 | }
36 | ]
37 | },
38 | resolve: {
39 | alias: {
40 | vue$: 'vue/dist/vue.esm.js'
41 | },
42 | extensions: ['*', '.js', '.vue', '.json']
43 | },
44 | devServer: {
45 | historyApiFallback: true,
46 | noInfo: true,
47 | overlay: true
48 | },
49 | performance: {
50 | hints: false
51 | },
52 | devtool: '#eval-source-map'
53 | };
54 |
55 | if (process.env.NODE_ENV === 'production') {
56 | delete module.exports.output.publicPath;
57 |
58 | module.exports.devtool = '#source-map';
59 | // http://vue-loader.vuejs.org/en/workflow/production.html
60 | module.exports.plugins = (module.exports.plugins || []).concat([
61 | new webpack.DefinePlugin({
62 | 'process.env': {
63 | NODE_ENV: '"production"'
64 | }
65 | }),
66 | new webpack.optimize.UglifyJsPlugin({
67 | sourceMap: true,
68 | compress: {
69 | warnings: false
70 | }
71 | }),
72 | new webpack.LoaderOptionsPlugin({
73 | minimize: true
74 | }),
75 | new HtmlWebpackPlugin({
76 | template: 'src/App.html'
77 | }),
78 | new HtmlWebpackIncludeAssetsPlugin({
79 | assets: [
80 | 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.1/css/bulma.min.css',
81 | 'https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'
82 | ],
83 | append: false
84 | }),
85 | new CopyWebpackPlugin([{ from: 'config/*', to: '[name].[ext]' }])
86 | ]);
87 | }
88 |
--------------------------------------------------------------------------------