Merge pull request #13 from LoredDev/1.0.0-dev
1.0.0-dev: 1.0.0 release features completed
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Project Lored
|
||||
Copyright (c) 2022 Lored
|
||||
Copyright (c) 2022 Gustavo "Guz" L. de Mello
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
@@ -2,6 +2,7 @@ export default {
|
||||
srcDir: 'src/',
|
||||
buildDir: 'dist',
|
||||
target: 'static',
|
||||
ssr: false,
|
||||
|
||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||
head: {
|
||||
@@ -34,13 +35,12 @@ export default {
|
||||
'@nuxt/image',
|
||||
|
||||
'@nuxtjs/color-mode',
|
||||
|
||||
'@nuxtjs/pwa'
|
||||
|
||||
'@nuxtjs/pwa',
|
||||
],
|
||||
|
||||
// Modules: https://go.nuxtjs.dev/config-modules
|
||||
modules: [
|
||||
],
|
||||
modules: [],
|
||||
|
||||
// PWA module configuration: https://go.nuxtjs.dev/pwa
|
||||
pwa: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ToToday",
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
|
||||
39
src/assets/styles/_colors.scss
Normal file
39
src/assets/styles/_colors.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
$eerie-black0: #161320;
|
||||
$eerie-black1: #1a1826;
|
||||
$eerie-black2: #1e1e2e;
|
||||
$eerie-black3: #302d41;
|
||||
|
||||
$independence-gray0: #575268;
|
||||
$independence-gray1: #6e6c7e;
|
||||
$independence-gray2: #988ba2;
|
||||
$independence-gray3: #c3bac6;
|
||||
|
||||
$azure-white: #d9e0ee;
|
||||
|
||||
$lavender0: #c9cbff;
|
||||
|
||||
$sky0: #8fe8f7;
|
||||
$sky1: #89dceb;
|
||||
$sky2: #79c4d1;
|
||||
$sky3: #63a0ab;
|
||||
$sky4: #3e646b;
|
||||
|
||||
$blue0: #96cdfb;
|
||||
$blue1: #8fcdef;
|
||||
$blue2: #7099ba;
|
||||
$blue3: #49647a;
|
||||
$blue4: #23303b;
|
||||
|
||||
$green0: #b3f5bc;
|
||||
$green1: #abe9b3;
|
||||
$green2: #97cf9e;
|
||||
$green3: #7ba881;
|
||||
$green4: #4c6950;
|
||||
|
||||
$begonia-red0: #fc6d6d;
|
||||
$begonia-red1: #e36262;
|
||||
$begonia-red2: #bd5151;
|
||||
$begonia-red3: #7d3636;
|
||||
$begonia-red4: #3d1a1a;
|
||||
|
||||
$pale2: #b5a6a3;
|
||||
@@ -31,3 +31,15 @@
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@mixin colorFadeAnim($fromColor, $toColor, $duration, $id) {
|
||||
@keyframes colorFadeAnim-#{$id} {
|
||||
from {
|
||||
stroke: $fromColor;
|
||||
}
|
||||
to {
|
||||
stroke: $toColor;
|
||||
}
|
||||
}
|
||||
animation: colorFadeAnim-#{$id} $duration;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
@import '~assets/styles/fonts.css';
|
||||
@import '~assets/styles/_colors.scss';
|
||||
|
||||
$light-background: #fff;
|
||||
$light-primary: #000;
|
||||
$light-secondary: #afafaf;
|
||||
$light-background: $azure-white;
|
||||
$light-primary: $eerie-black0;
|
||||
$light-secondary: $independence-gray1;
|
||||
|
||||
$dark-background: #181b23;
|
||||
$dark-primary: #eaf3f2;
|
||||
$dark-secondary: #697699;
|
||||
$dark-background: $eerie-black1;
|
||||
$dark-primary: $azure-white;
|
||||
$dark-secondary: $eerie-black3;
|
||||
|
||||
$main-font: 'Red Display', arial, helvetica, sans-serif;
|
||||
$secondary-font: 'Fira Code', monospace, sans-serif;
|
||||
|
||||
$checked-color: $green0;
|
||||
$unchecked-color: $begonia-red0;
|
||||
|
||||
@@ -21,11 +21,21 @@ body {
|
||||
.light-mode body {
|
||||
background-color: $light-background;
|
||||
color: $light-primary;
|
||||
* {
|
||||
stroke: $light-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-mode body {
|
||||
background-color: $dark-background;
|
||||
color: $dark-primary;
|
||||
* {
|
||||
stroke: $dark-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.preventAnimLoad * .preventAnim {
|
||||
animation-duration: 0s !important;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<p>
|
||||
Version: {{ version }}
|
||||
Version: {{ version }} {{ storage_updated }}
|
||||
<br />
|
||||
<a href="http://github.com/ProjectLored/ToToday.app" target="_blank">
|
||||
(c) 2022 Project Lored • MIT License
|
||||
(c) 2022 Lored • MIT License
|
||||
</a>
|
||||
<br />
|
||||
Icons provided by
|
||||
@@ -16,12 +16,29 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import appInfo from '~~/package.json';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'PageFooter',
|
||||
data() {
|
||||
return {version: appInfo.version};
|
||||
}
|
||||
return {
|
||||
version: appInfo.version,
|
||||
storage_updated: sm.get('date.updated', true)
|
||||
? `- Storage Updated: ${sm.get('date.updated', true).day.readable} ${
|
||||
sm.get('date.updated', true).hour.readable
|
||||
}`
|
||||
: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('localStorage-changed', () => {
|
||||
this.storage_updated = sm.get('date.updated', true)
|
||||
? `- Storage Updated: ${sm.get('date.updated', true).day.readable} ${
|
||||
sm.get('date.updated', true).hour.readable
|
||||
}`
|
||||
: '';
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
114
src/components/ProgressBar.vue
Normal file
114
src/components/ProgressBar.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="progressBar">
|
||||
<div class="barContainer">
|
||||
<div class="bar">
|
||||
<div
|
||||
class="progress"
|
||||
:style="`width: ${progress - 4 <= 0 ? 0 : progress - 4}%`"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="progressInfo">{{ Math.round(progress) }}%</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ProgressBar',
|
||||
data() {
|
||||
return {
|
||||
progress: 0,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.progress = this.getProgress().percentage;
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('localStorage-changed', () => {
|
||||
this.progress = this.getProgress().percentage;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getProgress() {
|
||||
const tasks: Task[] = sm.get('tasks');
|
||||
|
||||
const totalTasks =
|
||||
tasks.length +
|
||||
tasks
|
||||
.map((task) => task.subTasks)
|
||||
.map((st) => st.length)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
const checkedTasks =
|
||||
tasks.filter((t) => t.checked).length +
|
||||
tasks
|
||||
.map((task) => task.subTasks)
|
||||
.flat()
|
||||
.filter((sb) => sb.checked).length;
|
||||
|
||||
return {
|
||||
total: totalTasks,
|
||||
checked: checkedTasks,
|
||||
percentage: isNaN((100 * checkedTasks) / totalTasks)
|
||||
? 0
|
||||
: (100 * checkedTasks) / totalTasks,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
.progressInfo {
|
||||
margin-top: 0;
|
||||
.light-mode & {
|
||||
color: $light-secondary;
|
||||
}
|
||||
.dark-mode & {
|
||||
color: $dark-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.barContainer {
|
||||
width: 100%;
|
||||
|
||||
@include center;
|
||||
|
||||
animation: enter 0.5s ease-out;
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
.bar {
|
||||
$bar-height: 15px;
|
||||
|
||||
@include center;
|
||||
justify-content: left;
|
||||
|
||||
.light-mode & {
|
||||
background-color: $blue2;
|
||||
}
|
||||
.dark-mode & {
|
||||
background-color: $eerie-black0;
|
||||
}
|
||||
|
||||
width: 60%;
|
||||
height: $bar-height;
|
||||
border-radius: $bar-height;
|
||||
|
||||
.progress {
|
||||
transition: width 0.5s;
|
||||
|
||||
background-color: $checked-color;
|
||||
border-radius: $bar-height;
|
||||
height: 49%;
|
||||
// width: 100%-4%;
|
||||
margin-left: 2%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
143
src/components/SubTask/SubTaskComp.vue
Normal file
143
src/components/SubTask/SubTaskComp.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<li :id="`subTask${id}`" class="subTask">
|
||||
<div class="subTaskInfo">
|
||||
<input
|
||||
:id="`subTaskCheckboxInput${id}-p${parent}`"
|
||||
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="ts">
|
||||
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: number) {
|
||||
const element = document.querySelector(
|
||||
`input#subTaskCheckboxInput${this.id}-p${this.parent}`
|
||||
) as HTMLInputElement;
|
||||
|
||||
const localTasks: Task[] = sm.get('tasks');
|
||||
|
||||
localTasks[parentId].subTasks[this.id].checked = element.checked;
|
||||
|
||||
sm.set('tasks', localTasks);
|
||||
},
|
||||
deleteSubTask(parentId: number) {
|
||||
const localTasks: Task[] = 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;
|
||||
}
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 5px solid currentColor;
|
||||
border-radius: 50%;
|
||||
|
||||
&:checked {
|
||||
background-color: $checked-color;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-family: $secondary-font;
|
||||
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>
|
||||
171
src/components/SubTask/SubTaskInput.vue
Normal file
171
src/components/SubTask/SubTaskInput.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="alignInputContainerSubTask">
|
||||
<div class="subTaskInputContainer">
|
||||
<button class="addTask" @click="addSubTask(parent)">
|
||||
<IconAdd class="addIcon" />
|
||||
</button>
|
||||
<form class="subTaskInput">
|
||||
<input
|
||||
id="newSubTaskInputName"
|
||||
class="subTaskNameInput"
|
||||
type="text"
|
||||
name="subTaskNameInput"
|
||||
placeholder="Sub-Task Title..."
|
||||
maxlength="50"
|
||||
@keypress.enter="addSubTask(parent)"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SubTaskInput',
|
||||
props: {
|
||||
parent: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addSubTask: (parentId: number) => {
|
||||
if (sm.get('tasks') === undefined) sm.add('tasks', []);
|
||||
|
||||
const titleInput = document.querySelector(
|
||||
'input#newSubTaskInputName'
|
||||
) as HTMLInputElement;
|
||||
|
||||
const newTitle = titleInput.value;
|
||||
|
||||
if (!newTitle || newTitle === '') {
|
||||
if (!titleInput.className.includes(' warnTitle'))
|
||||
titleInput.className += ' warnTitle';
|
||||
|
||||
(
|
||||
document.querySelector(
|
||||
'input#newSubTaskInputName'
|
||||
) as HTMLInputElement
|
||||
).className = titleInput.className;
|
||||
|
||||
setTimeout(() => {
|
||||
(
|
||||
document.querySelector(
|
||||
'input#newSubTaskInputName'
|
||||
) as HTMLInputElement
|
||||
).className = titleInput.className.replace(' warnTitle', '');
|
||||
}, 1500);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const subTaskItem: SubTask = {
|
||||
description: newTitle,
|
||||
checked: false,
|
||||
};
|
||||
|
||||
const localTasks: Task[] = sm.get('tasks');
|
||||
const parentTask = localTasks[parentId];
|
||||
|
||||
if (!parentTask.subTasks) parentTask.subTasks = [];
|
||||
|
||||
parentTask.subTasks.push(subTaskItem);
|
||||
localTasks[parentId] = parentTask;
|
||||
|
||||
sm.set('tasks', localTasks);
|
||||
|
||||
(
|
||||
document.querySelector('input#newSubTaskInputName') as HTMLInputElement
|
||||
).value = '';
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
.alignInputContainerSubTask {
|
||||
@include center;
|
||||
justify-content: left;
|
||||
&,
|
||||
* {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
*::placeholder {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.subTaskInputContainer {
|
||||
@include center;
|
||||
width: 100%;
|
||||
|
||||
.addTask {
|
||||
$bg-color: #00000080;
|
||||
|
||||
background-color: $bg-color;
|
||||
|
||||
margin: 2px;
|
||||
margin-right: 2%;
|
||||
padding: 1px;
|
||||
border-radius: 50%;
|
||||
border: 0px;
|
||||
|
||||
@include center;
|
||||
|
||||
.addIcon {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
.light-mode & * {
|
||||
stroke: $sky3;
|
||||
}
|
||||
|
||||
&:hover * {
|
||||
stroke: $sky0;
|
||||
}
|
||||
}
|
||||
|
||||
.subTaskInput {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
.subTaskNameInput {
|
||||
font-family: $secondary-font;
|
||||
height: 1.5em;
|
||||
width: 90%;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 5px 10px;
|
||||
|
||||
.light-mode & {
|
||||
background-color: $blue0;
|
||||
color: $independence-gray0;
|
||||
&::placeholder {
|
||||
color: $independence-gray1;
|
||||
}
|
||||
}
|
||||
.dark-mode & {
|
||||
background-color: $eerie-black0;
|
||||
color: $dark-primary;
|
||||
&::placeholder {
|
||||
color: $dark-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.warnTitle {
|
||||
&::placeholder {
|
||||
color: $unchecked-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
src/components/SubTask/SubTaskList.vue
Normal file
121
src/components/SubTask/SubTaskList.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<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>
|
||||
<SubTaskInput :parent="parent" />
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
</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>
|
||||
402
src/components/Task/TaskComp.vue
Normal file
402
src/components/Task/TaskComp.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<li
|
||||
:id="`task${id}`"
|
||||
:class="`${preventAnim ? 'preventAnimLoad ' : ''}${
|
||||
newTask ? 'newTask ' : ''
|
||||
}taskContainer`"
|
||||
>
|
||||
<div class="taskControl">
|
||||
<div :class="`task`">
|
||||
<div class="taskInfo">
|
||||
<label
|
||||
class="checkbox"
|
||||
:for="`checkboxInput${id}`"
|
||||
@click="changeState()"
|
||||
>
|
||||
<input
|
||||
:id="`checkboxInput${id}`"
|
||||
type="checkbox"
|
||||
:checked="checked"
|
||||
/>
|
||||
<div class="icons animate preventAnim">
|
||||
<IconChecked class="checkedIcon" />
|
||||
<IconUnchecked class="uncheckedIcon" />
|
||||
</div>
|
||||
</label>
|
||||
<div class="info">
|
||||
<h1>{{ title }}</h1>
|
||||
<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()">
|
||||
<IconTrash class="trashIcon" />
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<SubTaskList :parent="id" :opened="openSubTasks" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TaskComp',
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Task Title',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'Task short description',
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
newTask: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openSubTasks: false,
|
||||
preventAnim: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.preventAnim = false;
|
||||
}, 1000);
|
||||
},
|
||||
methods: {
|
||||
changeState() {
|
||||
const element = document.querySelector(
|
||||
`input#checkboxInput${this.id}`
|
||||
) as HTMLInputElement;
|
||||
const tasks: Task[] = sm.get('tasks');
|
||||
|
||||
tasks[this.id].checked = element?.checked;
|
||||
|
||||
this.openSubTasks = false;
|
||||
|
||||
sm.set('tasks', tasks);
|
||||
},
|
||||
deleteTask() {
|
||||
const localTasks: Task[] = sm.get('tasks');
|
||||
localTasks.splice(this.id, 1);
|
||||
sm.set('tasks', localTasks);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
.newTask {
|
||||
animation-delay: 0s !important;
|
||||
}
|
||||
|
||||
.taskContainer {
|
||||
@for $i from 1 through 100 {
|
||||
&#task#{$i} {
|
||||
animation-delay: #{$i * 0.2}s;
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(50%);
|
||||
|
||||
animation: enter 0.5s ease-out;
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
@keyframes enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(50%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.taskControl {
|
||||
display: flex;
|
||||
|
||||
margin-top: 5%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
right: -6%;
|
||||
|
||||
opacity: 0;
|
||||
@media (pointer: none), (pointer: coarse) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
transition: opacity 0.2s;
|
||||
transition: right 0.3s;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
padding: 1px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
.trashIcon * {
|
||||
transition: stroke 0.2s;
|
||||
}
|
||||
&:hover .trashIcon * {
|
||||
stroke: $begonia-red0;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover .controls {
|
||||
transition: 0.2s 0.5s;
|
||||
opacity: 1;
|
||||
right: -7%;
|
||||
}
|
||||
|
||||
.task {
|
||||
.light-mode & {
|
||||
background-color: $blue0;
|
||||
color: $independence-gray0;
|
||||
}
|
||||
.dark-mode & {
|
||||
background-color: $eerie-black0;
|
||||
color: $dark-primary;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
|
||||
border-radius: 20px;
|
||||
padding: 10px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 5%;
|
||||
|
||||
.taskInfo {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.checkbox {
|
||||
padding: 1%;
|
||||
padding-right: 2%;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
input[type='checkbox']:checked + .icons {
|
||||
animation: iconAnim 1s;
|
||||
.checkedIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
.uncheckedIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkedIcon *,
|
||||
.uncheckedIcon * {
|
||||
.light-mode & {
|
||||
@include colorFadeAnim(
|
||||
$unchecked-color,
|
||||
$checked-color,
|
||||
1s,
|
||||
'checkLight'
|
||||
);
|
||||
stroke: $checked-color;
|
||||
}
|
||||
.dark-mode & {
|
||||
@include colorFadeAnim(
|
||||
$unchecked-color,
|
||||
$checked-color,
|
||||
1s,
|
||||
'checkDark'
|
||||
);
|
||||
stroke: $checked-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
input[type='checkbox']:not(:checked) + .icons {
|
||||
animation: iconAnimReverse 1s;
|
||||
.checkedIcon {
|
||||
display: none;
|
||||
}
|
||||
.uncheckedIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.checkedIcon *,
|
||||
.uncheckedIcon * {
|
||||
.light-mode & {
|
||||
@include colorFadeAnim(
|
||||
$checked-color,
|
||||
$unchecked-color,
|
||||
1s,
|
||||
'uncheckLight'
|
||||
);
|
||||
stroke: $unchecked-color;
|
||||
}
|
||||
.dark-mode & {
|
||||
@include colorFadeAnim(
|
||||
$checked-color,
|
||||
$unchecked-color,
|
||||
1s,
|
||||
'uncheckDark'
|
||||
);
|
||||
stroke: $unchecked-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.icons {
|
||||
@include center;
|
||||
$bg-color: #00000080;
|
||||
|
||||
background-color: $bg-color;
|
||||
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
|
||||
.checkedIcon *,
|
||||
.uncheckedIcon * {
|
||||
.light-mode & {
|
||||
stroke: $unchecked-color;
|
||||
}
|
||||
.dark-mode & {
|
||||
stroke: $unchecked-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iconAnim {
|
||||
from {
|
||||
transform: rotate(0) scale(1);
|
||||
}
|
||||
5% {
|
||||
transform: rotate(0) scale(0.7);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(360deg) scale(1.2);
|
||||
}
|
||||
90% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes iconAnimReverse {
|
||||
from {
|
||||
transform: rotate(0) scale(1);
|
||||
}
|
||||
5% {
|
||||
transform: rotate(0) scale(0.7);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(-360deg) scale(1);
|
||||
}
|
||||
to {
|
||||
transform: rotate(-360deg) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
h1 {
|
||||
font-size: 1.2em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: $secondary-font;
|
||||
font-size: 0.8em;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
329
src/components/Task/TaskInput.vue
Normal file
329
src/components/Task/TaskInput.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="alignInputContainer">
|
||||
<div class="taskInputContainer">
|
||||
<button class="addTask" @click="addTask()">
|
||||
<IconAdd class="addIcon" />
|
||||
</button>
|
||||
<form class="taskInputs">
|
||||
<div class="taskNameInputContainer">
|
||||
<input
|
||||
id="newTaskInputName"
|
||||
class="taskNameInput"
|
||||
type="text"
|
||||
name="TaskNameInput"
|
||||
placeholder="Task Title..."
|
||||
maxlength="28"
|
||||
@keypress.enter="addTask()"
|
||||
/>
|
||||
</div>
|
||||
<div class="taskDescriptionInputContainer">
|
||||
<textarea
|
||||
id="newTaskInputDescription"
|
||||
class="taskDescriptionInput"
|
||||
name="TaskDescriptionInput"
|
||||
placeholder="Task Description..."
|
||||
maxlength="150"
|
||||
></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TaskInput',
|
||||
methods: {
|
||||
addTask: () => {
|
||||
if (sm.get('tasks') === undefined) sm.add('tasks', []);
|
||||
|
||||
const titleInput = document.querySelector(
|
||||
'input#newTaskInputName'
|
||||
) as HTMLInputElement;
|
||||
|
||||
const newTitle = titleInput.value;
|
||||
|
||||
const newDescription = (
|
||||
document.querySelector(
|
||||
'textarea#newTaskInputDescription'
|
||||
) as HTMLTextAreaElement
|
||||
).value;
|
||||
|
||||
if (!newTitle || newTitle === '') {
|
||||
if (!titleInput.className.includes(' warnTitle'))
|
||||
titleInput.className += ' warnTitle';
|
||||
|
||||
(
|
||||
document.querySelector('input#newTaskInputName') as HTMLInputElement
|
||||
).className = titleInput.className;
|
||||
|
||||
setTimeout(() => {
|
||||
(
|
||||
document.querySelector('input#newTaskInputName') as HTMLInputElement
|
||||
).className = titleInput.className.replace(' warnTitle', '');
|
||||
}, 1500);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const taskItem: Task = {
|
||||
title: newTitle,
|
||||
description: newDescription || '',
|
||||
checked: false,
|
||||
newTask: true,
|
||||
autoCheck: true,
|
||||
subTasks: [],
|
||||
};
|
||||
|
||||
const localTasks: Task[] = sm.get('tasks');
|
||||
|
||||
localTasks.push(taskItem);
|
||||
|
||||
sm.set('tasks', localTasks);
|
||||
|
||||
(
|
||||
document.querySelector('input#newTaskInputName') as HTMLInputElement
|
||||
).value = '';
|
||||
|
||||
(
|
||||
document.querySelector(
|
||||
'textarea#newTaskInputDescription'
|
||||
) as HTMLInputElement
|
||||
).value = '';
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
.alignInputContainer {
|
||||
@include center;
|
||||
@media not screen and (pointer: none), (pointer: coarse) {
|
||||
&:not(:hover),
|
||||
.taskInputContainer *:not(:focus-within) {
|
||||
.taskInputContainer {
|
||||
animation: shrink 0.5s forwards;
|
||||
@keyframes shrink {
|
||||
from {
|
||||
padding: 5px 10px;
|
||||
margin: 10px 0;
|
||||
transform: scale(100%);
|
||||
width: 100%;
|
||||
}
|
||||
to {
|
||||
padding: 0px 25%;
|
||||
margin: 0px 0;
|
||||
transform: scale(60%);
|
||||
width: 0%;
|
||||
}
|
||||
}
|
||||
.taskInputs {
|
||||
animation: hideInputs 0.5s forwards;
|
||||
@keyframes hideInputs {
|
||||
from {
|
||||
transform: translateX(0%);
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
}
|
||||
to {
|
||||
transform: translateX(25%);
|
||||
opacity: 0;
|
||||
width: 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.taskInputContainer {
|
||||
animation: expand 0.5s forwards;
|
||||
@keyframes expand {
|
||||
from {
|
||||
padding: 0px 25%;
|
||||
margin: 0px 0;
|
||||
transform: scale(60%);
|
||||
width: 0%;
|
||||
}
|
||||
to {
|
||||
padding: 5px 10px;
|
||||
margin: 10px 0;
|
||||
transform: scale(100%);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.taskInputs {
|
||||
animation: showInputs 0.5s forwards;
|
||||
@keyframes showInputs {
|
||||
from {
|
||||
transform: translateX(25%);
|
||||
opacity: 0;
|
||||
width: 0%;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0%);
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.taskInputContainer {
|
||||
&,
|
||||
* {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
*::placeholder {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.light-mode & {
|
||||
background-color: $blue0;
|
||||
color: $independence-gray0;
|
||||
}
|
||||
.dark-mode & {
|
||||
background-color: $eerie-black0;
|
||||
color: $dark-primary;
|
||||
}
|
||||
|
||||
border-radius: 20px;
|
||||
@include center;
|
||||
|
||||
@media (pointer: none), (pointer: coarse) {
|
||||
padding: 5px 10px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.addTask {
|
||||
$bg-color: #00000080;
|
||||
|
||||
background-color: $bg-color;
|
||||
|
||||
margin: 2px;
|
||||
margin-right: 2%;
|
||||
padding: 1px;
|
||||
border-radius: 50%;
|
||||
border: 0px;
|
||||
|
||||
@include center;
|
||||
|
||||
.addIcon {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
.light-mode & * {
|
||||
stroke: $sky3;
|
||||
}
|
||||
|
||||
&:hover * {
|
||||
stroke: $sky0;
|
||||
}
|
||||
}
|
||||
|
||||
.taskInputs {
|
||||
text-align: left;
|
||||
|
||||
.taskNameInputContainer,
|
||||
.taskDescriptionInputContainer {
|
||||
width: 100%;
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 90%;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.light-mode & {
|
||||
color: $independence-gray0;
|
||||
&::placeholder {
|
||||
color: $independence-gray1;
|
||||
}
|
||||
}
|
||||
.dark-mode & {
|
||||
color: $dark-primary;
|
||||
&::placeholder {
|
||||
color: $dark-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
padding: 5px 10px;
|
||||
.light-mode & {
|
||||
background-color: $blue2;
|
||||
&::placeholder {
|
||||
color: $independence-gray0;
|
||||
}
|
||||
}
|
||||
.dark-mode & {
|
||||
background-color: $dark-background;
|
||||
&::placeholder {
|
||||
color: $dark-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskNameInputContainer {
|
||||
.taskNameInput {
|
||||
font-family: $main-font;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
& .taskNameInput:focus,
|
||||
& .taskNameInput:hover {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.warnTitle {
|
||||
&::placeholder {
|
||||
color: $unchecked-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.taskDescriptionInputContainer {
|
||||
.taskDescriptionInput {
|
||||
overflow: auto;
|
||||
|
||||
white-space: pre-wrap;
|
||||
|
||||
font-family: $secondary-font;
|
||||
font-size: 0.85em;
|
||||
|
||||
&:focus {
|
||||
height: 5em;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
&:not(:focus) {
|
||||
overflow: hidden;
|
||||
height: 1em !important;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
& .taskDescriptionInput:focus,
|
||||
& .taskDescriptionInput:hover {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
src/components/Task/TaskList.vue
Normal file
53
src/components/Task/TaskList.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="taskList">
|
||||
<TaskComp
|
||||
v-for="task in list"
|
||||
:id="list.indexOf(task)"
|
||||
:key="`task-comp-${list.indexOf(task)}`"
|
||||
:new-task="task.newTask"
|
||||
:title="task.title"
|
||||
:description="task.description"
|
||||
:checked="task.checked"
|
||||
/>
|
||||
</ul>
|
||||
<ProgressBar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
list: sm.get('tasks') ? sm.get('tasks') : [],
|
||||
};
|
||||
},
|
||||
beforeCreate() {
|
||||
if (sm.get('tasks') === undefined) sm.add('tasks', []);
|
||||
|
||||
const localTasks: Task[] = sm.get('tasks');
|
||||
|
||||
for (const task of localTasks) task.newTask = false;
|
||||
|
||||
sm.set('tasks', localTasks);
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('localStorage-changed', () => {
|
||||
this.list = sm.get('tasks') ? sm.get('tasks') : [];
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
.taskList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,8 @@
|
||||
<button
|
||||
@click="
|
||||
$colorMode.preference =
|
||||
$colorMode.preference === 'dark' ? 'light' : 'dark'
|
||||
$colorMode.preference === 'dark' ? 'light' : 'dark';
|
||||
updateThemeInfo($colorMode.preference);
|
||||
"
|
||||
>
|
||||
<IconMoon class="light-mode-element" />
|
||||
@@ -12,6 +13,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import sm from '~/libs/storageManagement';
|
||||
|
||||
export default Vue.extend({
|
||||
methods: {
|
||||
updateThemeInfo: (theme) => sm.set('theme', theme, false, true),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~assets/styles/_variables.scss';
|
||||
@import '~assets/styles/_mixins.scss';
|
||||
|
||||
30
src/components/icon/Add.vue
Normal file
30
src/components/icon/Add.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="M8 12H12M16 12H12M12 12V8M12 12V16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
name: 'IconAdd',
|
||||
});
|
||||
</script>
|
||||
30
src/components/icon/Checked.vue
Normal file
30
src/components/icon/Checked.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="M7 12.5L10 15.5L17 8.5"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
name: 'IconChecked',
|
||||
});
|
||||
</script>
|
||||
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>
|
||||
42
src/components/icon/Trash.vue
Normal file
42
src/components/icon/Trash.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<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="M19 11V20.4C19 20.7314 18.7314 21 18.4 21H5.6C5.26863 21 5 20.7314 5 20.4V11"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 17V11"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14 17V11"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M21 7L16 7M3 7L8 7M8 7V3.6C8 3.26863 8.26863 3 8.6 3L15.4 3C15.7314 3 16 3.26863 16 3.6V7M8 7L16 7"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
name: 'IconTrash',
|
||||
});
|
||||
</script>
|
||||
30
src/components/icon/Unchecked.vue
Normal file
30
src/components/icon/Unchecked.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="M8 12H16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
name: 'IconUnchecked',
|
||||
});
|
||||
</script>
|
||||
@@ -1,7 +1,19 @@
|
||||
const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
||||
const months = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
const date = {
|
||||
|
||||
dayNumber: () => new Date().getDate(),
|
||||
|
||||
month: () => new Date().getMonth(),
|
||||
@@ -10,37 +22,40 @@ const date = {
|
||||
|
||||
year: () => new Date().getFullYear(),
|
||||
|
||||
day: (): any => {
|
||||
day: () => {
|
||||
return {
|
||||
readable: `${String(date.dayNumber()).padStart(2, '0')} ${date.monthName(Number(date.month()))} ${date.year()}`,
|
||||
readable: `${String(date.dayNumber()).padStart(2, '0')} ${date.monthName(
|
||||
Number(date.month())
|
||||
)} ${date.year()}`,
|
||||
number: date.dayNumber(),
|
||||
month: date.month(),
|
||||
year: date.year(),
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
hours: () => {
|
||||
const d = new Date();
|
||||
|
||||
return {
|
||||
readable: `${d.getHours()}:${d.getMinutes()}`,
|
||||
readable: `${String(d.getHours()).padStart(2, '0')}:${String(
|
||||
d.getMinutes()
|
||||
).padStart(2, '0')}`,
|
||||
hours: d.getHours(),
|
||||
minutes: d.getMinutes(),
|
||||
seconds: d.getSeconds(),
|
||||
milliseconds: d.getMilliseconds(),
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
timeZone: () => String(new Date().getTimezoneOffset()),
|
||||
|
||||
full: () => {
|
||||
full: (): DateInfo => {
|
||||
return {
|
||||
day: date.day(),
|
||||
hour: date.hours(),
|
||||
timeZone: date.timeZone(),
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default date;
|
||||
|
||||
194
src/libs/storageManagement.ts
Normal file
194
src/libs/storageManagement.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import appInfo from '~~/package.json';
|
||||
import date from '~/libs/date';
|
||||
import obj from '~/libs/utils';
|
||||
|
||||
const storageIndex = 'ToToday-storage';
|
||||
|
||||
/**
|
||||
* (Local) Storage Management of the application.
|
||||
*/
|
||||
const sm = {
|
||||
/**
|
||||
* Adds new value on `data`. *Don't create or rewrite the value if it already exists.*
|
||||
* @param {string} path New value's path on `data`;
|
||||
* @param {any} value New value;
|
||||
*/
|
||||
add: (path: string, value: any): void => {
|
||||
let data = sm.getJSON().data;
|
||||
|
||||
if (obj.getByString(data, `data.${path}`))
|
||||
return window.alert('ERROR: Value already in storage');
|
||||
|
||||
data = obj.setByString(data, value, path);
|
||||
|
||||
sm.setJSON(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Rewrite a value inside of `data` with a new *(creates a new path/value if it's doesn't exist before)*.
|
||||
* @param {string} path Value's path on `data`;
|
||||
* @param {any} value The new value to replace;
|
||||
* @param {boolean | undefined} getOld Return the old value of the path?
|
||||
* @param {boolean | undefined} meta Update a value on `meta`?
|
||||
*
|
||||
* @returns The old value of the path, if `getOld` is `true`.
|
||||
*/
|
||||
set: (
|
||||
path: string,
|
||||
value: any,
|
||||
getOld?: boolean,
|
||||
meta?: boolean
|
||||
): void | any => {
|
||||
if (meta) {
|
||||
let metaData = sm.getJSON();
|
||||
metaData = obj.setByString(metaData, undefined, path);
|
||||
sm.setJSON(metaData, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let data = sm.getJSON().data;
|
||||
|
||||
if (obj.getByString(data, path)) return sm.add(path, value);
|
||||
|
||||
const oldValue = obj.getByString(data, path);
|
||||
|
||||
data = obj.setByString(data, value, path);
|
||||
|
||||
sm.setJSON(data);
|
||||
|
||||
if (getOld) return oldValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a value from localStorage*.
|
||||
* @param {string} path Value's path on `data`;
|
||||
* @param {boolean | undefined} meta Update a value on `meta`?
|
||||
*
|
||||
* @returns {any | undefined} Value on localStorage (or undefined if no value wasn't found).
|
||||
*/
|
||||
get: (path: string, meta?: boolean): any | undefined => {
|
||||
const storage = sm.getJSON();
|
||||
|
||||
if (meta && path) return obj.getByString(storage, `meta.${path}`);
|
||||
else if (meta) return storage.meta;
|
||||
|
||||
if (path && obj.getByString(storage, `data.${path}`) !== undefined)
|
||||
return obj.getByString(storage, `data.${path}`);
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a value from localStorage's data
|
||||
* @param {string} path Value's path on `data`;
|
||||
* @param {boolean | undefined} getOld Return the old value of the path?
|
||||
*
|
||||
* @returns The old value of the path, if `getOld` is `true`.
|
||||
*/
|
||||
remove: (path: string, getOld?: boolean): void | any => {
|
||||
let data = sm.getJSON().data;
|
||||
|
||||
const oldValue = obj.getByString(data, path);
|
||||
|
||||
data = obj.setByString(data, undefined, path);
|
||||
|
||||
sm.setJSON(data);
|
||||
|
||||
if (getOld) return oldValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets and parses the local storage JSON file.
|
||||
*
|
||||
* **Used internally by the storage management.**
|
||||
*/
|
||||
getJSON: (): StorageSchema => {
|
||||
sm.checkJSON();
|
||||
return JSON.parse(localStorage.getItem(storageIndex) + '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a new `data` on the local storage JSON file *(also updates the `date.updated` meta-information)*.
|
||||
* @param {DataInfo | object} newData New data object to be updated/set;
|
||||
* @param {boolean} meta Do new data parameters contain `meta` information?
|
||||
*
|
||||
* **Used internally by the storage management.**
|
||||
*/
|
||||
setJSON: (newData: DataInfo | object, meta?: boolean) => {
|
||||
if (meta) localStorage.setItem(storageIndex, JSON.stringify(newData));
|
||||
else {
|
||||
const newJSON: StorageSchema = { meta: sm.getJSON().meta, data: newData };
|
||||
|
||||
localStorage.setItem(storageIndex, JSON.stringify(newJSON));
|
||||
|
||||
const updatedMeta = sm.getJSON().meta;
|
||||
|
||||
updatedMeta.date.updated = date.full();
|
||||
|
||||
const updatedJSON: StorageSchema = {
|
||||
meta: updatedMeta,
|
||||
data: newData,
|
||||
};
|
||||
|
||||
localStorage.setItem(storageIndex, JSON.stringify(updatedJSON));
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('localStorage-changed'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if there's already an app's local storage file exists or if it's valid and creates a new one if there's isn't.
|
||||
*
|
||||
* **Used internally by the storage management.**
|
||||
*/
|
||||
checkJSON: () => {
|
||||
try {
|
||||
JSON.parse(localStorage.getItem(storageIndex) + '');
|
||||
} catch (error) {
|
||||
sm.createJSON(true);
|
||||
}
|
||||
if (
|
||||
!localStorage.getItem(storageIndex) ||
|
||||
!JSON.parse(localStorage.getItem(storageIndex) + '').meta
|
||||
)
|
||||
sm.createJSON(true);
|
||||
|
||||
if (
|
||||
!localStorage.getItem(storageIndex) ||
|
||||
!JSON.parse(localStorage.getItem(storageIndex) + '').data
|
||||
)
|
||||
sm.createJSON(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates and sets an entire new JSON file on the local storage with meta-information about the app and file itself.
|
||||
* @param {boolean} reset Force creation of a file even if it's already created?
|
||||
*
|
||||
* **Used internally by the storage management.**
|
||||
*/
|
||||
createJSON: (reset?: boolean) => {
|
||||
if (localStorage.getItem(storageIndex) && !reset)
|
||||
return window.alert('ERROR: Local storage already exists');
|
||||
|
||||
const colorTheme: 'light' | 'dark' | string =
|
||||
localStorage.getItem('nuxt-color-mode') || 'light';
|
||||
|
||||
const storage: StorageSchema = {
|
||||
meta: {
|
||||
index: storageIndex,
|
||||
name: appInfo.name,
|
||||
version: appInfo.version,
|
||||
theme: colorTheme,
|
||||
|
||||
date: {
|
||||
created: date.full(),
|
||||
updated: date.full(),
|
||||
},
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
sm.setJSON(storage, true);
|
||||
},
|
||||
};
|
||||
|
||||
export default sm;
|
||||
34
src/libs/utils.ts
Normal file
34
src/libs/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// * Thanks Alnitak for the original code https://stackoverflow.com/a/6491621
|
||||
|
||||
const resolvePath = (path: string) => {
|
||||
path = path.replace(/\[(\w+)\]/g, '.$1');
|
||||
path = path.replace(/^\./, '');
|
||||
return path.split('.');
|
||||
};
|
||||
|
||||
const obj = {
|
||||
setByString: (object: object | any, newValue: any, path: string) => {
|
||||
const a = resolvePath(path);
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const k = a[i];
|
||||
object[k] = newValue;
|
||||
}
|
||||
|
||||
return object;
|
||||
},
|
||||
getByString: (object: object | any, path: string) => {
|
||||
const a = resolvePath(path);
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const k = a[i];
|
||||
if (k in object) {
|
||||
object = object[k];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return object;
|
||||
},
|
||||
};
|
||||
|
||||
export default obj;
|
||||
@@ -1,18 +1,31 @@
|
||||
<template>
|
||||
<main class="container">
|
||||
<main :class="`${preventAnim ? 'preventAnimLoad ' : ''}container`">
|
||||
<div>
|
||||
<ToTodayLogo />
|
||||
<p>A single page web app to help yours day-to-day tasks</p>
|
||||
<ThemePicker />
|
||||
</div>
|
||||
<div>
|
||||
<TaskList />
|
||||
<TaskInput />
|
||||
</div>
|
||||
<PageFooter />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'IndexPage',
|
||||
data() {
|
||||
return {
|
||||
preventAnim: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.preventAnim = false;
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
4
src/static/icons/add-icon.svg
Normal file
4
src/static/icons/add-icon.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="M8 12H12M16 12H12M12 12V8M12 12V16" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
4
src/static/icons/completed-icon.svg
Normal file
4
src/static/icons/completed-icon.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="M7 12.5L10 15.5L17 8.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
4
src/static/icons/pending-icon.svg
Normal file
4
src/static/icons/pending-icon.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="M8 12H16" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
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 |
6
src/static/icons/trash.svg
Normal file
6
src/static/icons/trash.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<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="M19 11V20.4C19 20.7314 18.7314 21 18.4 21H5.6C5.26863 21 5 20.7314 5 20.4V11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 17V11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 17V11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 7L16 7M3 7L8 7M8 7V3.6C8 3.26863 8.26863 3 8.6 3L15.4 3C15.7314 3 16 3.26863 16 3.6V7M8 7L16 7" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 642 B |
37
src/types/Storage.d.ts
vendored
Normal file
37
src/types/Storage.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
type DateInfo = {
|
||||
day: {
|
||||
year: number;
|
||||
month: number;
|
||||
number: number;
|
||||
readable: string;
|
||||
};
|
||||
hour: {
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
milliseconds: number;
|
||||
readable: string;
|
||||
};
|
||||
timeZone: string | number;
|
||||
};
|
||||
|
||||
type MetaInfo = {
|
||||
index: string;
|
||||
name: string;
|
||||
theme: 'dark' | 'light' | string;
|
||||
version: `${number}.${number}.${number}` | string;
|
||||
date: {
|
||||
created: DateInfo;
|
||||
updated: DateInfo;
|
||||
};
|
||||
};
|
||||
|
||||
type DataInfo = {
|
||||
tasks?: Task[];
|
||||
[name: string | number]: any;
|
||||
};
|
||||
|
||||
type StorageSchema = {
|
||||
meta: MetaInfo;
|
||||
data: DataInfo;
|
||||
};
|
||||
13
src/types/Tasks.d.ts
vendored
Normal file
13
src/types/Tasks.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
type SubTask = {
|
||||
description: string;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
type Task = {
|
||||
title: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
newTask: boolean;
|
||||
autoCheck: boolean;
|
||||
subTasks: SubTask[];
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"typeRoots": ["node_modules/@types", "./src/types"],
|
||||
"target": "ES2018",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
|
||||
Reference in New Issue
Block a user