feat(subTasks): created SubTask and SubTaskList components.
Created the sub-tasks feature, now each Task component also has its sub-tasks and sub-task list, stored on LocalStorage parent object;
This commit is contained in:
141
src/components/SubTask/SubTaskComp.vue
Normal file
141
src/components/SubTask/SubTaskComp.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<li :id="`subTask${id}`" class="subTask">
|
||||
<div class="subTaskInfo">
|
||||
<input
|
||||
:id="`subTaskCheckboxInput${id}`"
|
||||
type="checkbox"
|
||||
name=""
|
||||
class="checkbox"
|
||||
:checked="checked"
|
||||
@click="changeState(parent)"
|
||||
/>
|
||||
<p class="description">{{ description }}</p>
|
||||
</div>
|
||||
<nav class="subTaskControls">
|
||||
<button class="deleteSubtaskButton" @click="deleteSubTask(parent)">
|
||||
<IconTrash class="trashIcon" />
|
||||
</button>
|
||||
</nav>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SubTaskComp',
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
parent: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'Task short description',
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeState(parentId) {
|
||||
const state = document.querySelector(`input#subTaskCheckboxInput${this.id}`)?.checked;
|
||||
|
||||
const localTasks = sm.get('tasks');
|
||||
|
||||
localTasks[parentId].subTasks[this.id].checked = state;
|
||||
|
||||
sm.set('tasks', localTasks);
|
||||
},
|
||||
deleteSubTask(parentId) {
|
||||
const localTasks = sm.get('tasks');
|
||||
|
||||
localTasks[parentId].subTasks.splice(this.id, 1);
|
||||
|
||||
sm.set('tasks', localTasks);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
.subTask {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.subTaskInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.checkbox {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
|
||||
margin-right: 10px;
|
||||
|
||||
.light-mode & {
|
||||
color: $blue3;
|
||||
background-color: transparent;
|
||||
}
|
||||
.dark-mode & {
|
||||
color: $dark-background;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
font: inherit;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 5px solid currentColor;
|
||||
border-radius: 50%;
|
||||
|
||||
&:checked {
|
||||
background-color: $checked-color;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 1px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subTaskControls {
|
||||
opacity: 0;
|
||||
@media (pointer: none), (pointer: coarse) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
transition: opacity 0.2s;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
padding: 1px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deleteSubtaskButton {
|
||||
.trashIcon * {
|
||||
transition: stroke 0.2s;
|
||||
}
|
||||
&:hover .trashIcon * {
|
||||
stroke: $begonia-red0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .subTaskControls {
|
||||
transition: 1s 0.2s;
|
||||
opacity: 1;
|
||||
right: -7%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
src/components/SubTask/SubTaskList.vue
Normal file
143
src/components/SubTask/SubTaskList.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<span :class="`${preventAnim ? 'preventAnimLoad ' : ''}`">
|
||||
<div :class="`${opened ? 'opened' : 'closed'} subTasks preventAnim`">
|
||||
<ul :id="`subTaskList${parent}`" class="list">
|
||||
<SubTaskComp
|
||||
v-for="subTask in subTaskList"
|
||||
:id="subTaskList.indexOf(subTask)"
|
||||
:key="`subtask-comp-${subTaskList.indexOf(subTask)}`"
|
||||
:parent="parent"
|
||||
:description="subTask.description"
|
||||
:checked="subTask.checked"
|
||||
/>
|
||||
</ul>
|
||||
<button @click="addSubTask(parent)">Add subtask</button>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
const subTaskItem = {
|
||||
description: 'Task',
|
||||
checked: false,
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SubTasks',
|
||||
props: {
|
||||
parent: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
opened: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
subTaskList: sm.get('tasks')[this.parent].subTasks
|
||||
? sm.get('tasks')[this.parent].subTasks
|
||||
: [],
|
||||
preventAnim: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('localStorage-changed', () => {
|
||||
this.subTaskList = sm.get('tasks')[this.parent].subTasks
|
||||
? sm.get('tasks')[this.parent].subTasks
|
||||
: [];
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.preventAnim = false;
|
||||
}, 1000);
|
||||
},
|
||||
methods: {
|
||||
addSubTask: (parentId: number) => {
|
||||
if (sm.get('tasks') === undefined) sm.add('tasks', []);
|
||||
|
||||
const localTasks: any[] = sm.get('tasks');
|
||||
const parentTask = localTasks[parentId];
|
||||
|
||||
if (!parentTask.subTasks) parentTask.subTasks = [];
|
||||
|
||||
subTaskItem.description = `Description sub task ${parentTask.subTasks.length}`;
|
||||
|
||||
parentTask.subTasks.push(subTaskItem);
|
||||
localTasks[parentId] = parentTask;
|
||||
|
||||
sm.set('tasks', localTasks);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
.opened {
|
||||
transform: translateY(-80px) scaleY(0.1);
|
||||
animation: open 0.5s forwards;
|
||||
@keyframes open {
|
||||
from {
|
||||
transform: translateY(-80px) scaleY(0.1);
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
to {
|
||||
transform: translateY(-20px) scaleY(1);
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.closed {
|
||||
transform: translateY(-20px) scaleY(1);
|
||||
animation: close 0.5s forwards;
|
||||
@keyframes close {
|
||||
from {
|
||||
transform: translateY(-20px) scaleY(1);
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
to {
|
||||
transform: translateY(-80px) scaleY(0.1);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subTasks {
|
||||
.light-mode & {
|
||||
background-color: $blue2;
|
||||
color: $independence-gray0;
|
||||
}
|
||||
.dark-mode & {
|
||||
background-color: $eerie-black2;
|
||||
color: $dark-primary;
|
||||
}
|
||||
|
||||
font-family: $secondary-font;
|
||||
|
||||
width: 90%;
|
||||
z-index: 2;
|
||||
|
||||
padding: 20px 5% 10px 5%;
|
||||
|
||||
border-radius: 0 0 20px 20px;
|
||||
|
||||
text-align: left;
|
||||
|
||||
.list {
|
||||
padding: 1% 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -28,6 +28,14 @@
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
:class="`${
|
||||
openSubTasks ? 'openedSubtasksButton' : 'closedSubtasksButton'
|
||||
} subtasksButton`"
|
||||
@click="openSubTasks = !openSubTasks"
|
||||
>
|
||||
<IconDropdown class="dropdownIcon" />
|
||||
</button>
|
||||
</div>
|
||||
<nav class="controls">
|
||||
<button class="deleteButton" @click="deleteTask()">
|
||||
@@ -35,6 +43,7 @@
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<SubTaskList :parent="id" :opened="openSubTasks" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -340,6 +349,49 @@ export default Vue.extend({
|
||||
}
|
||||
}
|
||||
|
||||
.subtasksButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
opacity: 0;
|
||||
@media (pointer: none), (pointer: coarse) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
transition: opacity 0.2s;
|
||||
|
||||
background-color: transparent;
|
||||
padding: 1px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.dropdownIcon {
|
||||
* {
|
||||
transition: stroke 0.2s;
|
||||
}
|
||||
&:hover * {
|
||||
.light-mode & {
|
||||
stroke: $independence-gray0;
|
||||
}
|
||||
.dark-mode & {
|
||||
stroke: $dark-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.openedSubtasksButton {
|
||||
.dropdownIcon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
&.closedSubtasksButton {
|
||||
.dropdownIcon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .subtasksButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
src/components/icon/Dropdown.vue
Normal file
30
src/components/icon/Dropdown.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
stroke-width="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 5H18C20.2091 5 22 6.79086 22 9V15C22 17.2091 20.2091 19 18 19H6C3.79086 19 2 17.2091 2 15V9C2 6.79086 3.79086 5 6 5Z"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.5 10.75L12 13.25L9.5 10.75"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
name: 'IconDropdown',
|
||||
});
|
||||
</script>
|
||||
4
src/static/icons/source_icons_down-round-arrow.svg
Normal file
4
src/static/icons/source_icons_down-round-arrow.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 5H18C20.2091 5 22 6.79086 22 9V15C22 17.2091 20.2091 19 18 19H6C3.79086 19 2 17.2091 2 15V9C2 6.79086 3.79086 5 6 5Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.5 10.75L12 13.25L9.5 10.75" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 435 B |
Reference in New Issue
Block a user