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,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];
}
},
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
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>