← Back to course

Final Project

Final Project

Welcome back.

This is the final lesson of the JavaScript course.

Yes.

The final boss.

But do not worry.

This boss does not have fire.

Mostly.

In the previous lessons, you learned many important JavaScript ideas:

Today we combine them.

We will build a small task manager.

Not a giant app.

Not a startup.

Not a productivity empire with a motivational dashboard and seven subscription plans.

Just a clean beginner project.

The app will:

This is a real frontend project.

Small.

Useful.

Very JavaScript.

What You Will Build

You will build a task manager.

The user can:

The app will also save tasks in local storage.

So if the user refreshes the page, the tasks stay there.

This means we will combine:

Basically, we are making JavaScript wear all its clothes at once.

Stylish?

Maybe.

Educational?

Absolutely.

Create the Project

Create a folder for this lesson:

mkdir javascript-lesson12
cd javascript-lesson12
touch index.html
touch script.js
touch tasks.json

Your project should look like this:

javascript-lesson12/
  index.html
  script.js
  tasks.json

Important:

Because we will use fetch(), run the project with a local server.

For example, with Caddy:

caddy file-server --listen :8080

Then open:

http://localhost:8080

Do not just double-click index.html.

The browser may not allow fetch() to load local JSON files from file://.

Browsers have rules.

Many rules.

Some of them are useful.

Some of them feel like they were written by a dragon with paperwork.

Create Starter Data

Open tasks.json and add:

[
  {
    "id": 1,
    "title": "Review JavaScript variables",
    "completed": false
  },
  {
    "id": 2,
    "title": "Practice DOM manipulation",
    "completed": false
  },
  {
    "id": 3,
    "title": "Build the final project",
    "completed": false
  }
]

This is an array of objects.

Each task has:

This is very common in real projects.

Tasks.

Products.

Users.

Blog posts.

Orders.

Very often, data is an array of objects.

JavaScript looks at arrays of objects and says:

Ah yes, home.

Write the HTML

Open index.html and add:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Final Project</title>

  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 60px auto;
      padding: 0 24px;
      background-color: #f3f4f6;
      color: #111827;
    }

    .card {
      background-color: white;
      padding: 24px;
      border: 2px solid #e5e7eb;
      border-radius: 18px;
    }

    h1 {
      font-size: 42px;
      margin-bottom: 8px;
    }

    p {
      font-size: 18px;
      line-height: 1.6;
    }

    form {
      display: flex;
      gap: 10px;
      margin-top: 20px;
    }

    input {
      flex: 1;
      padding: 12px;
      border: 2px solid #d1d5db;
      border-radius: 12px;
      font-size: 18px;
    }

    button {
      background-color: #2563eb;
      color: white;
      border: none;
      padding: 12px 18px;
      border-radius: 999px;
      font-weight: 700;
      cursor: pointer;
    }

    button:hover {
      background-color: #1d4ed8;
    }

    .secondary {
      background-color: #6b7280;
    }

    .secondary:hover {
      background-color: #4b5563;
    }

    .danger {
      background-color: #dc2626;
    }

    .danger:hover {
      background-color: #b91c1c;
    }

    .message {
      margin-top: 18px;
      font-weight: 700;
    }

    .task-list {
      list-style: none;
      padding: 0;
      margin-top: 22px;
      display: grid;
      gap: 12px;
    }

    .task {
      display: flex;
      justify-content: space-between;
      gap: 12px;
      align-items: center;
      padding: 16px;
      border: 2px solid #e5e7eb;
      border-radius: 14px;
      background-color: #f9fafb;
    }

    .task-title {
      font-size: 20px;
      font-weight: 700;
    }

    .completed .task-title {
      text-decoration: line-through;
      color: #6b7280;
    }

    .actions {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }

    @media (max-width: 600px) {
      form {
        flex-direction: column;
      }

      .task {
        align-items: flex-start;
        flex-direction: column;
      }
    }
  </style>
</head>
<body>
  <h1>Final Project</h1>

  <div class="card">
    <p>
      Add tasks, complete them, delete them, and refresh the page.
      Your tasks will stay saved in the browser.
    </p>

    <form id="taskForm">
      <input id="taskInput" type="text" placeholder="Write a new task">
      <button type="submit">Add Task</button>
    </form>

    <p id="message" class="message">Loading tasks...</p>

    <ul id="taskList" class="task-list"></ul>

    <button id="clearButton" class="danger">Clear All Tasks</button>
  </div>

  <script src="script.js"></script>
</body>
</html>

This HTML gives us:

The CSS makes it look like a small real app.

Not a gray page from the ancient internet museum.

Good.

We have standards now.

Mostly.

Plan the JavaScript

Before writing code, let us understand what the app needs.

We need to:

This sounds like a lot.

But it is just small pieces.

JavaScript projects are often like this.

One big scary thing becomes many small normal things.

Like cleaning a room.

Terrible if you think about the whole room.

Possible if you start with one sock.

Select Elements

Open script.js and add:

const taskForm = document.getElementById("taskForm");
const taskInput = document.getElementById("taskInput");
const taskListElement = document.getElementById("taskList");
const messageElement = document.getElementById("message");
const clearButton = document.getElementById("clearButton");

Now JavaScript can work with the page.

These variables connect JavaScript to the DOM.

The DOM is the bridge.

Without this bridge, JavaScript is just talking to itself in a corner.

Which is sometimes useful.

But not today.

Create the Tasks Array

Add this:

let tasks = [];

This array will store all tasks while the app is running.

Each task will be an object like this:

{
  id: 1,
  title: "Learn JavaScript",
  completed: false
}

So we have:

Arrays and objects together.

A classic team.

Like coffee and debugging.

Save Tasks to Local Storage

Add this function:

function saveTasks() {
  localStorage.setItem("tasks", JSON.stringify(tasks));
}

This saves the tasks array in local storage.

Because local storage stores strings, we use:

JSON.stringify(tasks)

This converts the array into a JSON string.

Without JSON.stringify, JavaScript will not save the array correctly.

It will become weird.

And JavaScript is already weird enough without help.

Load Tasks from Local Storage

Add this function:

function loadTasksFromStorage() {
  const savedTasks = localStorage.getItem("tasks");

  if (savedTasks === null) {
    return false;
  }

  tasks = JSON.parse(savedTasks);
  return true;
}

This function checks if tasks already exist in local storage.

If there are no saved tasks, it returns false.

If there are saved tasks, it loads them into the tasks array and returns true.

This helps us decide:

Use saved tasks if they exist.
Otherwise load starter tasks from JSON.

Very organized.

Suspiciously professional.

Load Starter Tasks with Fetch

Add this function:

async function loadDefaultTasks() {
  try {
    const response = await fetch("tasks.json");

    if (!response.ok) {
      throw new Error("Could not load starter tasks.");
    }

    tasks = await response.json();
    saveTasks();
  } catch (error) {
    messageElement.textContent = "Could not load starter tasks.";
    console.log(error);
  }
}

This function loads tasks from tasks.json.

It uses:

That is a lot of previous lessons in one function.

Look at you.

Using knowledge.

Dangerous.

But beautiful.

Render Tasks

Now we need to show tasks on the page.

Add this function:

function renderTasks() {
  taskListElement.innerHTML = "";

  if (tasks.length === 0) {
    messageElement.textContent = "No tasks yet. Add one above.";
    return;
  }

  messageElement.textContent = `You have ${tasks.length} task(s).`;

  for (const task of tasks) {
    const listItem = document.createElement("li");
    listItem.className = task.completed ? "task completed" : "task";

    listItem.innerHTML = `
      <span class="task-title">${task.title}</span>

      <div class="actions">
        <button data-id="${task.id}" class="secondary complete-button">
          ${task.completed ? "Undo" : "Done"}
        </button>

        <button data-id="${task.id}" class="danger delete-button">
          Delete
        </button>
      </div>
    `;

    taskListElement.appendChild(listItem);
  }
}

This function:

This is DOM rendering.

Data becomes HTML.

HTML becomes visible.

User becomes impressed.

Maybe.

Add a New Task

Now add this function:

function addTask(event) {
  event.preventDefault();

  const title = taskInput.value.trim();

  if (title === "") {
    messageElement.textContent = "Please write a task first.";
    return;
  }

  const newTask = {
    id: Date.now(),
    title: title,
    completed: false
  };

  tasks.push(newTask);
  saveTasks();
  renderTasks();

  taskInput.value = "";
}

This function:

This is a full workflow.

Input.

Validation.

Data update.

Storage.

DOM update.

Very frontend.

Very nice.

Very “I may actually understand JavaScript now.”

Complete a Task

Now we need a function that marks a task as completed.

Add this:

function toggleTask(id) {
  for (const task of tasks) {
    if (task.id === id) {
      task.completed = !task.completed;
    }
  }

  saveTasks();
  renderTasks();
}

This function receives an id.

Then it finds the matching task.

Then it flips completed.

If it was false, it becomes true.

If it was true, it becomes false.

That is what this does:

task.completed = !task.completed;

The ! means “not”.

Simple.

Powerful.

Tiny symbol.

Big attitude.

Delete a Task

Add this function:

function deleteTask(id) {
  tasks = tasks.filter(function (task) {
    return task.id !== id;
  });

  saveTasks();
  renderTasks();
}

This uses filter.

It creates a new array with all tasks except the one we want to delete.

This line:

return task.id !== id;

means:

Keep every task whose id is not the deleted id.

The task with the matching id disappears.

Very clean.

No drama.

The task leaves quietly.

Unlike some bugs.

Handle Task Button Clicks

The complete and delete buttons are created dynamically.

So we will listen for clicks on the whole task list.

Add this:

function handleTaskListClick(event) {
  const id = Number(event.target.dataset.id);

  if (event.target.classList.contains("complete-button")) {
    toggleTask(id);
  }

  if (event.target.classList.contains("delete-button")) {
    deleteTask(id);
  }
}

This uses:

event.target

to know which button was clicked.

And this:

event.target.dataset.id

reads the data-id from the button.

Because dataset values are strings, we convert it to a number:

Number(event.target.dataset.id)

This is event delegation.

Fancy name.

Simple idea.

Listen on the parent.

React to clicks from children.

Like a teacher watching the whole classroom instead of standing next to one student.

Clear All Tasks

Add this function:

function clearTasks() {
  tasks = [];
  saveTasks();
  renderTasks();
}

This removes all tasks from the array.

Then saves the empty array.

Then updates the page.

Simple.

Powerful.

Slightly dangerous.

That is why the button is red.

Red buttons mean:

Please think before clicking.

Usually.

Initialize the App

Now we need to start the app.

Add this function:

async function startApp() {
  const hasSavedTasks = loadTasksFromStorage();

  if (!hasSavedTasks) {
    await loadDefaultTasks();
  }

  renderTasks();
}

This does:

This gives the app memory.

First visit?

Load default tasks.

After that?

Use saved tasks.

Very practical.

Very real.

Add Event Listeners

Add this:

taskForm.addEventListener("submit", addTask);
taskListElement.addEventListener("click", handleTaskListClick);
clearButton.addEventListener("click", clearTasks);

startApp();

Now the app can react to:

And finally:

startApp();

starts everything.

The app wakes up.

Hopefully in a good mood.

Full JavaScript Code

Here is the full script.js:

const taskForm = document.getElementById("taskForm");
const taskInput = document.getElementById("taskInput");
const taskListElement = document.getElementById("taskList");
const messageElement = document.getElementById("message");
const clearButton = document.getElementById("clearButton");

let tasks = [];

function saveTasks() {
  localStorage.setItem("tasks", JSON.stringify(tasks));
}

function loadTasksFromStorage() {
  const savedTasks = localStorage.getItem("tasks");

  if (savedTasks === null) {
    return false;
  }

  tasks = JSON.parse(savedTasks);
  return true;
}

async function loadDefaultTasks() {
  try {
    const response = await fetch("tasks.json");

    if (!response.ok) {
      throw new Error("Could not load starter tasks.");
    }

    tasks = await response.json();
    saveTasks();
  } catch (error) {
    messageElement.textContent = "Could not load starter tasks.";
    console.log(error);
  }
}

function renderTasks() {
  taskListElement.innerHTML = "";

  if (tasks.length === 0) {
    messageElement.textContent = "No tasks yet. Add one above.";
    return;
  }

  messageElement.textContent = `You have ${tasks.length} task(s).`;

  for (const task of tasks) {
    const listItem = document.createElement("li");
    listItem.className = task.completed ? "task completed" : "task";

    listItem.innerHTML = `
      <span class="task-title">${task.title}</span>

      <div class="actions">
        <button data-id="${task.id}" class="secondary complete-button">
          ${task.completed ? "Undo" : "Done"}
        </button>

        <button data-id="${task.id}" class="danger delete-button">
          Delete
        </button>
      </div>
    `;

    taskListElement.appendChild(listItem);
  }
}

function addTask(event) {
  event.preventDefault();

  const title = taskInput.value.trim();

  if (title === "") {
    messageElement.textContent = "Please write a task first.";
    return;
  }

  const newTask = {
    id: Date.now(),
    title: title,
    completed: false
  };

  tasks.push(newTask);
  saveTasks();
  renderTasks();

  taskInput.value = "";
}

function toggleTask(id) {
  for (const task of tasks) {
    if (task.id === id) {
      task.completed = !task.completed;
    }
  }

  saveTasks();
  renderTasks();
}

function deleteTask(id) {
  tasks = tasks.filter(function (task) {
    return task.id !== id;
  });

  saveTasks();
  renderTasks();
}

function handleTaskListClick(event) {
  const id = Number(event.target.dataset.id);

  if (event.target.classList.contains("complete-button")) {
    toggleTask(id);
  }

  if (event.target.classList.contains("delete-button")) {
    deleteTask(id);
  }
}

function clearTasks() {
  tasks = [];
  saveTasks();
  renderTasks();
}

async function startApp() {
  const hasSavedTasks = loadTasksFromStorage();

  if (!hasSavedTasks) {
    await loadDefaultTasks();
  }

  renderTasks();
}

taskForm.addEventListener("submit", addTask);
taskListElement.addEventListener("click", handleTaskListClick);
clearButton.addEventListener("click", clearTasks);

startApp();

That is the full project.

Not toy code anymore.

Still beginner-friendly.

But real.

You now have state, rendering, events, storage, validation, and fetch.

JavaScript is no longer just console logs.

It is building things.

Dangerous.

In a good way.

Test the Project

Run the local server:

caddy file-server --listen :8080

Open:

http://localhost:8080

Now test:

If all of this works, congratulations.

You built a complete beginner JavaScript app.

If something breaks, congratulations too.

You are doing real programming.

Real programming is mostly:

Why is this not working?
Oh.
It was one letter.

Common Mistakes

Forgetting the Local Server

If tasks do not load, check how you opened the page.

Wrong:

file:///home/user/javascript-lesson12/index.html

Better:

caddy file-server --listen :8080

Then:

http://localhost:8080

Fetch wants a server.

Even a small one.

Browsers are strict.

Like school teachers with security certificates.

Wrong JSON File Name

If your code says:

fetch("tasks.json")

then the file must be named:

tasks.json

Not:

task.json

Not:

Tasks.json

Not:

final-boss-data.json

File names matter.

Computers are not emotionally flexible.

Forgetting JSON.stringify

Wrong:

localStorage.setItem("tasks", tasks);

Correct:

localStorage.setItem("tasks", JSON.stringify(tasks));

Arrays must be converted to strings before saving.

Otherwise JavaScript gives you soup.

Not good soup.

Forgetting JSON.parse

Wrong:

tasks = localStorage.getItem("tasks");

Correct:

tasks = JSON.parse(savedTasks);

When you read JSON from local storage, parse it.

Stringify when saving.

Parse when loading.

Pack suitcase.

Unpack suitcase.

Do not wear the suitcase.

Practice

Add a task counter that shows:

Completed: 2 / 5

Hint:

You can count completed tasks like this:

const completedTasks = tasks.filter(function (task) {
  return task.completed;
});

Then use:

completedTasks.length

This will practice:

A small feature.

A useful improvement.

A nice little victory.

Mini Challenge

Add task priority.

Each task should have:

For example:

{
  id: Date.now(),
  title: "Learn fetch",
  priority: "high",
  completed: false
}

Add a <select> in HTML:

<select id="priorityInput">
  <option value="low">Low</option>
  <option value="medium">Medium</option>
  <option value="high">High</option>
</select>

Then show priority inside each task card.

Bonus:

Use different text for different priorities.

No need to make it perfect.

Just make it work.

Then improve.

That is how projects grow.

First potato.

Then polished potato.

Then product.

Summary

Today you built a complete beginner JavaScript project.

You used:

This is a huge achievement.

You did not just learn syntax.

You built something.

That is the difference.

Syntax is vocabulary.

Projects are conversation.

Now JavaScript is no longer only theory.

It is a tool.

A strange tool.

Sometimes dramatic.

Sometimes confusing.

But very powerful.

Course Complete

You finished the beginner JavaScript course.

You now understand the core ideas needed to continue with:

Do not rush.

Practice.

Build small things.

Break them.

Fix them.

Then build slightly bigger things.

That is the path.

Not glamorous.

Not instant.

But real.

And now you have enough JavaScript to start walking it.

Congratulations.

The final boss has been defeated.

For now.

JavaScript always has another boss hidden somewhere.