vue-todo/src/components/TodoRoot.vue
2023-03-13 11:17:55 +01:00

341 lines
8.6 KiB
Vue

<!--
Root component for the Todo app that is displayed if the user is logged in.
Part of the basebox sample Todo app.
https://basebox.tech
-->
<template>
<main>
<h1>
ToDo List
</h1>
<div id="filter-form">
<label class="switch">
<input type="checkbox" v-model="showCompleted">
Show completed
</label>
<div class="form-control">
<label for="list-selector">List: </label>
<select id="list-selector" v-model="currentList">
<option :value="0">All</option>
<option v-for="item in lists" :key="item.id" :value="item.id">{{ item.title }}</option>
</select>
</div>
</div>
<div id="todo-container">
<transition-group name="list" tag="div">
<div :class="todoItemClass(task)" v-for="(task, i) in filteredItems" :id="'task-' + task.id" :key="task.id">
<div :class="todoCheckedClass(task)" @click="toggleCompleted(task)">
&nbsp;
</div>
<div class="todo-v-container">
<input class="todo-title" type="text" v-model="task.title">
<input class="todo-description" type="text" v-model="task.description">
</div>
<div class="todo-meta">
<img src="@/assets/img/trash-solid.png" class="btn-icon">
</div>
</div>
</transition-group>
</div>
<div class="btn-container">
<button type="button" @click="addItem()" class="btn btn-primary btn-large">Add Item</button>
</div>
</main>
</template>
<script>
import {gqlQuery} from "../util/net";
import {store} from "../store";
export default {
name: "TodoRoot",
components: [],
/** Current state of the component */
data() {
return {
/** whether to show completed items */
showCompleted: true,
/* array of todo lists currently known */
lists: [
{ id: 1, title: "Default"},
{ id: 2, title: "House" },
{ id: 3, title: "Car" },
{ id: 4, title: "Work" },
],
/* the id of the list currently being shown */
currentList: 1,
/* known tasks */
tasks: [
{ id: 1, completed: false, title: "Go to dentist", description: "Last time is way too long ago...", list: { id: 1 } },
{ id: 2, completed: true, title: "Change engine oil", description: "Use the good one", list: { id: 3 } },
{ id: 3, completed: false, title: "Clean windows", description: "Regualar stuff", list: { id: 1 } },
{ id: 4, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 5, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 6, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 7, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 8, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 9, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 10, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 11, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 12, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 13, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 14, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 15, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 16, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
{ id: 17, completed: false, title: "Oil stove hinges", description: "They are stiff and screechy", list: { id: 2 } },
],
}
},
methods: {
/**
* Add an item to the current list.
*/
addItem() {
if (this.currentList === 0) {
window.alert("Please select a list at the top right before adding an item.");
return;
}
/* fake ID of new item */
const newItemId = this.tasks.length + 1;
this.tasks.push({
id: newItemId,
completed: false,
title: "Enter task here",
description: "",
list: {
id: this.currentList,
}
});
/* wait a moment, then scroll list to the bottom */
setTimeout(function() {
document.getElementById("todo-container").scrollTo({
top: 100000,
behavior: "smooth"
});
/* select text in new item's title field */
const titleInput = document.querySelector(`#task-${newItemId} .todo-title`);
titleInput.setSelectionRange(0, 1000);
titleInput.focus();
}, 100);
},
/**
* Toggle completed state of a todo item.
* @param task the todo item to toggle.
*/
toggleCompleted(task) {
task.completed = !task.completed;
},
/**
* Return class object for a todo checked
*
* @param task the item
*/
todoCheckedClass(task) {
return {
"todo-checked": true,
"completed": task.completed,
"uncompleted": !task.completed,
}
},
/**
* Return class object for a todo item
*
* @param task the item
*/
todoItemClass(task) {
return {
"todo-item": true,
"completed": task.completed,
}
},
},
computed: {
/**
* Return array of tasks to show.
*/
filteredItems() {
return this.tasks.filter((item) => {
/* hide completed items if the option to show them is not set */
if (!this.showCompleted && item.completed) {
return false;
}
/* only return items of the current list */
if (this.currentList && item.list.id !== this.currentList) {
return false;
}
return true;
})
}
},
/**
* mounted lifecycle hook.
*/
mounted() {
if (!store.session) {
console.error("TodoRoot component must not be loaded if user is not logged in.");
return;
}
/* Load user info and his/her lists and todos */
gqlQuery(`query {
getUser(username: "${store.session.userName}") {
name
lists {
id
title
}
tasks {
id
title
description
completed
list {
id
}
}
}
}`).then(rspJson => {
}).catch(err => {
});
},
}
</script>
<style lang="scss" scoped>
#todo-container {
height: calc(100vh - 200px);
overflow: auto;
margin-top: 20px;
}
.todo-item {
display: flex;
background-color: var(--color-item-background);
box-shadow: var(--item-box-shadow);
margin-bottom: 10px;
padding: 15px;
border-radius: .3em;
border: 3px solid transparent;
&.completed {
border: 3px solid #41ab57;
}
input {
border: none;
border-bottom: 1px solid var(--color-border);
outline: none;
margin-bottom: 15px;
color: var(--color-item-text);
background: transparent;
&:focus {
outline: none;
}
}
.todo-title {
font-size: 1.5rem;
}
}
h1 {
margin-bottom: 25px;
}
.todo-v-container {
display: flex;
flex-direction: column;
flex-grow: 1;
background-color: inherit;
}
.todo-meta {
padding: 5px 5px 5px 25px;
}
.btn-icon {
width: 16px;
height: 16px;
cursor: pointer;
}
.todo-checked {
width: 80px;
cursor: pointer;
margin-right: 20px;
min-height: 80px;
border-radius: 50%;
border: 4px solid var(--color-border);
background-size: 80%;
background-position: center center;
background-repeat: no-repeat;
&.uncompleted {
&:hover {
background-image: url(../assets/img/check-gray.svg);
}
}
&.completed {
border-color: #41ab57;
background-image: url(../assets/img/check-green.svg);
}
}
.header-buttons {
float:right;
font-size: 1rem;
}
.switch {
input {
transform: scale(150%);
margin-right: 6px;
}
}
.list-enter-active {
animation: bounce-in 0.4s;
}
.list-leave-active {
animation: bounce-in 0.4s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
#filter-form {
display: flex;
justify-content: space-between;
}
</style>