Learning Quasar V1: Cross-Platform Apps (with Vue 2, Vuex & Firebase) Notes

https://www.udemy.com/course/quasarframework/

In my opinion, the best thing I learned in here was how to manage state with vuex. Finally understood how this state management works.

Intro Vue - check here if needed to refresh your vue knowledge

Routes and Layouts

Example

router/routes.js
const routes = [
  {
    path: '/',
    component: () => import('layouts/Layout.vue'),
    children: [
      { 
        path: '', 
        component: () => import('pages/PageTodo.vue') 
      },
      { 
        path: '/settings', 
        component: () => import('pages/PageSettings.vue') 
      }
    ]
  },

  // Always leave this as last one,
  // but you can also remove it
  {
    path: '*',
    component: () => import('pages/Error404.vue')
  }
]

export default routes
layouts/Layout.vue
<template>
  <q-layout view="hHh lpR fFf">
    <q-header elevated>
      <q-toolbar>
        <q-toolbar-title class="absolute-center">
          Awesome App
        </q-toolbar-title>
      </q-toolbar>
    </q-header>

    <q-footer>
      <q-tabs>
        <q-route-tab 
        v-for="(nav, index) in navs"
          :to="nav.to"
          :icon="nav.icon" 
          :label="nav.label"
          :key="index" />
      </q-tabs>
    </q-footer>

    <q-drawer
      v-model="leftDrawerOpen"
      :width="250"
      show-if-above
      bordered
      :breakpoint="767"
      content-class="bg-primary"
    >
      <q-list dark>
        <q-item-label
          header
          class="text-white"
        >
          Navigation
        </q-item-label>
        
        <q-item
          v-for="(nav, index) in navs"
          :to="nav.to"
          exact
          clickable
          :key=index
          class="text-white">
          <q-item-section avatar>
            <q-icon :name="nav.icon" />
          </q-item-section>
          <q-item-section>
            {{nav.label}}
          </q-item-section>
        </q-item>
        
      </q-list>
    </q-drawer>

    <q-page-container>
      <router-view />
    </q-page-container>
  </q-layout>
</template>

<script>
export default {
  name: 'MainLayout',
  data () {
    return {
      leftDrawerOpen: false,
      navs: [
        {
          label: 'Todo',
          icon: 'list',
          to: '/'
        },
        {
          label: 'Settings',
          icon: 'settings',
          to: '/settings'
        }
      ]
    }
  }
}
</script>

<style scoped lang="scss">
  @media screen and (min-width:768px) {
    .q-footer {
      display:none;
    }
  }
  .q-drawer {
    .q-router-link--exact-active {
      font-weight:bold;
    }
  }
</style>

Vuex Set state

pages/PageTodo.vue

<template>
  <q-page class="q-pa-md">
    <div class="q-pa-md" style="max-width: 350px">
      <q-list 
        separator
        bordered >
        <task v-for="(task, key) in tasks"
          :key="key"
          :task="task"
          :id="key"
          ></task>
      </q-list>
    </div>
  </q-page>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters('tasks', ['tasks'])
  },
  components: {
    'task' : require('components/Tasks/Task.vue').default
  }
}
</script>

<style scoped>
</style>

Create components/Tasks/Task.vue

<template>
    
        <q-item 
          @click="task.completed = !task.completed"
          :class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
          clickable
          v-ripple>
          <q-item-section side top>
            <q-checkbox v-model="task.completed" />
          </q-item-section>
          <q-item-section>
            <q-item
              :class="{ 'text-strike' : task.completed }"
              >{{ task.name }}</q-item>
          </q-item-section>
          <q-item-section side >
            <div class="row">
              <div class="column justify-center">
                <q-icon 
                size="18px"
                name="event"
                class="q-mr-xs" />
              </div>
              <div class="column">
                <q-item-label 
                  class="row justify-end"
                  caption>{{ task.dueDate }}</q-item-label>
                <q-item-label caption>
                  <small>{{ task.dueTime }}</small>
                </q-item-label>
              </div>
              
            </div>
          </q-item-section>
        </q-item>
</template>

<script>
export default {
    props: ['task', 'id']
}
</script>

Create store/store-tasks.js

store/store-tasks.js

const state = {
  tasks: {
    'ID1' : {
      name:'Go to shop',
      completed: false,
      dueDate: '2019/05/12',
      dueTime: '18:30'
    },
    'ID2' : {
      name:'Get bananas',
      completed: true,
      dueDate: '2019/05/13',
      dueTime: '5:30'
      
    },
    'ID3' : {
      name:'Get apples',
      completed: false,
      dueDate: '2019/05/14',
      dueTime: '3:30'
    }
  }
}

const mutations = {

}store/store-tasks.js

const state = {
  tasks: {
    'ID1' : {
      name:'Go to shop',
      completed: false,
      dueDate: '2019/05/12',
      dueTime: '18:30'
    },
    'ID2' : {
      name:'Get bananas',
      completed: true,
      dueDate: '2019/05/13',
      dueTime: '5:30'
      
    },
    'ID3' : {
      name:'Get apples',
      completed: false,
      dueDate: '2019/05/14',
      dueTime: '3:30'
    }
  }
}

const mutations = {

}

const actions = {

}

const getters = {
  tasks: (state) => {
    return state.tasks
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

const actions = {

}

const getters = {
  tasks: (state) => {
    return state.tasks
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

in store/index.js add these

import tasks from './store-tasks'

modules: {
      tasks
},

Vuex Action and Mutation

Dialog

// Quasar plugins
      plugins: [
        'Notify',
        'Dialog'
      ]
promptToDelete(id) {
    this.$q.dialog({
        title: 'Confirm',
        message: 'Really delete?',
        ok: {
            push: true
        },
        cancel: {
            color: 'negative'
        },
        persistent: true
    }).onOk(() => {
        this.deleteTask(id)
    })
}

Mostly in Delete and Update

store/store-tasks.js

import Vue from 'vue'
const state = {
  tasks: {
    'ID1' : {
      name:'Go to shop',
      completed: false,
      dueDate: '2019/05/12',
      dueTime: '18:30'
    },
    'ID2' : {
      name:'Get bananas',
      completed: false,
      dueDate: '2019/05/13',
      dueTime: '5:30'
      
    },
    'ID3' : {
      name:'Get apples',
      completed: false,
      dueDate: '2019/05/14',
      dueTime: '3:30'
    }
  }
}

const mutations = {
  updateTask(state, payload) {
    Object.assign(state.tasks[payload.id], payload.updates)
  },
  deleteTask(state,id) {
    Vue.delete(state.tasks, id)
  }
}

const actions = {
  updateTask({ commit }, payload) {
    commit('updateTask', payload)
  },
  deleteTask({ commit }, id) {
    commit('deleteTask', id)
    delete state.tasks['id']
  }
}

const getters = {
  tasks: (state) => {
    return state.tasks
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}
components/Tasks/Task.vue

<template>
    
        <q-item 
          @click="updateTask({id: id, updates: {completed: !task.completed}})"
          :class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
          clickable
          v-ripple>
          <q-item-section side top>
            <q-checkbox 
                :value="task.completed"
                class="no-pointer-events" />
          </q-item-section>
          <q-item-section>
            <q-item
              :class="{ 'text-strike' : task.completed }"
              >{{ task.name }}</q-item>
          </q-item-section>
          <q-item-section side >
            <div class="row">
              <div class="column justify-center">
                <q-icon 
                size="18px"
                name="event"
                class="q-mr-xs" />
              </div>
              <div class="column">
                <q-item-label 
                  class="row justify-end"
                  caption>{{ task.dueDate }}</q-item-label>
                <q-item-label caption>
                  <small>{{ task.dueTime }}</small>
                </q-item-label>
              </div>
              
            </div>
          </q-item-section>

          <q-item-section side >
              <q-btn 
                @click.stop="promptToDelete(id)"
                flat 
                round 
                dense
                color="red"
                icon="delete" />
          </q-item-section>
        </q-item>
</template>

<script>
import { mapActions } from 'vuex'
export default {
    props: ['task', 'id'],
    methods: {
        ...mapActions('tasks', ['updateTask', 'deleteTask']),
        promptToDelete(id) {
            this.$q.dialog({
                title: 'Confirm',
                message: 'Really delete?',
                ok: {
                    push: true
                },
                cancel: {
                    color: 'negative'
                },
                persistent: true
            }).onOk(() => {
                this.deleteTask(id)
            })
        }
    }
}
</script>

Form, Fields, validation

components/Tasks/Modals/AddTask

<template>
    <q-card>
      <q-card-section class="row">
          <div class="text-h6">Add Task</div>
          <q-space />
          <q-btn 
            v-close-popup
            dense 
            flat 
            round 
            icon="close" />
      </q-card-section>
      <form @submit.prevent="submitForm">
        <q-card-section>
          <div class="row q-mb-sm">

            <q-input outlined 
            v-model="taskToSubmit.name" label="Task Name" 
            :rules="[val => !!val || 'Field is required']"
            ref="name"
            autofocus
            class="col">
              <template v-slot:append>
                <q-icon
                  v-if="taskToSubmit.name"
                  @click="taskToSubmit.name = ''"
                  name="close"
                  class="cursor-pointer" />
              </template>
            </q-input>
          </div>

          <div class="row q-mb-sm">
            <q-input
              outlined
              v-model="taskToSubmit.dueDate"
              label="Due Date">
              <template v-slot:append>
                <q-icon
                  v-if="taskToSubmit.dueDate"
                  @click="clearDueDate"
                  name="close"
                  class="cursor-pointer" />
                <q-icon name="event" class="cursor-pointer">
                  <q-popup-proxy>
                    <q-date v-model="taskToSubmit.dueDate" />
                  </q-popup-proxy>
                </q-icon>
              </template>
            </q-input>
          </div>
          

          <div
            v-if="taskToSubmit.dueDate"
            class="row q-mb-sm">
            <q-input
              outlined
              label="Due time"
              v-model="taskToSubmit.dueTime"
              class="col">
              <template  v-slot:append>
                <q-icon
                  v-if="taskToSubmit.dueTime"
                  @click="taskToSubmit.dueTime = ''"
                  name="close"
                  class="cursor-pointer" />
                <q-icon name="access_time" class="cursor-pointer">
                  <q-popup-proxy>
                    <q-time v-model="taskToSubmit.dueTime" />
                  </q-popup-proxy>
                </q-icon>
              </template>
            </q-input>
          </div>
        </q-card-section>
        <q-card-actions align="right">
          <q-btn
            type="submit"
            label="Save"
            color="primary" />
        </q-card-actions>
      </form>
    </q-card>
</template>

<script>
import { mapActions } from 'vuex'
export default {
  data() {
    return {
      taskToSubmit: {
        name:'',
        completed: false,
        dueDate: '',
        dueTime: ''
      }
    }
  },
  methods: {
    ...mapActions('tasks', ['addTask']),
    submitForm() {
      this.$refs.name.validate()
      if (!this.$refs.name.hasError) {
        this.submitTask()
      }
    },
    submitTask() {
      this.addTask(this.taskToSubmit)
      this.$emit('close')
    },
    clearDueDate(){
      this.taskToSubmit.dueDate = ''
      this.taskToSubmit.dueTime = ''
    }
  }
}
</script>

adding button and modal add task

pages/PageTodo

<template>
  <q-page class="q-pa-md">
    <div class="q-pa-md">
      <q-list 
        v-if=Object.keys(tasks).length
        separator
        bordered >

        <task v-for="(task, key) in tasks"
          :key="key"
          :task="task"
          :id="key"
          ></task>

      </q-list>
      <div class="absolute-bottom text-center q-mb-lg">
        <q-btn
          @click="showAddTask=true"
          round
          color="primary"
          size="24px"
          icon="add" />
      </div>

      <q-dialog
        v-model="showAddTask">
        <add-task @close="showAddTask = false" />
      </q-dialog>
    </div>
  </q-page>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  data() {
    return {
      showAddTask: false
    }
  },
  computed: {
    ...mapGetters('tasks', ['tasks'])
  },
  components: {
    'task' : require('components/Tasks/Task.vue').default,
    'add-task' : require('components/Tasks/Modals/AddTask.vue').default
  }
}
</script>

<style scoped>
</style>
components/AddTask.vue

<template>
    <q-card>
      <q-card-section class="row">
          <div class="text-h6">Add Task</div>
          <q-space />
          <q-btn 
            v-close-popup
            dense 
            flat 
            round 
            icon="close" />
      </q-card-section>
      <form @submit.prevent="submitForm">
        <q-card-section>
          <div class="row q-mb-sm">

            <q-input outlined 
            v-model="taskToSubmit.name" label="Task Name" 
            :rules="[val => !!val || 'Field is required']"
            ref="name"
            autofocus
            class="col">
              <template v-slot:append>
                <q-icon
                  v-if="taskToSubmit.name"
                  @click="taskToSubmit.name = ''"
                  name="close"
                  class="cursor-pointer" />
              </template>
            </q-input>
          </div>

          <div class="row q-mb-sm">
            <q-input
              outlined
              v-model="taskToSubmit.dueDate"
              label="Due Date">
              <template v-slot:append>
                <q-icon
                  v-if="taskToSubmit.dueDate"
                  @click="clearDueDate"
                  name="close"
                  class="cursor-pointer" />
                <q-icon name="event" class="cursor-pointer">
                  <q-popup-proxy>
                    <q-date v-model="taskToSubmit.dueDate" />
                  </q-popup-proxy>
                </q-icon>
              </template>
            </q-input>
          </div>
          

          <div
            v-if="taskToSubmit.dueDate"
            class="row q-mb-sm">
            <q-input
              outlined
              label="Due time"
              v-model="taskToSubmit.dueTime"
              class="col">
              <template  v-slot:append>
                <q-icon
                  v-if="taskToSubmit.dueTime"
                  @click="taskToSubmit.dueTime = ''"
                  name="close"
                  class="cursor-pointer" />
                <q-icon name="access_time" class="cursor-pointer">
                  <q-popup-proxy>
                    <q-time v-model="taskToSubmit.dueTime" />
                  </q-popup-proxy>
                </q-icon>
              </template>
            </q-input>
          </div>
        </q-card-section>
        <q-card-actions align="right">
          <q-btn
            type="submit"
            label="Save"
            color="primary" />
        </q-card-actions>
      </form>
    </q-card>
</template>

<script>
import { mapActions } from 'vuex'
export default {
  data() {
    return {
      taskToSubmit: {
        name:'',
        completed: false,
        dueDate: '',
        dueTime: ''
      }
    }
  },
  methods: {
    ...mapActions('tasks', ['addTask']),
    submitForm() {
      this.$refs.name.validate()
      if (!this.$refs.name.hasError) {
        this.submitTask()
      }
    },
    submitTask() {
      this.addTask(this.taskToSubmit)
      this.$emit('close')
    },
    clearDueDate(){
      this.taskToSubmit.dueDate = ''
      this.taskToSubmit.dueTime = ''
    }
  }
}
</script>
store/store-tasks.vue

import Vue from 'vue'
import { uid } from 'quasar'
const state = {
  tasks: {
    // 'ID1' : {
    //   name:'Go to shop',
    //   completed: false,
    //   dueDate: '2019/05/12',
    //   dueTime: '18:30'
    // },
    // 'ID2' : {
    //   name:'Get bananas',
    //   completed: false,
    //   dueDate: '2019/05/13',
    //   dueTime: '5:30'
      
    // },
    // 'ID3' : {
    //   name:'Get apples',
    //   completed: false,
    //   dueDate: '2019/05/14',
    //   dueTime: '3:30'
    // }
  }
}

const mutations = {
  updateTask(state, payload) {
    Object.assign(state.tasks[payload.id], payload.updates)
  },
  deleteTask(state,id) {
    Vue.delete(state.tasks, id)
  },
  addTask(state, payload) {
    Vue.set(state.tasks, payload.id, payload.task)
  }
}

const actions = {
  updateTask({ commit }, payload) {
    commit('updateTask', payload)
  },
  deleteTask({ commit }, id) {
    commit('deleteTask', id)
    delete state.tasks['id']
  },
  addTask({ commit }, task) {
    let taskId = uid()
    let payload = {
      id: taskId,
      task: task
    }
    commit('addTask', payload)
  }
}

const getters = {
  tasks: (state) => {
    return state.tasks
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

Child Component in-depth

components/Tasks/Modal/AddTask.vue
<template>
    <q-card>
      <modal-header>Add Task</modal-header>
      <form @submit.prevent="submitForm">
        <q-card-section>
          <modal-task-name :name.sync="taskToSubmit.name"
          ref="modalTaskName" />
          <modal-due-date 
            :dueDate.sync="taskToSubmit.dueDate"
            @clear="clearDueDate" />
          <modal-due-time
            v-if="taskToSubmit.dueDate"
            :dueTime.sync="taskToSubmit.dueTime" />
        </q-card-section>
        <modal-buttons></modal-buttons>
      </form>
    </q-card>
</template>

<script>
import { mapActions } from 'vuex'
export default {
  data() {
    return {
      taskToSubmit: {
        name:'',
        completed: false,
        dueDate: '',
        dueTime: ''
      }
    }
  },
  methods: {
    ...mapActions('tasks', ['addTask']),
    submitForm() {
      this.$refs.modalTaskName.$refs.name.validate()
      if (!this.$refs.modalTaskName.$refs.name.hasError) {
        this.submitTask()
      }
    },
    submitTask() {
      this.addTask(this.taskToSubmit)
      this.$emit('close')
    },
    clearDueDate(){
      this.taskToSubmit.dueDate = ''
      this.taskToSubmit.dueTime = ''
    }
  },
  components: {
    'modal-header': require('components/Tasks/Modals/Shared/ModalHeader.vue').default,
    'modal-task-name': require('components/Tasks/Modals/Shared/ModalTaskName.vue').default,
    'modal-due-date': require('components/Tasks/Modals/Shared/ModalDueDate.vue').default,
    'modal-due-time': require('components/Tasks/Modals/Shared/ModalDueTime.vue').default,
    'modal-buttons': require('components/Tasks/Modals/Shared/ModalButtons.vue').default,
  }
}
</script>

<modal-header>Add Task</modal-header>

components/Tasks/Modals/Shared/ModalHeader.vue
<template>
    <q-card-section class="row">
        <div class="text-h6"><slot></slot></div>
        <q-space />
        <q-btn 
        v-close-popup
        dense 
        flat 
        round 
        icon="close" />
    </q-card-section>
</template>

<script>
export default {
    
}
</script>

<modal-task-name :name.sync="taskToSubmit.name" ref="modalTaskName" />

components/Tasks/Modals/Shared/ModalTaskName.vue
<template>
    <div class="row q-mb-sm">
        <q-input outlined 
        :value="name"
        @input="$emit('update:name', $event)"
        :rules="[val => !!val || 'Field is required']"
        ref="name"
        autofocus
        class="col">
            <template v-slot:append>
            <q-icon
                v-if="name"
                @click="$emit('update:name', '')"
                name="close"
                class="cursor-pointer" />
            </template>
        </q-input>
    </div>
</template>

<script>
export default {
    props: ['name']
}
</script>

<modal-due-date :dueDate.sync="taskToSubmit.dueDate" @clear="clearDueDate" />

components/Tasks/Modals/Shared/ModalDueDate.vue
<template>
    <div class="row q-mb-sm">
    <q-input
        outlined
        :value="dueDate"
        @input="$emit('update:dueDate', $event)"
        label="Due Date">
        <template v-slot:append>
        <q-icon
            v-if="dueDate"
            @click="$emit('clear')"
            name="close"
            class="cursor-pointer" />
        <q-icon name="event" class="cursor-pointer">
            <q-popup-proxy>
            <q-date :value="dueDate"  @input="$emit('update:dueDate', $event)"/>
            </q-popup-proxy>
        </q-icon>
        </template>
    </q-input>
    </div>
</template>

<script>
export default {
    props: ['dueDate']
}
</script>

<modal-due-time v-if="taskToSubmit.dueDate" :dueTime.sync="taskToSubmit.dueTime" />

components/Tasks/Modals/Shared/ModalDueTime.vue
<template>
    <div
        class="row q-mb-sm">
        <q-input
            outlined
            label="Due time"
            :value="dueTime"
            @input="$emit('update:dueTime', $event)"
            class="col">
            <template  v-slot:append>
            <q-icon
                v-if="dueTime"
                @click="$emit('update:dueTime', '')"
                name="close"
                class="cursor-pointer" />
            <q-icon name="access_time" class="cursor-pointer">
                <q-popup-proxy>
                <q-time
                  :value="dueTime"
                  @input="$emit('update:dueTime', $event)" />
                </q-popup-proxy>
            </q-icon>
            </template>
        </q-input>
        </div>
</template>

<script>
export default {
    props: ['dueTime']
}
</script>

<modal-buttons></modal-buttons>

components/Tasks/Modals/Shared/ModalDueTime.vue
<template>
    <q-card-actions align="right">
        <q-btn
        type="submit"
        label="Save"
        color="primary" />
    </q-card-actions>
</template>

Split tasks, event global listener

store/store-tasks.js

const getters = {
  tasksTodo: (state) => {
    let tasks = {}
    Object.keys(state.tasks).forEach(function(key) {
      let task = state.tasks[key]
      if (!task.completed) {
        tasks[key] = task
      }
    })
    return tasks
  },
  tasksCompleted: (state) => {
    let tasks = {}
    Object.keys(state.tasks).forEach(function(key) {
      let task = state.tasks[key]
      if (task.completed) {
        tasks[key] = task
      }
    })
    return tasks
  }
}

on mounted this.$root.$on('showAddTask', () => { this.showAddTask = true }) this listen from the event from NoTasks.vue ← global event listener

<template>
  <q-page class="q-pa-md">
    <div class="q-pa-md">
      <no-tasks
        v-if=!Object.keys(tasksTodo).length
        class="q-mb-lg"></no-tasks>
      <tasks-todo 
        v-else
        :tasksTodo="tasksTodo"
        />
      <tasks-completed 
        v-if=Object.keys(tasksCompleted).length
        :tasksCompleted="tasksCompleted"
        />
      
      <div class="absolute-bottom text-center q-mb-lg">
        <q-btn
          @click="showAddTask=true"
          round
          color="primary"
          size="24px"
          icon="add" />
      </div>

      <q-dialog
        v-model="showAddTask">
        <add-task @close="showAddTask = false" />
      </q-dialog>
    </div>
  </q-page>
</template>

<script>
import { mapGetters } from 'vuex'
import TasksTodo from '../components/Tasks/TasksTodo.vue'

export default {
  data() {
    return {
      showAddTask: false
    }
  },
  computed: {
    ...mapGetters('tasks', ['tasksTodo', 'tasksCompleted'])
  },
  mounted() {
    this.$root.$on('showAddTask', () => {
      this.showAddTask = true
    })
  },
  components: {
    'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
    'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
    'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
    'no-tasks' : require('components/Tasks/NoTasks.vue').default
  }
}
</script>

<style scoped>
</style>
components/Tasks/TasksTodo.vue

<template>
    <div>
    <list-header 
        bgcolor="bg-orange-4"
        >Todo</list-header>
    <q-list 
        separator
        bordered >

        <task v-for="(task, key) in tasksTodo"
          :key="key"
          :task="task"
          :id="key"
          ></task>
      </q-list>
    </div>
</template>

<script>
export default {
    props: ['tasksTodo'],
    components: {
        'task' : require('components/Tasks/Task.vue').default,
        'list-header' : require('components/Shared/ListHeader.vue').default,
    }
}
</script>
components/Tasks/TasksCompleted.vue

<template>
    <div class="q-mt-lg">
        <list-header bgcolor="bg-green-4">Completed</list-header>
        <q-list 
            separator
            bordered >

            <task v-for="(task, key) in tasksCompleted"
            :key="key"
            :task="task"
            :id="key"
            ></task>
        </q-list>
    </div>
</template>

<script>
export default {
    props: ['tasksCompleted'],
    components: {
        'task' : require('components/Tasks/Task.vue').default,
        'list-header' : require('components/Shared/ListHeader.vue').default,
    }
}
</script>

@click="$root.$emit('showAddTask')" this will fire event to PageTodo.vue

components/Tasks/NoTasks.vue

<template>
    <q-banner class="bg-grey-3">
        <template v-slot:avatar>
            <q-icon name="check" color="primary" />
        </template>
        No tasks to do today!
        <template v-slot:action>
            <q-btn
              @click="$root.$emit('showAddTask')"
              flat
              color="primary"
              label="Add Task" />
        </template>
    </q-banner>
</template>

<script>
export default {
    
}
</script>

had trouble in here, because the the parent page uses just bgcolor where before it always uses :bgcolor, i think the difference is in, if the value is static just normal string then in the parent use bgcolor instead of :bgcolor where you need to have value in data

<template>
    <q-banner
      dense
      inline-actions
      :class="bgcolor"
      class="list-header text-white text-center">
        <span class="text-bold text-subtitle1"><slot></slot></span>
    </q-banner>
</template>

<script>
export default {
  props: ['bgcolor']
}
</script>

<style>
  .list-header {
    border-top-left-radius: 5px;
    border-top-right-radius: 5px;
  }
</style>

Note : Why are using mapState instead of creating a Getter?  Because we don't need to manipulate this property before we get it (as we did with task). So we might as well map directly from the state.

components/Tasks/Tools/Search.vue
<template>
    <q-input
      outlined
      class="col"
      v-model="searchField"
      label="Search">
        <template v-slot:append>
            <q-icon v-if="searchField !== ''" name="close" @click="searchField = ''" class="cursor-pointer" />
            <q-icon name="search" />
        </template>
    </q-input>
</template>

<script>
import { mapState, mapActions } from 'vuex'
export default {
    computed: {
        ...mapState('tasks', ['search']),
        searchField: {
            get() {
                return this.search
            },
            set(value) {
                this.setSearch(value)
            }
        }
    },
    methods: {
        ...mapActions('tasks', ['setSearch'])
    }
}
</script>

Updated PageTodo.vue

<template>
  <q-page class="q-pa-md">
    
    <div class="row q-mb-lg">
      <search />
    </div>

    <p v-if="search && !Object.keys(tasksTodo).length && !Object.keys(tasksCompleted).length">No Search results.</p>

    <div>
      <no-tasks
        v-if="!Object.keys(tasksTodo).length && !search"
        class="q-mb-lg"></no-tasks>
      <tasks-todo 
        v-if="Object.keys(tasksTodo).length"
        :tasksTodo="tasksTodo"
        />
      <tasks-completed 
        v-if=Object.keys(tasksCompleted).length
        :tasksCompleted="tasksCompleted"
        />

      <div class="absolute-bottom text-center q-mb-lg">
        <q-btn
          @click="showAddTask=true"
          round
          color="primary"
          size="24px"
          icon="add" />
      </div>

      <q-dialog
        v-model="showAddTask">
        <add-task @close="showAddTask = false" />
      </q-dialog>
    </div>
  </q-page>
</template>

<script>
import { mapGetters, mapState } from 'vuex'

export default {
  data() {
    return {
      showAddTask: false
    }
  },
  computed: {
    ...mapGetters('tasks', ['tasksTodo', 'tasksCompleted']),
    ...mapState('tasks', ['search'])
  },
  mounted() {
    this.$root.$on('showAddTask', () => {
      this.showAddTask = true
    })
  },
  components: {
    'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
    'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
    'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
    'no-tasks' : require('components/Tasks/NoTasks.vue').default,
    'search' : require('components/Tasks/Tools/Search.vue').default
  }
}
</script>

<style scoped>
</style>

Updated store-tasks.vue

import Vue from 'vue'
import { uid } from 'quasar'
const state = {
  tasks: {
    'ID1' : {
      name:'Go to shop',
      completed: false,
      dueDate: '2019/05/12',
      dueTime: '18:30'
    },
    'ID2' : {
      name:'Get bananas',
      completed: false,
      dueDate: '2019/05/13',
      dueTime: '5:30'
      
    },
    'ID3' : {
      name:'Get apples',
      completed: false,
      dueDate: '2019/05/14',
      dueTime: '3:30'
    }
  },
  search: ''
}

const mutations = {
  updateTask(state, payload) {
    Object.assign(state.tasks[payload.id], payload.updates)
  },
  deleteTask(state,id) {
    Vue.delete(state.tasks, id)
  },
  addTask(state, payload) {
    Vue.set(state.tasks, payload.id, payload.task)
  },
  setSearch(state, value) {
    state.search = value
  }
}

const actions = {
  updateTask({ commit }, payload) {
    commit('updateTask', payload)
  },
  deleteTask({ commit }, id) {
    commit('deleteTask', id)
    delete state.tasks['id']
  },
  addTask({ commit }, task) {
    let taskId = uid()
    let payload = {
      id: taskId,
      task: task
    }
    commit('addTask', payload)
  },
  setSearch({ commit}, value) {
    commit('setSearch', value)
  }
}

const getters = {
  tasksFiltered: (state) => {
    let tasksFiltered = {}
    if (state.search) {
      Object.keys(state.tasks).forEach(function(key) {
        let task = state.tasks[key],
            taskNameLowerCase = task.name.toLowerCase(),
            searchLowerCase = state.search.toLowerCase()
        if (taskNameLowerCase.includes(searchLowerCase)) {
          tasksFiltered[key] = task
        }
      })
      return tasksFiltered
    }
    return state.tasks
  },
  tasksTodo: (state, getters) => {
    let tasksFiltered = getters.tasksFiltered
    let tasks = {}
    Object.keys(tasksFiltered).forEach(function(key) {
      let task = tasksFiltered[key]
      if (!task.completed) {
        tasks[key] = task
      }
    })
    return tasks
  },
  tasksCompleted: (state, getters) => {
    let tasksFiltered = getters.tasksFiltered
    let tasks = {}
    Object.keys(tasksFiltered).forEach(function(key) {
      let task = tasksFiltered[key]
      if (task.completed) {
        tasks[key] = task
      }
    })
    return tasks
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

Add a sort Dropdown

store/store-tasks.js

import Vue from 'vue'
import { uid } from 'quasar'
const state = {
  tasks: {
    'ID1' : {
      name:'Go to shop',
      completed: false,
      dueDate: '2019/05/12',
      dueTime: '18:30'
    },
    'ID2' : {
      name:'Get bananas',
      completed: false,
      dueDate: '2019/05/13',
      dueTime: '5:30'
      
    },
    'ID3' : {
      name:'Get apples',
      completed: false,
      dueDate: '2019/05/14',
      dueTime: '3:30'
    }
  },
  search: '',
	sort: 'dueDate',
}

const mutations = {
  updateTask(state, payload) {
    Object.assign(state.tasks[payload.id], payload.updates)
  },
  deleteTask(state,id) {
    Vue.delete(state.tasks, id)
  },
  addTask(state, payload) {
    Vue.set(state.tasks, payload.id, payload.task)
  },
  setSearch(state, value) {
    state.search = value
  },
  setSort(state, value) {
    state.sort = value
  }
}

const actions = {
  updateTask({ commit }, payload) {
    commit('updateTask', payload)
  },
  deleteTask({ commit }, id) {
    commit('deleteTask', id)
    delete state.tasks['id']
  },
  addTask({ commit }, task) {
    let taskId = uid()
    let payload = {
      id: taskId,
      task: task
    }
    commit('addTask', payload)
  },
  setSearch({ commit}, value) {
    commit('setSearch', value)
  },
  setSort({ commit}, value) {
    commit('setSort', value)
  }
}

const getters = {
  tasksSorted: (state) => {
    let tasksSorted = {},
      keysOrdered = Object.keys(state.tasks)
    
      keysOrdered.sort((a,b) => {
				sort: 'dueDate',let taskAProp = state.tasks[a][state.sort].toLowerCase(),
          taskBProp = state.tasks[b][state.sort].toLowerCase()

        if (taskAProp > taskBProp) return 1
        else if (taskAProp < taskBProp)
        return -1
        else return 0

      })

    keysOrdered.forEach((key) => {
      tasksSorted[key] = state.tasks[key]
    })

    return tasksSorted
  },
  tasksFiltered: (state, getters) => {
    let tasksSorted = getters.tasksSorted,
    tasksFiltered = {}
    if (state.search) {
      Object.keys(tasksSorted).forEach(function(key) {
        let task = tasksSorted[key],
            taskNameLowerCase = task.name.toLowerCase(),
            searchLowerCase = state.search.toLowerCase()
        if (taskNameLowerCase.includes(searchLowerCase)) {
          tasksFiltered[key] = task
        }
      })
      return tasksFiltered
    }
    return tasksSorted
  },
  tasksTodo: (state, getters) => {
    let tasksFiltered = getters.tasksFiltered
    let tasks = {}
    Object.keys(tasksFiltered).forEach(function(key) {
      let task = tasksFiltered[key]
      if (!task.completed) {
        tasks[key] = task
      }
    })
    return tasks
  },
  tasksCompleted: (state, getters) => {
    let tasksFiltered = getters.tasksFiltered
    let tasks = {}
    Object.keys(tasksFiltered).forEach(function(key) {
      let task = tasksFiltered[key]
      if (task.completed) {
        tasks[key] = task
      }
    })
    return tasks
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}
pages/PageTodo.vue

<template>
  <q-page class="q-pa-md">
    
    <div class="row q-mb-lg">
      <search />
      <sort />
    </div>

    <p v-if="search && !Object.keys(tasksTodo).length && !Object.keys(tasksCompleted).length">No Search results.</p>

    <div>
      <no-tasks
        v-if="!Object.keys(tasksTodo).length && !search"
        class="q-mb-lg"></no-tasks>
      <tasks-todo 
        v-if="Object.keys(tasksTodo).length"
        :tasksTodo="tasksTodo"
        />
      <tasks-completed 
        v-if=Object.keys(tasksCompleted).length
        :tasksCompleted="tasksCompleted"
        />

      <div class="absolute-bottom text-center q-mb-lg">
        <q-btn
          @click="showAddTask=true"
          round
          color="primary"
          size="24px"
          icon="add" />
      </div>

      <q-dialog
        v-model="showAddTask">
        <add-task @close="showAddTask = false" />
      </q-dialog>
    </div>
  </q-page>
</template>

<script>
import { mapGetters, mapState } from 'vuex'

export default {
  data() {
    return {
      showAddTask: false
    }
  },
  computed: {
    ...mapGetters('tasks', ['tasksTodo', 'tasksCompleted']),
    ...mapState('tasks', ['search'])
  },
  mounted() {
    this.$root.$on('showAddTask', () => {
      this.showAddTask = true
    })
  },
  components: {
    'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
    'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
    'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
    'no-tasks' : require('components/Tasks/NoTasks.vue').default,
    'search' : require('components/Tasks/Tools/Search.vue').default,
    'sort' : require('components/Tasks/Tools/Sort.vue').default
  }
}
</script>

<style scoped>
</style>
components/Tasks/Tools/Sort.vue

<template>
    <q-select
      filled
      v-model="sortBy"
      :options="options"
      emit-value
      map-options
      class="col q-ml-sm"
      label="Sort By"
      stack-label/>
</template>

<script>
import { mapState, mapActions } from 'vuex'
export default {
    data() {
        return {
            options: [
                {
                    label: 'Name',
                    value: 'name'
                },
                {
                    label: 'Date',
                    value: 'dueDate'
                }
            ],
        }
    },
    computed: {
        ...mapState('tasks', ['sort']),
        sortBy: {
            get() {
                return this.sort
            },
            set(value) {
                this.setSort(value)
            }
        }
    },
    methods: {
        ...mapActions('tasks', ['setSort'])
    }
}
</script>

<style scoped>
    .q-select {
        flex: 0 0 112px;
    }
</style>

Transitions, Directives, Filters, mixins & Scroll area

Directive scoped select all

components/Tasks/Modals/Shared/ModalTaskName.vue

<template>
    <div class="row q-mb-sm">
        <q-input outlined 
        :value="name"
        @input="$emit('update:name', $event)"
        :rules="[val => !!val || 'Field is required']"
        ref="name"
        autofocus
        v-select-all
        class="col">
            <template v-slot:append>
            <q-icon
                v-if="name"
                @click="$emit('update:name', '')"
                name="close"
                class="cursor-pointer" />
            </template>
        </q-input>
    </div>
</template>

<script>
export default {
    props: ['name'],
    directives: {
        selectAll: {
            inserted(el) {
                let input = el.querySelector('.q-field__native')
                input.addEventListener('focus', () => {
                    if (input.value.length) {
                        input.select()
                    }
                })
            }
        }
    }
}
</script>

Change to Global Directive

components/Tasks/Modals/Shared/ModalTaskName.vue
<template>
    <div class="row q-mb-sm">
        <q-input outlined 
        :value="name"
        @input="$emit('update:name', $event)"
        :rules="[val => !!val || 'Field is required']"
        ref="name"
        autofocus
        v-select-all
        class="col">
            <template v-slot:append>
            <q-icon
                v-if="name"
                @click="$emit('update:name', '')"
                name="close"
                class="cursor-pointer" />
            </template>
        </q-input>
    </div>
</template>

<script>
import { selectAll } from 'src/directives/directive-select-all'

export default {
    props: ['name'],
    directives: {
        selectAll        
    }
}
</script>

move it to another file

src/directives/directive-select-all

export const selectAll = {
  inserted(el) {
      let input = el.querySelector('.q-field__native')
      input.addEventListener('focus', () => {
          if (input.value.length) {
              input.select()
          }
      })
  }
}
components/Tasks/Tools/Search.vue
<template>
    <q-input
      outlined
      class="col"
      v-model="searchField"
      v-select-all
      label="Search">
        <template v-slot:append>
            <q-icon v-if="searchField !== ''" name="close" @click="searchField = ''" class="cursor-pointer" />
            <q-icon name="search" />
        </template>
    </q-input>
</template>

<script>
import { mapState, mapActions } from 'vuex'
import { selectAll } from 'src/directives/directive-select-all'
export default {
    computed: {
        ...mapState('tasks', ['search']),
        searchField: {
            get() {
                return this.search
            },
            set(value) {
                this.setSearch(value)
            }
        }
    },
    methods: {
        ...mapActions('tasks', ['setSearch'])
    },
    directives: {
        selectAll
    }
}
</script>

Hold for 1 second to open a modal & nicer date with filter

components/Tasks/Task.vue
<template>
        <q-item 
          @click="updateTask({id: id, updates: {completed: !task.completed}})"
          :class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
          v-touch-hold:1000.mouse="showEditTaskModal"
          clickable
          v-ripple>
          <q-item-section side top>
            <q-checkbox 
                :value="task.completed"
                class="no-pointer-events" />
          </q-item-section>
          <q-item-section>
            <q-item
              :class="{ 'text-strike' : task.completed }"
							v-html="$options.filters.searchHighlight(task.name, search)"
              ></q-item>
          </q-item-section>
          <q-item-section
            v-if="task.dueDate"
            side>
            <div class="row">
              <div class="column justify-center">
                <q-icon 
                size="18px"
                name="event"
                class="q-mr-xs" />
              </div>
              <div class="column">
                <q-item-label 
                  class="row justify-end"
                  caption>{{ task.dueDate | niceDate }}</q-item-label>
                <q-item-label caption>
                  <small>{{ task.dueTime }}</small>
                </q-item-label>
              </div>
              
            </div>
          </q-item-section>

          <q-item-section side >
            <div class="row">
              <q-btn 
                @click.stop="showEditTaskModal"
                flat 
                round 
                dense
                color="primary"
                icon="edit" />
              <q-btn 
                @click.stop="promptToDelete(id)"
                flat 
                round 
                dense
                color="red"
                icon="delete" />
            </div>
          </q-item-section>
          <q-dialog
            v-model="showEditTask">
            <edit-task    
              @close="showEditTask = false"
              :task="task"
              :id="id" />
          </q-dialog>
        </q-item>
</template>

<script>
import { mapState, mapActions } from 'vuex'
import { date } from 'quasar'
export default {
    props: ['task', 'id'],
    data() {
      return {
        showEditTask: false
      }
    },
    computed: {
      ...mapState('tasks', ['search'])
    },
    methods: {
        ...mapActions('tasks', ['updateTask', 'deleteTask']),
        showEditTaskModal() {
          this.showEditTask = true
        },
        promptToDelete(id) {
            this.$q.dialog({
                title: 'Confirm',
                message: 'Really delete?',
                ok: {
                    push: true
                },
                cancel: {
                    color: 'negative'
                },
                persistent: true
            }).onOk(() => {
                this.deleteTask(id)
            })
        }
    },
		filters: {
      niceDate(value) {
        return date.formatDate(value, 'MMM D')
      },
			searchHighlight(value, search) {
        if (search) {
          let searchRegExp = new RegExp(search, 'ig')
          return value.replace(searchRegExp, (match) => {
            return '<span class="bg-yellow-6">'+ match+'</span>'
          })
        }
        return value
      }
    },
    components: {
      'edit-task': require('components/Tasks/Modals/EditTask.vue').default
    }
}
</script>

Mixin, reformat files

make it partials

components/Tasks/Modals/AddTask.vue
<template>
    <q-card>
      <modal-header>Add Task</modal-header>
      <form @submit.prevent="submitForm">
        <q-card-section>
          <modal-task-name :name.sync="taskToSubmit.name"
          ref="modalTaskName" />
          <modal-due-date 
            :dueDate.sync="taskToSubmit.dueDate"
            @clear="clearDueDate" />
          <modal-due-time
            v-if="taskToSubmit.dueDate"
            :dueTime.sync="taskToSubmit.dueTime" />
        </q-card-section>
        <modal-buttons></modal-buttons>
      </form>
    </q-card>
</template>

<script>
import { mapActions } from 'vuex'
import mixinAddEditTask from 'src/mixins/mixin-add-edit-task'
export default {
  mixins: [mixinAddEditTask],
  data() {
    return {
      taskToSubmit: {
        name:'',
        completed: false,
        dueDate: '',
        dueTime: ''
      }
    }
  },
  methods: {
    ...mapActions('tasks', ['addTask']),
    submitTask() {
      this.addTask(this.taskToSubmit)
      this.$emit('close')
    },
  },
}
</script>
components/Tasks/Modals/EditTask.vue
<template>
    <q-card>
      <modal-header>Edit Task</modal-header>
      <form @submit.prevent="submitForm">
        <q-card-section>
          <modal-task-name :name.sync="taskToSubmit.name"
          ref="modalTaskName" />
          <modal-due-date 
            :dueDate.sync="taskToSubmit.dueDate"
            @clear="clearDueDate" />
          <modal-due-time
            v-if="taskToSubmit.dueDate"
            :dueTime.sync="taskToSubmit.dueTime" />
        </q-card-section>
        <modal-buttons></modal-buttons>
      </form>
    </q-card>
</template>

<script>
import { mapActions } from 'vuex'
import mixinAddEditTask from 'src/mixins/mixin-add-edit-task'

export default {
  mixins: [mixinAddEditTask],
  props: ['task', 'id'],
  data() {
    return {
      taskToSubmit: {}
    }
  },
  methods: {
    ...mapActions('tasks', ['updateTask']),
    submitTask() {
      this.updateTask({
        id: this.id,
        updates: this.taskToSubmit
      })
      this.$emit('close')
    },
  },
  mounted() {
    this.taskToSubmit = Object.assign({}, this.task)
  }
}
</script>
mixins/mixin-add-edit-task.js
export default {
  methods: {
    submitForm() {
      this.$refs.modalTaskName.$refs.name.validate()
      if (!this.$refs.modalTaskName.$refs.name.hasError) {
        this.submitTask()
      }
    },
    clearDueDate(){
      this.taskToSubmit.dueDate = ''
      this.taskToSubmit.dueTime = ''
    }
  },
  components: {
    'modal-header': require('components/Tasks/Modals/Shared/ModalHeader.vue').default,
    'modal-task-name': require('components/Tasks/Modals/Shared/ModalTaskName.vue').default,
    'modal-due-date': require('components/Tasks/Modals/Shared/ModalDueDate.vue').default,
    'modal-due-time': require('components/Tasks/Modals/Shared/ModalDueTime.vue').default,
    'modal-buttons': require('components/Tasks/Modals/Shared/ModalButtons.vue').default,
  }
}

To add transition (animation)

this is like a div just wrap it up

<transition 
        appear
        enter-active-class="animated zoomIn"
        leave-active-class="animated zoomOut"
        absolute-top>
</transition>

Add Scrollable Area

<template>
  <q-page>
    
    <div class="q-pa-md absolute full-width full-height column">
      <div class="row q-mb-lg">
        <search />
        <sort />
      </div>

      <p v-if="search && !Object.keys(tasksTodo).length && !Object.keys(tasksCompleted).length">No Search results.</p>

      <q-scroll-area class="q-scroll-area-tasks">
        <no-tasks
          v-if="!Object.keys(tasksTodo).length && !search"
          class="q-mb-lg"></no-tasks>
        <tasks-todo 
          v-if="Object.keys(tasksTodo).length"
          :tasksTodo="tasksTodo"
          />
        <tasks-completed 
          v-if=Object.keys(tasksCompleted).length
          :tasksCompleted="tasksCompleted"
          class="q-mb-xl"
          />
      </q-scroll-area>
      <div class="absolute-bottom text-center q-mb-lg">
        <q-btn
          @click="showAddTask=true"
          round
          color="primary"
          size="24px"
          icon="add" />
      </div>
      <q-dialog
        v-model="showAddTask">
        <add-task @close="showAddTask = false" />
      </q-dialog>
    </div>
  </q-page>
</template>

<script>
import { mapGetters, mapState } from 'vuex'

export default {
  data() {
    return {
      showAddTask: false
    }
  },
  computed: {
    ...mapGetters('tasks', ['tasksTodo', 'tasksCompleted']),
    ...mapState('tasks', ['search'])
  },
  mounted() {
    this.$root.$on('showAddTask', () => {
      this.showAddTask = true
    })
  },
  components: {
    'add-task' : require('components/Tasks/Modals/AddTask.vue').default,
    'tasks-todo' : require('components/Tasks/TasksTodo.vue').default,
    'tasks-completed' : require('components/Tasks/TasksCompleted.vue').default,
    'no-tasks' : require('components/Tasks/NoTasks.vue').default,
    'search' : require('components/Tasks/Tools/Search.vue').default,
    'sort' : require('components/Tasks/Tools/Sort.vue').default
  }
}
</script>

<style>
.q-scroll-area-tasks {
  display:flex;
  flex-grow:1;
}
</style>

Page Settings

make a different store for settings data

import { LocalStorage } from 'quasar'

const state = {
  settings: {
    show12HourTimeFormat: false,
    showTasksInOneList: false,
  }
}

const mutations = {
  setShow12HourTimeFormat(state, value) {
    state.settings.show12HourTimeFormat = value
  },
  setShowTasksInOneList(state, value) {
    state.settings.showTasksInOneList = value
  },
  setSettings(state, settings) {
    Object.assign(state.settings, settings)
  }
}

const actions = {
  setShow12HourTimeFormat({ commit, dispatch }, value) {
    commit('setShow12HourTimeFormat', value)
    dispatch('saveSettings')

  },
  setShowTasksInOneList({ commit, dispatch }, value) {
    commit('setShowTasksInOneList', value)
    dispatch('saveSettings')
  },
  saveSettings({state}) {
    LocalStorage.set('settings', state.settings)
  },
  getSettings({commit}) {
    let settings = LocalStorage.getItem('settings')
    if (settings) {
      commit('setSettings', settings)
    }
  }
}

const getters = {
  settings: state => {
    return state.settings
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

update store/index.js

import settings from './store-settings'
modules: {
  tasks, settings
},
pages/PageSettings.vue

<template>
  <q-page padding>
    <q-list bordered padding>
      <q-item-label header>Settings</q-item-label>

      <q-item tag="label" v-ripple>
        <q-item-section>
          <q-item-label>Show 12 hour time format</q-item-label>
        </q-item-section>
        <q-item-section side >
          <q-toggle color="blue" v-model="show12HourTimeFormat" />
        </q-item-section>
      </q-item>

      <q-item tag="label" v-ripple>
        <q-item-section>
          <q-item-label>Show tasks in one list</q-item-label>
        </q-item-section>
        <q-item-section side >
          <q-toggle color="blue" v-model="showTasksInOneList" />
        </q-item-section>
      </q-item>
    </q-list>
  </q-page>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
export default {
  computed: {
    ...mapGetters('settings', ['settings']),
    show12HourTimeFormat: {
      get() {
        return this.settings.show12HourTimeFormat
      },
      set(value) {
        this.setShow12HourTimeFormat(value)
      }
    },
    showTasksInOneList: {
      get() {
        return this.settings.showTasksInOneList
      },
      set(value) {
        this.setShowTasksInOneList(value)
      }
    }
  },
  methods: {
    ...mapActions('settings', ['setShow12HourTimeFormat', 'setShowTasksInOneList' ])
  }
}
</script>

<style scoped>
</style>
components/Tasks/TasksTodo.vue

<template>

    <transition 
        appear
        enter-active-class="animated zoomIn"
        leave-active-class="animated zoomOut"
        absolute-top>
        <div>
        <list-header 
            v-if="!settings.showTasksInOneList"
            bgcolor="bg-orange-4"
            >Todo</list-header>
        <q-list 
            separator
            bordered >

            <task v-for="(task, key) in tasksTodo"
            :key="key"
            :task="task"
            :id="key"
            ></task>
        </q-list>
        </div>
    </transition>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
    props: ['tasksTodo'],
    computed: {
        ...mapGetters('settings', ['settings'])
    },
    components: {
        'task' : require('components/Tasks/Task.vue').default,
        'list-header' : require('components/Shared/ListHeader.vue').default,
    }
}
</script>
TasksCompleted.vue
<template>
    <transition 
        appear
        enter-active-class="animated zoomIn"
        leave-active-class="animated zoomOut">
        <div 
            :class="{ 'q-mt-lg' : !settings.showTasksInOneList }">
            <list-header 
            v-if="!settings.showTasksInOneList" bgcolor="bg-green-4">Completed</list-header>
            <q-list 
                separator
                bordered >

                <task v-for="(task, key) in tasksCompleted"
                :key="key"
                :task="task"
                :id="key"
                ></task>
            </q-list>
        </div>
    </transition>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
    props: ['tasksCompleted'],
    computed: {
        ...mapGetters('settings', ['settings'])
    },
    components: {
        'task' : require('components/Tasks/Task.vue').default,
        'list-header' : require('components/Shared/ListHeader.vue').default,
    }
}
</script>
Task.vue
<template>
        <q-item 
          @click="updateTask({id: id, updates: {completed: !task.completed}})"
          :class="!task.completed ? 'bg-orange-1' : 'bg-green-1'"
          v-touch-hold:1000.mouse="showEditTaskModal"
          clickable
          v-ripple>
          <q-item-section side top>
            <q-checkbox 
                :value="task.completed"
                class="no-pointer-events" />
          </q-item-section>
          <q-item-section>
            <q-item
              :class="{ 'text-strike' : task.completed }"
              v-html="$options.filters.searchHighlight(task.name, search)"
              ></q-item>
          </q-item-section>
          <q-item-section
            v-if="task.dueDate"
            side>
            <div class="row">
              <div class="column justify-center">
                <q-icon 
                size="18px"
                name="event"
                class="q-mr-xs" />
              </div>
              <div class="column">
                <q-item-label 
                  class="row justify-end"
                  caption>{{ task.dueDate | niceDate }}</q-item-label>
                <q-item-label caption>
                  <small>{{ taskDueTime }}</small>
                </q-item-label>
              </div>
              
            </div>
          </q-item-section>

          <q-item-section side >
            <div class="row">
              <q-btn 
                @click.stop="showEditTaskModal"
                flat 
                round 
                dense
                color="primary"
                icon="edit" />
              <q-btn 
                @click.stop="promptToDelete(id)"
                flat 
                round 
                dense
                color="red"
                icon="delete" />
            </div>
          </q-item-section>
          <q-dialog
            v-model="showEditTask">
            <edit-task    
              @close="showEditTask = false"
              :task="task"
              :id="id" />
          </q-dialog>
        </q-item>
</template>

<script>
import { mapState, mapActions, mapGetters } from 'vuex'
import { date } from 'quasar'
export default {
    props: ['task', 'id'],
    data() {
      return {
        showEditTask: false
      }
    },
    computed: {
      ...mapState('tasks', ['search']),
      ...mapGetters('settings', ['settings']),
      taskDueTime() {
        if (this.settings.show12HourTimeFormat) {
          return date.formatDate(this.task.dueDate + ' ' + this.task.dueTime, 'h:mm A')
        }
        return this.task.dueTime
      }
    },
    methods: {
        ...mapActions('tasks', ['updateTask', 'deleteTask']),
        showEditTaskModal() {
          this.showEditTask = true
        },
        promptToDelete(id) {
            this.$q.dialog({
                title: 'Confirm',
                message: 'Really delete?',
                ok: {
                    push: true
                },
                cancel: {
                    color: 'negative'
                },
                persistent: true
            }).onOk(() => {
                this.deleteTask(id)
            })
        }
    },
    filters: {
      niceDate(value) {
        return date.formatDate(value, 'MMM D')
      },
      searchHighlight(value, search) {
        if (search) {
          let searchRegExp = new RegExp(search, 'ig')
          return value.replace(searchRegExp, (match) => {
            return '<span class="bg-yellow-6">'+ match+'</span>'
          })
        }
        return value
      }
    },
    components: {
      'edit-task': require('components/Tasks/Modals/EditTask.vue').default
    }
}
</script>

Localstorage

you can use javascript

localStorage.setItem('show12HourTimeFormat', value)

save to localstorage quasar localstorage

store/store-settings.js
import { LocalStorage } from 'quasar'

const actions = {
  setShow12HourTimeFormat({ commit, dispatch }, value) {
    commit('setShow12HourTimeFormat', value)
    dispatch('saveSettings')

  },
  setShowTasksInOneList({ commit, dispatch }, value) {
    commit('setShowTasksInOneList', value)
    dispatch('saveSettings')
  },
  saveSettings({state}) {
    LocalStorage.set('settings', state.settings)
  }
}

To retrive data from local storage

App.vue

<template>
  <div id="q-app">
    <router-view />
  </div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
  methods: {
    ...mapActions('settings', ['getSettings'])
  },
  mounted() {
    this.getSettings()
  }
}
</script>
const actions = {
  setShow12HourTimeFormat({ commit, dispatch }, value) {
    commit('setShow12HourTimeFormat', value)
    dispatch('saveSettings')

  },
  setShowTasksInOneList({ commit, dispatch }, value) {
    commit('setShowTasksInOneList', value)
    dispatch('saveSettings')
  },
  saveSettings({state}) {
    LocalStorage.set('settings', state.settings)
  },
  getSettings({commit}) {
    let settings = LocalStorage.getItem('settings')
    if (settings) {
      commit('setSettings', settings)
    }
  }
}
const mutations = {
  setShow12HourTimeFormat(state, value) {
    state.settings.show12HourTimeFormat = value
  },
  setShowTasksInOneList(state, value) {
    state.settings.showTasksInOneList = value
  },
  setSettings(state, settings) {
    Object.assign(state.settings, settings)
  }
}

New help page

PageSetting.vue

<q-list bordered padding>
  <q-item-label header>More</q-item-label>

  <q-item
    to="/settings/help"
    tag="label"
    v-ripple>
    <q-item-section>
      <q-item-label>Help</q-item-label>
    </q-item-section>
    <q-item-section side >
      <q-icon name="chevron_right"/>
    </q-item-section>
  </q-item>

</q-list>
pages/PageHelp.vue

<template>
    <q-page padding>
        <q-btn
          to="/settings"
          color="primary"
          icon="chevron_left"
          label="Back" 
          flat/>
        <div>
            <h5>How to</h5>
            <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia, sint eum quae, consequuntur voluptate sunt minus fugit dicta quam accusamus porro temporibus, quis inventore asperiores beatae eveniet id libero pariatur!</p>
        </div>
    </q-page>
</template>

OpenURL (opens a new page)

<q-item
  @click="visitOurWebsite"
  tag="label"
  v-ripple>
  <q-item-section>
    <q-item-label>Visit our website</q-item-label>
  </q-item-section>
  <q-item-section side >
    <q-icon name="chevron_right"/>
  </q-item-section>
</q-item>
methods: {
    ...mapActions('settings', ['setShow12HourTimeFormat', 'setShowTasksInOneList' ]),
    visitOurWebsite() {
      openURL('<http://www.suratpembaca.com>')
    }
  }

Create login and register page

Add new route

{ 
  path: '/auth', 
  component: () => import('pages/PageAuth.vue') 
}

Add a button on the top right

Layout.vue
<q-btn
    to="/auth"
    flat
    icon-right="account_circle"
    label="Login"
    class="absolute-right" />

Create new Auth Page

pages/PageAuth.vue
<template>
    <q-page padding>
        <q-card class="auth-tabs">
            <q-tabs
            v-model="tab"
            dense
            class="text-grey"
            active-color="primary"
            indicator-color="primary"
            align="justify"
            narrow-indicator
            >
            <q-tab name="login" label="Login" />
            <q-tab name="register" label="Register" />
            </q-tabs>

            <q-separator />

            <q-tab-panels v-model="tab" animated>
                <q-tab-panel name="login">
                    <login-register :tab="tab" />
                </q-tab-panel>

                <q-tab-panel name="register">
                    <login-register  :tab="tab" />
                </q-tab-panel>
            </q-tab-panels>
        </q-card>
    </q-page>
</template>

<script>
export default {
    data () {
        return {
            tab: 'register'
        }
    },
    components: {
        'login-register' : require('components/Auth/LoginRegister.vue').default
    }
}
</script>

<style scoped>
    .auth-tabs {
        max-width: 500px;
        margin: 0 auto;
    }
</style>
components/Auth/LoginRegister.vue
<template>
    <form @submit.prevent="submitForm">
        <div class="row q-mb-md">
            <q-banner class="bg-grey-3 col">
                <template v-slot:avatar>
                    <q-icon name="account_circle" color="primary" />
                </template>
                Register to access your Todos anywhere!
            </q-banner>
        </div>
        <div class="row q-mb-md">
            <q-input
              outlined
              v-model="formData.email"
              class="col"
              ref="email"
              :rules="[ val => isValidEmailAddress(val) || 'Please enter a valid email address.']"
              label="Email"
              stack-label />
        </div>
        <div class="row q-mb-md">
            <q-input
              outlined
              ref="password"
              v-model="formData.password"
              :rules="[ val => val.length >= 6 || 'Please enter at least 6 characters.']"
              lazy-rules
              type="password"
              class="col"
              label="Password"
              stack-label />
        </div>
        <div class="row">
            <q-space />
            <q-btn
              color="primary"
              label="Register"
              type="submit" />
        </div>
    </form>
</template>

<script>
export default {
    props: ['tab'],
    data() {
        return {
            formData: {
                email: '',
                password: ''
            }
        }
    },
    methods: {
        submitForm() {
            this.$refs.email.validate()
            this.$refs.password.validate()
            if (!this.$refs.email.hasError && !this.$refs.password.hasError) {
                if (this.tab == 'login') {
                    console.log('login the user')
                } else {
                    console.log('register the user')
                }
            }
        },
        isValidEmailAddress(email) {
            const re = /^(([^<>()[\\]\\\\.,;:\\s@\\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\\"]+)*)|(\\".+\\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;
            return re.test(email);
        }
    }
}
</script>

Firebase

create app in https://console.firebase.google.com/

create a boot quasar file

quasar new boot <name>

update quasar.conf.js

boot: [
  'firebase'
],
boot/firebase.js

import firebase from "firebase/app"
import "firebase/auth"

var firebaseConfig = {
  apiKey: "-",
  authDomain: "udemy-quasar-awesome-todo.firebaseapp.com",
  databaseURL: "<https://udemy-quasar-awesome-todo-default-rtdb.asia-southeast1.firebasedatabase.app>",
  projectId: "udemy-quasar-awesome-todo",
  storageBucket: "udemy-quasar-awesome-todo.appspot.com",
  messagingSenderId: "691846352087",
  appId: "1:691846352087:web:b2e897dd2853e1f7b17492"
}
let firebaseApp = firebase.initializeApp(firebaseConfig)
let firebaseAuth = firebaseApp.auth()
export { firebaseAuth }
store/store-auth.js

import { LocalStorage } from 'quasar'
import { firebaseAuth } from 'boot/firebase'
import { showErrorMessage } from 'src/functions/function-show-error-message'

const state = {
  loggedIn: false
}

const mutations = {
  setLoggedIn(state, value) {
    state.loggedIn = value
  }
}

const actions = {
  registerUser({}, payload) {
    firebaseAuth.createUserWithEmailAndPassword(
      payload.email, payload.password
    ).then(response => {
      console.log('response: ', response )
    })
    .catch(error => {
			showErrorMessage(error.message)
    })
  },
  loginUser({}, payload) {
    firebaseAuth.signInWithEmailAndPassword(
        payload.email, payload.password
    ).then(response => {
      console.log('response: ', response )
    })
    .catch(error => {
			showErrorMessage(error.message)
    })
  },
  logoutUser() {
    console.log('logoutUser')
    firebaseAuth.signOut()
  },
  handleAuthStateChange({commit}) {
    firebaseAuth.onAuthStateChanged(user => {
      if (user) {
        commit('setLoggedIn', true)
        LocalStorage.set('loggedIn', true)
        this.$router.push('/').catch(err => {})
      } else {
        commit('setLoggedIn', false)
        LocalStorage.set('loggedIn', false)
        this.$router.replace('/auth').catch(err => {})
      }
    })
  }
}

const getters = {
}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}
components/Auth/LoginRegister.vue

<template>
    <form @submit.prevent="submitForm">
        <div class="row q-mb-md">
            <q-banner class="bg-grey-3 col">
                <template v-slot:avatar>
                    <q-icon name="account_circle" color="primary" />
                </template>
                {{ tab | titleCase }} to access your Todos anywhere!
            </q-banner>
        </div>
        <div class="row q-mb-md">
            <q-input
              outlined
              v-model="formData.email"
              class="col"
              ref="email"
              :rules="[ val => isValidEmailAddress(val) || 'Please enter a valid email address.']"
              label="Email"
              stack-label />
        </div>
        <div class="row q-mb-md">
            <q-input
              outlined
              ref="password"
              v-model="formData.password"
              :rules="[ val => val.length >= 6 || 'Please enter at least 6 characters.']"
              lazy-rules
              type="password"
              class="col"
              label="Password"
              stack-label />
        </div>
        <div class="row">
            <q-space />
            <q-btn
              color="primary"
              :label="tab"
              type="submit" />
        </div>
    </form>
</template>

<script>
import { mapActions } from "vuex"
export default {
    props: ['tab'],
    data() {
        return {
            formData: {
                email: '',
                password: ''
            }
        }
    },
    methods: {
        ...mapActions('auth', ['registerUser', 'loginUser']),
        submitForm() {
            this.$refs.email.validate()
            this.$refs.password.validate()
            if (!this.$refs.email.hasError && !this.$refs.password.hasError) {
                if (this.tab == 'login') {
                    this.loginUser(this.formData)
                } else {
                    this.registerUser(this.formData)
                }
            }
        },
        isValidEmailAddress(email) {
            const re = /^(([^<>()[\\]\\\\.,;:\\s@\\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\\"]+)*)|(\\".+\\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;
            return re.test(email);
        }
    },
    filters: {
        titleCase(value) {
            return value.charAt(0).toUpperCase() + value.slice(1)
        }
    }
}
</script>
<template>
  <q-layout view="hHh lpR fFf">
    <q-header elevated>
      <q-toolbar>
        <q-toolbar-title class="absolute-center">
          Awesome App
        </q-toolbar-title>

        <q-btn
          v-if="!loggedIn"
          to="/auth"
          flat
          icon-right="account_circle"
          label="Login"
          class="absolute-right" />

        <q-btn
          v-else
          @click="logoutUser"
          flat
          icon-right="account_circle"
          label="Logout"
          class="absolute-right" />

      </q-toolbar>
    </q-header>

    <q-footer>
      <q-tabs>
        <q-route-tab 
        v-for="(nav, index) in navs"
          :to="nav.to"
          :icon="nav.icon" 
          :label="nav.label"
          :key="index" />
      </q-tabs>
    </q-footer>

    <q-drawer
      v-model="leftDrawerOpen"
      :width="250"
      show-if-above
      bordered
      :breakpoint="767"
      content-class="bg-primary"
    >
      <q-list dark>
        <q-item-label
          header
          class="text-white"
        >
          Navigation
        </q-item-label>
        
        <q-item
          v-for="(nav, index) in navs"
          :to="nav.to"
          exact
          clickable
          :key=index
          class="text-white">
          <q-item-section avatar>
            <q-icon :name="nav.icon" />
          </q-item-section>
          <q-item-section>
            {{nav.label}}
          </q-item-section>
        </q-item>
        
      </q-list>
    </q-drawer>

    <q-page-container>
      <router-view />
    </q-page-container>
  </q-layout>
</template>

<script>
import {mapState, mapActions} from 'vuex'
import {openURL} from 'quasar'
export default {
  name: 'MainLayout',
  data () {
    return {
      leftDrawerOpen: false,
      navs: [
        {
          label: 'Todo',
          icon: 'list',
          to: '/'
        },
        {
          label: 'Settings',
          icon: 'settings',
          to: '/settings'
        }
      ]
    }
  },
  computed: {
    ...mapState('auth', ['loggedIn'])
  },
  methods: {
    ...mapActions('auth', ['logoutUser']),
    openURL
  }
}
</script>

<style scoped lang="scss">
  @media screen and (min-width:768px) {
    .q-footer {
      display:none;
    }
  }
  .q-drawer {
    .q-router-link--exact-active {
      font-weight:bold;
    }
  }
</style>

Adding guard

quasar new boot router-auth
boot: [
  'firebase',
  'router-auth'
],
boot/router-auth.js
import { LocalStorage } from "quasar"
export default ({ router}) => {
  router.beforeEach((to, from, next) => {
    let loggedIn = LocalStorage.getItem('loggedIn')
    if (!loggedIn && to.path !== '/auth') {
      next('/auth')
    } else {
      next()
    }
  })
}

Handling Error & quasar dialog

functions/function-show-error-message.js

import { Dialog, Loading } from "quasar";
export function showErrorMessage(errorMessage) {
  Loading.hide()
  Dialog.create({
    title: 'Error',
    message: errorMessage
  })
}

Firebase Reading

store/store-tasks.js

const actions = {
  updateTask({ commit }, payload) {
    commit('updateTask', payload)
  },
  deleteTask({ commit }, id) {
    commit('deleteTask', id)
    delete state.tasks['id']
  },
  addTask({ commit }, task) {
    let taskId = uid()
    let payload = {
      id: taskId,
      task: task
    }
    commit('addTask', payload)
  },
  setSearch({ commit}, value) {
    commit('setSearch', value)
  },
  setSort({ commit}, value) {
    commit('setSort', value)
  },
  fbReadData({commit}) {
    let userId = firebaseAuth.currentUser.uid
    let userTasks = firebaseDb.ref('tasks/' + userId)

    //child added
    userTasks.on('child_added', snapshot => {
      let task = snapshot.val()
      let payload = {
        id: snapshot.key,
        task: task
      }
      commit('addTask', payload)
    })

		//child changed
    userTasks.on('child_changed', snapshot => {
      let task = snapshot.val()
      let payload = {
        id: snapshot.key,
        updates: task
      }
      commit('updateTask', payload)
    })

		//child removed
    userTasks.on('child_removed', snapshot => {
      let taskId = snapshot.key
      commit('deleteTask', taskId)
    })
  }
}
handleAuthStateChange({commit, dispatch}) {
  firebaseAuth.onAuthStateChanged(user => {
    Loading.hide()
    if (user) {
      commit('setLoggedIn', true)
      LocalStorage.set('loggedIn', true)
      this.$router.push('/').catch(err => {})
      dispatch('tasks/fbReadData', null, { root: true })
    } else {
      commit('setLoggedIn', false)
      LocalStorage.set('loggedIn', false)
      this.$router.replace('/auth').catch(err => {})
    }
  Loading.hide()
 })

Firebase Writing

Change the default from commit to dispatch

addTask({ dispatch }, task) {
  let taskId = uid()
  let payload = {
    id: taskId,
    task: task
  }
  dispatch('fbAddTask', payload)
},
updateTask({ dispatch }, payload) {
  dispatch('fbUpdateTask', payload)
},
deleteTask({ dispatch }, id) {
  dispatch('fbDeleteTask', id)
},
fbAddTask({}, payload) {
	let userId = firebaseAuth.currentUser.uid
	let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
	taskRef.set(payload.task)
},
fbUpdateTask({}, payload) {
  let userId = firebaseAuth.currentUser.uid
  let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
  taskRef.update(payload.updates)
},
fbDeleteTask({}, taskId) {
  let userId = firebaseAuth.currentUser.uid
  let taskRef = firebaseDb.ref('tasks/' + userId + '/' + taskId)
  taskRef.remove()
}

Loading Experience

const state = {
  tasks: {
  },
  search: '',
  sort: 'name',
  tasksDownloaded: false
}
const mutations = {
  setTasksDownloaded(state, value) {
    state.tasksDownloaded = value
  }
}
fbReadData({commit}) {
    let userId = firebaseAuth.currentUser.uid
    let userTasks = firebaseDb.ref('tasks/' + userId)

    // initial check for data
    userTasks.once('value', snapshot => {
      commit('setTasksDownloaded', true)
    })

    //child added
    userTasks.on('child_added', snapshot => {
      let task = snapshot.val()
      let payload = {
        id: snapshot.key,
        task: task
      }
      commit('addTask', payload)
    })

    //child changed
    userTasks.on('child_changed', snapshot => {
      let task = snapshot.val()
      let payload = {
        id: snapshot.key,
        updates: task
      }
      commit('updateTask', payload)
    })

    //child removed
    userTasks.on('child_removed', snapshot => {
      let taskId = snapshot.key
      commit('deleteTask', taskId)
    })
  },
<template v-if="tasksDownloaded">
</template>
      
<template
  v-else
>
  <span class="absolute-center">
    <q-spinner color="primary" size="3em" />
  </span>
</template>
handleAuthStateChange({commit, dispatch}) {
    firebaseAuth.onAuthStateChanged(user => {
      Loading.hide()
      if (user) {
        commit('setLoggedIn', true)
        LocalStorage.set('loggedIn', true)
        this.$router.push('/').catch(err => {})
        dispatch('tasks/fbReadData', null, { root: true })
      } else {
        commit('tasks/setTasksDownloaded', false, {root: true})
        commit('setLoggedIn', false)
        LocalStorage.set('loggedIn', false)
        this.$router.replace('/auth').catch(err => {})
      }
    Loading.hide()
    })
  }

Multiple Users

After logout need to set the tasks as empty tasks

const mutations = {
  clearTasks(state) {
    state.tasks = {}
  },
}
handleAuthStateChange({commit, dispatch}) {
    firebaseAuth.onAuthStateChanged(user => {
      Loading.hide()
      if (user) {
        commit('setLoggedIn', true)
        LocalStorage.set('loggedIn', true)
        this.$router.push('/').catch(err => {})
        dispatch('tasks/fbReadData', null, { root: true })
      } else {
        commit('tasks/clearTasks', null, {root: true})
        commit('tasks/setTasksDownloaded', false, {root: true})
        commit('setLoggedIn', false)
        LocalStorage.set('loggedIn', false)
        this.$router.replace('/auth').catch(err => {})
      }
    Loading.hide()
    })
  }

Rules on firebase to prevent other user write or read one another

{
  "rules": {
    "tasks": {
      "$uid": {
        ".read" : "auth.uid == $uid",
      	".write" : "auth.uid == $uid",
      }
    }
  }
}

Handle Errors

const actions = {
  
  fbAddTask({}, payload) {
    let userId = firebaseAuth.currentUser.uid
    let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
    taskRef.set(payload.task, error => {
      if (error) {
        showErrorMessage(error.message)
      }
    })
  },
  fbUpdateTask({}, payload) {
    let userId = firebaseAuth.currentUser.uid
    let taskRef = firebaseDb.ref('tasks/' + userId + '/' + payload.id)
    taskRef.update(payload.updates, error => {
      if (error) {
        showErrorMessage(error.message)
      }
    })
  },
  fbDeleteTask({}, taskId) {
    let userId = firebaseAuth.currentUser.uid
    let taskRef = firebaseDb.ref('tasks/' + userId + '/' + taskId)
    taskRef.remove( error => {
      if (error) {
        showErrorMessage(error.message)
      }
    })
  }
}

To learn about production build

Subscribe to You Live What You Learn

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe