Project - Task Manager
Project: Task Manager
You’re going to build a console-based Task Manager: a small program that can add tasks, mark them complete, remove them, and list them with filters, all held in memory while the script runs. There’s no file saving and no browser involved, everything lives in variables for the life of the program.
This is a fitting capstone because a task manager touches almost everything from this course: a class to represent a task, a class to manage a collection of tasks, a closure to generate unique ids, array methods to search and filter, template literals to format output, and custom errors to handle a task that doesn’t exist. Nothing here is new. The goal is to combine it all in one working program.
Requirements
Build this in a single Node script. Work through the sections in order, since later pieces depend on earlier ones.
The Task Class
Write a Task class with a constructor that accepts id, title, and priority, and sets:
id: the value passed intitle: the value passed inpriority: the value passed in, one of the strings"low","medium", or"high"completed: always starts asfalsecreatedAt: anew Date()created at construction time
The class itself does not generate the id. That’s the closure generator’s job, described next.
The Closure-Based Id Generator
Write a function createIdGenerator that returns another function. The outer function holds a count variable starting at 0. Each time the returned function is called, it increments count and returns the new value. This is the closures pattern from Module 4: the inner function keeps a private reference to count that nothing outside can reach directly.
function createIdGenerator() {
let count = 0;
return function nextId() {
count += 1;
return count;
};
}
const generateId = createIdGenerator();
Call generateId() once per new task, and use the result as that task’s id. Every task in the program should share the same generator, so ids never repeat.
The TaskManager Class
Write a TaskManager class that stores its tasks in a private field, #tasks, initialized to an empty array. Nothing outside the class should be able to read or overwrite #tasks directly, this is the encapsulation pattern from Module 7.
Implement these methods:
addTask(title, priority): creates a newTaskusinggenerateId()for the id, pushes it onto#tasks, and returns it.findTask(id): uses.find()to look for a task with a matchingid. If no task matches, throw aTaskNotFoundError(see below). Otherwise return the task.completeTask(id): callsfindTask(id)to get the task (which throws if it’s missing), setscompletedtotrue, and returns the task.removeTask(id): callsfindTask(id)to confirm the task exists (which throws if it’s missing), then uses.filter()to rebuild#taskswithout that task, and returns the removed task.listTasks({ filterByPriority, onlyIncomplete } = {}): accepts a single options object, destructured with a default empty object solistTasks()works with no arguments at all. WhenfilterByPriorityis provided, keep only tasks with a matchingpriorityusing.filter(). WhenonlyIncompleteis truthy, keep only tasks wherecompletedisfalse, also with.filter(). Return the matching tasks formatted as strings using.map()(see formatting below), not rawTaskobjects.
Every one of these methods must use .find(), .filter(), or .map() internally, don’t reach for a manual for loop where an array method already does the job.
Error Handling
Write a TaskNotFoundError class that extends Error. In its constructor, accept the missing id, call super() with a message like Task with id 5 was not found., and set this.name = "TaskNotFoundError".
findTask, completeTask, and removeTask must all throw a TaskNotFoundError when given an id with no matching task, instead of returning undefined or silently doing nothing. This is the custom error pattern from Module 8.
Callers should catch it like this:
try {
manager.completeTask(999);
} catch (error) {
if (error instanceof TaskNotFoundError) {
console.log(`Caught error: ${error.message}`);
} else {
throw error;
}
}
Formatting Output
Write a formatTask(task) function that returns a single-line string using a template literal, in this exact shape:
[HIGH] Buy groceries (incomplete)
The priority is uppercased, wrapped in square brackets, followed by the title, followed by (complete) or (incomplete) depending on the task’s completed value. Use this function inside listTasks so every returned line is already formatted and ready to print.
Example Session
Here is a full reference run. The code below was actually executed to produce the output shown, so you can build this same sequence yourself and expect identical results.
const manager = new TaskManager();
const t1 = manager.addTask("Buy groceries", "high");
const t2 = manager.addTask("Read chapter 4", "medium");
const t3 = manager.addTask("Email Priya about the project", "low");
const t4 = manager.addTask("Prepare demo for Jordan Reyes", "high");
console.log("All tasks after adding four:");
manager.listTasks().forEach((line) => console.log(line));
manager.completeTask(t1.id);
console.log("\nAfter completing 'Buy groceries':");
console.log(formatTask(manager.findTask(t1.id)));
console.log("\nTrying to complete a task that does not exist:");
try {
manager.completeTask(999);
} catch (error) {
if (error instanceof TaskNotFoundError) {
console.log(`Caught error: ${error.message}`);
} else {
throw error;
}
}
console.log("\nIncomplete high priority tasks:");
manager.listTasks({ filterByPriority: "high", onlyIncomplete: true }).forEach((line) => console.log(line));
console.log("\nAll incomplete tasks:");
manager.listTasks({ onlyIncomplete: true }).forEach((line) => console.log(line));
console.log("\nRemoving 'Read chapter 4':");
manager.removeTask(t2.id);
manager.listTasks().forEach((line) => console.log(line));
Running this prints:
All tasks after adding four:
[HIGH] Buy groceries (incomplete)
[MEDIUM] Read chapter 4 (incomplete)
[LOW] Email Priya about the project (incomplete)
[HIGH] Prepare demo for Jordan Reyes (incomplete)
After completing 'Buy groceries':
[HIGH] Buy groceries (complete)
Trying to complete a task that does not exist:
Caught error: Task with id 999 was not found.
Incomplete high priority tasks:
[HIGH] Prepare demo for Jordan Reyes (incomplete)
All incomplete tasks:
[MEDIUM] Read chapter 4 (incomplete)
[LOW] Email Priya about the project (incomplete)
[HIGH] Prepare demo for Jordan Reyes (incomplete)
Removing 'Read chapter 4':
[HIGH] Buy groceries (complete)
[LOW] Email Priya about the project (incomplete)
[HIGH] Prepare demo for Jordan Reyes (incomplete)
Notice that after completing “Buy groceries”, it no longer shows up in “Incomplete high priority tasks”, but it still shows up in the full list, marked (complete). That’s the filtering logic working as intended.
Stretch Goals
These are optional. Try them once the required program above is fully working.
Recursive priority counter. Add a countTasksByPriority(priority) method to TaskManager. A loop or .filter().length would solve this in one line, and that’s fine in real code, but for practice, implement it recursively instead: walk the internal array one index at a time, adding 1 to an accumulator when the priority matches, and calling itself with the next index until it runs past the end of the array. For example:
countTasksByPriority(priority, index = 0, count = 0) {
if (index >= this.#tasks.length) {
return count;
}
const matches = this.#tasks[index].priority === priority ? 1 : 0;
return this.countTasksByPriority(priority, index + 1, count + matches);
}
Due dates. Add an optional dueDate field to Task (a Date or null if not set). Then add a getOverdueTasks() method to TaskManager that uses .filter() to return every incomplete task whose dueDate is earlier than new Date().
With this working, you’ve built one complete program from everything this course has taught. Next: a set of standalone practical challenges to sharpen the same skills on problems you haven’t seen laid out for you first.