initial commit
This commit is contained in:
24
README.md
24
README.md
@@ -1,24 +0,0 @@
|
||||
# worktime
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
81
package-lock.json
generated
81
package-lock.json
generated
@@ -9,9 +9,11 @@
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"reset-css": "^5.0.1",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0"
|
||||
"vuex": "^4.0.0-0",
|
||||
"vuex-persist": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
@@ -12244,6 +12246,11 @@
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/reset-css": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/reset-css/-/reset-css-5.0.1.tgz",
|
||||
"integrity": "sha512-VyuJdNFfp5x/W6e5wauJM59C02Vs0P22sxzZGhQMPaqu/NGTeFxlBFOOw3eq9vQd19gIDdZp7zi89ylyKOJ33Q=="
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.20.0.tgz",
|
||||
@@ -14800,6 +14807,31 @@
|
||||
"vue": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/vuex-persist": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vuex-persist/-/vuex-persist-3.1.3.tgz",
|
||||
"integrity": "sha512-QWOpP4SxmJDC5Y1+0+Yl/F4n7z27syd1St/oP+IYCGe0X0GFio0Zan6kngZFufdIhJm+5dFGDo3VG5kdkCGeRQ==",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"flatted": "^3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuex": ">=2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/vuex-persist/node_modules/deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuex-persist/node_modules/flatted": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz",
|
||||
"integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA=="
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.nlark.com/watchpack/download/watchpack-1.7.5.tgz?cache=0&sync_timestamp=1621437900992&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fwatchpack%2Fdownload%2Fwatchpack-1.7.5.tgz",
|
||||
@@ -17767,7 +17799,8 @@
|
||||
"version": "4.5.13",
|
||||
"resolved": "https://registry.nlark.com/@vue/cli-plugin-vuex/download/@vue/cli-plugin-vuex-4.5.13.tgz?cache=0&sync_timestamp=1623216431849&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40vue%2Fcli-plugin-vuex%2Fdownload%2F%40vue%2Fcli-plugin-vuex-4.5.13.tgz",
|
||||
"integrity": "sha1-mGRti8HmnPbGpsui/tPqzgNWw2A=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@vue/cli-service": {
|
||||
"version": "4.5.13",
|
||||
@@ -18029,7 +18062,8 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npm.taobao.org/@vue/preload-webpack-plugin/download/@vue/preload-webpack-plugin-1.1.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40vue%2Fpreload-webpack-plugin%2Fdownload%2F%40vue%2Fpreload-webpack-plugin-1.1.2.tgz",
|
||||
"integrity": "sha1-zrkktOyzucQ4ccekKaAvhCPmIas=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.1.5",
|
||||
@@ -18276,7 +18310,8 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.nlark.com/acorn-jsx/download/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "7.2.0",
|
||||
@@ -18306,13 +18341,15 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npm.taobao.org/ajv-errors/download/ajv-errors-1.0.1.tgz?cache=0&sync_timestamp=1616886041666&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fajv-errors%2Fdownload%2Fajv-errors-1.0.1.tgz",
|
||||
"integrity": "sha1-81mGrOuRr63sQQL72FAUlQzvpk0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npm.taobao.org/ajv-keywords/download/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha1-MfKdpatuANHC0yms97WSlhTVAU0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"alphanum-sort": {
|
||||
"version": "1.0.2",
|
||||
@@ -25219,13 +25256,15 @@
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npm.taobao.org/icss-utils/download/icss-utils-5.1.0.tgz?cache=0&sync_timestamp=1605801506037&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ficss-utils%2Fdownload%2Ficss-utils-5.1.0.tgz",
|
||||
"integrity": "sha1-xr5oWKvQE9do6YNmrkfiXViHsa4=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"postcss-modules-extract-imports": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npm.taobao.org/postcss-modules-extract-imports/download/postcss-modules-extract-imports-3.0.0.tgz?cache=0&sync_timestamp=1602588177787&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpostcss-modules-extract-imports%2Fdownload%2Fpostcss-modules-extract-imports-3.0.0.tgz",
|
||||
"integrity": "sha1-zaHwR8CugMl9vijD52pDuIAldB0=",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"postcss-modules-local-by-default": {
|
||||
"version": "4.0.0",
|
||||
@@ -26116,6 +26155,11 @@
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"reset-css": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/reset-css/-/reset-css-5.0.1.tgz",
|
||||
"integrity": "sha512-VyuJdNFfp5x/W6e5wauJM59C02Vs0P22sxzZGhQMPaqu/NGTeFxlBFOOw3eq9vQd19gIDdZp7zi89ylyKOJ33Q=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.20.0.tgz",
|
||||
@@ -28271,6 +28315,27 @@
|
||||
"@vue/devtools-api": "^6.0.0-beta.11"
|
||||
}
|
||||
},
|
||||
"vuex-persist": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vuex-persist/-/vuex-persist-3.1.3.tgz",
|
||||
"integrity": "sha512-QWOpP4SxmJDC5Y1+0+Yl/F4n7z27syd1St/oP+IYCGe0X0GFio0Zan6kngZFufdIhJm+5dFGDo3VG5kdkCGeRQ==",
|
||||
"requires": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"flatted": "^3.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
},
|
||||
"flatted": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz",
|
||||
"integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.nlark.com/watchpack/download/watchpack-1.7.5.tgz?cache=0&sync_timestamp=1621437900992&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fwatchpack%2Fdownload%2Fwatchpack-1.7.5.tgz",
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"register-service-worker": "^1.7.1",
|
||||
"reset-css": "^5.0.1",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0"
|
||||
"vuex": "^4.0.0-0",
|
||||
"vuex-persist": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
|
||||
32
src/App.vue
32
src/App.vue
@@ -1,30 +1,18 @@
|
||||
<template>
|
||||
<div id="nav">
|
||||
<router-link to="/">Home</router-link> |
|
||||
<router-link to="/about">About</router-link>
|
||||
</div>
|
||||
<div class="app-backdrop"></div>
|
||||
<router-view/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
@import "@/scss/style.scss";
|
||||
|
||||
#nav {
|
||||
padding: 30px;
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
|
||||
&.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
}
|
||||
.app-backdrop {
|
||||
z-index: -10000;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: $darker;
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
1
src/assets/pause.svg
Normal file
1
src/assets/pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M224,435.8V76.1c0-6.7-5.4-12.1-12.2-12.1h-71.6c-6.8,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6 C218.6,448,224,442.6,224,435.8z"/><path d="M371.8,64h-71.6c-6.7,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6c6.7,0,12.2-5.4,12.2-12.2V76.1 C384,69.4,378.6,64,371.8,64z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 664 B |
1
src/assets/play.svg
Normal file
1
src/assets/play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M405.2,232.9L126.8,67.2c-3.4-2-6.9-3.2-10.9-3.2c-10.9,0-19.8,9-19.8,20H96v344h0.1c0,11,8.9,20,19.8,20 c4.1,0,7.5-1.4,11.2-3.4l278.1-165.5c6.6-5.5,10.8-13.8,10.8-23.1C416,246.7,411.8,238.5,405.2,232.9z"/></svg>
|
||||
|
After Width: | Height: | Size: 566 B |
1
src/assets/trash.svg
Normal file
1
src/assets/trash.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg fill="none" height="24" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
@@ -1,61 +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-pwa" target="_blank" rel="noopener">pwa</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</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 lang="scss">
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
94
src/components/TheCategoryBar.vue
Normal file
94
src/components/TheCategoryBar.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="category-bar">
|
||||
<input
|
||||
class="add-category"
|
||||
v-model="newCategory"
|
||||
placeholder="New category"
|
||||
@keypress.enter="addCategory"
|
||||
@mouseenter="newCategory=''"
|
||||
@mouseleave="blurInput"
|
||||
/>
|
||||
<div class="categories">
|
||||
<div
|
||||
class="category"
|
||||
v-for="category in categories" :key="category"
|
||||
:style="{ background: stringToColor(category) }"
|
||||
>
|
||||
{{ category }}
|
||||
<span @click="removeCategory(category)">×</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import toColor from '@/stringToColor';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
newCategory: '+',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addCategory() {
|
||||
this.$store.commit('addCategory', this.newCategory);
|
||||
this.newCategory = '';
|
||||
},
|
||||
removeCategory(category) {
|
||||
this.$store.commit('removeCategory', category);
|
||||
},
|
||||
blurInput(event) {
|
||||
this.newCategory = '+';
|
||||
event.target.blur();
|
||||
},
|
||||
stringToColor(str) {
|
||||
return toColor(str);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
categories: (state) => state.categories,
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.category-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid $light;
|
||||
height: 48px;
|
||||
background: $dark;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 64px;
|
||||
|
||||
.add-category {
|
||||
color: $darker;
|
||||
background: $light;
|
||||
height: 32px;
|
||||
padding: 4px 12px;
|
||||
width: 32px;
|
||||
box-sizing: border-box;
|
||||
transition: width .6s;
|
||||
|
||||
&:hover {
|
||||
width: 220px
|
||||
}
|
||||
}
|
||||
|
||||
.categories {
|
||||
display: flex;
|
||||
margin-left: 32px;
|
||||
|
||||
.category {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,4 +4,8 @@ import './registerServiceWorker';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
|
||||
createApp(App).use(store).use(router).mount('#app');
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import Home from '../views/Home.vue';
|
||||
import Home from '../views/Home/Index.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -7,14 +7,14 @@ const routes = [
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
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: '/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'),
|
||||
// },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
12
src/scss/_variables.scss
Normal file
12
src/scss/_variables.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
$dark-blue: #05263B;
|
||||
$misty-blue: #AEB8C4;
|
||||
$blue-grotto: #163B50;
|
||||
$slate: #9CA6B8;
|
||||
|
||||
$dark: $blue-grotto;
|
||||
$darker: $dark-blue;
|
||||
$light: $slate;
|
||||
$lighter: $misty-blue;
|
||||
|
||||
|
||||
$max-width: 1024px;
|
||||
24
src/scss/style.scss
Normal file
24
src/scss/style.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
@import 'reset-css';
|
||||
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
color: $light;
|
||||
background: $darker;
|
||||
|
||||
padding-top: 48px;
|
||||
|
||||
input, button {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.category {
|
||||
border-radius: 16px;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
color: $darker;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,75 @@
|
||||
import { createStore } from 'vuex';
|
||||
import VuexPersistence from 'vuex-persist';
|
||||
|
||||
const vuexLocal = new VuexPersistence({
|
||||
storage: window.localStorage,
|
||||
});
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
categories: [],
|
||||
tasks: [],
|
||||
},
|
||||
mutations: {
|
||||
addCategory(state, category) {
|
||||
if (category && !state.categories.includes(category)) {
|
||||
state.categories = [...state.categories, category];
|
||||
}
|
||||
},
|
||||
removeCategory(state, category) {
|
||||
state.categories = state.categories.filter((element) => element !== category);
|
||||
},
|
||||
|
||||
addTask(state, name) {
|
||||
if (name) {
|
||||
const task = {
|
||||
name,
|
||||
startedAt: undefined,
|
||||
running: false,
|
||||
totalTime: 0,
|
||||
category: undefined,
|
||||
};
|
||||
state.tasks = [...state.tasks, task];
|
||||
}
|
||||
},
|
||||
removeTask(state, name) {
|
||||
state.tasks = state.tasks.filter((task) => task.name !== name);
|
||||
},
|
||||
startTask(state, name) {
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
if (newTask.name === name) {
|
||||
newTask.running = true;
|
||||
newTask.startedAt = Date.now();
|
||||
} else if (newTask.running) {
|
||||
newTask.running = false;
|
||||
newTask.totalTime += Date.now() - newTask.startedAt;
|
||||
newTask.startedAt = undefined;
|
||||
}
|
||||
return newTask;
|
||||
});
|
||||
},
|
||||
stopTask(state, name) {
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
if (newTask.name === name) {
|
||||
newTask.running = false;
|
||||
newTask.totalTime += Date.now() - newTask.startedAt;
|
||||
newTask.startedAt = undefined;
|
||||
}
|
||||
return newTask;
|
||||
});
|
||||
},
|
||||
|
||||
assignCategory(state, { name, category }) {
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
if (newTask.name === name) {
|
||||
newTask.category = category;
|
||||
}
|
||||
return newTask;
|
||||
});
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
},
|
||||
plugins: [vuexLocal.plugin],
|
||||
});
|
||||
|
||||
20
src/stringToColor.js
Normal file
20
src/stringToColor.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-disable */
|
||||
export default function stringToColor(str) {
|
||||
var colors = ['#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6',
|
||||
'#3366E6', '#99FF99', '#B34D4D', '#80B300', '#E6B3B3',
|
||||
'#6680B3', '#66991A', '#FF99E6', '#CCFF1A', '#FF1A66',
|
||||
'#E6331A', '#33FFCC', '#B366CC', '#CC80CC', '#991AFF',
|
||||
'#E666FF', '#4DB3FF', '#1AB399', '#E666B3', '#CC9999',
|
||||
'#B3B31A', '#00E680', '#E6FF80', '#1AFF33', '#FF3380',
|
||||
'#CCCC00', '#66E64D', '#4D80CC', '#E64D66', '#4DB380',
|
||||
'#FF4D4D', '#99E6E6', '#6666FF'];
|
||||
|
||||
var hash = 0;
|
||||
if (str.length === 0) return hash;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash;
|
||||
}
|
||||
hash = ((hash % colors.length) + colors.length) % colors.length;
|
||||
return colors[hash];
|
||||
}
|
||||
@@ -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>
|
||||
63
src/views/Home/CategoryDropdown.vue
Normal file
63
src/views/Home/CategoryDropdown.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="category-dropdown">
|
||||
<div class="title">Assign Category</div>
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
class="category"
|
||||
:style="{ background: stringToColor(category) }"
|
||||
@click="$emit('selected', category)"
|
||||
>
|
||||
{{ category }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import toColor from '@/stringToColor';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
stringToColor(str) {
|
||||
return toColor(str);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
categories: (state) => state.categories,
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.category-dropdown {
|
||||
position: absolute;
|
||||
background: $light;
|
||||
padding: 48px 12px 12px 12px;
|
||||
border-radius: 16px;
|
||||
width: max-content;
|
||||
left: 50%;
|
||||
top: -12px;
|
||||
z-index: 100;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: fit-content;
|
||||
color: $light;
|
||||
background: $dark;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.category {
|
||||
margin-top: 8px;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/views/Home/Index.vue
Normal file
19
src/views/Home/Index.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<TheCategoryBar />
|
||||
<TheTaskList />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TheCategoryBar from '@/components/TheCategoryBar.vue';
|
||||
import TheTaskList from './TheTaskList.vue';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
TheCategoryBar,
|
||||
TheTaskList,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
187
src/views/Home/Task.vue
Normal file
187
src/views/Home/Task.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div
|
||||
class="task"
|
||||
:class="{active: task.running}"
|
||||
>
|
||||
<div class="time">
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
<div class="toggle-state">
|
||||
<div v-if="task.running" @click="stopTask">
|
||||
<img src="@/assets/pause.svg">
|
||||
</div>
|
||||
<div v-else @click="startTask">
|
||||
<img src="@/assets/play.svg">
|
||||
</div>
|
||||
</div>
|
||||
<div class="name">
|
||||
{{ task.name }}
|
||||
</div>
|
||||
<div class="spacer"/>
|
||||
<div
|
||||
v-if="task.category"
|
||||
class="category"
|
||||
:style="{ background: stringToColor(task.category) }"
|
||||
>
|
||||
{{ task.category }}
|
||||
<span @click="removeCategory">×</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="select-category"
|
||||
>
|
||||
Assign Category
|
||||
<CategoryDropdown
|
||||
class="dropdown"
|
||||
@selected="assignCategory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="delete"
|
||||
@click="removeTask"
|
||||
>
|
||||
<img src="@/assets/trash.svg">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import toColor from '@/stringToColor';
|
||||
|
||||
import CategoryDropdown from './CategoryDropdown.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CategoryDropdown,
|
||||
},
|
||||
props: {
|
||||
task: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
timePassed: 0,
|
||||
openDropdown: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
startTask() {
|
||||
this.$store.commit('startTask', this.task.name);
|
||||
},
|
||||
stopTask() {
|
||||
this.$store.commit('stopTask', this.task.name);
|
||||
},
|
||||
assignCategory(category) {
|
||||
this.$store.commit('assignCategory', { name: this.task.name, category });
|
||||
},
|
||||
removeCategory() {
|
||||
this.$store.commit('assignCategory', { name: this.task.name, category: undefined });
|
||||
},
|
||||
stringToColor(str) {
|
||||
return toColor(str);
|
||||
},
|
||||
removeTask() {
|
||||
this.$store.commit('removeTask', this.task.name);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
time() {
|
||||
if (this.task.startedAt) {
|
||||
return this.task.totalTime + this.timePassed;
|
||||
}
|
||||
return this.task.totalTime;
|
||||
},
|
||||
formattedTime() {
|
||||
let { time } = this;
|
||||
const msInASec = 1000;
|
||||
const msInAMin = msInASec * 60;
|
||||
const msInAHour = msInAMin * 60;
|
||||
const msInADay = msInAHour * 24;
|
||||
|
||||
const days = Math.floor(time / msInADay);
|
||||
time -= days * msInADay;
|
||||
const hours = Math.floor(time / msInAHour);
|
||||
time -= hours * msInAHour;
|
||||
const mins = Math.floor(time / msInAMin);
|
||||
|
||||
return `${days}d ${hours}h ${mins}m`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
setInterval(() => {
|
||||
this.timePassed = Date.now() - this.task.startedAt;
|
||||
}, 1000);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.task {
|
||||
color: $lighter;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.time {
|
||||
width: 124px;
|
||||
}
|
||||
|
||||
.toggle-state {
|
||||
margin-right: 16px;
|
||||
|
||||
> * {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: $light;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
> * {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.select-category {
|
||||
position: relative;
|
||||
color: $darker;
|
||||
background: $dark;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.dropdown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
margin-left: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {opacity: .3;}
|
||||
to {opacity: 1;}
|
||||
}
|
||||
|
||||
.task.active {
|
||||
.time {
|
||||
animation: pulse 1s infinite alternate;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
60
src/views/Home/TheTaskList.vue
Normal file
60
src/views/Home/TheTaskList.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="task-list">
|
||||
<input
|
||||
class="add-task"
|
||||
v-model=newTask
|
||||
placeholder="Add new task"
|
||||
@keypress.enter="addTask"
|
||||
/>
|
||||
<Task
|
||||
v-for="task in tasks"
|
||||
:key="task.startedAt"
|
||||
:task=task
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
import Task from './Task.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Task,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newTask: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addTask() {
|
||||
this.$store.commit('addTask', this.newTask);
|
||||
this.newTask = '';
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
tasks: (state) => state.tasks,
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.task-list {
|
||||
max-width: $max-width;
|
||||
margin: 32px auto;
|
||||
|
||||
.add-task {
|
||||
width: 100%;
|
||||
background: $light;
|
||||
color: $darker;
|
||||
padding: 4px 12px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
11
vue.config.js
Normal file
11
vue.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
css: {
|
||||
loaderOptions: {
|
||||
sass: {
|
||||
prependData: `
|
||||
@import "@/scss/_variables.scss";
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user