Ejercicio de Manipulaci贸n del DOM

19/53

Lectura

驴Qu茅 tipo de material es este?

Hola, espero que hasta aqu铆 est茅s disfrutando el curso, recuerda que para dominar un lenguaje o framework de programaci贸n debes practicar. Justamente por esto he creado este material para ti, puedes hacerlo en tu entorno local o puedes hacerlo en alguna herramienta como codepen.io. La idea es que practiques lo que has aprendido hasta este punto del curso, te invito a que revises el c贸digo de tus compa帽eros y que te animes a dar feedback as铆 todos podr谩n ir creciendo.

Manipulaci贸n del DOM con Vue.js

Este ejercicio consiste en practicar la funcionalidad de renderizado declarativo que provee Vue.js, para eso vamos a crear una peque帽a aplicaci贸n web que nos permita hacer seguimiento de tareas utilizando el local storage del Browser. As铆 vamos a reforzar los conceptos b谩sicos que nos ofrece Vue.js para manipular e interactuar con el DOM.

Ejercicio:

  • Crear dentro de data una propiedad 鈥渘ame鈥 de tipo String y una propiedad 鈥渢asks鈥 de tipo de Array.

  • Agregar una expresi贸n para mostrar el valor de name y utilizar la directiva apropiada para para mostrar en una lista cada uno de los elementos dentro de task. Cada 鈥渢ask鈥 es un objeto con una propiedad 鈥渢itle鈥 y otra 鈥渢ime鈥. Agreguemos las expresiones necesarias para que en cada tarea podamos visualizar ambas propiedades.

  • Agregar funcionalidad para crear una nueva tarea:

    • Vamos a necesitar una nueva propiedad llamada 鈥渘ewTask鈥 que sea un Object. Dentro de este objeto tambi茅n agregamos una propiedad 鈥渢ilte鈥 de tipo String y una propiedad 鈥渢ime鈥 de tipo Number. Recuerda inicializar las propiedades con valores default.

    • Vamos a crear un m茅todo llamado 鈥渁ddTask鈥 que agregue una nueva tarea al array 鈥渢asks鈥. Una vez agregada tambi茅n va a reiniciar los valores dentro de 鈥渘ewTaks鈥. Ten en cuenta que antes de agregar la propiedad debemos chequear con los valores de 鈥渘ewTask.title鈥 y 鈥渘ewTask.time鈥 existan (sean distintos de 鈥渇alsy鈥). Por otro lado es importante que cada elemento nuevo que agreguemos al array de 鈥渢asks鈥 sea un objeto nuevo y no la instancia de 鈥渘ewTask鈥.

    • Vamos a agregar el HTML, para esto necesitamos dos 鈥渋nputs鈥 y un 鈥渂utton鈥. Tambi茅n debemos agregar las directivas correspondientes para enlazar el c贸digo con la vista.

    • Creamos tambi茅n una funcionalidad para cancelar, para eso debemos crear un m茅todo llamado 鈥渃ancel鈥 que simplemente reinicie los valores de las propiedades de newTask. Recordemos agregar un button de HTML donde enlazar este nuevo m茅todo.

    • Es momento de saber cuantas horas llevamos trabajadas, para eso vamos a crear una computed property llamada 鈥渢otalTime鈥 donde se recorran todas las tareas y se calculo el total del tiempo trabajado. Tambi茅n vamos agregar un elemento HTML con la expresi贸n necesaria para visualizar la propiedad.

    • Debemos integrar la app con el local storage del browser. Dentro del metodo 鈥渁ddTask鈥, guardamos toda la lista de tareas en dicho storage usando este metodo: 鈥渓ocalStorage.setItem(鈥榯asks鈥, JSON.stringify(this.tasks))鈥.

    • Guardando las tareas en el browser podemos persistir la informaci贸n aunque estemos cerrando o refrescando la p谩gina. Adem谩s, al momento de crearse el componente, debemos leer esta informaci贸n para poder cargar la lista de tareas. Para eso dentro del hook 鈥渃reated鈥, escribimos el siguiente c贸digo: 鈥渢his.tasks = JSON.parse(localStorage.getItem(鈥榯asks鈥)) || []鈥

    • Lo 煤ltimo que nos queda es poder eliminar las tareas que ya no queremos. Para eso vamos a crear un m茅todo que se llame 鈥渞emoveTask鈥. Este m茅todo debe recibir por par谩metro el indice de la tarea y podemos utilizar ese indice (en conjunto con el m茅todo 鈥渟plice鈥 de Array) para eliminar el elemento. Recordemos que tendremos que agregar un bot贸n por cada tarea y cada uno de estos se encarga de llamar al m茅todo 鈥渞emoveTask鈥 enviando por par谩metro el indice correspondiente. Recordemos invocar la funcionalidad que ya pusimos en 鈥渁ddTask鈥, para actualizar el local storage del Browser.

    • Por 煤ltimo vamos a mejorar la UI, cuando no haya tareas podemos mostrar un mensaje que indica que no hay ninguna tarea cargada y por otro lado ocultar el lista vac铆a.

Si en alg煤n punto del ejercicio te sientes perdido, te dejo la versi贸n que yo hice para que puedas consultarla en cualquier momento: https://codepen.io/ianaya89/pen/NgEeVO

Aportes 41

Preguntas 0

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesi贸n.

Reutilizando el ejercicio del Curso b谩sico de Vue y agregando las funcionalidades de este ejercicio:

CodePen Link

Reto Completado!!
C贸digo: https://codepen.io/paolojoaquin/pen/OJpQZxo

Dejo mi c贸digo:

<template>
  <div id="app">
    <div class="tareas">
      <input type="text" name="" id="taskTitle" placeholder="Tarea">
      <input type="number" name="" id="taskTime" placeholder="Horas" min="1" step="1">
      <button @click="addTask">Agregar tarea</button>
    </div>
    <section v-if="tasks.length > 0">
      <div class="listaTareas">
        <ul>
          <li
            v-for="task, index in tasks"
            :key="index"
          >
            {{ task.title }} -> {{ task.time }} horas
          <button
            :key="index"
            @click="deleteTask"
          >
            Borrar tarea {{ task.title }}
          </button>
          </li>
        </ul>
      </div>

      <div class="tiempo">
        <p>Tiempo total: {{ totalTime }} horas</p>
      </div>
    </section>
    <section v-else>
      <h1>No hay tareas por el momento</h1>
    </section>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tasks: [],
      newTask: {
        title: '',
        time: 0
      }
    };
  },

  methods: {
    addTask() {
      const title = document.getElementById('taskTitle').value;
      const time = parseInt(document.getElementById('taskTime').value);
      if (title !== '' && time !== '') {
        this.newTask.title = title;
        this.newTask.time = time;
        this.tasks.push(this.newTask);
        this.newTask = {
          title: '',
          time: 0
        };
      } else {
        alert('La tarea y el tiempo deben tener un valor');
      }

      localStorage.setItem('tasks', JSON.stringify(this.tasks));
    },

    deleteTask(task) {
      const index = this.tasks.indexOf(task);
      this.tasks.splice(index, 1);
      localStorage.setItem('tasks', JSON.stringify(this.tasks));
    }
  },

  computed: {
    totalTime() {
      let total = 0;
      this.tasks.forEach(task => {
        total += parseInt(task.time);
      });
      return total;
    }
  },

  created() {
    const tasks = localStorage.getItem('tasks');
    if (tasks) {
      this.tasks = JSON.parse(tasks) || [];
    }
  }
};
</script>

<style lang="scss">

</style>

Este es mi prototipo 100% funcional ademas le a帽ad铆 un plus con las actividades completada y una barra de progreso para saber como vamos.

HTML

<template lang="pug">
  #app
    .columns
      .column
      .column
        h1 Tareas y horas
        br
        input.input.is-8(type="text" v-model="title" name="title" placeholder="title" )
        input.input.is-8(type="text" v-model="time" name="time" placeholder="time" ) 
        input.button.link(type="submit" name="submit" @click="addTask(title,time)")
        p(v-if="totalTime!==0") {{totalTime}}
        div(v-for="(task,i) in tasks")
          ul
            li {{task.title}} - {{task.time}}   
            button.delete.is-medium(@click="removeTask(i)")
            br
      .column
</template>

JS

<script>
export default {
  name: 'app',
  
  data () {
    return {
      title:'',
      time:0,
      tasks:[]
    }
  },

  computed:{
    totalTime () {
      let total = 0
      this.tasks.map(function(sum){
        total += sum.time
      })
      return total
    }
  },
  created(){
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  methods:{
    addTask (title, time) {
      if(title, time){
        const newTask ={
          title,
          time : parseInt(time)        
        }
        this.tasks.push(newTask);
      }else(
        alert('debe llenar todos los campos')
      )
      localStorage.setItem('tasks',JSON.stringify(this.tasks))
      this.cancel ()
    },
    cancel () {
      this.title = '',
      this.time = ''
    },
    removeTask (i) {
      let remove = this.tasks.splice(i,1);
      localStorage.setItem('tasks',JSON.stringify(this.tasks))
      return remove
    } 
  }
}
</script>

Mi frontend no es muy bueno, pero aqu铆 va, sigo usando bulma, no me gust贸 que no tiene clases como tan practicas en comparaci贸n de bootstrap para listas y hay que desarrollar mas del lado del css, pero bueno, la funcionalidad est谩.

<template lang="pug">
  #app
    section.section
      nav.nav.has-shadow
        .container
          .field
            label.label T铆tulo de la tarea
              .control
                input.input.is-large(type="text", 
                                      placeholder="T铆tulo de la tarea" 
                                      v-model="title")
          .field
            label.label Tiempo de trabajo
              .control
                input.input.is-large(type="text", 
                                      placeholder="Tiempo trabajado" 
                                      v-model="time")
          .field.is-grouped.is-grouped-right
            .control
              a.button.is-info.is-large(@click="addTask") Agregar Tarea
              a.button.is-danger.is-large(@click="deleteTask") &times; Eliminar Lista completa
          br
          br
          h2 {{ timeWork }}
          p 
            small {{ numberTask }}
          
      .container
        .ul
          .li(v-for="(t, index) in tasks") {{ t.title }} - {{ t.time }}
            a.button.is-danger(@click="deleteThisTask(index)") &times;
        p.error 
          small {{ mensajeError }}

</template>

<script>
const tasks = []
export default {
  name: 'app',
  created(){
    this.tasks = JSON.parse(localStorage.getItem(tasks)) || []
  },
  data () {
    return {
      title: '',
      time: '',
      tasks: [],
      mensajeError: '',
      indexList: ''
    }
  },
  computed:{
    numberTask(){
      return `Tareas Pendientes: ${this.tasks.length}`
    },
    timeWork(){
      let horasTrabajadas = 0
      for(let item of this.tasks){
        horasTrabajadas += Number(item.time)
      }
      return `Hasta el momento hemos trabajado ${horasTrabajadas} hrs.`
    }
  },
  methods:{
    addTask(){
      if(this.title === '' || this.time === ''){
        this.mensajeError = 'Debe indicar los dos elementos t铆tulo y horario'
      }else{
        if (!/^([0-9])*$/.test(this.time)){
          this.mensajeError = 'El tiempo debe ser un n煤mero'
        }else{
          this.tasks.push({title: this.title, time: this.time})
          this.title = ''
          this.time = ''
          this.mensajeError = ''
          localStorage.setItem(tasks, JSON.stringify(this.tasks))
        }
      }
    },
    deleteTask(){
      this.tasks = []
      localStorage.setItem(tasks, JSON.stringify(this.tasks))
    },
    deleteThisTask(index){
      this.tasks.splice(index, 1)
      localStorage.setItem(tasks, JSON.stringify(this.tasks))
    }
  }
}
</script>

<style lang="scss">
  @import './scss/main.scss';
  .results {
    margin-top: 15px;
  }
  .error {
    color: red;
    font-weight: bolder;
  }
  .button {
    margin-right: 10px;
  }
</style>

Originalmente lo hice con html porque no me gusta pug pero lo converti a pug para ustedes

.conteiner
  nav.nav.justify-content-center.p-3.mb-2.bg-info.text-white
    a.nav-link.text-dark(href='#') Home
  .conteiner
    | {{ name }}
  .conteiner
    .col-12
      .d-flex.justify-content-center
        .form-group
          label(for='') Name
          input.form-control(type='text' placeholder='Add Name' v-model='title')
          small.form-text.text-muted Add the new Title Name
        .d-flex.justify-content-center
          .form-group
            label(for='') Time
            input.form-control(type='text' placeholder='Add time' v-model='time')
            small.form-text.text-muted Add the new time Name
          button.btn.btn-primary(type='button' v-on:click='addTask()' data-toggle='modal' data-target='#exampleModal')
            | Add
          button.btn.btn-danger(type='button' v-on:click='restartTask()' data-toggle='modal' data-target='#exampleModal')
            | cancel
  table.table
    thead
      tr
        th Title
        th Time
    tbody
      tr(v-for='(item, index) in task' :key='item')
        td(scope='row') {{ item.title }}
        td(scope='row') {{ item.time }}
        td(scope='row')
          button.btn.btn-danger(type='button' v-on:click='remove(index)' data-toggle='modal' data-target='#exampleModal')
            | &times;
    p.text-success.totalTime {{ totalTime }}
  #exampleModal.modal.fade(v-if='showMessage' tabindex='-1' role='dialog' aria-labelledby='exampleModalLabel' aria-hidden='true')
    .modal-dialog(role='document')
      .modal-content
        .modal-header
          h5.modal-title Modal title
          button.close(type='button' data-dismiss='modal' aria-label='Close')
            span(aria-hidden='true') &times;
        .modal-body
          p {{ message }}```

Hola a todos, realic茅 el ejercicio con unas cuantas funciones extras de crud.
鈥>
Screenshot:

TaskManager.vue

/* eslint-disable vue/html-self-closing */
<template lang="pug">
  #app
    .container
        .section.p-0
            div.mb-6
                <div :class="messageClass" v-if="message">
                    <button class="delete"  aria-label="delete"  @click="message=false"></button>
                    | {{messageText}}
                </div>
            p.title.is-large.is-info.mt-6.mb-0
                | Task Manager
            p.has-text-right
                button.button.is-small.is-primary(@click="modalClass='modal is-active'; clear()")
                   | <i class="fa fa-plus" aria-hidden="true"></i> <strong> New task</strong>
                button.button.is-small.is-danger.ml-4(@click="deleteSelected")
                   | <i class="fa fa-trash" aria-hidden="true"></i> <strong class="ml-2"> Delete selected tasks</strong>
        table.table.is-bordered.is-hoverable.mt-2.is-fullwidth(v-show="tasks.length > 0")
          thead
            tr
              th
                input(type="checkbox" v-model="selectedAll")
              th Name
              th Description
              th Time
              th Ops.
          tbody
            tr(v-for="(task, index) in tasks")
              td
                input(type="checkbox" v-model="selected" :value="index")
              td {{task.name}}
              td {{task.description}}
              td {{task.time}} hrs
              td
                a.button.is-danger.is-small.mr-2(@click="deleteTask(index)") <i class="fa fa-times" aria-hidden="true"></i>
                a.button.is-info.is-small(@click="editTask(task, index)") <i class="fa fa-edit" aria-hidden="true"></i>
          tfoot
            tr
              td.has-text-right(colspan="3")
                strong Total Time
              td.has-text-left(colspan="2")
                | {{getTotalTime}} hrs
        div(v-if="!tasks.length")
            p.message.is-info.p-4.mt-4
                strong No hay tareas a煤n
    div
        <div :class="modalClass">
            <div class="modal-background"></div>
                <div class="modal-content">
                    div.my-4
                        <div :class="messageClass" v-if="messageInDialog">
                            <button class="delete"  aria-label="delete"  @click="messageInDialog=false"></button>
                            | {{messageText}}
                        </div>

                    .card.p-5
                        .card-header.title.has-text-right
                            span(v-if="editTaskIndex == -1") Create a new task
                            span(v-else) Edit task
                        .card-content
                            .content
                                .field
                                    input.input(type="text" v-model="aTask.name" placeholder="Task name")
                                .field
                                    textarea.textarea(v-model="aTask.description" placeholder="Task Description")
                                .field.has-text-left
                                    input.input(type="number" v-model="aTask.time" placeholder="Time" style="width:120px")
                                .field(v-if="editTaskIndex == -1")
                                    a.button.is-primary.is-medium(@click="addTask") Save task
                                    a.button.is-secondary.is-medium.ml-3(@click="modalClass='modal'") Close
                                .field(v-else)
                                    a.button.is-primary.is-medium(@click="updateTask") Update task
                </div>
            <button class="modal-close is-large" @click="modalClass='modal'" aria-label="close"></button>
        </div>

</template>
<script>

export default {

  name: "Tasks",
  data () {
    return {
      selected: [],
      editTaskIndex: -1,
      messageText: "The task was successfully saved.",
      messageClass: "notification is-success",
      message: false,
      messageInDialog: false,
      aTask: {
        name: "",
        time: 0,
        description: ""
      },
      totalTime: 0,
      modalClass: "modal",
      tasks: []
    };
  },
  computed: {
    getTotalTime (key = "time") {
      return this.tasks.reduce((a, b) => +a + +b.time, 0);
    },
    selectedAll: {
      set (val) {
        this.selected = [];
        if (val) {
          for (let i = 0; i < this.tasks.length; i++) {
            this.selected.push(i);
          }
        }
      },
      get () {
        return this.selected.length === this.tasks.length;
      }
    }
  },
  watch: {
  },
  created () {
    this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
  },
  mounted () {
    console.log("App Mounted");
  },
  methods: {
    deleteSelected () {
      const sel = this.selected.length;
      if (sel <= 0) {
        this.showMessage("notification is-warning", true, "You haven't selected any task to be deleted");
        return;
      }
      if (sel > 0) {
        for (let i = 0; i <= (sel); i++) {
          // Delete selected items
          this.tasks.splice(i, sel);
        }
        localStorage.setItem("tasks", JSON.stringify(this.tasks));
        this.showMessage("notification is-info", true, sel + " task(s) deleted successfully");
        this.selected = [];
      }
    },
    updateTask () {
      if (this.aTask.name.length > 0 && !isNaN(this.aTask.time)) {
        this.tasks[this.editTaskIndex] = { name: this.aTask.name, time: this.aTask.time, description: this.aTask.description };
        localStorage.setItem("tasks", JSON.stringify(this.tasks));
        this.showMessage("notification is-success", true, "The task '" + this.tasks[this.editTaskIndex].name + "' was edited");
        this.editTaskIndex = -1;
        this.modalClass = "modal";
        setTimeout(() => {
          this.message = false;
        }, 3000);
      } else {
        this.showMessage("notification is-danger", true, "Please review you information", true);
      }
    },
    editTask (task, index) {
      this.modalClass = "modal is-active";
      this.aTask = task;
      this.editTaskIndex = index;
    },
    showMessage (messageClass = "notification is-info", show = true, messageText, inDialog = false) {
      this.messageText = messageText;
      this.messageClass = messageClass;
      if (inDialog) {
        this.messageInDialog = true;
      } else {
        this.message = show;
      }
      setTimeout(() => {
        !inDialog ? this.message = false : this.messageInDialog = false;
      }, 3000);
    },
    deleteTask (index) {
      const taskName = this.tasks[index].name;
      this.tasks.splice(index, 1);
      localStorage.setItem("tasks", JSON.stringify(this.tasks));
      this.showMessage("notification is-info", true, `The task ${taskName} was deleted`);
    },
    clear () {
      this.aTask.name = "";
      this.aTask.time = 0;
      this.aTask.description = "";
      this.editTaskIndex = -1;
    },
    addTask () {
      if (this.aTask.name.length > 0 && !isNaN(this.aTask.time)) {
        if (this.aTask.time > 0) {
          const newTask = { name: this.aTask.name, time: this.aTask.time, description: this.aTask.description };
          this.tasks.push(newTask);
          localStorage.setItem("tasks", JSON.stringify(this.tasks));
          this.modalClass = "modal";
          this.showMessage("notification is-success", true, "The task was successfully saved");
          this.clear();
        } else {
          this.showMessage("notification is-danger", true, "Data for time must be a positive number", true);
        }
      } else {
        this.showMessage("notification is-danger", true, "Please review you information", true);
        // this.modalClass = "modal";
        setTimeout(() => {
          this.message = false;
        }, 3000);
      }
    }
  }
};
</script>

main.scss

@import '../../node_modules/bulma/bulma.sass';

<template lang="pug" >
#home
  .container.mio
    .columns
      .column.is-6.is-offset-3.is-child.box
        p.has-text-black.has-text-weight-bold.pb-4 Lista de Actividades
          .field.is-horizontal
            .field-label.is-normal
            label.labelmio Nombre Actividad:
            .field-body
              .field
                .control
                  input.input(placeholder="task", v-model="newTask.title")
          .field.is-horizontal
            .field-label.is-normal
            label.labelmio Tiempo ejecucion:
            .field-body
              .field
                .control
                  input.input(placeholder="time", v-model="newTask.time", type="number")
                  button.button.buttonmio(@click="addTasks") Agregar
                  button.button.buttonmio(@click="cancel") Cancelar
    .columns
      .column.is-8.is-offset-2.box.mio
        p(v-show="!tasks.length") Aun no hay tareas cargadas
        p(v-show="tasks.length").has-text-black.has-text-weight-bold Actividades realizadas
          p(v-for="t in tasks") Actividad: {{ t.title }} Tiempo de ejecucion: {{ t.time }}
            button.button.buttonmio2.is-danger.is-outlined(@click="removeTask(t)")
              span.icon.is-small
                i.fas.fa-times
          p Total tiempo {{ totalTime }}
</template>
<script>
export default {
  name: 'Home',
  data () {
    return {
      name: '',
      tasks: [],
      newTask: {
        title: '',
        time: 0
      }
    }
  },
  methods: {
    cancel () {
      this.newTask.title = this.newTask.time = ''
    },
    addTasks () {
      var copi = Object.assign({}, this.newTask)
      this.tasks.push(copi)
      this.cancel()
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    },
    removeTask (task) {
      var i = this.tasks.indexOf(task)
      console.log(i)
      if (i !== -1) {
        this.tasks.splice(i, 1)
      }
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    }
  },
  computed: {
    totalTime () {
      var suma = 0
      for (var t = 0; t < this.tasks.length; t++) {
        const element = this.tasks[t].time
        suma = suma + parseInt(element)
      }
      return suma
    }
  },
  created () {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  components: {}
}
</script>
<style scoped>
.mio{
  background: white;
  margin-top: 50px;
}
.labelmio{
  margin-left: -70px;
  margin-right: 30px;
}
.buttonmio{
  margin-top: 20px;
  }
  .buttonmio2{
    margin-left: 90px;
    padding: 20px;
    width: 15px;
  }
</style>```

Done! :3

<template>

  <h1>Nombre: {{ name }}</h1>

  <input type="text" v-model="newTask.title" placeholder="Nombre de la tarea">
  <input type="text" v-model="newTask.time" placeholder="Tiempo de la tarea">
  <button @click="addTask">A帽adir tarea</button>

  <div v-show="tasks.length > 0">

    <ul>
      <li v-for="(task, i) in tasks" :key="i">
        <b>Nombre de la tarea:</b> {{ task.title }} 
        <b>Tiempo:</b> {{ task.time }} 
        <button class="is-danger" @click="removeTask(i)">&times;</button>
      </li>
    </ul>

    <p>Se han trabajado {{ totalTime }} horas</p>

  </div>

  <p v-show="tasks.length <= 0">No hay tareas para mostrar</p>

</template>

<script>
export default {
  name: 'App',
  components: {},
  created() {
    this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
  },
  data() {
    return {
      name: "",
      tasks: [],
      newTask: {
        title: "",
        time: ""
      }
    }
  },
  methods: {
    saveInLocalSotrage() {
      localStorage.setItem("tasks", JSON.stringify(this.tasks));
    },
    addTask() {
      const task = this.newTask;
      if (task.title != "" && task.time != "") {
        
        this.tasks.push({
          title: task.title,
          time: task.time
        });
        task.title = "";
        task.time = "";
        this.saveInLocalSotrage();
      }
    },
    removeTask(index){ 
      this.tasks.splice(index, 1);
      this.saveInLocalSotrage();
    }
  },
  computed: {
    totalTime() {
      let hours = 0;
      const tasks = this.tasks;
      for (const task of tasks)
        hours += parseInt(task.time);
      return hours;
    }
  }
}
</script>

<style lang="scss">
/* Es una buena p艜actica importar los estiulos generales desde el componente App.vue */
@import "./scss/main.scss";
</style>

Les presento mi pr谩ctica:

<template lang="pug">
.container.is-fluid
  section.section
    center
        img(src="../../assets/images/logo_azael.png", width="150")
    h1.is-size-2.tasks-title.has-text-centered.is-paddingless Administrador de Tareas
    br
    .columns
      .column.is-6.is-offset-one-quarter
        .card
          header.card-header
            h1.card-header-title
              | Crear Tarea Nueva
          .card-content
            .content
              .notification.is-warning(v-show="warning")
                |Debe completar los campos correctamente
                button.delete(@click="warning = false") 
              .field
                label.label Tarea:
                p.control.has-icons-left
                  input.input(
                      type='text', 
                      placeholder='Nombre de tu tarea',
                      v-model="newTask.task")
                  span.icon.is-small.is-left
                    i.fa.fa-clipboard-check
              .field
                label.label Horas:
                p.control.has-icons-left
                  input.input(
                      type='number',
                      placeholder='Tiempo invertido',
                      v-model="newTask.time")
                  span.icon.is-small.is-left
                    i.fa.fa-stopwatch
              
              button.button.is-primary.addTask(@click="addTask") Agregar
              button.button(@click="cancel") Cancelar

    .columns
      .column.is-6.is-offset-one-quarter
        .card
          header.card-header
            h1.card-header-title
              | Listado de Tareas
          .card-content
            .content.table_wrapper
                table.table.has-text-centered
                    thead
                        tr
                            th Nombre tarea
                            th Tiempo Invertido
                            th Eliminar
                    tbody
                        tr(v-show="alert")
                            td.has-text-centered(colspan="3", v-if="!tasks.length")
                                | No se encontraron tareas registradas                            
                        tr(v-for="t in tasks")  
                            td {{ t.task }}
                            td {{ t.time }}
                            td
                                button.button.is-danger(@click="deleteTask(t)")
                                    i.fa.fa-times                            
          footer.card-footer
            p.card-footer-item
                span 
                    strong {{ `Total de Horas: ${totalTime} ` }}                                    
</template>

<script>
export default {
    name: "tasks",
    data () {
        return {
            alert: true,
            warning: false,
            newTask: { task : '', time : 0 },
            tasks: [],
            sumaHoras: 0
        }
    },
    methods:{
        addTask () {
            const self = this;

            if (self.newTask.task == '' || self.newTask.time == 0 ) {
                self.warning = true
                return false
            } else {
                self.warning = false
            }  

            if (self.tasks.length > 0) {
                self.alert = false
            }else{
                self.alert = true  
            }
            
            self.tasks.push(self.newTask); 
            this.cancel();   
        },
        cancel () {
            const self = this;
            self.newTask = {task : '', time : 0}
        },
        deleteTask (task) {
            const self = this;
            let index  = self.tasks.indexOf(task);
            self.tasks.splice(index, 1)
        }
    },
    computed:{
        totalTime () {
            const self  = this;
            let suma = 0;

            self.tasks.forEach(task => {
                suma = parseInt(suma) + parseInt(task.time)
            });

            return suma;
        }
    }     
}
</script>

<style scoped>
.table_wrapper{
    overflow-x: auto;   
}
.addTask{
    margin:0 10px;
}
</style>

Comparto mi ejercicio, me cost贸 trabajo por que lo hice sin bootstrap
https://codepen.io/carlos-fuentes-the-selector/pen/WWQxYK

Listo mi ejercicio, dejo aqu铆 mi Codepen

https://codepen.io/jgorocica/full/OGpEop

Saludos

Aqu铆 mi c贸digo

<template lang="pug">
  #container
    h2 {{ name }}
    .columns
      .column.is-4
        .field
          label.label.labelLeft(for="name") Nombre
          input.input.is-small(type="text" v-model:name="newTask.name" id="name")
        .field
          label.label.labelLeft(for="time") Tiempo
          input.input.is-small(type="text" v-model:name="newTask.time" id="time")
        .buttonLayout
          button.button.is-success.buttonLeft(@click="addTask") Crear tarea
          button.button.is-info.buttonLeft(@click="cancel") Cancelar
      .column.is-8
        div.table
          table(v-show="tasks.length")
            tr
              td Nombre
              td Titulo
              td
            tr(v-for="(task, index) in tasks")
              td {{task.name}}
              td {{ task.time }}
              td
                button.button.is-danger(@click="removeTask(index)" ) X
          h2(v-show="!tasks.length") no hay ninguna tarea cargada
    p.message.is-danger.message-body.center(v-show="tasks.length") Tiempo total de trabajo {{ totalTime }}

</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      name: 'Servicio de tareas',
      tasks: [],
      newTask: {
        name: '',
        time: 0
      }
    }
  },
  created () {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  methods: {
    addTask () {
      if (this.newTask.name === '' || this.newTask.time === 0) {
        return
      }
      let name = this.newTask.name
      let time = this.newTask.time
      this.tasks.push({
        name,
        time
      })
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    },
    cancel () {
      this.newTask.name = ''
      this.newTask.time = 0
    },
    removeTask (index) {
      this.tasks.splice(index, 1)
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    }
  },
  computed: {
    totalTime () {
      let timeTotal = 0
      for (let i in this.tasks) {
        timeTotal += parseInt(this.tasks[i].time)
      }
      return timeTotal
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
@import '~bulma/bulma.sass';
table {
  width: 100%;
  margin-left: 5%;
  margin-right: 5%;
}

.inputLayout {
  display: inline;
}
h2 {
  margin-top: 5%;
  margin-bottom: 5%;
}
.labelLeft{
  text-align: left;
}
.buttonLeft{
  margin-left: 5%;
}
.center {
  text-align: center;
}
</style>

<template>
  <div id="app">
    <section class="section">
      <nav class="nav has-shadow">
        <div class="container">
          <div class="field has-addons">
            <div class="control is-expanded">
              <input
                v-model="newTask.title" 
                type="text" 
                placeholder="Titulo" 
                class="input is-large">
            </div>
            <div class="control is-expanded">
              <input
                v-model="newTask.time" 
                type="number" 
                placeholder="Tiempo" 
                class="input is-large">
            </div>
            <div class="control">
              <a class="button is-info is-large" @click="addTask">Add Task</a>
            </div>
            <div class="control">
              <a class="button is-danger is-large" @click="cancel">&times;</a>
            </div>
          </div>
          <p><small>{{ totalTime }}</small></p>
        </div>
        <div class="container">
          <p>{{ name }}</p>
        </div>
        <div class="container results">
          <div class="columns" >
            <template v-if="tasks.length != 0">
              <div class="column" v-for="(t, i) in tasks">
                <p>{{ t.title }} - {{ t.time }}</p>
                <br>
                <a class="button is-danger is-large" @click="removeTask(i)">&times;</a>
              </div>
            </template>
            <template v-else>
              <p>No hay tasks</p>
            </template>
          </div>
        </div>
      </nav>
    </section>
  </div>
</template>

<script>

export default {
  name: 'app',
  data () {
    return {
      name: '',
      tasks: [],
      newTask: {
        tilte: '',
        time: 0
      }
    }
  },
  created: function () {
    this.tasks = JSON.parse(window.localStorage.getItem('tasks')) || []
  },
  computed: {
    totalTime () {
      var total = 0
      for (var i = 0; i < this.tasks.length; i++) {
        total = parseInt(total) + parseInt(this.tasks[i].time)
      }
      return `Tiempo total: ${total}`
    }
  },
  methods: {
    removeTask (i) {
      this.tasks.splice(i)
      window.localStorage.setItem('tasks', JSON.stringify(this.tasks))
    },
    cancel () {
      this.newTask = {
        tilte: '',
        time: 0
      }
    },
    addTask () {
      if (this.newTask.title && this.newTask.time) {
        this.tasks.push({
          title: this.newTask.title,
          time: this.newTask.time
        })
      }
      this.newTask = {
        tilte: '',
        time: 0
      }
      window.localStorage.setItem('tasks', JSON.stringify(this.tasks))
    }
  }
}
</script>

<style lang="scss">
  @import './assets/scss/main.scss';

  .results{
    margin-top: 50px;
  }
</style>

Adjunto mi aporte

<template lang ="pug">
  #app
    section.section
      nav.nav.has-shadow
        .container
          h1.title.is-1.has-text-centered Gestor de Tareas
          .container(v-show="!name")
              .field.is-3.has-addons
                .control
                  .columns
                    .column
                      input#name.input.is-large(type="text", placeholder="Ingresa tu nombre", v-model="newName")
                .control
                  .columns
                    .column
                      a.button.is-primary.is-large(type="submit", @click="addName") Ok

      h2.title.is-4(v-show="name") Tareas de {{ name }}
      hr
      .columns(v-show="name")
        .column
          h2.title.is-3.has-text-centered Agregar Tarea
          .columns.is-mobile
            .column.is-three-quarters
              .field
                .control
                  input.input(type="text", v-model="task.title", placeholder="Titulo de Tarea")
            .column
              .field
                .control
                  input.input.is-primary(type="number", placeholder="0", v-model="task.time")
                hr
                a.button.is-primary(@click="addTask") +
                a.button.is-danger(@click="cancel") x
        .column
          h2.title.is-3.has-text-centered Lista de Tareas
          p.has-text-centered(v-show="!tasks.length") Aun no hay tareas cargadas
          div(v-show="tasks.length")
            ol
              li.has-text-centered(v-for="t in tasks")

                  .columns
                    .column
                      .tags.has-addons
                        p.tag.is-medium.is-info {{ t.title }}
                        span.tag.is-medium.is-warning {{ t.time }}
                    .column
                      button.button.tag.is-danger(@click="removeTask") eliminar

        .column
          h2.title.is-3.has-text-centered Tiempo Requerido
            .tags.has-addons.has-text-centered
              span.tag.is-medium.is-warning {{ totalTime }}
              span.tag.is-medium.is-medium Horas






    </section>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      newName: null,
      name: '',
      tasks: [],
      task: {
        title: '',
        time: 0
      }
    }
  },
  created () {
    this.tasks = JSON.parse(window.localStorage.getItem('tasks')) || []
    this.name = JSON.parse(window.localStorage.getItem('name'))
  },
  computed: {
    totalTime () {
      if (!this.tasks.length) {
        return 0
      }
      let total = 0
      this.tasks.forEach(t => {
        total += parseInt(t.time)
      })
      return total
    }
  },
  watch: {
  },
  methods: {
    addName () {
      this.name = this.newName
      window.localStorage.setItem('name', JSON.stringify(this.name))
    },
    addTask () {
      if (!this.task.title || !this.task.time) {
        return
      }
      this.tasks.push({
        title: this.task.title,
        time: this.task.time
      })
      this.task.title = ''
      this.task.time = 0
      window.localStorage.setItem('tasks', JSON.stringify(this.tasks))
    },
    cancel () {
      this.task.title = ''
      this.task.time = 0
    },
    removeTask (index) {
      this.tasks.splice(index, 1)
      window.localStorage.setItem('tasks', JSON.stringify(this.tasks))
    }
  }
}
</script>

<style lang="scss">
  @import './scss/main.scss';
  html {
  padding: 20px;
  color: #3d3d3d;
  }
</style>

Les dejo mi ejercicio: https://codepen.io/ludwingperezt/pen/gEWPGx

No agregu茅 estilos porque quer铆a enfocarme en la soluci贸n del c贸digo con vue. Ahora que veo lo que hicieron ustedes me da verg眉enza como dej茅 el ejercicio, probablemente lo haga luego jejeje

<template>
  <div id="app">
    <div class="container is-fluid">
      <nav class="level">
        <div class="level-left">
          <div class="level-item">
            <div class="field">
              <div class="control">
                <input class="input is-info" type="text" placeholder="Titulo de la tarea" v-model="newTasks.value">
              </div>
            </div>
          </div>
          <div class="level-item">
            <div class="field">
              <div class="control">
                <input class="input is-primary" type="number" placeholder="Tiempo de la tarea" v-model="newTasks.time">
              </div>
            </div>
          </div>
          <div class="level-item">
            <a class="has-text-centered button is-link is-outlined is-medium" v-on:click="addTask(newTasks)">A帽adir Tarea</a>
          </div>
          <div class="level-item">
            <a class="has-text-centered button is-primary is-outlined is-medium" v-on:click="cancelar">Cancelar</a>
          </div>
          <div class="level-item" v-show="!mostrar">
            <article class="message is-danger">
              <div class="message-body">
                Error el titulo no debe de estar vacio y el tiempo de la tarea debe de ser mayor a "0".
              </div>
            </article>
          </div>
        </div>
        <div class="level-right">
          <div class="level-item">
            <p class="has-text-centered">
              <a class="link is-info">{{name}}</a>
            </p>
          </div>
        </div>
      </nav>
      <div v-show="tasks != ''">
        <article class="media" v-for="task in tasks" :key="task">
          <figure class="media-left">
            <div class="page__toggle">
              <label class="toggle">
                <input class="toggle__input" type="checkbox" v-if="task.check" checked="checked">
                <input class="toggle__input" type="checkbox" v-else>
                <span class="toggle__label">
                  <span class="toggle__text"></span>
                </span>
              </label>
            </div>
          </figure>
          <div class="media-content">
            <div class="content">
              <div class="columns is-multiline is-variable is-1">
                <div class="column is-two-thirds">
                  <p class="notification is-info">{{task.value}}</p>
                </div>
                <div class="column">
                  <p class="notification is-primary">{{task.time}}</p>
                </div>
              </div>
            </div>
          </div>
          <div class="media-right">
            <button class="delete" v-on:click="removeTask(task)"></button>
          </div>
        </article>
        <div class="media columns is-multiline is-variable is-1">
          <div class="column is-full">
            <p class="notification is-link">{{totalTimeResult}}</p>
          </div>
        </div>
      </div>
      <article class="media" v-show="tasks == ''">
        <div class="column is-full">
          <p class="notification is-warning">No existe ninguna tarea</p>
        </div>
      </article>
    </div>
  </div>
</template>
<script>
const tracks = [
  { id: 1, name: 'Muchacha', artist: 'Luis Alberto Spinetta' },
  { id: 2, name: 'Hoy aca en el baile', artist: 'El Pepo' },
  { id: 3, name: 'I was made for loving you', artist: 'Kiss' }
]

export default {
  name: 'app',

  data () {
    return {
      name: 'Angel Infanti',
      rayas: 16,
      mostrar: true,
      tasks: [],
      newTasks: {
        value: '',
        time: 0,
        check: true
      }
    }
  },

  created () {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  computed: {
    searchMessage () {
      return `Encontrados: ${this.tracks.length}`
    },
    totalTimeResult () {
      var totalTime = 0
      for (var recorrido = 0; recorrido < this.tasks.length; recorrido++) {
        console.log(this.tasks[recorrido].time)
        totalTime = totalTime + this.tasks[recorrido].time
      }
      return `Tiempo total de ectividades: ${totalTime}`
    }
  },

  methods: {
    addTask (newTasks) {
      newTasks.time = parseInt(newTasks.time)
      if (newTasks.value !== '' && newTasks.time > 0) {
        this.tasks.push(newTasks)
        this.newTasks = {
          value: '',
          time: 0,
          check: true
        }
        this.mostrar = true
        localStorage.setItem('tasks', JSON.stringify(this.tasks))
      } else {
        this.mostrar = false
      }
    },
    removeTask (indice) {
      this.tasks.splice(indice, 1)
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    },
    cancelar () {
      this.newTasks = {
        value: '',
        time: 0,
        check: false
      }
    },
    search () {
      this.tracks = tracks
    }
  }
}
</script>

<style lang="scss">@import './scss/main.scss'</style>
<style type="scss">
  .toggle{
    --uiToggleSize: var(--toggleSize, 20px);
    --uiToggleIndent: var(--toggleIndent, .4em);
    --uiToggleBorderWidth: var(--toggleBorderWidth, 2px);
    --uiToggleColor: var(--toggleColor, #000);
    --uiToggleDisabledColor: var(--toggleDisabledColor, #868e96);
    --uiToggleBgColor: var(--toggleBgColor, #fff);
    --uiToggleArrowWidth: var(--toggleArrowWidth, 2px);
    --uiToggleArrowColor: var(--toggleArrowColor, #fff);

    display: inline-block;
    position: relative;
  }
  .toggle__input{
    position: absolute;
    left: -99999px;
  }
  .toggle__label{
    display: inline-flex;
    cursor: pointer;
    min-height: var(--uiToggleSize);
    padding-left: calc(var(--uiToggleSize) + var(--uiToggleIndent));
  }
  .toggle__label:before, .toggle__label:after{
    content: "";
    box-sizing: border-box;
    width: 1em;
    height: 1em;
    font-size: var(--uiToggleSize);

    position: absolute;
    left: 0;
    top: 0;
  }
  .toggle__label:before{
    border: var(--uiToggleBorderWidth) solid var(--uiToggleColor);
    z-index: 2;
  }

  .toggle__input:disabled ~ .toggle__label:before{
    border-color: var(--uiToggleDisabledColor);
  }

  .toggle__input:focus ~ .toggle__label:before{
    box-shadow: 0 0 0 2px var(--uiToggleBgColor), 0 0 0px 4px var(--uiToggleColor);
  }

  .toggle__input:not(:disabled):checked:focus ~ .toggle__label:after{
    box-shadow: 0 0 0 2px var(--uiToggleBgColor), 0 0 0px 4px var(--uiToggleColor);
  }

  .toggle__input:not(:disabled) ~ .toggle__label:after{
    background-color: var(--uiToggleColor);
    opacity: 0;
  }

  .toggle__input:not(:disabled):checked ~ .toggle__label:after{
    opacity: 1;
  }

  .toggle__text{
    margin-top: auto;
    margin-bottom: auto;
  }

  /*
  The arrow size and position depends from sizes of square because I needed an arrow correct positioning from the top left corner of the element toggle
  */

  .toggle__text:before{
    content: "";
    box-sizing: border-box;
    width: 0;
    height: 0;
    font-size: var(--uiToggleSize);

    border-left-width: 0;
    border-bottom-width: 0;
    border-left-style: solid;
    border-bottom-style: solid;
    border-color: var(--uiToggleArrowColor);

    position: absolute;
    top: .5428em;
    left: .2em;
    z-index: 3;

    transform-origin: left top;
    transform: rotate(-40deg) skew(10deg);
  }

  .toggle__input:not(:disabled):checked ~ .toggle__label .toggle__text:before{
    width: .5em;
    height: .25em;
    border-left-width: var(--uiToggleArrowWidth);
    border-bottom-width: var(--uiToggleArrowWidth);
    will-change: width, height;
    transition: width .1s ease-out .2s, height .2s ease-out;
  }

  /*
  =====
  LEVEL 2. PRESENTATION STYLES
  =====
  */
  /*
  The demo skin
  */
  .toggle__label:before, .toggle__label:after{
    border-radius: 2px;
  }
  /*
  The animation of switching states
  */
  .toggle__input:not(:disabled) ~ .toggle__label:before,
  .toggle__input:not(:disabled) ~ .toggle__label:after{
    opacity: 1;
    transform-origin: center center;
    will-change: transform;
    transition: transform .2s ease-out;
  }

  .toggle__input:not(:disabled) ~ .toggle__label:before{
    transform: rotateY(0deg);
    transition-delay: .2s;
  }

  .toggle__input:not(:disabled) ~ .toggle__label:after{
    transform: rotateY(90deg);
  }

  .toggle__input:not(:disabled):checked ~ .toggle__label:before{
    transform: rotateY(-90deg);
    transition-delay: 0s;
  }

  .toggle__input:not(:disabled):checked ~ .toggle__label:after{
    transform: rotateY(0deg);
    transition-delay: .2s;
  }

  .toggle__text:before{
    opacity: 0;
  }

  .toggle__input:not(:disabled):checked ~ .toggle__label .toggle__text:before{
    opacity: 1;
    transition: opacity .1s ease-out .3s, width .1s ease-out .5s, height .2s ease-out .3s;
  }

  /*
  =====
  LEVEL 3. SETTINGS
  =====
  */

  .toggle{
    --toggleColor: #690e90;
    --toggleBgColor: #9b59b6;
    --toggleSize: 50px;
  }
</style>

Sigo construyendo mis interfaces de usuario con Vuetify. Este es el resultado final:

A continuaci贸n presento el c贸digo:

<template lang="pug">
v-app
  v-content
    v-container(fluid)
      v-layout(align-center justify-center)
        v-flex(xs12 sm8 md6 lg5)
          v-card.elevation-12
            v-toolbar(color="secondary" dark card prominent)
              v-toolbar-title.headline.text-uppercase
                span.font-weight-light {{ name }}
            v-card-text
              form(@submit.prevent="addTask")
                v-container.pa-0(fluid)
                  v-layout(grid-list-xs wrap)
                    v-flex.px-1(xs12 sm8 md9 lg10)
                      v-text-field(v-model="newTask.title" label="T铆tulo")
                    v-flex.px-1(xs12 sm4 md3 lg2)
                      v-text-field(v-model="newTask.time" label="Tiempo (min.)" type="number" min="1")
                    v-flex.px-1(xs6)
                      v-btn(type="submit" color="primary" block) Add Task
                    v-flex.px-1(xs6)
                      v-btn(@click="cancel" block) Cancel
            v-divider
            v-list(two-line)
              v-subheader Tareas
                span.ml-1.font-weight-light (Tiempo total: {{ totalTime }} hrs.)
              template(v-if="tasks.length > 0")
                v-list-tile(v-for="(task, idx) in tasks" :key="idx" @click="")
                  v-list-tile-content
                    v-list-tile-title {{ task.title }}
                    v-list-tile-sub-title Tiempo: {{ task.time }} min.
                  v-list-tile-action
                    v-btn(@click="removeTask(idx)" color="error" flat fab)
                      v-icon delete
              v-list-tile(v-else)
                v-list-tile-content
                  v-list-tile-title.text-xs-center No tasks
  v-snackbar(v-model="msg.show" :color="msg.color" :timeout="3000" bottom multi-line dark) {{ msg.text }}
</template>

<script>
export default {
  name: 'App',
  created () {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  data () {
    return {
      msg: {
        show: false,
        text: '',
        color: ''
      },
      name: 'Leonardo Campo R.',
      tasks: [
      ],
      newTask: {
        title: '',
        time: 0
      }
    }
  },
  methods: {
    addTask () {
      if (!this.newTask.title || !this.newTask.time) {
        this.showMsg('error', 'Debe ingresar un t铆tulo y el tiempo')
        return
      }
      this.tasks.push({ title: this.newTask.title, time: parseInt(this.newTask.time) })
      this.save()
      this.cancel()
    },
    removeTask (idx) {
      this.tasks.splice(idx, 1)
      this.save()
    },
    cancel () {
      this.newTask.title = ''
      this.newTask.time = 0
    },
    save () {
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    },
    showMsg (color, text) {
      this.msg.text = text
      this.msg.color = color
      this.msg.show = true
    }
  },
  computed: {
    totalTime () {
      let time = 0
      this.tasks.forEach((task) => {
        time += task.time
      })
      return parseFloat((time / 60.0).toFixed(2))
    }
  }
}
</script>

Ejercicio:

<template lang="pug">
  #app
    .columns
      .column {{ name }}
        h1.title.has-background-info(v-if="!comprobarLista") las horas que debes invertir para ser un poco menos ignorante son: {{ totalTime }}
        h1.title.has-background-warning(v-else) No hay tareas que mostrar
      .column
        h1 Agrega una nueva Tarea
        input.input(v-model="newTask.title")
        span(v-show="mensajeTitle").tag.is-warning.is-large El texto no es valido
        input.input(v-model="newTask.time")
        span(v-show="mensajeTime").tag.is-warning.is-large ingresa un n煤mero valido
        br
        br
        button.button(@click="addTask") Agregar tarea
        button.button.is-danger(@click="cancelTask") cancelar Tarea
    .columns
      .column
        h1 Tus Tareas Pendientes son
        ul
          li(v-for="(task, index) in tasks")
            span.tag.is-success {{index}} {{ task.title }}, Tiempo Estimado {{ task.time }} horas
              button.delete(@click="deleteTask($event, index)")
</template>

<script>

export default {
  name: 'app',
  created () {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  data () {
    return {
      name: 'HERMESTO',
      tasks: [
        {
          title: 'titulo 1',
          time: 2
        },
        {
          title: 'titulo 2',
          time: 5
        }
      ],
      newTask: {
        title: '',
        time: 0
      },
      mensajeTitle: false,
      mensajeTime: false
    }
  },
  computed: {
    totalTime () {
      let total = 0
      for (let i = 0; i < this.tasks.length; i++) {
        total += parseInt(this.tasks[i].time)
      }
      return total
    },
    comprobarLista () {
      if (this.tasks.length === 0) {
        return true
      } else {
        return false
      }
    }
  },
  methods: {
    addTask () {
      if (this.newTask.title.trim().length === 0) {
        this.mensajeTitle = true
        return false
      } else {
        this.mensajeTitle = false
      }
      if (parseInt(this.newTask.time) > 0) {
        this.mensajeTime = false
      } else {
        this.mensajeTime = true
        console.log('falio')
        return false
      }
      let tmpTask = {
        title: '',
        time: 0
      }
      tmpTask.title = this.newTask.title
      tmpTask.time = this.newTask.time
      this.newTask.title = ''
      this.newTask.time = 0
      this.tasks.push(tmpTask)
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    },
    cancelTask () {
      this.newTask.title = ''
      this.newTask.time = 0
    },
    deleteTask ($event, index) {
      this.tasks.splice(index, 1)
    }
  }

}

</script>

<style lang="scss">
  @import './scss/main.scss';
  .results {
    margin-top: 50px
  }
</style>

Dejo el m铆o. Me falta implementar algunas cosas pero funciona
https://codepen.io/hiteple/pen/XweZvO

Hecho con bulma en codepen!

https://codepen.io/davos_/pen/arryLP

Ejercicio completado

Adjunto mi ejercicio

<template lang="pug">
  #app
    .panel.is-primary
      .panel-heading Ejercicio de Manipulaci贸n del DOM by {{ name}}
      .panel-block
        .columns
          .column
            .box
              .field
                  p.is-size-3 New Task
              .field
                label.control Title:
                input.control(type="text", placeholder="Title", v-model="title")
              .field
                label.control Time
                input.control(type="numeric", placeholder="Time", v-model="time")
              .field
                button.button.is-info(@click="addTask") Add Task
                button.button.is-danger(@click="resetValues") Cancel
          .column
              .fieldset(v-if="tasks.length>0")
                p.is-size-5(v-show="totalTime") Horas trabajadas: {{ totalTime }}
                .columns
                  .column(v-for="(t, key) in tasks")
                    .box
                      p.is-size-7.has-text-weight-bold {{ t.title }}
                      p.is-size-7 {{ t.time}}
                      button.button.is-small(@click="removeTask(key)") Delete
              .fieldset(v-else)
                h1 No tasks yet!
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      name: 'ANDRES RUIZ',
      tasks: [],
      newTask: {},
      title: '',
      time: 0,
      existsTasks: false
    }
  },
  computed: {
    totalTime () {
      var time = 0
      for (let index = 0; index < this.tasks.length; index++) {
        time += parseInt(this.tasks[index].time)
      }
      return time
    }
  },
  created () {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  methods: {
    addTask () {
      if ((this.title !== '') && (this.time > 0)) {
        this.tasks.push({
          title: this.title,
          time: this.time
        })
        alert('Added correctly!')
        this.resetValues()
        localStorage.setItem('tasks', JSON.stringify(this.tasks))
      } else {
        alert('Check your values!')
      }
    },
    resetValues () {
      this.title = ''
      this.time = 0
      this.newTask = {}
    },
    removeTask (key) {
      this.tasks.splice(key, 1)
      alert('Deleted correctly!')
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    }
  }
}
</script>

<style lang="scss">
  @import './scss/main.scss'
</style>

Aporto mi ejercicio, trate de hacer algo diferente pero implementando todos los puntos requeridos:
https://github.com/menkar91/note

Mi Trabajo
JS


export default {
  name: "AddList",

  data() {
    return {
      name: "",
      task: [],
      newTask: {},
      time: 0,
      title: "",
      showMessage: false,
      message: ""
    };
  },
  methods: {
    addTask() {
      if (this.title != "" && this.time > 0) {
        this.task.push({ time: this.time, title: this.title });
        localStorage.setItem("tasks", JSON.stringify(this.tasks));
        this.showMessage = true;
        this.message = "Time was add";
      } else {
        this.showMessage = true;
        this.message = "Time wasn't added";
      }
    },
    restartTask() {
      this.title = "";
      this.time = 0;
      this.rewTask = {};
      this.showMessage = true;
      this.message = "The time is restart";
    },
    remove(index) {
      this.task.splice(index, 1);
      this.showMessage = true;
      this.message = "The time was deleted";
    }
  },
  computed: {
    totalTime() {
      let sumTime = 0;
      for (const key in this.task) {
        sumTime += parseInt(this.task[key].time);
      }
      return sumTime;
    }
  },
  created() {
    this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
  }
};

Javascript

var app = new Vue({
  el: '#app',
  data: {
    name: 'Lucas Moreno',
    tasks: [],
    newTask: {title:'', time: null},
    showMessage: false
  },
  methods: {
    removeTask(index){
      this.tasks.splice(index, 1);
      localStorage.setItem("tasks", JSON.stringify(this.tasks));
    },
    addTask(){
      if(this.newTask.title != '' && this.newTask.time != null){
        this.tasks.push({title: this.newTask.title, time: this.newTask.time});
        localStorage.setItem("tasks", JSON.stringify(this.tasks));
        this.newTask.title = '';
        this.newTask.time = null;
        this.showMessage = false;
      }
      else{
        this.showMessage = true;
      }
    },
    cancel(){
      this.newTask.title = '';
      this.newTask.time = null;
    }
  },
  computed:{
    totalTime(){
      let suma = 0;
      for(let i = 0; i < this.tasks.length; ++i){
        suma += parseInt(this.tasks[i].time);
      }
      return suma;
    }
  },
  created(){
    this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
  }
})

pug

#app
  h1 Organizador de tareas :D
  p Nombre: {{ name }}
  section
    input(type="text" v-model="newTask.title" placeholder="Titulo de la tarea")
    input(type="number" v-model="newTask.time" placeholder="Tiempo de la tarea")
    button(@click="addTask") Agregar Tarea
    button(@click="cancel") Cancelar Tarea
  span(v-if="showMessage") Porfavor, no deje ningun espacio vacio
  h2 Tareas por hacer:
  ul
    li(v-for="(task, index) in tasks")
      div
        span Titulo: {{task.title}} <br>
        span Hora: {{task.time}}
      button(@click="removeTask(index)") Tarea realizada
      
  h2 Horas a trabajar: {{ totalTime }}

Hice el ejercicio en codepen, este es el link: https://codepen.io/hectortllo/pen/eYpYxQb?editors=1010

<template lang="pug">
  #pxMoveDom
    h1 {{ name }}
    section.section
      p Ingresa el nombre de la Tarea
      input.input.is-medium( type="text"
      placeholder="T铆tulo"
      v-model="newTask.title")
      p Ingresa el tiempo para realizarla
      input.input.is-medium( type="number"
      placeholder="Tiempo"
      v-model="newTask.time")
      a.button.is-info.is-large(@click='addTask') Agregar
      a.button.is-danger.is-large(@click='cancel') &times
      div(v-show="tasks.length")
        ul
          li(v-for="(t,i) in tasks") {{ t.title }} - {{ t.time }}
            a.button.is-danger(@click='removeTask(i)') &times
        h1 Tiempo total trabjado: {{ totalTime }}
      div(v-show="!tasks.length")
        p No hay elementos en la lista
</template>
<script>
export default {
  name: 'pxMoveDOM',
  data() {
    return {
      name: 'Lalo Rivero',
      tasks: [],
      newTask: { title: '', time: null }
    }
  },
  methods: {
    addTask() {
      if (this.newTask.title != '' && this.newTask.time != null) {
        let newObj = JSON.parse(JSON.stringify(this.newTask))
        this.tasks.push(newObj)
        localStorage.setItem('tasks', JSON.stringify(this.tasks))
        this.newTask.title = ''
        this.newTask.time = null
      }
      console.log(this.tasks)
    },
    cancel() {
      this.newTask.title = ''
      this.newTask.time = null
    },
    removeTask(index) {
      this.tasks.splice(index, 1)
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    }
  },
  computed: {
    totalTime() {
      let time = null
      this.tasks.map(sum => {
        time += parseInt(sum.time)
      })
      return time
    }
  },
  created() {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  }
}
</script>

Ejercicio realizado:

<template lang="pug">
  #app
    .container
      .control
        input.input(
          type="text"
          placeholder="Titulo",
          v-model="newTask.title"
        )
      .control
        input.input(
          type="text"
          placeholder="Tiempo",
          v-model="newTask.time"
        )
      .control
        button.button.is-info(@click="addTask") Agregar
      .control
        button.button.is-danger(@click="clearNewTask") Cancelar
    .container
      div Nombre: {{ name }}
      div Tiempo total trabajado: {{ totalTime }}
      table.table
        thead
          tr
            th Titulo
            th Tiempo
        tbody
          tr(v-for="(t, index) in tasks")
            td {{ t.title }}
            td {{ t.time }}
            td
              .control
                button.button.is-info(@click="removeTask(index)") Eliminar
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      name: "",
      tasks: [],
      newTask: {
        title: "",
        time: ""
      }
    }
  },
  methods: {
    addTask () {
      console.debug("Agregando tarea...");
      try {
        const titleNewTask = this.newTask.title;
        const timeNewTask = this.newTask.time;
        if (!(titleNewTask && timeNewTask)) {
          throw new Error("Campos titulo y tiempo son obligatorios!");
        }
        this.tasks.push({
          title: titleNewTask,
          time: parseInt(timeNewTask)
        });
        localStorage.setItem("tasks", JSON.stringify(this.tasks));
        this.clearNewTask();
        console.debug("Tarea agregada!");
      } catch (err) {
        console.error(`Error agregando tarea ${JSON.stringify(this.newTask)}`, err.stack);
      }
    },
    clearNewTask (){
      this.newTask = {
        title: "",
        time: ""
      };
    },
    removeTask (taskIndex) {
      this.tasks.splice(taskIndex, 1);
      localStorage.setItem("tasks", JSON.stringify(this.tasks));
    }
  },
  computed: {
    totalTime () {
      let totalTime = 0;

      this.tasks.forEach(t => {
        totalTime += t.time;
      });

      return totalTime;
    }
  },
  created () {
    this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
  }
}
</script>

<style lang="scss">
@import './scss/main.scss'
</style>

Solucion del problema para el a帽o 2020

<template>
  <div>
    <section class="section">
      <nav class="navbar">
        <div class="field has-addons">
          <div class="control"><input class="input" type="text" placeholder="Find songs" v-model="new_task.title" /></div>
          <div class="control"><input class="input" type="number" placeholder="Find songs" v-model="new_task.time" /></div>
          <div class="control"><button class="button is-info" @click="addTask">Find</button></div>
          <div class="control"><button class="button is-danger" @click="cancel">&times;</button></div>
          <div class="control">
            <button class="button">
              <span class="is-size-7">Total hours worked: {{ totalTime }}</span>
            </button>
          </div>
        </div>
      </nav>
      <div class="container custom">
        <ul v-if="tasks.length !== 0">
          <li v-for="(task, index) in tasks" :key="task.id">
            <p>{{ task.title }} - {{ task.time }}</p>
            <div class="control"><button class="button is-danger" @click="deleteTask(index)">&times;</button></div>
          </li>
        </ul>
        <p v-else>La lista esta vacia</p>
      </div>
    </section>
  </div>
</template>

<script>
export default {
  name: "Test2",
  data() {
    return {
      name: "",
      tasks: [
        { title: "Job", time: 8 },
        { title: "Eat", time: 2 }
      ],
      new_task: { title: "", time: null }
    };
  },
  created() {
    this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
  },
  methods: {
    addTask() {
      if (this.new_task.title && this.new_task.time) {
        this.tasks.unshift(this.new_task);
        this.new_task = { title: "", time: null };
        localStorage.setItem("tasks", JSON.stringify(this.tasks));
      }
    },
    deleteTask(index) {
      this.tasks.forEach((element, i) => {
        if (index == i) {
          this.tasks.splice(i, 1);
          localStorage.setItem("tasks", JSON.stringify(this.tasks));
        }
      });
    },
    cancel() {
      this.new_task = { title: "", time: null };
    }
  },
  computed: {
    totalTime() {
      let summation = 0;
      this.tasks.forEach(element => {
        summation += parseInt(element.time, 10);
      });
      return summation;
    }
  }
};
</script>

<style lang="scss">
@import "../scss/main.scss";
.input {
  border: 1px solid black;
}
</style>

Mi version con uso de _lodash

<template lang="pug">
  #app
    h2 {{ name }}

    input(type="text" placeholder="title" v-model="newTask.title" required)
    input(type="number" placeholder="time in hours" v-model="newTask.time" required)
    button(v-on:click="addTask()" class="button is-primary") Generar
    button(v-on:click="cancel()" class="button is-danger") X

    p(v-show="tasks.length === 0 ? true : false") No se han agregado tareas

    ul
      li(v-for="(t, index) in tasks" :key="index")
        p {{ index }} {{ t.title }} - {{ t.time }}
        button.button.is-danger(v-on:click="removeTask(index)") x

    h5(v-show="tasks.length !== 0 ? true : false") Tiempo {{ totalTime }} horas

</template>

<script>
export default {
  name: "App",
  data() {
    return {
      name: "Lista de Tareas",
      tasks: [],
      newTask: {
        title: "",
        time: ""
      }
    };
  },
  created() {
    this.tasks = JSON.parse(localStorage.getItem("tasks")) || [];
  },
  computed: {
    totalTime() {
      let tiempos = this._.map(this.tasks, "time");
      let tiemposNum = this._.map(tiempos, this._.parseInt);
      let totalTime = this._.sum(tiemposNum);
      return totalTime;
    }
  },
  methods: {
    addTask() {
      this.tasks.push({ title: this.newTask.title, time: this.newTask.time });
      localStorage.setItem("tasks", JSON.stringify(this.tasks));
      this.newTask.title = "";
      this.newTask.time = "";
    },
    cancel() {
      this.newTask.title = "";
      this.newTask.time = "";
    },
    removeTask(ind) {
      this.tasks.splice(ind, 1);
      console.log(this.tasks);
    }
  }
};
</script>

<style lang="scss">
@import "@/assets/scss/main.scss";
</style>

<template>
<div id=鈥渁pp鈥 class=鈥渞ow mt-5鈥>
<div class=鈥渃ol-12鈥>
<h1>Gestor de Tareas</h1>
<p class=鈥渉6鈥>Total de horas trabajadas {{totalTime}}</p>
</div>
<div class=鈥渃ol-4鈥>
<input class=鈥渇orm-control input-lg鈥 type="text"
placeholder=鈥淣ueva tarea鈥 v-model=鈥渘ewTask.title鈥>
</div>
<div class=鈥渃ol-4鈥>
<input v-model=鈥渘ewTask.time鈥 class=鈥渇orm-control鈥 type=鈥渘umber鈥 placeholder=鈥0鈥>
</div>
<div class=鈥渃ol-4鈥>
<a class=鈥渂tn btn-info btn-lg mr-2鈥 @click=鈥渁ddTask鈥> Agregar </a>
<a class=鈥渂tn btn-danger btn-lg鈥 @click=鈥渃ancel鈥 > Cancelar </a>
</div>

    <table v-if="tasks.length > 0" class="table">
      <thead>
      <tr>
        <th scope="col">Tarea</th>
        <th scope="col">Horas</th>
        <th scope="col">Eliminar</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="(task, index) in tasks" :key="index" >
        <td>{{task.title}}</td>
        <td>{{task.time}}</td>
        <td><a class="btn btn-danger" v-on:click="removetask(index)">Eliminar</a></td>
      </tr>
      </tbody>
    </table>
    <h1 class="h1" v-else>No hay tareas</h1>
</div>

</template>

<script>
export default {
name: 鈥楢pp鈥,
data () {
return {
tasks: [],
newTask: {
title: 鈥樷,
time: 0
}
}
},
computed: {

totalTime () {
  let total = 0
  this.tasks.forEach(elem => {
    total += parseInt(elem.time)
  })
  return total
}

},
created () {
this.tasks = JSON.parse(localStorage.getItem(鈥榯asks鈥)) || []
},
methods: {
addTask () {
if (this.newTask.title !== 鈥樷 && this.newTask.time > 0) {
this.tasks.push({ title: this.newTask.title, time: this.newTask.time })
localStorage.setItem(鈥榯asks鈥, JSON.stringify(this.tasks))
this.cancel ()
} else {
console.log(鈥榲alor no aceptado鈥)
}
},
cancel () {
this.newTask.title = ''
this.newTask.time = 0
},
removetask (index) {
this.tasks.splice(index, 1)
localStorage.setItem(鈥榯asks鈥, JSON.stringify(this.tasks))
}

}

}
</script>

<style lang=鈥渟css鈥>
@import 鈥./scss/main.scss鈥;
</style>

Lo hice con bootstrap

<template>
    <div id="app" class="row mt-6 mx-6">

        <div class="container col-6">
          <div class="input-group input-group-lg">
            <input class="form-control input-lg" type="text"
                   placeholder="Nueva tarea"  v-model="newtask.title">
          </div>
          <p class="h6">Total de horas {{totalhoras}}</p>
        </div>

          <select  class="col-2 form-select form-select-lg mb-3" aria-label=".form-select-lg example" v-model="newtask.time">
            <option value = "0" selected>Horas</option>
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
            <option value="4">4</option>
            <option value="5">5</option>
            <option value="6">6</option>
            <option value="7">7</option>
            <option value="8">8</option>
            <option value="9">9</option>
            <option value="10">10</option>
            <option value="11">11</option>
            <option value="12">12</option>
          </select>

        <div class="col-4">
          <a class="btn btn-info btn-lg mr-2" v-on:click="agregar"> Agregar </a>
          <a class="btn btn-danger btn-lg" v-on:click="borrar" > Borrar lista </a>
        </div>

        <div class="coninater col-12 mt-3">
          <ul>
<!--            <li v-for="t in tracks" v-bind:key="t">{{t.name}} - {{ t.artist}}</li>-->
          </ul>
        </div>

        <table v-if="tasks.length > 0" class="table">
          <thead>
          <tr>
            <th scope="col">Tarea</th>
            <th scope="col">Horas</th>
            <th scope="col">Eliminar</th>
          </tr>
          </thead>
          <tbody>
          <tr v-for="task in tasks" v-bind:key="task.title">
            <td>{{task.title}}</td>
            <td>{{task.time}}</td>
            <td><a class="btn btn-danger" v-on:click="removetask(task)">Eliminar</a></td>
          </tr>

          </tbody>
        </table>
        <h1 class="h1" v-else>No hay tareas</h1>

    </div>

</template>

<script>
// const reducer = (accumulator, currentValue) => accumulator + currentValue

export default {
  name: 'App',
  data () {
    return {
      name: '',
      time: '',
      tasks: [],
      delete: 0,

      newtask: {
        title: '',
        time: 0
      }

    }
  },
  computed: {

    totalhoras () {
      // this.newtask = this.tasks.time.slice()
      let horas = 0
      for (const task of this.tasks) {
        horas += parseInt(task.time)
      }
      return horas
    }
  },
  created () {
    this.tasks = JSON.parse(localStorage.getItem('tasks')) || []
  },
  methods: {

    agregar () {
      if (!(this.tasks.find(tarea => tarea.title === this.newtask.title)) && this.newtask.time > 0) {
        this.tasks.push({ title: `${this.newtask.title}`, time: `${this.newtask.time}` })
        localStorage.setItem('tasks', JSON.stringify(this.tasks))
        this.newtask = []
      } else {
        console.log('valor no aceptado')
      }
    },
    borrar () {
      this.tasks = []
    },
    removetask (titulo) {
      this.delete = this.tasks.indexOf(titulo)
      // console.log(this.tasks.indexOf(titulo))
      this.tasks.splice(this.delete, 1)
      localStorage.setItem('tasks', JSON.stringify(this.tasks))
    }

  }

}
</script>

<style lang="scss">
@import "./scss/main.scss";
</style>

Hola a todos!!

Creo que el dise帽o no esta chevere pero me gusto construir la funcionalidad鈥
Agregue un elemento de validaci贸n al input que se muestra si se intenta guardar con errores

馃槈

Codepen- Manipulaci贸n del DOM