
Managing Content on Small Websites: Guide for 2025 & Beyond
22 Nov 2022
In this tutorial, you will build a headless task manager web app that can send SMS alerts. You will build the UI in Nuxt, which is Vue’s official meta framework. You will use BCMS, a headless CMS, to persist the data from your task manager. You will use <a href="https://zapier.com/" target="_blank">Zapier</a> and <a href="https://www.twilio.com/en-us" target="_blank">Twilio</a> to send SMS alerts based on the data from your task manager.
You don’t need to have used BCMS, Zapier, and Twilio before.
You will start by using the BCMS CLI tool to scaffold BCMS’s Nuxt "simple blog" starter template for you. You will then move on to data modelling in the BCMS Cloud dashboard and create the required templates there. Review the 'requirements' and 'technologies' sections below, and let's get started!
You need to have the following installed:
Node.js interpreter (any version greater than 18.3, preferably version 22, which this tutorial uses)
npm package manager
Typescript
You will use the following technologies in this tutorial:
HTML, CSS, JS (basic knowledge required)
Nuxt (no prior knowledge required)
Typescript (basic familiarity)
Tailwind (no prior knowledge required)
BCMS (no prior knowledge required)
Use the command below to get started. You can replace "headless-task-manager" with whatever name you want to give to the project. Note that this name will start appearing in your BCMS Cloud dashboard as a project, meaning that you are creating something here and on the BCMS Cloud platform as well.
npx @thebcms/cli --create nuxt --starter simple-blog --project-nam headless-task-manager
The terminal will give you a URL to paste into a browser window, or it may automatically open the link for you. There, you can log in to your BCMS account, for which you will need to create an account.
Note: This tutorial uses the Cloud version of BCMS. You may also use the self-hosted version, instead.
Once logged in, the browser will say so and ask you to go back to the terminal and follow up there.
Note: You cannot create an empty Nuxt project using the BCMS CLI tool at the time of writing this tutorial.
From here on, the CLI utility will work on its own and get the command done. From the outputs on the terminal, you can see that it does four jobs.
Clone the relevant repo that we want
Create the project files
Create a BCMS project on BCMS Cloud
It will output, penultimately, the URL on which you can access the created project on BCMS Cloud.
This will create an API key in the Cloud and insert it into your .env
file along with other keys. The CLI utility has integrated the Nuxt project with the Cloud API for you. Altogether, you will see four keys in your .env
file.
The last parts of the output are terminal commands for you to navigate to your project in the terminal and run two npm commands.
Let's run all three commands.
cd headless-task-manager && npm install && npm run dev
Navigate to the created project’s folder and run npm install
there to install the required dependencies.
Then run the last command npm run dev
and open up "localhost:3000", as it says in the terminal output, in a browser.
It will first show the "Starting Nuxt" screen, after which the demo app will appear.
You can leave your Nuxt project here for the moment and jump to data modelling.
Make sure the npm package @thebcms/client is at least version 1.5.1: "<i>@thebcms/client": "1.5.1</i>
"
BCMS provides a few options to structure and store data. These are templates, widgets, groups, media, and entries.
Think of a template as a database table schema, i. e. the columns. And think of entries as individual database records that can be created only according to a specific template. Media are files, e. g. photos and videos that you can upload to BCMS.
Let’s log on to the BCMS website. Under the BCMS logo, if not already selected, select the project name from the dropdown that you entered in the terminal command. You will notice some templates, entries, and media files in this project.
These are the entries that are being displayed on localhost:3000.
Click on "Entries” from the toggleable pane on the left and delete all four blog entries. Also, click on Media Manager in the toggleable pane on the left and delete all four image files that are present there. Likewise, delete everything in “Templates” as well. You will create two templates: task
and project
.
For the task template/table, you need the following fields:
title
description
timestamps (created_at and updated_at)
priority
complete
due_date
The project
template only needs a “title”. Let's first create the project
template.
Click on "Templates" from the toggleable pane on the left and click on the "Create new template +" button in the upper right-hand corner. Enter “project” in the name field and click “Create”. BCMS adds five properties by default to a template.
Two of them are visible: title and slug. Three of them are not visible: _id, createdAt
, and updatedAt
.
There is a reason that you chose to create the project template first. It is so that you can then add an "Entry Pointer" property in the task
template that points to project.
This will create a one-to-many relationship between project
and task
.
Let's create the Task
template:
Click on "Templates" from the toggleable pane on the left and click on the "Create new template +" button in the upper right-hand corner.
Enter “task” in the name field and click “Create”. Drag a String property from the right and name it “description”.
Add a Boolean property and name it “complete”.
Add a Date property and name it “due_date”.
Add an enumeration property by the name of priority and add four enumerations to it: ‘P1’, ‘P2’, ‘P3’, and ‘P4’.
Add an Entry Pointer property by the name of project and select "project" in the "Select template" dropdown.
Set all of these properties as required.
Let's add two task
entries using the UI here. You will need a project
entry beforehand so that you can assign those two tasks to it. In the toggleable pane on the left, click on “project” under Entries. Then click on "Create new entry" in the top right-hand corner. Set the title to "Inbox", and click "Create".
Same way, click on “task” under Entries in the toggleable pane on the left. Click on "Create new entry" in the top right-hand corner. Create a random task, set the project to Inbox, and click "Create".
Create another random task
entry, set the project to Inbox, and click "Create". You are done with the data modelling part.
Go to the API tab in the Settings option and give the API key (already there) permission to perform all four operations on both templates.
Now that you have changed the templates and entries in BCMS Cloud, you can run `bcms --pull types --lng ts`
in the root of the project, so that the BCMS CLI will update the project to reflect the changes.
What it does is that: before, you had "BCMSBlogEntry" and other types and interfaces available to be `import`ed in your code, it will replace them with new interfaces and types. `npm run dev`
runs two commands: `bcms --pull types --lng ts`
followed by `nuxt dev`.
Let's first delete some of the files that came with the simple blog starter kit. Delete the contents of composables
, layouts
, assests/media
, public
, components
, server
, pages
, and utils
folders.
Let's install Moment to handle dates.
Note: From the Moment docs: We recognize that many existing projects may continue to use Moment, but we would like to discourage Moment from being used in new projects going forward…<strong>We now generally consider Moment to be a legacy project in maintenance mode. It is not dead, but it is indeed done.</strong>
npm install --save-dev @nuxtjs/moment
Edit the nuxt.config.js
file and declare the module there:
export default { buildModules: [ '@nuxtjs/moment' ] }
Create an app.config.ts
file in the root of the directory with the following code:
export default defineAppConfig({ title: 'Task Manager' });
You have defined the browser title of your app here so that you can reuse it in multiple places in your app. Let's now create the Nitro (the server engine that comes with Nuxt) API endpoints that you will call from your Vue components.
Create a folder called api
inside the ./server
folder and then two folders inside of that: ./server/api/project and ./server/api/task
. You need to define the CRUD operations for each resource. Create three files inside each folder for now:
... - pages - server - api - task /* all.ts /* create.ts /* update.ts - project /* all.ts /* create.ts /* update.ts .env .gitignore ...
To perform these CRUD operations, you need to import the BCMS client module (node_modules/@thebcms/client/handlers/entry.mjs
) in all six files:
import { bcms } from '~/bcms-client';
bcms.entry
lets you call fourteen methods to perform operations that have to do with templates and entries. You will use getAll
, create
, and update
out of these for the API endpoints.
Browse to the BCMS client module in your node_modules
folder and take a look at these methods and familiarize yourself with their signatures. For getAll
, you just need to pass the template name as the only argument. For create,
you need to pass the template name and the entry data. The entry data should be of the type EntryParsedCreateData
:
// node_modules/@thebcms/client/handlers/entry.d.ts export interface EntryParsedCreateData { statuses: EntryStatusValue[]; meta: Array<{ lng: string; data: EntryParsedDataProps; }>; content: Array<{ lng: string; nodes: EntryContentNode[]; }>; }
For update
, you need to pass the template name, the _id
of the entry being updated, and the updated entry data. The updated entry data should be of the type EntryParsedCreateData
:
export interface EntryParsedUpdateData { lng: string; status?: string; meta: EntryParsedDataProps; content: EntryContentNode[]; }
For create
and update
, you will receive the data from the Vue components. Use Nuxt's readBody()
function to store this data in a variable like this:
import { bcms } from '~/bcms-client'; export default defineEventHandler(async (event) => { const body = await readBody(event); });
For the templates, BCMS provides TypeScript types that you can typehint in your code.
Check out the project.d.ts
and task.d.ts
files inside ./bcms/types/ts/entry
.
These two files give you six types in total: ProjectEntry
, ProjectEntryMeta
, ProjectEntryMetaItem
, TaskEntry
, TaskEntryMeta
, TaskEntryMetaItem.
You will use them throughout the API endpoint files.
You will create two types of our own, too: TasksResponse
and ProjectsResponse
. Let's look at these in the full code for task/all.ts and project/all.ts
:
// ./server/api/task/all.ts import { bcms } from '~/bcms-client'; import type { TaskEntry, TaskEntryMeta, TaskEntryMetaItem } from '~/bcms/types/ts'; export type TasksResponse = TaskEntryMetaItem & { id: number }; export default defineEventHandler(async () => { const tasks = (await bcms.entry.getAll('task')) as TaskEntry[]; const res: TasksResponse = tasks.map((task: TaskEntryMeta) => { let id = task._id task = task.meta.en as TaskEntryMetaItem; task.id = id; return task; }); return res; });
// ./server/api/project/all.ts import { bcms } from '~/bcms-client'; import type { ProjectEntry, ProjectEntryMeta, ProjectEntryMetaItem } from '~/bcms/types/ts'; export type ProjectsResponse = ProjectEntryMeta & { id: number }; export default defineEventHandler(async () => { const projects = (await bcms.entry.getAll('project')) as ProjectEntry[]; const res: ProjectsResponse = projects.map((project: ProjectEntryMetaItem) => { let id = project._id project = project.meta.en as ProjectEntryMeta; project.id = id; return project }); return res; });
The code for both these files is mostly the same. You are going to import these TasksResponse
and ProjectsResponse
types later in your Vue component so that you can typehint the responses from useFetch()
. In both of these files, we map the response from bcms.entry.getAll()
and keep only the <i>id </i>
and the <i>meta.en</i>
object. The <i>meta.en</i>
contains the entry data like "title", "duedate", etc.
Check out the TaskEntryMetaItemPriority.d.ts
file inside ./bcms/types/ts/enum
; it simply checks if the priority property is one of the four values: 'P1', 'P2', 'P3', or 'P4'. Take a look at the full code for the create.ts
files:
// ./server/api/task/create.ts import { EntryParsedCreateData } from '@thebcms/client'; import { bcms } from '~/bcms-client'; import { TaskEntryMetaItemPriority } from '~/bcms/types/ts'; export default defineEventHandler(async (event) => { const body = await readBody(event); const request: EntryParsedCreateData = { meta: [ { lng: "en", data: { title: body.title, slug: body.title, description: body.description, due_date: { timestamp: body.due_date.timestamp, timezoneOffset: body.due_date.timezoneOffset }, complete: body.complete, priority: body.priority as TaskEntryMetaItemPriority, project: { entryId: body.projectId, templateId: '67c8fd885ab1d0bab17d107f' } } } ], statuses: [], content: [{ lng: "en", nodes: [], plainText: "" }] }; await bcms.entry.create('task', request); });
// ./server/api/project/create.ts import { EntryParsedCreateData } from '@thebcms/client'; import { bcms } from '~/bcms-client'; import type { ProjectEntryMetaItem } from '~/bcms/types/ts'; export default defineEventHandler(async (event) => { const body = await readBody(event); const request: EntryParsedCreateData = { meta: [ { lng: "en", data: { title: body.title, slug: body.title } as ProjectEntryMetaItem } ], statuses: [], content: [{ lng: "en", nodes: [], plainText: "" }] }; await bcms.entry.create('project', request); });
You have followed the EntryParsedCreateData
type and created the request objects according to it. The "due_date" property is of the type DATE
in the BCMS Cloud, so it needs to be in the format:
{ timestamp: number, timezoneOffset: number }
The project property is of the type ENTRY_POINTER
, so it needs to be in the format:
{ entryId: string, templateId: string }
Note that there is no option at the moment to pass the template name when specifying the entry pointer. I got the template id
from the browser's URL bar when visiting the "project Entries" page in the BCMS Cloud dashboard:
.../na-2d5cf2d3594b/i/headlesstaskmanager/bcms/template/67c8fd885ab1d0bab17d107f/... ↑ project's template id
You have to follow the EntryParsedUpdateData
type in your code for the update.ts
files:
// ./server/api/project/update.ts import { EntryParsedUpdateData } from '@thebcms/client'; import { bcms } from '~/bcms-client'; import { ProjectEntryMetaItem } from '~/bcms/types/ts'; export default defineEventHandler(async (event) => { const body = await readBody(event); const request: EntryParsedUpdateData = { meta: { title: body.title, slug: body.title } as ProjectEntryMetaItem, content: [{ lng: "en", nodes: [], plainText: "" }], lng: "en" }; const res = await bcms.entry.update('project', body.projectId, request); });
// ./server/api/task/update.ts import { EntryParsedUpdateData } from '@thebcms/client'; import { bcms } from '~/bcms-client'; import { TaskEntryMetaItemPriority } from '~/bcms/types/ts'; export default defineEventHandler(async (event) => { const body = await readBody(event); const request: EntryParsedUpdateData = { meta: { title: body.title, slug: body.title, description: body.description, priority: body.priority as TaskEntryMetaItemPriority, due_date: body.due_date, complete: body.complete, project: { entryId: body.projectId, templateId: '67c8fd885ab1d0bab17d107f' } }, content: [{ lng: "en", nodes: [], plainText: "" }], lng: "en" }; await bcms.entry.update('task', body.taskId, request); });
Do you think you can implement the delete API endpoints yourself?
Hint: For the <i>deleteById </i>
method, you need to pass the template name and the <i>_id</i>
of the entry being deleted.
You are finished with the Nitro API. Let's create an index.vue
file in the pages folder with the following contents:
<template> <div id="wrapper" class="my-0 mx-auto w-[100%] md:w-[80%] lg:w-[60%]"> <h1 class="my-5 italic text-5 text-gray-500 font-mono ml-[20px]">{{ useAppConfig().title }}</h1> <App></App> </div> </template>
I am using the title property here, which was defined earlier in the app.config.ts
file. You have placed the <App> component here, which you will create.
You will create three components in total: <App>
, <Task>
, and <Project>
. Your <App>
component will fetch all project entries and all task entries using our API and iteratively display them on the page using the <Project>
and <Task>
components.
Your <App>
component also handles the responsibility for creating new tasks as well as new projects. The <Task>
and <Project>
components handle updating and deleting tasks and projects, respectively. You fetch all the tasks and projects in <App>
like this:
const [tasksReq, projectsReq] = await Promise.all([ useFetch<TasksResponse>('/api/task/all'), useFetch<ProjectsResponse>('/api/project/all') ]); const tasks = tasksReq.data; const projects = projectsReq.data; projects.value.forEach(project => project.tasks = tasks.value.filter(task => task.project._id === project.id)); // *
After fetching the total data, you (*) iterate over projects
and assign the belonging tasks to each project. We do it for one reason mainly: so that you can count the number of tasks that belong to each project and display that number to the left of the project name in the projects pane. I will show you the full code for the <App>
component last.
You pass the tasks and projects data that you fetch to your <Project>
and <Task>
components in the form of props, and you listen for emitted events from them in your <App>
component. Let's first take a look at the full code for <Project>
:
<template> <li @mouseover="projectHover=true" @mouseleave="projectHover=false" @click="$emit('changeSelectedProject')" @dblclick="showInput" v-show="!projectEditInput" :class="{'cursor-pointer': projectHover}"> {{title}} ({{tasks.length}}) <span v-show="projectHover" class="ml-[10px] text-[11px] text-sky-700 border-[1px] border-gray-200 px-1 rounded" @click="deleteProject">❌</span> </li> <form @submit.prevent="updateProject" name="project-edit"> <input ref="projectEdit" placeholder="Project name" class="w-[70%]" v-show="projectEditInput" v-model="title" required> </form> </template> <script setup lang="ts"> const props = defineProps(['id', 'title', 'tasks']); const emit = defineEmits(['changeSelectedProject', 'deleteProject']); let id = ref(props.id); let title = ref(props.title); let tasks = ref(props.tasks); let projectEdit = ref(null) let projectEditInput = ref(false); let projectHover = ref(false); const showInput = async () => { projectEditInput.value=!projectEditInput.value; await nextTick(); projectEdit.value.focus(); } const updateProject = async () => { projectEditInput.value = !projectEditInput.value; await $fetch('/api/project/update', { method: 'PUT', headers: {}, body: { projectId: id.value, title: title.value } }) } const deleteProject = async () => { await $fetch('/api/project/delete', { method: 'DELETE', headers: {}, body: { projectId: id.value } }); emit('deleteProject'); }; </script>
You make $fetch
requests to your Nitro API to update and delete projects. You emit the changeSelectedProject
event when a specific project's name is clicked in the projects pane. And in the deleteProject
function, you emit the deleteProject
event. You listen for both of these events in the <App>
component:
<Project v-for="project in projects" :key="project.id" :id="project.id" :title="project.title" :tasks="project.tasks" @changeSelectedProject="changeCurrentProject(project)" @deleteProject="removeProject(project.id)" />
This is your removeProject
function in the <App>
component:
const removeProject = projectId => { projects.value.splice(projects.value.indexOf(projects.value.find(p => p.id === projectId)), 1); };
You simply splice the particular project from the projects
ref. This is the changeCurrentProject
in the <App>
component:
const changeCurrentProject = project => { selectedProject.value = project.id; useHead({ title: project.title + ' | ' + title // title come from "const title = useAppConfig().title;" }); };
Whenever a project's name is clicked on in the projects pane, we toggle to that project's tasks in the tasks pane. You do it like this:
<div v-for="task in tasks" :key="task.id" v-show="task.project._id===selectedProject"> <Task @deleteTask="removeTask(task)" :id="task.id" :title="task.title" :description="task.description" :priority="task.priority" :due_date="task.due_date" :complete="task.complete" :project="task.project" /> </div>
You may notice that you are listening to a similar deleteTask
event from <Task>
and handling it with a similar removeTask
function. Below is the removeTask
function from the <App>
component:
const removeTask = task => { tasks.value.splice(tasks.value.indexOf(tasks.value.find(t => t.id === task.id)), 1); // remove the task from the projects as well projects.value.find(p => p.id === task.project._id).tasks.splice( projects.value.find(p => p.id === task.project._id).tasks .find(t => t.id === task.id), 1 ); };
Remember how you iterated over the projects
ref and assigned all the belonging tasks to each project? So, not only do you have to splice the tasks
ref, but you also need to splice the project's tasks array to which the task being deleted belongs. Below is the full code for the <Task>
component:
<template> <section class="pb-[20px] border-t-[1px]" :class="{'opacity-30': complete, 'cursor-pointer': taskHover}" @dblclick="taskEditForm=!taskEditForm" v-show="!taskEditForm" @mouseover="taskHover=true" @mouseleave="taskHover=false"> <h3 class="task-title mt-[15px] font-bold text-slate-700"> <input class="task-complete ml-[-28px] mr-[10px]" type="checkbox" @input="complete=!complete; updateTask()" :checked="complete"> {{title}} </h3> <span v-show="taskHover" class="float-right border-gray-200 px-1 text-sky-700 border-[1px] rounded" @click="deleteTask">❌</span> <p class="mt-[2px] mb-[15px]">{{description}}</p> <time class="italic mr-[30px]">Due {{moment(due_date.timestamp).fromNow()}}</time> <span class="italic">Priority: {{getPriority()}}</span> </section> <form class="border-t-[1px] pt-[15px]" v-show="taskEditForm" name="task-edit" @submit.prevent="taskEditForm=!taskEditForm; updateTask()"> <div class="input-group"> <label for="task-title">Title:</label> <input placeholder="Name your task" type="text" id="task-title" required v-model="title"> </div> <div class="input-group"> <label for="task-description">Description:</label> <textarea placeholder="Describe your task" required v-model="description">{{description}}</textarea> </div> <div class="input-group"> <label for="task-priority">Priority:</label> <select required v-model="priority"> <option value="P1" :selected="priority === 'P1'">Very imortant</option> <option value="P2" :selected="priority === 'P2'">Important</option> <option value="P3" :selected="priority === 'P3'">Normal</option> <option value="P4" :selected="priority === 'P4'">Not important</option> </select> </div> <div class="input-group"> <label for="task-due-date">Due:</label> <input type="datetime-local" required :value="moment(due_date.timestamp).format('YYYY-MM-DDThh:mm')" @input="e => due_date.timestamp = (new Date(e.target.value)).getTime()"> </div> <button class="task-edit-button btn mb-[15px]">Update task</button> </form> </template> <script setup lang="ts"> import moment from 'moment'; const props = defineProps(['id', 'title', 'description', 'due_date', 'priority', 'complete', 'project']); const emit = defineEmits(['deleteTask']); let id = ref(props.id); let title = ref(props.title); let description = ref(props.description); let priority = ref(props.priority); let due_date = ref(props.due_date); let project = ref(props.project); let complete = ref(props.complete); let taskEditForm = ref(false); let taskHover = ref(false); const getPriority = () => { switch (priority._value) { case 'P1': return 'Very important'; case 'P2': return 'Important'; case 'P3': return 'Normal'; case 'P4': return 'Not important'; } }; const updateTask = async () => { await $fetch('/api/task/update', { method: 'PUT', body: { taskId: id.value, title: title.value, description: description.value, priority: priority.value, due_date: due_date.value, complete: complete.value, projectId: project.value._id } }); }; const deleteTask = async () => { await $fetch('/api/task/delete', { method: 'DELETE', body: {taskId: id.value} }); emit('deleteTask'); } </script>
Here is the full code for the <App>
component:
<template> <div class="mx-[20px]"> <div id="column-1" class="float-left w-[25%]"> <h2 class="text-lg font-bold text-sky-700 mb-[20px]">Projects</h2> <div id="projects"> <ul class="list-disc"> <Project v-for="project in projects" :key="project.id" :id="project.id" :title="project.title" :tasks="project.tasks" @changeSelectedProject="changeCurrentProject(project)" @deleteProject="removeProject(project.id)" /> </ul> </div> <div class="mt-[20px]"> <button @click="showInput" class="btn" v-show="!projectAddInput">New project</button> <form @submit.prevent="addProject" name="project-add"> <input placeholder="Project name" class="w-[70%]" required v-model="newProjectName" v-show="projectAddInput" > </form> </div> </div> <div id="column-2" class="float-left w-[75%]"> <h2 class="text-lg font-bold text-sky-700 float-left">Tasks</h2> <button class="mb-[20px] btn float-right" :class="taskAddForm ? 'text-white bg-slate-200' : ''" @click="taskAddForm=!taskAddForm">New task</button> <div class="mt-[50px]"> <form name="task-add" v-show="taskAddForm" @submit.prevent="addTask"> <div class="input-group"> <label for="task-title">Title:</label> <input placeholder="Name your task" type="text" required v-model="newTitle"> </div> <div class="input-group"> <label for="task-description">Description:</label> <textarea placeholder="Describe your task" required v-model="newDescription"></textarea> </div> <div class="input-group"> <label for="task-priority">Priority:</label> <select required v-model="newPriority"> <option value="P1">Very Imortant</option> <option value="P2">Important</option> <option value="P3">Normal</option> <option value="P4">Not Important</option> </select> </div> <div class="input-group"> <label for="task-due-date">Due:</label> <input type="datetime-local" required v-model="newDueDate"> </div> <button class="mb-[20px] btn">Add task</button> </form> </div> <div id="tasks"> <div v-for="task in tasks" :key="task.id" v-show="task.project._id===selectedProject"> <Task @deleteTask="removeTask(task)" :id="task.id" :title="task.title" :description="task.description" :priority="task.priority" :due_date="task.due_date" :complete="task.complete" :project="task.project" /> </div> </div> </div> </div> </template> <script setup lang="ts"> import type { TasksResponse } from '~/server/api/task/all'; import type { ProjectsResponse } from '~/server/api/project/all'; import type { TaskEntryMetaItem } from '~/bcms/types/ts'; let projectAddInput = ref(false); let projectAdd = ref(null); let taskAddForm = ref(false); let newTitle = ref(''); let newDescription = ref(''); let newPriority = ref('P3'); let newDueDate = ref(); let newProjectName = ref(''); const [tasksReq, projectsReq] = await Promise.all([ useFetch<TasksResponse>('/api/task/all'), useFetch<ProjectsResponse>('/api/project/all') ]); const tasks = tasksReq.data; const projects = projectsReq.data; projects.value.forEach(project => project.tasks = tasks.value.filter(task => task.project._id === project.id)); let selectedProject = ref(projects.value[0].id); const title = useAppConfig().title; useHead({ title: projects.value[0].title + ' | ' + title }) const showInput = async () => { projectAddInput.value = !projectAddInput.value; await nextTick(); projectAdd.value.focus(); } const removeProject = projectId => { projects.value.splice(projects.value.indexOf(projects.value.find(p => p.id === projectId)), 1); }; const removeTask = task => { tasks.value.splice(tasks.value.indexOf(tasks.value.find(t => t.id === task.id)), 1); // remove the task from the projects as well projects.value.find(p => p.id === task.project._id).tasks.splice( projects.value.find(p => p.id === task.project._id).tasks .find(t => t.id === task.id), 1 ); }; const changeCurrentProject = project => { selectedProject.value = project.id; useHead({ title: project.title + ' | ' + title }); }; const addProject = async () => { projectAddInput.value = !projectAddInput.value; projects.value.push({title: newProjectName, id: Math.random().toString(36).slice(2), tasks: []}); selectedProject.value = projects.value[projects.value.length - 1].id; useHead({ title: newProjectName.value + ' | ' + title }) await $fetch('/api/project/create', { method: 'POST', body: {title: newProjectName.value} }); }; const addTask = async () => { taskAddForm.value = !taskAddForm.value; let newTask: TaskEntryMetaItem = { id: Math.random().toString(36).slice(2), title: newTitle.value, description: newDescription.value, complete: false, priority: newPriority.value, due_date: { timestamp: new Date(newDueDate.value).getTime(), timezoneOffset: new Date(newDueDate.value).getTimezoneOffset() }, project: { _id: selectedProject.value, title: projects.value.filter(project => project.id === selectedProject.value).title } }; tasks.value.push(newTask); projects.value.find(project => project.id == selectedProject.value).tasks.push(newTask); await $fetch('/api/task/create', { method: 'POST', body: { title: newTask.title, description: newTask.description, complete: newTask.complete, priority: newTask.priority, due_date: newTask.due_date, projectId: selectedProject.value, } }); } </script>
In the addProject
and addTask
functions, you generate random ids using Math.random().toString(36).slice(2)
. You do not need to retrieve the _ids
generated by BCMS back when a task or project is created, to make your code work. You can simply avoid that. Check out the full source code for this project here: https://github.com/shammadahmed/sms-alerts-task-manager.
You can deploy your Nuxt project, including the Nitro API, for free on serverless platforms like Netlify, Cloudflare, and Vercel. And there is no configuration required to deploy, the platforms automatically detect that we are using Nuxt.
One thing that you do need to do is: for Nuxt, you need to set the env
variables separately. Before now, we have been running the npm run dev command, which makes use of the .env
file. The npm run
build command doesn't use the .env
file.
You need to specify the ENV variables in the command. You could also set them using your specific platform's environment variables settings. Check out the live version of this app deployed on Netlify: https://tasksnsms.netlify.app/.
Let's get started with the SMS alerts part and create a Twilio account. After you are inside the Twilio dashboard, you may need to click a specific button to reveal and obtain your Twilio phone number. You will use this phone number in the “From” field when you call the Twilio API from Zapier. Other than this phone number, you will also need the Account SID and the Auth Token. Keep the Twilio tab open so that you can copy these values to Zapier.
I am using Twilio to send the actual SMS and Zapier to set up Cron jobs.
Create an account on Zapier. After you are inside the Zapier dashboard, click “+ Create” and then “Zaps”. A zap is simply an automated workflow that Zapier performs for you however you want. Click on the first step that says “Trigger” and select “Schedule” under “Popular built-in tools”.
In the edit pane that appears on the right, set “Trigger event” to “Everyday” in the Setup tab. In the Configure tab, you can set the Time of Day to be any time you like, e.g., 6 o’clock in the morning. Set “Trigger on Weekends?” to “yes”.
In the Test tab, click “Test trigger”. After the test completes, click “Continue with selected record”. Zapier will now ask you to select a tool for the second step. Select “Code” under “Popular built-in tools” and name it “Capture tasks and send SMS” by clicking on the pencil icon next to where it says “Select the event”.
Select “Run JavaScript” for “Action event” in the Setup tab for this step. In the Configure tab, paste the following code in the “Code” input:
const accountSid = '<account sid>'; const authToken = '<auth token>'; const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const headers = { 'Authorization': 'Basic ' + btoa(`${accountSid}:${authToken}`) } const res = await fetch('https://tasksnsms.netlify.app/api/task/all'); const tasks = await res.json(); const getPriority = (priority) => { switch (priority) { case 'P1': return 'Very important'; case 'P2': return 'Important'; case 'P3': return 'Normal'; case 'P4': return 'Not important'; } }; const dueToday = tasks.filter(task => { let today = new Date(task.due_date.timestamp).getDay() === (new Date()).getDay(); if (today && !task.complete) { return true; } }); for (const task of dueToday) { await fetch(url, { method: 'POST', headers: headers, body: (new URLSearchParams({ 'To': '<to phone number>', 'From': '<from phone number>', 'Body': `Due Today: ${task.title} \nDescription: ${task.description} \nPriority: ${getPriority(task.priority)} \nProject: ${task.project.meta.en.title}` })).toString() }); } return {};
Replace the strings for Account SID, Auth Token, “To” phone number, and the “From” Phone number. Right now, I have used my own phone number in the “To” field. You may recognize the getPriority
function from the <Task>
component. In this code, you make a fetch request to your Nitro task/all API
endpoint and retrieve all tasks.
You then filter the tasks array and store the ones that are due today and still incomplete in dueToday
. You loop over dueToday
and send an SMS for each task.
I use (new URLSearchParams).toString()
since I need to send the data to Twilio, not as JSON but in the application/
<i>x-www-form-urlencoded</i>
MIME type. As for the Twilio URL, I obtained it from their SMS demo at “https://console.twilio.com/us1/develop/sms/try-it-out/send-an-sms”.
Before you run the test for this step, make sure that at least one task in your app is incomplete and has a due date of today. In the Test tab, click the “Test step” button. You will receive an SMS:
Finally, click the “Publish” button in the top right-hand corner of the website. Your Zap is now live, and you will receive your tasks in SMSes on time:
One thing that we are missing in this application is authentication. You can add authentication using Nuxt's built-in auth features. You can also use Auth-as-a-Service services like Auth0 and Clerk. You will need to modify the project
template to add a "user_id" field to it.
To sum up, you started with a scaffolded blog template and then created the data templates in BCMS Cloud. You then coded the Nitro API endpoints and connected them to your Vue components. We used a serverless platform to deploy the project. In the last section, we created a zap in Zapier to run a block of code every day to obtain tasks, whenever due, in SMS form.
I enjoyed working on this project. I hope that you also enjoyed following along and reading this tutorial. Feel free to ask any questions that you may have. Till then, happy coding!
Get all the latest BCMS updates, news and events.
By submitting this form you consent to us emailing you occasionally about our products and services. You can unsubscribe from emails at any time, and we will never pass your email to third parties.
There are many actionable insights in this blog post. Learn more: