Activity Tracker Application: Wrapping Up the MVP

Completing the minimum viable product for the activity tracker — the to-do data structure, frontend TypeScript models, and the full user journey from goals to activities.

21 June 2026

I set goals in various categories yearly, such as gym, finance, etc. Those goals are often written in my Evernote. At the end of the year, I ask myself many questions. For example, if I set a goal to read three books, how many books did I read? And sometimes, those answers need more proof, meaning I need evidence that I read three books. I introduce the Activity Tracker Application, essentially an application to track all the activities associated with my goals.

The application has three entities to list/create/update/delete: Goals, Activities, and To-dos. These have been set up already. Please see below:

Pre-requisite

To-do

To-do aims to capture ideas or steps that are part of the activity. In one of the articles, to-do was broken up as the following:

from activity.models import Activity


class Todo(models.Model):
   title = models.CharField(max_length=200)
   created = models.DateTimeField(auto_now_add=True)
   activity = models.ForeignKey(Activity, null=True, on_delete=models.SET_NULL)

   def __str__(self):
       return self.title


class TodoItem(models.Model):
   title = models.CharField(max_length=200)
   description =  models.TextField(default="")
   is_done = models.IntegerField(default=0)
   todo = models.ForeignKey(Todo, on_delete=models.CASCADE)

   def __str__(self):
       return self.title

However, during the coding part, the above representation of the previous representation of todo had a problem. Some of the questions involved were:

  • How could I specify if I want to change the order of a to-do item?

  • What if I wanted to remove it?

Due to these constraints, I see it fit to represent items of a todo as a list. And here is the backend code for to-do models.py:

def validate_todo_items(items):
    items_lst = json.loads(items.replace('\'', '\"'))
    c = 1
    for item in items_lst:
        if not ('title' in item):
            raise ValidationError("item %s does not have a title" % c)

        if 'description' not in item:
            raise ValidationError("item %s does not have a description" % c)

        if 'is_done' not in item:
            raise ValidationError("item %s is missing is done" % c)
        c += 1

class Todo(models.Model):
    title = models.CharField(max_length=200)
    created = models.DateTimeField(auto_now_add=True)
    activity = models.ForeignKey(Activity, null=True, on_delete=models.SET_NULL)
    items = models.TextField(default='[]', validators=[validate_todo_items])

    def __str__(self):
        return self.title

Items represent the to-do list items in a text field with a validator and an array’s default value. Representing it as an array implies a lot of things made more straightforward, such as:

  • We can reorder items easily as this is a list

  • We can remove items easily as well, as this is a list

  • The list can implicitly state the order

Here, we use the implicit property of a list to answer some of the questions above.

The validator parses this into JSON and ensures that each item contains the attribute for a to-do item; otherwise, it will raise an error.

To-do Models in Frontend

Create the todo file in models and paste the following:

export interface TodoDto {
  id?: number;
  title: string;
  activity?: number;
  items: string;
}

export interface Todo {
  id?: number;
  title: string;
  activity?: number;
  items: TodoItem[];
}

export const toTodo = (dto: TodoDto): Todo => {
  return {
    id: dto.id,
    title: dto.title,
    activity: dto.activity,
    items: (JSON.parse(dto.items.replaceAll("'", '"')) as TodoItemDto[]).map(
      toTodoItem
    ),
  };
};

export const toTodoDto = (m: Todo): TodoDto => {
  return {
    id: m.id,
    title: m.title,
    activity: m.activity,
    items: JSON.stringify(m.items.map(toTodoItemDto)),
  };
};

export interface TodoItemDto {
  id?: number;
  title: string;
  description: string;
  is_done: number;
}

export interface TodoItem {
  id?: number;
  title: string;
  description: string;
  isDone: boolean;
}

export const toTodoItem = (dto: TodoItemDto): TodoItem => {
  return {
    id: dto?.id,
    title: dto.title,
    description: dto.description,
    isDone: dto.is_done === 1,
  };
};

export const toTodoItemDto = (m: TodoItem): TodoItemDto => {
  return {
    id: m.id,
    title: m.title,
    description: m.description,
    is_done: m.isDone ? 1 : 0,
  };
};

To-do gateway

export async function deleteTodoAction({ params }: { params: Params<"id"> }) {
  await fetch(`${process.env.VITE_REMOTE}todos/${params.id}/`, {
    method: "DELETE",
  });
  return redirect(`/todos`);
}

export async function todoAction({
  params,
  request,
}: {
  request: Request;
  params: Params<"id">;
}) {
  const formData = await request.formData();
  const id = formData.get("id")?.toString() ?? undefined;
  const activity = formData.get("activity")?.toString() ?? undefined;
  const items = formData.get("items")?.toString() ?? "[]";

  const todo: Todo = {
    id: !!id ? parseInt(id, 10) : undefined,
    activity: !!activity ? parseInt(activity, 10) : undefined,
    title: formData.get("title")?.toString() ?? "",
    items: JSON.parse(items) as TodoItem[],
  };

  const path = !!id ? `todos/${id}/` : "todos/";
  const response = await fetch(`${process.env.VITE_REMOTE}/${path}`, {
    method: !!id ? "PUT" : "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(toTodoDto(todo)),
  }).then((data) => data);

  if (response.status === 400) {
    const errors = await response.json();
    return new APIError("Application", errors);
  }

  return redirect(`/activities/${params.id}/`);
}

To-do & Activity

The activity gateway and models are left to the user for exercise. (If you need a hint, read about how I did it for the Goal here.)

With everything wired up, this is the journey so far:

List goals: in this case, I have 6 goals. Some of these goals cover the entire year and specific periods, such as Financial Q1, which covers all my financial activities from January to Mars. Others, such as Gym and Book, cover the entire year.

Press enter or click to view image in full size

When you click on a particular goal, you see the details of the goal. In this case, I chose Book. I set up a moderate goal of reading three books on the topic of Entrepreneurship, social and Computer Science (CS)

Press enter or click to view image in full size

You should see all activities associated with the goal from the goal details page. And create one if you want to. In the image below, I added an activity called Growth Hacker Marketing.

Press enter or click to view image in full size

Once saved successfully, it will redirect you to the activity details page.

Press enter or click to view image in full size

I am rereading this book as I am looking into marketing again for a side project with a friend. We are selling a product on Amazon.

Conclusion

This wraps up the MVP of the Activity Application. In my next article, I will show what the design will look like.

Was this helpful?