WIP: Port over work from twigs-nextcloud
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
71a4d8e8a5
commit
1752c5d2d0
32 changed files with 1611 additions and 219 deletions
1
.env.development
Normal file
1
.env.development
Normal file
|
@ -0,0 +1 @@
|
|||
VUE_APP_API_URL=https://3000code.brawner.home
|
54
README.md
54
README.md
|
@ -1,24 +1,38 @@
|
|||
# twigs-web
|
||||
# Twigs
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
## Vue Migration Checklist
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
_Could also be used as a testing checklist_
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
- [ ] Login
|
||||
- [ ] Logout
|
||||
- [ ] Register
|
||||
- [ ] Budget list
|
||||
- [ ] Create budget
|
||||
- [ ] Budget details
|
||||
- [ ]
|
||||
- [ ] Edit budget
|
||||
- [ ] Change name
|
||||
- [ ] Change description
|
||||
- [ ] Change users/permissions
|
||||
- [ ] Delete budget
|
||||
- [ ] Category list
|
||||
- [ ] Create category
|
||||
- [ ] Category details
|
||||
- [ ] Edit category
|
||||
- [ ] Change name
|
||||
- [ ] Change description
|
||||
- [ ] Change expense/income
|
||||
- [ ] Change amount
|
||||
- [ ] Delete category
|
||||
- [ ] Transaction list
|
||||
- [ ] Create transaction
|
||||
- [ ] Transaction details
|
||||
- [ ] Edit transaction
|
||||
- [ ] Change name
|
||||
- [ ] Change description
|
||||
- [ ] Change expense/income
|
||||
- [ ] Change amount
|
||||
- [ ] Change date
|
||||
- [ ] Delete transaction
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
|
160
package-lock.json
generated
160
package-lock.json
generated
|
@ -1739,6 +1739,16 @@
|
|||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
||||
|
@ -1765,6 +1775,34 @@
|
|||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"find-cache-dir": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz",
|
||||
|
@ -1786,6 +1824,25 @@
|
|||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
@ -1850,6 +1907,16 @@
|
|||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||
|
@ -1866,6 +1933,18 @@
|
|||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-yR+BS90EOXTNieasf8ce9J3TFCpm2DGqoqdbtiwQ33hon3FyIznLX7sKavAq1VmfBnOeV6It0Htg4aniv8ph1g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -11153,87 +11232,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.6.tgz",
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link rel="stylesheet" href="<%= BASE_URL %>style.css">
|
||||
<title>Twigs</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
<strong>We're sorry but Twigs doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
37
public/style.css
Normal file
37
public/style.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
:root {
|
||||
--good-color: green;
|
||||
--warn-color: yellow;
|
||||
--danger-color: red;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.good {
|
||||
color: var(--good-color);
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: var(--warn-color);
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--danger-color);
|
||||
}
|
35
src/App.vue
35
src/App.vue
|
@ -1,32 +1,21 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<div id="nav">
|
||||
<router-link to="/">Home</router-link> |
|
||||
<router-link to="/about">About</router-link>
|
||||
</div>
|
||||
<router-view/>
|
||||
<div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
components: {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
.app-twigs {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#nav {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#nav a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#nav a.router-link-exact-active {
|
||||
color: #42b983;
|
||||
.app-twigs > div {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.7 KiB |
|
@ -1,58 +0,0 @@
|
|||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
63
src/components/ProgressBar.vue
Normal file
63
src/components/ProgressBar.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="progress-bar">
|
||||
<div class="progress" :class="status" :style="{ width: progress + '%' }" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProgressBar',
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
invertColors: Boolean,
|
||||
},
|
||||
computed: {
|
||||
progress: function() {
|
||||
return Math.round((this.value / this.max) * 100)
|
||||
},
|
||||
status: function() {
|
||||
if (this.progress <= 33) {
|
||||
return this.invertColors ? 'danger' : 'good'
|
||||
}
|
||||
|
||||
if (this.progress <= 66) {
|
||||
return 'warn'
|
||||
}
|
||||
|
||||
if (this.progress <= 100) {
|
||||
return this.invertColors ? 'good' : 'danger'
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.progress-bar {
|
||||
height: 0.5em;
|
||||
background: #f1f1f1;
|
||||
border-radius: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: width ease-in-out 0.5s;
|
||||
}
|
||||
.progress.good {
|
||||
background: var(--good-color);
|
||||
}
|
||||
.progress.warn {
|
||||
background: var(--warn-color);
|
||||
}
|
||||
.progress.danger {
|
||||
background: var(--danger-color);
|
||||
}
|
||||
</style>
|
20
src/components/TwigsHome.vue
Normal file
20
src/components/TwigsHome.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="twigs-home">
|
||||
<h2>Welcome to Twigs!</h2>
|
||||
<p>To get started, create a new budget or select one from the list to the left.</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'TwigsHome',
|
||||
components: {
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
}
|
||||
</script>
|
118
src/components/budget/BudgetDetails.vue
Normal file
118
src/components/budget/BudgetDetails.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="budget" class="budget">
|
||||
<div class="header">
|
||||
<div class="header-info">
|
||||
<h2>{{ budget.name }}</h2>
|
||||
<p>{{ budget.description }}</p>
|
||||
<h3
|
||||
v-if="balance"
|
||||
>Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</h3>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button @click="addTransaction()">
|
||||
<span class="icon-add" /> Add Transaction
|
||||
</button>
|
||||
<button @click="addCategory()">
|
||||
<span class="icon-add" /> Add Category
|
||||
</button>
|
||||
<button @click="editBudget()">Edit</button>
|
||||
<button @click="deleteBudget()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget-details">
|
||||
<div class="card income">
|
||||
<h3>Income</h3>
|
||||
<CategoryList :budget-id="budget.id" :expense="false" />
|
||||
</div>
|
||||
<div class="card expenses">
|
||||
<h3>Expenses</h3>
|
||||
<CategoryList :budget-id="budget.id" :expense="true" />
|
||||
</div>
|
||||
<div class="card transactions">
|
||||
<h3>Recent Transactions</h3>
|
||||
<TransactionList :budget-id="budget.id" :limit="5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!budget">
|
||||
<p>Select a budget from the list to the left to get started.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters, mapState } from "vuex";
|
||||
import CategoryList from "../category/CategoryList";
|
||||
import TransactionList from "../transaction/TransactionList";
|
||||
|
||||
export default {
|
||||
name: "BudgetDetails",
|
||||
components: {
|
||||
CategoryList,
|
||||
TransactionList
|
||||
},
|
||||
computed: {
|
||||
...mapState(["budgets", "currentBudget"]),
|
||||
...mapGetters(["budget"]),
|
||||
balance: function(state) {
|
||||
if (!state.currentBudget) {
|
||||
return 0;
|
||||
}
|
||||
return this.$store.getters.budgetBalance(state.currentBudget) / 100;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$store.dispatch("budgetDetailsViewed", this.$route.params.id);
|
||||
},
|
||||
addTransaction() {
|
||||
this.$store.dispatch("addTransactionClicked");
|
||||
},
|
||||
addCategory() {
|
||||
this.$store.dispatch("addCategoryClicked");
|
||||
},
|
||||
editBudget() {
|
||||
this.$store.dispatch("editBudgetClicked", this.$route.params.id);
|
||||
},
|
||||
deleteBudget() {
|
||||
this.$store.dispatch("deleteBudgetClicked", this.$route.params.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.budget-details {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
grid-gap: 0.5em;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.income {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.expenses {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.transactions {
|
||||
grid-column: 1;
|
||||
}
|
||||
</style>
|
90
src/components/budget/BudgetForm.vue
Normal file
90
src/components/budget/BudgetForm.vue
Normal file
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="!loading" class="add-edit-budget">
|
||||
<h2>{{ budget.id ? 'Edit' : 'Add' }} Budget</h2>
|
||||
<input v-model="budget.name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
title="Name">
|
||||
<textarea v-model="budget.description" placeholder="Description" title="Description" />
|
||||
<div class="sharing">
|
||||
<h3>Sharing</h3>
|
||||
<input v-model="user"
|
||||
type="test"
|
||||
placeholde="User"
|
||||
title="User"
|
||||
@keyup.enter="addPermission()">
|
||||
<ul v-if="budget.users" class="sharing-users">
|
||||
<li v-for="userPermission in budget.users" :key="userPermission.user">
|
||||
<span v-if="userPermission.user">
|
||||
{{ userPermission.user }}
|
||||
</span>
|
||||
<span v-if="userPermission.permission">
|
||||
: {{ userPermission.permission }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button @click="saveBudget()">
|
||||
Save Budget
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="loading" class="icon-loading" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'BudgetForm',
|
||||
components: {
|
||||
},
|
||||
props: {
|
||||
budget: {
|
||||
default: {},
|
||||
type: () => {},
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
saving: false,
|
||||
user: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loading: state => state.budget === undefined || state.saving,
|
||||
},
|
||||
methods: {
|
||||
addPermission() {
|
||||
const user = this.user
|
||||
this.user = undefined
|
||||
this.budget.users = this.budget.users.filter(u => u.user !== user)
|
||||
this.budget.users.push({
|
||||
user: user,
|
||||
permission: 2,
|
||||
})
|
||||
},
|
||||
saveBudget() {
|
||||
this.saving = true
|
||||
this.$store.dispatch('budgetFormSaveClicked', this.budget)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.add-edit-budget > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
.radio-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-container label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.icon-loading {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
31
src/components/budget/BudgetList.vue
Normal file
31
src/components/budget/BudgetList.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<ul>
|
||||
<li @click="newBudget()">New Budget</li>
|
||||
<li v-for="budget in budgets" :key="budget.id">
|
||||
<router-link :to="{ name: 'budgetDetails', params: { id: budget.id } }">{{ budget.name }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "BudgetList",
|
||||
components: {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["budgets"])
|
||||
},
|
||||
mounted() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
load: function() {
|
||||
this.$store.dispatch("budgetListViewed");
|
||||
},
|
||||
newBudget: function() {
|
||||
this.$store.dispatch("addBudgetClicked");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
20
src/components/budget/EditBudget.vue
Normal file
20
src/components/budget/EditBudget.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<BudgetForm :budget="budget" />
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import BudgetForm from './BudgetForm'
|
||||
|
||||
export default {
|
||||
name: 'EditBudget',
|
||||
components: {
|
||||
BudgetForm,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['budget']),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('editBudgetViewed', this.$route.params.id)
|
||||
},
|
||||
}
|
||||
</script>
|
20
src/components/budget/NewBudget.vue
Normal file
20
src/components/budget/NewBudget.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<BudgetForm :budget="budget" />
|
||||
</template>
|
||||
<script>
|
||||
import BudgetForm from './BudgetForm'
|
||||
|
||||
export default {
|
||||
name: 'NewBudget',
|
||||
components: {
|
||||
BudgetForm,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
budget: {
|
||||
users: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
66
src/components/category/CategoryDetails.vue
Normal file
66
src/components/category/CategoryDetails.vue
Normal file
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div v-if="category" class="category-details">
|
||||
<div class="header-info">
|
||||
<h2>{{ category.title }}</h2>
|
||||
<h3>
|
||||
Balance: {{ balance.toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}
|
||||
</h3>
|
||||
<div class="actions">
|
||||
<button @click="addTransaction()">
|
||||
<span class="icon-add" /> Add Transaction
|
||||
</button>
|
||||
<button @click="editCategory()">
|
||||
Edit
|
||||
</button>
|
||||
<button @click="deleteCategory()">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Transactions</h3>
|
||||
<TransactionList :category-id="category.id" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import TransactionList from '../transaction/TransactionList'
|
||||
|
||||
export default {
|
||||
name: 'CategoryDetails',
|
||||
components: {
|
||||
TransactionList,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['categories', 'currentCategory']),
|
||||
category: function(state) {
|
||||
if (state.categories.length === 0 || !state.currentCategory) {
|
||||
return false
|
||||
}
|
||||
return state.categories.find((category) => category.id === Number.parseInt(state.currentCategory))
|
||||
},
|
||||
balance: function(state) {
|
||||
if (!state.currentCategory) {
|
||||
return 0
|
||||
}
|
||||
return this.$store.getters.categoryBalance(state.currentCategory) / 100
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$store.dispatch('categoryDetailsViewed', this.$route.params.id)
|
||||
},
|
||||
addTransaction() {
|
||||
this.$store.dispatch('addTransactionClicked')
|
||||
},
|
||||
editCategory() {
|
||||
this.$store.dispatch('editCategoryClicked', this.$route.params.id)
|
||||
},
|
||||
deleteCategory() {
|
||||
this.$store.dispatch('deleteCategoryClicked', this.$route.params.id)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
91
src/components/category/CategoryForm.vue
Normal file
91
src/components/category/CategoryForm.vue
Normal file
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="!loading" class="add-edit-category">
|
||||
<h2>{{ category.id ? 'Edit' : 'Add' }} Category</h2>
|
||||
<input v-model="category.title"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
title="Name">
|
||||
<input v-model.number="category.amount"
|
||||
type="number"
|
||||
placeholder="Amount"
|
||||
title="Amount">
|
||||
<div class="radio-container">
|
||||
<input id="expense"
|
||||
v-model="category.expense"
|
||||
type="radio"
|
||||
:value="true">
|
||||
<label for="expense">Expense</label>
|
||||
<input id="income"
|
||||
v-model="category.expense"
|
||||
type="radio"
|
||||
:value="false">
|
||||
<label for="income">Income</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input v-model="category.archived" type="checkbox"/>
|
||||
<label>Archived</label>
|
||||
</div>
|
||||
<select v-model="category.budgetId" @change="updateCategories()">
|
||||
<option disabled value>
|
||||
Select a budget
|
||||
</option>
|
||||
<option v-for="budget in budgets" :key="budget.id" :value="budget.id">
|
||||
{{ budget.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="saveCategory()">
|
||||
Save Category
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="loading" class="icon-loading" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'CategoryForm',
|
||||
components: {
|
||||
},
|
||||
props: {
|
||||
category: {
|
||||
default: () => {},
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
saving: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['budgets']),
|
||||
loading: state => state.category === undefined || state.saving,
|
||||
},
|
||||
methods: {
|
||||
saveCategory() {
|
||||
this.saving = true
|
||||
this.$store.dispatch('categoryFormSaveClicked', this.category)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.add-edit-category > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
.radio-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-container label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.icon-loading {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
85
src/components/category/CategoryList.vue
Normal file
85
src/components/category/CategoryList.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<ul>
|
||||
<li v-for="category in filteredCategories" :key="category.id">
|
||||
<a class="category-summary" @click="view(category.id)">
|
||||
<div class="category-info">
|
||||
<p class="category-name">{{ category.title }}</p>
|
||||
<p class="category-balance">
|
||||
{{ category.expense ? "Remaining" : "Pending" }}:
|
||||
{{
|
||||
(
|
||||
categoryRemainingBalance(category) / 100
|
||||
).toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar
|
||||
:max="category.amount"
|
||||
:value="Math.abs(categoryBalance(category.id))"
|
||||
:invert-colors="!category.expense" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import ProgressBar from '../ProgressBar'
|
||||
|
||||
export default {
|
||||
name: 'CategoryList',
|
||||
components: {
|
||||
ProgressBar,
|
||||
},
|
||||
props: {
|
||||
budgetId: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
expense: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['categories', 'currentCategory']),
|
||||
...mapGetters(['categoryBalance', 'categoryRemainingBalance']),
|
||||
filteredCategories: function(state) {
|
||||
return state.categories.filter(
|
||||
category => category.expense === this.expense && category.archived === false
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
view: function(id) {
|
||||
this.$store.dispatch('categoryClicked', id)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.category-summary {
|
||||
padding: 0.5em;
|
||||
height: 4em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-summary * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-summary:hover {
|
||||
background: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.category-summary .category-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
</style>
|
20
src/components/category/EditCategory.vue
Normal file
20
src/components/category/EditCategory.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<CategoryForm :category="category" />
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import CategoryForm from './CategoryForm'
|
||||
|
||||
export default {
|
||||
name: 'EditCategory',
|
||||
components: {
|
||||
CategoryForm,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['category']),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('editCategoryViewed', this.$route.params.id)
|
||||
},
|
||||
}
|
||||
</script>
|
21
src/components/category/NewCategory.vue
Normal file
21
src/components/category/NewCategory.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<CategoryForm :category="category" />
|
||||
</template>
|
||||
<script>
|
||||
import CategoryForm from './CategoryForm'
|
||||
|
||||
export default {
|
||||
name: 'NewCategory',
|
||||
components: {
|
||||
CategoryForm,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
category: {
|
||||
expense: true,
|
||||
budgetId: this.$store.state.currentBudget,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
20
src/components/transaction/EditTransaction.vue
Normal file
20
src/components/transaction/EditTransaction.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<TransactionForm :transaction="transaction" />
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import TransactionForm from './TransactionForm'
|
||||
|
||||
export default {
|
||||
name: 'EditTransaction',
|
||||
components: {
|
||||
TransactionForm,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['transaction']),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('editTransactionViewed', this.$route.params.id)
|
||||
},
|
||||
}
|
||||
</script>
|
23
src/components/transaction/NewTransaction.vue
Normal file
23
src/components/transaction/NewTransaction.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<TransactionForm :transaction="transaction" />
|
||||
</template>
|
||||
<script>
|
||||
import TransactionForm from './TransactionForm'
|
||||
|
||||
export default {
|
||||
name: 'NewTransaction',
|
||||
components: {
|
||||
TransactionForm,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
transaction: {
|
||||
date: new Date(),
|
||||
expense: true,
|
||||
budgetId: this.$store.state.currentBudget,
|
||||
categoryId: this.$store.state.currentCategory,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
69
src/components/transaction/TransactionDetails.vue
Normal file
69
src/components/transaction/TransactionDetails.vue
Normal file
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div v-if="transaction" class="transaction-details">
|
||||
<h2>{{ transaction.title }}</h2>
|
||||
<h3
|
||||
:class="transaction.expense ? 'danger' : 'good'">
|
||||
{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }} {{ transaction.expense ? 'Expense' : 'Income' }}
|
||||
</h3>
|
||||
<p class="transaction-info date">
|
||||
{{ new Date(transaction.date).toLocaleDateString() }}
|
||||
</p>
|
||||
<p class="transaction-info description">
|
||||
{{ transaction.description }}
|
||||
</p>
|
||||
<p v-if="category" class="transaction-info category">
|
||||
Category: {{ category.title }}
|
||||
</p>
|
||||
<p v-if="budget" class="transaction-info budget">
|
||||
Budget: {{ budget.name }}
|
||||
</p>
|
||||
<p class="transaction-info registered-by">
|
||||
Registered By:
|
||||
<!-- <UserBubble
|
||||
:user="transaction.createdBy"
|
||||
:display-name="transaction.createdBy" /> -->
|
||||
{{ new Date(transaction.createdDate).toLocaleDateString() }}
|
||||
</p>
|
||||
<p v-if="transaction.updatedBy" class="transaction-info updated-by">
|
||||
Updated By:
|
||||
<!-- <UserBubble
|
||||
:user="transaction.updatedBy"
|
||||
:display-name="transaction.updatedBy" /> -->
|
||||
{{ new Date(transaction.updatedDate).toLocaleDateString() }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'TransactionDetails',
|
||||
components: {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['transaction']),
|
||||
category: function() {
|
||||
const transaction = this.$store.getters.transaction
|
||||
if (!transaction || !transaction.categoryId) {
|
||||
return undefined
|
||||
}
|
||||
return this.$store.getters.categories.find(category => category.id === transaction.categoryId)
|
||||
},
|
||||
budget: function() {
|
||||
const transaction = this.$store.getters.transaction
|
||||
if (!transaction || !transaction.budgetId) {
|
||||
return undefined
|
||||
}
|
||||
return this.$store.getters.budgets.find(budget => budget.id === transaction.budgetId)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.$store.dispatch('transactionDetailsViewed', this.$route.params.id)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
95
src/components/transaction/TransactionForm.vue
Normal file
95
src/components/transaction/TransactionForm.vue
Normal file
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="!loading" class="add-edit-transaction">
|
||||
<h2>{{ transaction.id ? "Edit" : "Add" }} Transaction</h2>
|
||||
<input v-model="transaction.title" type="text" placeholder="Name" title="Name" />
|
||||
<textarea v-model="transaction.description" placeholder="Description" title="Description" />
|
||||
<input v-model.number="transaction.amount" type="number" placeholder="Amount" title="Amount" />
|
||||
<!-- <DatetimePicker :value="transaction.date" type="datetime" /> -->
|
||||
<div class="radio-container">
|
||||
<input id="expense" v-model="transaction.expense" type="radio" :value="true" />
|
||||
<label for="expense">Expense</label>
|
||||
<input id="income" v-model="transaction.expense" type="radio" :value="false" />
|
||||
<label for="income">Income</label>
|
||||
</div>
|
||||
<select v-model="transaction.budgetId" @change="updateCategories()">
|
||||
<option disabled value>Select a budget</option>
|
||||
<option v-for="budget in budgets" :key="budget.id" :value="budget.id">
|
||||
{{ budget.name }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="transaction.categoryId">
|
||||
<option disabled value>Select a category</option>
|
||||
<option
|
||||
v-for="category in filteredCategories"
|
||||
:key="category.id"
|
||||
:value="category.id"
|
||||
>{{ category.title }}</option>
|
||||
</select>
|
||||
<button @click="saveTransaction()">Save Transaction</button>
|
||||
</div>
|
||||
<div v-if="loading" class="icon-loading" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "TransactionForm",
|
||||
components: {},
|
||||
props: {
|
||||
transaction: {
|
||||
default: () => {},
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["budgets"]),
|
||||
filteredCategories: function(state) {
|
||||
return this.$store.getters.categories.filter(function(category) {
|
||||
return category.budgetId === state.transaction.budgetId && category.expense === state.transaction.expense;
|
||||
});
|
||||
},
|
||||
loading: state => state.transaction === undefined || state.saving
|
||||
},
|
||||
mounted() {
|
||||
this.updateCategories();
|
||||
},
|
||||
methods: {
|
||||
updateCategories() {
|
||||
if (!this.transaction) return;
|
||||
this.$store.dispatch(
|
||||
"addEditTransactionBudgetSelected",
|
||||
this.transaction.budgetId
|
||||
);
|
||||
},
|
||||
saveTransaction() {
|
||||
this.saving = true;
|
||||
this.$store.dispatch("transactionFormSaveClicked", this.transaction);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.add-edit-transaction > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
.radio-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-container label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
.icon-loading {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
85
src/components/transaction/TransactionList.vue
Normal file
85
src/components/transaction/TransactionList.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<ul>
|
||||
<li v-for="transaction in filteredTransactions" :key="transaction.id">
|
||||
<a class="transaction" @click="view(transaction.id)">
|
||||
<div class="transaction-details">
|
||||
<p class="transaction-name">{{ transaction.title }}</p>
|
||||
<p class="transaction-date">{{ new Date(transaction.date).toLocaleDateString() }}</p>
|
||||
</div>
|
||||
<p
|
||||
class="transaction-amount"
|
||||
:class="transaction.expense ? 'danger' : 'good'">{{ (transaction.amount / 100).toLocaleString(undefined, {style: 'currency', currency: 'USD'}) }}</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'TransactionList',
|
||||
components: {},
|
||||
props: {
|
||||
budgetId: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
categoryId: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
limit: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['transactions', 'currentTransaction']),
|
||||
filteredTransactions: function(state) {
|
||||
const transactions = state.transactions.filter(function(transaction) {
|
||||
if (state.budgetId) {
|
||||
return transaction.budgetId === state.budgetId
|
||||
}
|
||||
if (state.categoryId) {
|
||||
return transaction.categoryId === state.categoryId
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (this.limit !== 0) {
|
||||
return transactions.slice(0, this.limit)
|
||||
} else {
|
||||
return transactions
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
view: function(id) {
|
||||
this.$store.dispatch('transactionClicked', id)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.transaction {
|
||||
padding: 0.5em;
|
||||
height: 4em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.transaction * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.transaction:hover {
|
||||
background: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.transaction-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
55
src/components/user/Login.vue
Normal file
55
src/components/user/Login.vue
Normal file
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="!loading" class="login">
|
||||
<h2>Login</h2>
|
||||
<input v-model="username"
|
||||
type="text"
|
||||
placeholder="Username or Email"
|
||||
title="Username or Email">
|
||||
<input v-model="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
title="Password">
|
||||
<button @click="login()">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="loading" class="icon-loading" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
},
|
||||
props: [
|
||||
'username',
|
||||
'password'
|
||||
],
|
||||
data: function() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
login() {
|
||||
this.loading = true
|
||||
this.$store.dispatch('loginSubmitClicked', {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.login > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
.icon-loading {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
0
src/components/user/Profile.vue
Normal file
0
src/components/user/Profile.vue
Normal file
0
src/components/user/Register.vue
Normal file
0
src/components/user/Register.vue
Normal file
|
@ -1,23 +1,81 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import TwigsHome from '../components/TwigsHome'
|
||||
import Login from '../components/user/Login'
|
||||
import NewBudget from '../components/budget/NewBudget'
|
||||
import EditBudget from '../components/budget/EditBudget'
|
||||
import BudgetDetails from '../components/budget/BudgetDetails'
|
||||
import BudgetList from '../components/budget/BudgetList'
|
||||
import NewCategory from '../components/category/NewCategory'
|
||||
import EditCategory from '../components/category/EditCategory'
|
||||
import CategoryDetails from '../components/category/CategoryDetails'
|
||||
import NewTransaction from '../components/transaction/NewTransaction'
|
||||
import EditTransaction from '../components/transaction/EditTransaction'
|
||||
import TransactionDetails from '../components/transaction/TransactionDetails'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
name: 'home',
|
||||
component: TwigsHome,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||
}
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
},
|
||||
{
|
||||
path: '/budgets',
|
||||
name: 'budgets',
|
||||
component: BudgetList,
|
||||
},
|
||||
{
|
||||
path: '/budgets/new',
|
||||
name: 'newBudget',
|
||||
component: NewBudget,
|
||||
},
|
||||
{
|
||||
path: '/budgets/:id',
|
||||
name: 'budgetDetails',
|
||||
component: BudgetDetails,
|
||||
},
|
||||
{
|
||||
path: '/budgets/:id/edit',
|
||||
name: 'editBudget',
|
||||
component: EditBudget,
|
||||
},
|
||||
{
|
||||
path: '/categories/new',
|
||||
name: 'newCategory',
|
||||
component: NewCategory,
|
||||
},
|
||||
{
|
||||
path: '/categories/:id',
|
||||
name: 'categoryDetails',
|
||||
component: CategoryDetails,
|
||||
},
|
||||
{
|
||||
path: '/categories/:id/edit',
|
||||
name: 'editCategory',
|
||||
component: EditCategory,
|
||||
},
|
||||
{
|
||||
path: '/transactions/new',
|
||||
name: 'newTransaction',
|
||||
component: NewTransaction,
|
||||
},
|
||||
{
|
||||
path: '/transactions/:id',
|
||||
name: 'transactionDetails',
|
||||
component: TransactionDetails,
|
||||
},
|
||||
{
|
||||
path: '/transactions/:id/edit',
|
||||
name: 'editTransaction',
|
||||
component: EditTransaction,
|
||||
},
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
|
|
|
@ -1,15 +1,399 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import router from '../router'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const API_URL = process.env.VUE_APP_API_URL;
|
||||
|
||||
function authenticatedHeaders(authentication) {
|
||||
let authorization;
|
||||
if (authentication !== undefined) {
|
||||
authorization = authentication
|
||||
} else {
|
||||
const authCookie = document.cookie.split(';')
|
||||
.map(cookie => cookie.trim())
|
||||
.find(cookie => cookie.indexOf('authorization') === 0);
|
||||
if (authCookie) {
|
||||
authorization = authCookie.slice(14)
|
||||
}
|
||||
}
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
});
|
||||
if (authorization) {
|
||||
headers.append('Authorization', authorization);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
user: undefined,
|
||||
budgets: [],
|
||||
budgetBalances: {},
|
||||
currentBudget: 0,
|
||||
categories: [],
|
||||
categoryBalances: {},
|
||||
currentCategory: 0,
|
||||
transactions: [],
|
||||
currentTransaction: 0,
|
||||
},
|
||||
mutations: {
|
||||
getters: {
|
||||
user: (state) => state.user,
|
||||
budgets: (state) => state.budgets,
|
||||
budget: (state) => state.budgets.find(budget => budget.id === state.currentBudget),
|
||||
budgetBalance: (state) => (id) => state.budgetBalances[id],
|
||||
categories: (state) => state.categories,
|
||||
category: (state) => state.categories.find(category => category.id === state.currentCategory),
|
||||
categoryBalance: (state) => (categoryId) => {
|
||||
return state.categoryBalances[categoryId] || 0
|
||||
},
|
||||
categoryRemainingBalance: (state, getters) => (category) => {
|
||||
const modifier = category.expense ? -1 : 1
|
||||
return category.amount - (getters.categoryBalance(category.id) * modifier)
|
||||
},
|
||||
transactions: (state) => state.transactions,
|
||||
transaction: (state) => state.transactions.find(transaction => transaction.id === state.currentTransaction),
|
||||
},
|
||||
actions: {
|
||||
loginClicked() {
|
||||
router.push({ name: 'login' })
|
||||
},
|
||||
modules: {
|
||||
loginSubmitClicked({ commit }, credentials) {
|
||||
const auth = `Basic ${btoa(credentials.username + ':' + credentials.password)}`.trim();
|
||||
get('users/me')
|
||||
.then(user => {
|
||||
document.cookie = `authorization=${auth}`;
|
||||
commit('setUser', user)
|
||||
router.push({ name: 'budgets' })
|
||||
})
|
||||
},
|
||||
addBudgetClicked({ commit }) {
|
||||
router.push({ name: 'newBudget' })
|
||||
},
|
||||
budgetListViewed({ commit }) {
|
||||
get('budgets')
|
||||
.then(budgets => {
|
||||
commit('setBudgets', budgets)
|
||||
budgets.forEach(budget => {
|
||||
get(`budgets/${budget.id}/balance`)
|
||||
.then(balance => {
|
||||
commit({
|
||||
type: 'setBudgetBalance',
|
||||
...balance,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
budgetClicked({ commit }, budgetId) {
|
||||
router.push({ name: 'budgetDetails', params: { id: budgetId } })
|
||||
},
|
||||
editBudgetViewed({ commit, state, getters }, budgetId) {
|
||||
commit('setCurrentBudget', budgetId)
|
||||
if (budgetId !== undefined && getters.budget === undefined) {
|
||||
get(`${API_URL}/budgets/${budgetId}`)
|
||||
.then(budgets => {
|
||||
commit('setBudgets', [budgets])
|
||||
})
|
||||
}
|
||||
},
|
||||
budgetFormSaveClicked({ commit }, budget) {
|
||||
let request
|
||||
if (budget.id) {
|
||||
request = put(`budgets/${budget.id}`, budget)
|
||||
} else {
|
||||
request = post(`budgets`, budget)
|
||||
}
|
||||
request.then(budget => {
|
||||
commit('addBudget', budget)
|
||||
router.push({ name: 'budgetDetails', params: { id: budget.id } })
|
||||
})
|
||||
},
|
||||
budgetDetailsViewed({ commit, state, getters }, budgetId) {
|
||||
commit('setCurrentBudget', budgetId)
|
||||
if (budgetId !== undefined && getters.budget === undefined) {
|
||||
fetch(`${API_URL}/budgets`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(budgets => {
|
||||
commit('setBudgets', budgets)
|
||||
})
|
||||
}
|
||||
commit('setCategories', [])
|
||||
commit('setTransactions', [])
|
||||
commit('setCurrentCategory', undefined)
|
||||
fetch(`${API_URL}/categories?budgetIds=${budgetId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(categories => {
|
||||
commit('setCategories', categories)
|
||||
categories.forEach(category => {
|
||||
fetch(`${API_URL}/categories/${category.id}/balance`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(balance => {
|
||||
commit({
|
||||
type: 'setCategoryBalance',
|
||||
...balance,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
fetch(`${API_URL}/transactions?budgetId=${budgetId}?count=10`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(transactions => commit('setTransactions', transactions))
|
||||
},
|
||||
editBudgetClicked({ commit }, budgetId) {
|
||||
router.push({ name: 'editBudget', params: { id: budgetId } })
|
||||
},
|
||||
deleteBudgetClicked({ commit }, budgetId) {
|
||||
fetch(`${API_URL}/budgets/${budgetId}`, {
|
||||
method: 'DELETE',
|
||||
headers: authenticatedHeaders()
|
||||
})
|
||||
.then(() => {
|
||||
commit('deleteBudget', budgetId)
|
||||
router.push({ name: 'home' })
|
||||
})
|
||||
},
|
||||
categoryClicked({ commit }, categoryId) {
|
||||
router.push({ name: 'categoryDetails', params: { id: categoryId } })
|
||||
},
|
||||
addCategoryClicked({ commit }) {
|
||||
router.push({ name: 'newCategory' })
|
||||
},
|
||||
editCategoryClicked({ commit }, categoryId) {
|
||||
router.push({ name: 'editCategory', params: { id: categoryId } })
|
||||
},
|
||||
editCategoryViewed({ commit, state, getters }, categoryId) {
|
||||
commit('setCurrentCategory', categoryId)
|
||||
if (categoryId !== undefined && getters.category === undefined) {
|
||||
fetch(`${API_URL}/categories/${categoryId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(category => {
|
||||
commit('setCategories', [category])
|
||||
})
|
||||
}
|
||||
},
|
||||
categoryDetailsViewed({ commit, state }, categoryId) {
|
||||
commit('setCurrentCategory', categoryId)
|
||||
if (state.categories.length === 0) {
|
||||
fetch(`${API_URL}/categories/${categoryId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(category => {
|
||||
commit('setCategories', [category])
|
||||
})
|
||||
}
|
||||
fetch(`${API_URL}/transactions?categoryId=${categoryId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(transactions => commit('setTransactions', transactions))
|
||||
},
|
||||
categoryFormSaveClicked({ commit }, category) {
|
||||
let request
|
||||
if (category.id) {
|
||||
request = put(`categories/${category.id}`, category)
|
||||
} else {
|
||||
request = post('categories', category)
|
||||
}
|
||||
request.then(category => {
|
||||
commit('addCategory', category)
|
||||
router.push({ name: 'categoryDetails', params: { id: category.id } })
|
||||
})
|
||||
},
|
||||
deleteCategoryClicked({ commit, state }, categoryId) {
|
||||
fetch(`${API_URL}/categories/${categoryId}`, {
|
||||
method: 'DELETE',
|
||||
headers: authenticatedHeaders(),
|
||||
})
|
||||
.then(() => {
|
||||
commit('setCurrentCategory', undefined)
|
||||
commit('deleteCategory', categoryId)
|
||||
router.push({ name: 'budgetDetails', params: { id: state.currentBudget } })
|
||||
})
|
||||
},
|
||||
addTransactionClicked({ commit }) {
|
||||
router.push({ name: 'newTransaction' })
|
||||
},
|
||||
editTransactionViewed({ commit, state, getters }, transactionId) {
|
||||
commit('setCurrentTransaction', transactionId)
|
||||
if (transactionId !== undefined && getters.transaction === undefined) {
|
||||
fetch(`${API_URL}/transactions/${transactionId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(transactions => {
|
||||
commit('setTransactions', [transactions])
|
||||
})
|
||||
}
|
||||
},
|
||||
addEditTransactionBudgetSelected({ commit, state }, budgetId) {
|
||||
commit('setCategories', [])
|
||||
if (!budgetId) return
|
||||
fetch(`${API_URL}/categories?budgetId=${budgetId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(categories => {
|
||||
commit('setCategories', categories)
|
||||
})
|
||||
},
|
||||
transactionFormSaveClicked({ commit }, transaction) {
|
||||
let request
|
||||
if (transaction.id) {
|
||||
request = put(`transactions/${transaction.id}`, transaction)
|
||||
} else {
|
||||
request = post('transactions', transaction)
|
||||
}
|
||||
request.then(res => res.json())
|
||||
.then(transaction => {
|
||||
commit('addTransaction', transaction)
|
||||
router.push({ name: 'transactionDetails', params: { id: transaction.id } })
|
||||
})
|
||||
},
|
||||
transactionClicked({ commit }, transactionId) {
|
||||
router.push({ name: 'transactionDetails', params: { id: transactionId } })
|
||||
},
|
||||
transactionDetailsViewed({ commit, state }, transactionId) {
|
||||
commit('setCurrentTransaction', transactionId)
|
||||
|
||||
if (state.transactions.length === 0) {
|
||||
fetch(`${API_URL}/transactions/${transactionId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(transaction => {
|
||||
commit('setTransactions', [transaction])
|
||||
if (state.categories.length === 0) {
|
||||
fetch(`${API_URL}/categories?budgetId=${transaction.budgetId}`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(categories => {
|
||||
commit('setCategories', categories)
|
||||
categories.forEach(category => {
|
||||
fetch(`${API_URL}/categories/${category.id}/balance`, { headers: authenticatedHeaders() })
|
||||
.then(res => res.json())
|
||||
.then(balance => {
|
||||
commit({
|
||||
type: 'setCategoryBalance',
|
||||
...balance,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setUser(state, user) {
|
||||
state.user = user
|
||||
},
|
||||
addBudget(state, budget) {
|
||||
state.budgets = [
|
||||
...state.budgets.filter(b => b.id !== budget.id),
|
||||
budget,
|
||||
]
|
||||
},
|
||||
setCurrentBudget(state, budgetId) {
|
||||
state.currentBudget = Number.parseInt(budgetId)
|
||||
},
|
||||
setBudgetBalance(state, data) {
|
||||
state.budgetBalances = {
|
||||
...state.budgetBalances,
|
||||
[data.budgetId]: data.balance,
|
||||
}
|
||||
},
|
||||
setBudgets(state, budgets) {
|
||||
state.budgets = budgets
|
||||
},
|
||||
deleteBudget(state, budget) {
|
||||
state.budgets = [
|
||||
...state.budgets.filter(b => b.id !== budget.id),
|
||||
]
|
||||
},
|
||||
addCategory(state, category) {
|
||||
state.categories = [
|
||||
...state.categories.filter(c => c.id !== category.id),
|
||||
category,
|
||||
]
|
||||
},
|
||||
setCurrentCategory(state, categoryId) {
|
||||
state.currentCategory = Number.parseInt(categoryId)
|
||||
},
|
||||
setCategories(state, data) {
|
||||
state.categories = data
|
||||
},
|
||||
setCategoryBalance(state, data) {
|
||||
state.categoryBalances = {
|
||||
...state.categoryBalances,
|
||||
[data.id]: data.balance,
|
||||
}
|
||||
},
|
||||
deleteCategory(state, category) {
|
||||
state.categories = [
|
||||
...state.categories.filter(c => c.id !== category.id),
|
||||
]
|
||||
},
|
||||
addTransaction(state, transaction) {
|
||||
state.transactions = [
|
||||
...state.transactions.filter(t => t.id !== transaction.id),
|
||||
transaction,
|
||||
]
|
||||
},
|
||||
setTransactions(state, data) {
|
||||
state.transactions = data
|
||||
},
|
||||
setCurrentTransaction(state, transactionId) {
|
||||
state.currentTransaction = Number.parseInt(transactionId)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function get(path) {
|
||||
return fetch(`${API_URL}/${path}`, { headers: authenticatedHeaders() })
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw Error(res.statusText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function post(path, body) {
|
||||
return fetch(`${API_URL}/${path}`, {
|
||||
method: 'POST',
|
||||
headers: authenticatedHeaders(),
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw Error(res.statusText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function put(path, body) {
|
||||
return fetch(`${API_URL}/${path}`, {
|
||||
method: 'PUT',
|
||||
headers: authenticatedHeaders(),
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw Error(res.statusText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function del(path) {
|
||||
return fetch(`${API_URL}/${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: authenticatedHeaders()
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw Error(res.statusText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
|
@ -1,18 +0,0 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<img alt="Vue logo" src="../assets/logo.png">
|
||||
<HelloWorld msg="Welcome to Your Vue.js App"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
HelloWorld
|
||||
}
|
||||
}
|
||||
</script>
|
Loading…
Reference in a new issue