Added global controls
This commit is contained in:
5
src/assets/reset.svg
Normal file
5
src/assets/reset.svg
Normal 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
3
src/assets/stop.svg
Normal 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 |
@@ -84,9 +84,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
categories: (state) => state.categories,
|
||||
}),
|
||||
...mapState(['categories']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -104,6 +102,7 @@ export default {
|
||||
align-items: center;
|
||||
padding: 0 64px;
|
||||
box-sizing: border-box;
|
||||
z-index: 900;
|
||||
|
||||
.add-category {
|
||||
color: $darker;
|
||||
|
||||
@@ -7,14 +7,6 @@ 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'),
|
||||
// },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&::placeholder {
|
||||
color: $dark;
|
||||
|
||||
@@ -52,6 +52,9 @@ export default createStore({
|
||||
removeTask(state, name) {
|
||||
state.tasks = state.tasks.filter((task) => task.name !== name);
|
||||
},
|
||||
removeAllTasks(state) {
|
||||
state.tasks = [];
|
||||
},
|
||||
startTask(state, name) {
|
||||
state.tasks = state.tasks.map((task) => {
|
||||
const newTask = task;
|
||||
|
||||
@@ -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>
|
||||
143
src/views/Home/GlobalTaskControls.vue
Normal file
143
src/views/Home/GlobalTaskControls.vue
Normal 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>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<TheCategoryBar @select="select" />
|
||||
<GlobalTaskControls />
|
||||
<TheTaskList :selectedCategory="selectedCategory" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,12 +10,14 @@
|
||||
import { mapState, mapMutations } from 'vuex';
|
||||
|
||||
import TheCategoryBar from '@/components/TheCategoryBar.vue';
|
||||
import GlobalTaskControls from './GlobalTaskControls.vue';
|
||||
import TheTaskList from './TheTaskList.vue';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
TheCategoryBar,
|
||||
GlobalTaskControls,
|
||||
TheTaskList,
|
||||
},
|
||||
data() {
|
||||
|
||||
55
src/views/Home/RemoveAllModal.vue
Normal file
55
src/views/Home/RemoveAllModal.vue
Normal 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>
|
||||
55
src/views/Home/ResetAllModal.vue
Normal file
55
src/views/Home/ResetAllModal.vue
Normal 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>
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="task" :class="{ active: task.running }">
|
||||
<div class="time">
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
<div class="task">
|
||||
<Time
|
||||
:time="time"
|
||||
:pulsing="task.running"
|
||||
/>
|
||||
<div class="toggle-state">
|
||||
<div v-if="task.running" @click="stopTask">
|
||||
<img src="@/assets/pause.svg" />
|
||||
@@ -44,10 +45,12 @@
|
||||
<script>
|
||||
import toColor from '@/stringToColor';
|
||||
|
||||
import Time from './Time.vue';
|
||||
import CategoryModal from './CategoryModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Time,
|
||||
CategoryModal,
|
||||
},
|
||||
props: {
|
||||
@@ -86,23 +89,8 @@ export default {
|
||||
}
|
||||
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(() => {
|
||||
this.timePassed = Date.now() - this.task.startedAt;
|
||||
}, 1000);
|
||||
@@ -156,16 +144,6 @@ export default {
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
// .dropdown {
|
||||
// display: none;
|
||||
// }
|
||||
|
||||
// &:hover {
|
||||
// .dropdown {
|
||||
// display: block;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.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) {
|
||||
.task {
|
||||
min-height: 64px;
|
||||
|
||||
@@ -41,9 +41,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
tasks: (state) => state.tasks,
|
||||
}),
|
||||
...mapState(['tasks']),
|
||||
filteredTasks() {
|
||||
if (this.selectedCategory) {
|
||||
return this.tasks.filter((task) => task.category === this.selectedCategory);
|
||||
|
||||
55
src/views/Home/Time.vue
Normal file
55
src/views/Home/Time.vue
Normal 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>
|
||||
Reference in New Issue
Block a user