initial commit

This commit is contained in:
2021-08-07 15:30:43 +03:00
parent 1297c8b4f6
commit b81e6a6c37
23 changed files with 660 additions and 153 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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>

View 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>

View File

@@ -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');

View File

@@ -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
View 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
View 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;
}
}

View File

@@ -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];
}
},
actions: {
removeCategory(state, category) {
state.categories = state.categories.filter((element) => element !== category);
},
modules: {
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;
});
},
},
plugins: [vuexLocal.plugin],
});

20
src/stringToColor.js Normal file
View 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];
}

View File

@@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -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>

View 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
View 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
View 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>

View 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
View File

@@ -0,0 +1,11 @@
module.exports = {
css: {
loaderOptions: {
sass: {
prependData: `
@import "@/scss/_variables.scss";
`,
},
},
},
};