Added global controls

This commit is contained in:
2021-12-19 03:03:37 +03:00
parent 9c881bd430
commit b4a408eddb
15 changed files with 334 additions and 123 deletions

5
src/assets/reset.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#f7f7f7" class="bi bi-clock-history" viewBox="0 0 16 16">
<path d="M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022l-.074.997zm2.004.45a7.003 7.003 0 0 0-.985-.299l.219-.976c.383.086.76.2 1.126.342l-.36.933zm1.37.71a7.01 7.01 0 0 0-.439-.27l.493-.87a8.025 8.025 0 0 1 .979.654l-.615.789a6.996 6.996 0 0 0-.418-.302zm1.834 1.79a6.99 6.99 0 0 0-.653-.796l.724-.69c.27.285.52.59.747.91l-.818.576zm.744 1.352a7.08 7.08 0 0 0-.214-.468l.893-.45a7.976 7.976 0 0 1 .45 1.088l-.95.313a7.023 7.023 0 0 0-.179-.483zm.53 2.507a6.991 6.991 0 0 0-.1-1.025l.985-.17c.067.386.106.778.116 1.17l-1 .025zm-.131 1.538c.033-.17.06-.339.081-.51l.993.123a7.957 7.957 0 0 1-.23 1.155l-.964-.267c.046-.165.086-.332.12-.501zm-.952 2.379c.184-.29.346-.594.486-.908l.914.405c-.16.36-.345.706-.555 1.038l-.845-.535zm-.964 1.205c.122-.122.239-.248.35-.378l.758.653a8.073 8.073 0 0 1-.401.432l-.707-.707z"/>
<path d="M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0v1z"/>
<path d="M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
src/assets/stop.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-stop-fill" viewBox="0 0 16 16">
<path d="M5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5A1.5 1.5 0 0 1 5 3.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@@ -84,9 +84,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState({ ...mapState(['categories']),
categories: (state) => state.categories,
}),
}, },
}; };
</script> </script>
@@ -104,6 +102,7 @@ export default {
align-items: center; align-items: center;
padding: 0 64px; padding: 0 64px;
box-sizing: border-box; box-sizing: border-box;
z-index: 900;
.add-category { .add-category {
color: $darker; color: $darker;

View File

@@ -7,14 +7,6 @@ const routes = [
name: 'Home', name: 'Home',
component: 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'),
// },
]; ];
const router = createRouter({ const router = createRouter({

View File

@@ -17,6 +17,7 @@
border: none; border: none;
border-radius: 16px; border-radius: 16px;
font-size: 16px; font-size: 16px;
cursor: pointer;
&::placeholder { &::placeholder {
color: $dark; color: $dark;

View File

@@ -52,6 +52,9 @@ export default createStore({
removeTask(state, name) { removeTask(state, name) {
state.tasks = state.tasks.filter((task) => task.name !== name); state.tasks = state.tasks.filter((task) => task.name !== name);
}, },
removeAllTasks(state) {
state.tasks = [];
},
startTask(state, name) { startTask(state, name) {
state.tasks = state.tasks.map((task) => { state.tasks = state.tasks.map((task) => {
const newTask = task; const newTask = task;

View File

@@ -1,64 +0,0 @@
<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;
min-width: 200px;
max-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>

View File

@@ -0,0 +1,143 @@
<template>
<div class="global-controls">
<Time
:time="totalTime"
:pulsing="atLeastOneRunning"
/>
<div>
Total
</div>
<div class="buttons">
<transition name="slide-up">
<div
v-if="atLeastOneRunning"
class="stop"
@click="stop"
>
<img src="@/assets/stop.svg" />
</div>
</transition>
<div @click="$refs.resetAllModal.open()">
<img src="@/assets/reset.svg" />
</div>
<div @click="$refs.removeAllModal.open()">
<img src="@/assets/trash.svg" />
</div>
</div>
<ResetAllModal ref="resetAllModal"/>
<RemoveAllModal ref="removeAllModal"/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import Time from './Time.vue';
import RemoveAllModal from './RemoveAllModal.vue';
import ResetAllModal from './ResetAllModal.vue';
export default {
components: {
Time,
RemoveAllModal,
ResetAllModal,
},
data() {
return {
totalTime: 0,
};
},
computed: {
...mapState(['tasks']),
atLeastOneRunning() {
return this.tasks.reduce(
(atLeastOneRunning, task) => task.running || atLeastOneRunning,
false,
);
},
},
beforeMount() {
setInterval(() => {
this.totalTime = this.getTotalTime();
}, 1000);
},
methods: {
getTotalTime() {
return this.tasks.reduce((acc, task) => {
if (task.startedAt) {
const timePassed = Date.now() - task.startedAt;
return acc + task.totalTime + timePassed;
}
return acc + task.totalTime;
}, 0);
},
stop() {
this.tasks.forEach((task) => {
if (task.running) {
this.$store.commit('stopTask', task.name);
}
});
},
},
};
</script>
<style lang="scss" scoped>
.global-controls {
max-width: $max-width;
height: 32px;
margin: 32px auto;
padding: 0 8px;
box-sizing: border-box;
display: flex;
align-items: center;
.time {
min-width: 104px;
}
.buttons {
display: flex;
flex-grow: 1;
justify-content: flex-end;
> * {
margin-left: 16px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.stop {
width: 32px;
height: 32px;
background: $light;
border-radius: 50%;
> * {
width: 20px;
height: 20px;
}
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 0.5s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(-64px);
}
}
}
@media screen and (max-width: $max-width) {
.global-controls {
margin: 16px auto -16px;
}
}
</style>

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="home"> <div class="home">
<TheCategoryBar @select="select" /> <TheCategoryBar @select="select" />
<GlobalTaskControls />
<TheTaskList :selectedCategory="selectedCategory" /> <TheTaskList :selectedCategory="selectedCategory" />
</div> </div>
</template> </template>
@@ -9,12 +10,14 @@
import { mapState, mapMutations } from 'vuex'; import { mapState, mapMutations } from 'vuex';
import TheCategoryBar from '@/components/TheCategoryBar.vue'; import TheCategoryBar from '@/components/TheCategoryBar.vue';
import GlobalTaskControls from './GlobalTaskControls.vue';
import TheTaskList from './TheTaskList.vue'; import TheTaskList from './TheTaskList.vue';
export default { export default {
name: 'Home', name: 'Home',
components: { components: {
TheCategoryBar, TheCategoryBar,
GlobalTaskControls,
TheTaskList, TheTaskList,
}, },
data() { data() {

View File

@@ -0,0 +1,55 @@
<template>
<Modal ref="modal">
<div class="title">Are you sure you want to delete all of your tasks?</div>
<div class="buttons">
<button @click="close">
Cancel
</button>
<button @click="removeAllTasksAndClose">
Delete
</button>
</div>
</Modal>
</template>
<script>
import { mapMutations } from 'vuex';
import Modal from '@/components/Modal.vue';
export default {
components: {
Modal,
},
methods: {
...mapMutations(['removeAllTasks']),
removeAllTasksAndClose() {
this.removeAllTasks();
this.close();
},
open() {
this.$refs.modal.open();
},
close() {
this.$refs.modal.close();
},
},
};
</script>
<style lang="scss" scoped>
.title {
margin-right: 32px;
}
.buttons {
margin-top: 16px;
display: flex;
justify-content: space-evenly;
> * {
padding: 6px 12px;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<Modal ref="modal">
<div class="title">Are you sure you want to reset all of your tasks to 0?</div>
<div class="buttons">
<button @click="close">
Cancel
</button>
<button @click="resetTasksAndClose">
Reset
</button>
</div>
</Modal>
</template>
<script>
import { mapMutations } from 'vuex';
import Modal from '@/components/Modal.vue';
export default {
components: {
Modal,
},
methods: {
...mapMutations(['resetTasks']),
resetTasksAndClose() {
this.resetTasks();
this.close();
},
open() {
this.$refs.modal.open();
},
close() {
this.$refs.modal.close();
},
},
};
</script>
<style lang="scss" scoped>
.title {
margin-right: 32px;
}
.buttons {
margin-top: 16px;
display: flex;
justify-content: space-evenly;
> * {
padding: 6px 12px;
}
}
</style>

View File

@@ -1,8 +1,9 @@
<template> <template>
<div class="task" :class="{ active: task.running }"> <div class="task">
<div class="time"> <Time
{{ formattedTime }} :time="time"
</div> :pulsing="task.running"
/>
<div class="toggle-state"> <div class="toggle-state">
<div v-if="task.running" @click="stopTask"> <div v-if="task.running" @click="stopTask">
<img src="@/assets/pause.svg" /> <img src="@/assets/pause.svg" />
@@ -44,10 +45,12 @@
<script> <script>
import toColor from '@/stringToColor'; import toColor from '@/stringToColor';
import Time from './Time.vue';
import CategoryModal from './CategoryModal.vue'; import CategoryModal from './CategoryModal.vue';
export default { export default {
components: { components: {
Time,
CategoryModal, CategoryModal,
}, },
props: { props: {
@@ -86,23 +89,8 @@ export default {
} }
return this.task.totalTime; 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() { beforeMount() {
setInterval(() => { setInterval(() => {
this.timePassed = Date.now() - this.task.startedAt; this.timePassed = Date.now() - this.task.startedAt;
}, 1000); }, 1000);
@@ -156,16 +144,6 @@ export default {
padding: 4px 12px; padding: 4px 12px;
border-radius: 16px; border-radius: 16px;
cursor: pointer; cursor: pointer;
// .dropdown {
// display: none;
// }
// &:hover {
// .dropdown {
// display: block;
// }
// }
} }
.delete { .delete {
@@ -174,21 +152,6 @@ export default {
} }
} }
@keyframes pulse {
from {
opacity: 0.3;
}
to {
opacity: 1;
}
}
.task.active {
.time {
animation: pulse 1s infinite alternate;
}
}
@media screen and (max-width: $max-width) { @media screen and (max-width: $max-width) {
.task { .task {
min-height: 64px; min-height: 64px;

View File

@@ -41,9 +41,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState({ ...mapState(['tasks']),
tasks: (state) => state.tasks,
}),
filteredTasks() { filteredTasks() {
if (this.selectedCategory) { if (this.selectedCategory) {
return this.tasks.filter((task) => task.category === this.selectedCategory); return this.tasks.filter((task) => task.category === this.selectedCategory);

55
src/views/Home/Time.vue Normal file
View File

@@ -0,0 +1,55 @@
<template>
<div
class="time"
:class="{ pulsing }"
>
{{ formattedTime }}
</div>
</template>
<script>
export default {
props: {
time: {
type: Number,
required: true,
},
pulsing: {
type: Boolean,
default: false,
},
},
computed: {
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`;
},
},
};
</script>
<style lang="scss" scoped>
@keyframes pulse {
from {
opacity: 0.3;
}
to {
opacity: 1;
}
}
.pulsing {
animation: pulse 1s infinite alternate;
}
</style>