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:
Guz
2022-02-01 11:59:03 -03:00
committed by GitHub
33 changed files with 1947 additions and 29 deletions

View File

@@ -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

View File

@@ -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: {

View File

@@ -1,6 +1,6 @@
{
"name": "ToToday",
"version": "0.1.0",
"version": "0.7.0",
"private": true,
"scripts": {
"dev": "nuxt",

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}
/*

View File

@@ -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 &bull; MIT License
(c) 2022 Lored &bull; 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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';

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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;

View 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
View 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;

View File

@@ -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>

View 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

View 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

View 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

View 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

View 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
View 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
View 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[];
};

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"typeRoots": ["node_modules/@types", "./src/types"],
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",