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:
- variables;
- conditions;
- functions;
- arrays;
- objects;
- DOM;
- events;
- forms;
- validation;
- local storage;
- fetch.
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:
- load starter tasks from a JSON file;
- show tasks on the page;
- let the user add a new task;
- validate empty input;
- mark tasks as completed;
- delete tasks;
- save tasks in local storage;
- remember tasks after refresh.
This is a real frontend project.
Small.
Useful.
Very JavaScript.
What You Will Build
You will build a task manager.
The user can:
- see a list of tasks;
- add new tasks;
- mark tasks as done;
- delete tasks;
- clear all tasks.
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:
- HTML structure;
- CSS styling;
- JavaScript data;
- DOM rendering;
- event listeners;
- form handling;
- validation;
- local storage;
- fetch;
- JSON.
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:
id;title;completed.
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:
- a form;
- an input;
- a task list;
- a message area;
- a clear button.
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:
- select HTML elements;
- store tasks in an array;
- load tasks from local storage;
- if there are no saved tasks, load starter tasks from
tasks.json; - render tasks on the page;
- add a new task from the form;
- validate empty input;
- mark a task as completed;
- delete a task;
- clear all tasks;
- save changes to local storage.
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:
- an array for the list;
- objects for individual tasks.
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:
fetch;await;response.ok;response.json;try;catch.
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:
- clears the list;
- checks if there are tasks;
- creates an
<li>for each task; - adds buttons for complete and delete;
- puts everything on the page.
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:
- stops form reload with
preventDefault; - reads the input value;
- validates empty input;
- creates a new task object;
- adds it to the array;
- saves tasks;
- renders tasks again;
- clears the input.
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:
- try to load tasks from local storage;
- if none exist, load starter tasks from JSON;
- render tasks.
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:
- form submit;
- task button clicks;
- clear button click.
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:
- starter tasks load from
tasks.json; - adding an empty task shows a warning;
- adding a real task works;
- clicking Done marks a task as completed;
- clicking Undo restores it;
- clicking Delete removes a task;
- refreshing keeps tasks saved;
- Clear All removes all tasks.
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:
- arrays;
- filter;
- DOM updates;
- rendering logic.
A small feature.
A useful improvement.
A nice little victory.
Mini Challenge
Add task priority.
Each task should have:
- title;
- priority.
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:
- variables to store references and data;
- arrays to store tasks;
- objects to represent tasks;
- functions to organize logic;
- DOM methods to render tasks;
- events to react to users;
- forms to collect input;
- validation to prevent empty tasks;
- local storage to remember tasks;
- fetch to load starter data;
- JSON to store structured data.
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:
- TypeScript;
- React;
- Next.js;
- Astro;
- frontend projects;
- API-based websites;
- full-stack applications.
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.