JavaScript Academy

Master JavaScript from absolute scratch. 150 coding problems covering variables, functions, DOM manipulation, events, async patterns, API calls, and modern ES6+ features — all with clear solutions and common mistake explanations.

Module 1 – Variables & Data Types

Learn how to declare variables, understand data types, and avoid the most common beginner mistakes with var, let, and const.

Problem 1: var vs let vs const — which to use?

One of the most confusing parts for beginners is choosing the right variable keyword. Here's the definitive guide:

Common Mistake
// Using var everywhere (old-school, causes bugs)
var name = "John";
var name = "Jane";  // No error! var allows re-declaration
console.log(name);  // "Jane" — silently overwrites

// var is function-scoped, not block-scoped
if (true) {
  var leaked = "I escaped!";
}
console.log(leaked); // "I escaped!" — var ignores block scope!
Correct Approach
// Use const for values that never change (default choice)
const siteName = "My Website";
const maxItems = 10;

// Use let for values that will be reassigned
let count = 0;
count = count + 1; // ✅ Works fine

// let and const are block-scoped (safer)
if (true) {
  let blockScoped = "I stay here";
}
// console.log(blockScoped); // ❌ ReferenceError — correctly blocked

// Rule of thumb: Use const by default, let when needed, never var
💡 Pro Tip

Start with const for everything. Only change to let when the linter or your logic tells you the value needs reassignment. This prevents accidental mutation bugs.

Problem 2: Understanding JavaScript data types

JavaScript has 7 primitive types and 1 reference type. Knowing the difference prevents countless bugs.

All Data Types
// Primitive Types (immutable, stored by value)
const myString = "Hello World";      // String
const myNumber = 42;                 // Number (integers & decimals)
const myBoolean = true;              // Boolean (true or false)
const myNull = null;                 // Null (intentional absence)
let myUndefined;                     // Undefined (not yet assigned)
const myBigInt = 9007199254740991n;  // BigInt (very large numbers)
const mySymbol = Symbol("unique");   // Symbol (unique identifier)

// Reference Type (mutable, stored by reference)
const myObject = { name: "John", age: 30 };
const myArray = [1, 2, 3];          // Arrays are objects
const myFunction = function() {};    // Functions are objects too

// Check type with typeof
console.log(typeof myString);   // "string"
console.log(typeof myNumber);   // "number"
console.log(typeof myBoolean);  // "boolean"
console.log(typeof myNull);     // "object" ← famous JS bug!
console.log(typeof myArray);    // "object" ← use Array.isArray() instead
⚠️ Watch Out

typeof null returns "object" — this is a known bug in JavaScript since 1995 that was never fixed. To check for null, use value === null instead.

Problem 3: String concatenation vs template literals

Template literals (backtick strings) are the modern, cleaner way to build strings with variables.

Old-School Concatenation
// Messy string concatenation with + operator
const name = "Sarah";
const age = 28;
const city = "Manchester";

const greeting = "Hello, my name is " + name + ". I am " + age + " years old and I live in " + city + ".";
// Hard to read, easy to miss spaces and quotes
Modern Template Literals
// Clean template literals with backticks (`)
const name = "Sarah";
const age = 28;
const city = "Manchester";

const greeting = `Hello, my name is ${name}. I am ${age} years old and I live in ${city}.`;

// Multi-line strings (no \n needed!)
const html = `
  <div class="card">
    <h2>${name}</h2>
    <p>Age: ${age}</p>
    <p>Location: ${city}</p>
  </div>
`;

// Expressions inside ${}
const message = `Next year ${name} will be ${age + 1} years old.`;
💡 Pro Tip

Always use backtick template literals when you need to include variables or multi-line strings. They're more readable and less error-prone than concatenation.

Problem 4: Type coercion traps

JavaScript automatically converts types in certain operations. This causes bizarre bugs if you're not careful.

Type Coercion Gotchas
// Loose equality (==) performs type coercion
console.log(0 == "");        // true  (both coerced to 0)
console.log(0 == false);     // true  (false coerced to 0)
console.log("" == false);    // true  (both coerced to 0)
console.log(null == undefined); // true (special JS rule)
console.log("5" == 5);       // true  (string coerced to number)

// String + number concatenation surprise
console.log("5" + 3);    // "53" (number coerced to string)
console.log("5" - 3);    // 2   (string coerced to number)
console.log("5" * 2);    // 10  (string coerced to number)
Safe Comparisons
// ALWAYS use strict equality (===) — no type coercion
console.log(0 === "");        // false ✅
console.log(0 === false);     // false ✅
console.log("5" === 5);       // false ✅

// Explicitly convert types when needed
const userInput = "42";
const num = Number(userInput);     // Convert string → number
const str = String(42);           // Convert number → string
const bool = Boolean("hello");    // Convert to boolean (truthy check)

// Parse numbers from strings
const price = parseInt("£19.99", 10);  // NaN (can't parse £)
const price2 = parseFloat("19.99");    // 19.99 ✅

Module 2 – Functions & Scope

Master function declarations, expressions, arrow functions, and understand how scope and closures work in JavaScript.

Problem 5: Function declaration vs expression vs arrow

JavaScript has three ways to create functions. Each has different behaviour with hoisting and this.

Three Function Styles
// 1. Function Declaration (hoisted — can call before defined)
function greet(name) {
  return `Hello, ${name}!`;
}

// 2. Function Expression (NOT hoisted)
const greet2 = function(name) {
  return `Hello, ${name}!`;
};

// 3. Arrow Function (modern, shorter syntax)
const greet3 = (name) => {
  return `Hello, ${name}!`;
};

// Arrow function shorthand (implicit return for one-liners)
const greet4 = (name) => `Hello, ${name}!`;

// Single parameter — parentheses optional
const double = n => n * 2;

// No parameters — empty parentheses required
const getRandom = () => Math.random();
💡 When to Use Which

Use arrow functions for callbacks, array methods, and short utility functions. Use function declarations for top-level named functions and methods that need their own this context.

Problem 6: Understanding scope and closures

Scope determines where variables are accessible. Closures are functions that "remember" their outer scope.

Scope Chain
// Global scope — accessible everywhere
const globalVar = "I'm global";

function outerFunction() {
  // Function scope — accessible inside this function
  const outerVar = "I'm outer";

  function innerFunction() {
    // Block scope — accessible only here
    const innerVar = "I'm inner";

    console.log(globalVar); // ✅ Can access global
    console.log(outerVar);  // ✅ Can access outer (closure!)
    console.log(innerVar);  // ✅ Can access own
  }

  innerFunction();
  // console.log(innerVar); // ❌ ReferenceError
}

// Practical closure example: counter factory
function createCounter() {
  let count = 0; // "Private" variable

  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount();  // 2
// count is not accessible directly — it's enclosed!
Problem 7: Default parameters and rest/spread

Modern JavaScript lets you set default values for parameters and collect/spread arguments flexibly.

Modern Parameters
// Default parameters
function createUser(name, role = "viewer", active = true) {
  return { name, role, active };
}
createUser("Alice");              // { name: "Alice", role: "viewer", active: true }
createUser("Bob", "admin");       // { name: "Bob", role: "admin", active: true }

// Rest parameters — collect remaining args into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15

// Spread operator — expand arrays/objects
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

const defaults = { theme: "light", lang: "en" };
const userPrefs = { theme: "dark" };
const settings = { ...defaults, ...userPrefs };
// { theme: "dark", lang: "en" } — userPrefs overrides defaults

Module 3 – DOM Manipulation

Learn how to select, create, modify, and delete HTML elements using JavaScript. This is the foundation of making websites interactive.

Problem 8: Selecting elements from the DOM

Before you can change anything on a page, you need to "grab" the element. Here are all the selection methods:

Modern Selection Methods
// querySelector — select ONE element (first match)
const hero = document.querySelector(".hero-section");
const title = document.querySelector("#page-title");
const firstBtn = document.querySelector("button");

// querySelectorAll — select ALL matching elements (NodeList)
const allCards = document.querySelectorAll(".card");
const allLinks = document.querySelectorAll("a[href^='http']");

// Loop through NodeList
allCards.forEach(card => {
  console.log(card.textContent);
});

// Old methods (still work, but querySelector is preferred)
const byId = document.getElementById("myId");
const byClass = document.getElementsByClassName("myClass"); // HTMLCollection
const byTag = document.getElementsByTagName("p");           // HTMLCollection
💡 Pro Tip

Always use querySelector and querySelectorAll — they accept any CSS selector and are consistent. The old methods return live HTMLCollections which can cause unexpected bugs.

Problem 9: Changing text, HTML, and attributes

Once you've selected an element, here's how to change its content and properties:

Modifying Elements
const heading = document.querySelector("h1");

// Change text content (safe — no HTML parsing)
heading.textContent = "New Heading Text";

// Change inner HTML (parses HTML — use cautiously)
heading.innerHTML = "New <em>Heading</em> Text";

// Change attributes
const img = document.querySelector("img");
img.setAttribute("src", "images/new-photo.jpg");
img.setAttribute("alt", "A beautiful sunset");
img.src = "images/new-photo.jpg"; // Shorthand for common attributes

// Change CSS styles directly
heading.style.color = "#ca8a04";
heading.style.fontSize = "2rem";
heading.style.marginBottom = "16px";

// Toggle CSS classes (best practice for styling)
heading.classList.add("highlighted");
heading.classList.remove("hidden");
heading.classList.toggle("active"); // Add if missing, remove if present
heading.classList.contains("active"); // Check if class exists
⚠️ Security Warning

Never use innerHTML with user-provided content. It executes HTML/scripts and creates XSS vulnerabilities. Use textContent for user input instead.

Problem 10: Creating and removing elements dynamically

Build new HTML elements from JavaScript and insert them into the page:

Creating Elements
// Create a new element
const card = document.createElement("div");
card.classList.add("card");
card.innerHTML = `
  <h3>New Service</h3>
  <p>We now offer web design consulting.</p>
  <a href="/contact">Get in Touch</a>
`;

// Insert into the page
const container = document.querySelector(".services-grid");
container.appendChild(card);         // Add at the end
container.prepend(card);             // Add at the beginning
container.insertBefore(card, ref);   // Insert before a reference element

// Modern insertion methods
container.insertAdjacentHTML("beforeend", `<div class="card">...</div>`);
// Positions: "beforebegin", "afterbegin", "beforeend", "afterend"

// Remove an element
const oldCard = document.querySelector(".card.outdated");
oldCard.remove(); // Modern way
// oldCard.parentNode.removeChild(oldCard); // Old way (IE support)

Module 4 – Events & Interactivity

Make your website respond to user actions — clicks, typing, scrolling, form submissions, and more.

Problem 11: Adding event listeners properly

The correct way to handle events is addEventListener — never use inline HTML event attributes.

Bad Practice
<!-- Inline event handlers — DON'T do this -->
<button onclick="handleClick()">Click Me</button>
<input onchange="validateField()">
Correct: addEventListener
// Select the element
const button = document.querySelector("#submit-btn");

// Add a click listener
button.addEventListener("click", function(event) {
  console.log("Button clicked!");
  console.log(event.target); // The element that was clicked
});

// Arrow function version
button.addEventListener("click", (e) => {
  e.preventDefault(); // Prevent default behaviour (e.g., form submit)
  console.log("Handled!");
});

// Common event types:
// "click"       — mouse click
// "dblclick"    — double click
// "mouseover"   — mouse enters element
// "mouseout"    — mouse leaves element
// "keydown"     — key pressed
// "keyup"       — key released
// "input"       — input field value changes
// "submit"      — form submitted
// "scroll"      — page scrolled
// "load"        — page/resource loaded
// "DOMContentLoaded" — HTML parsed (best for init code)
Problem 12: Event delegation for dynamic content

When you have many similar elements (or elements added later), attach one listener to the parent instead of each child.

Inefficient — Listener on Each Item
// Adding listeners to 100 buttons individually — slow!
const buttons = document.querySelectorAll(".action-btn");
buttons.forEach(btn => {
  btn.addEventListener("click", () => {
    console.log(btn.textContent);
  });
});
// Problem: new buttons added via JS won't have listeners!
Efficient — Event Delegation
// One listener on the parent — catches all child clicks
const container = document.querySelector(".button-grid");

container.addEventListener("click", (e) => {
  // Check if the clicked element is a button
  const btn = e.target.closest(".action-btn");
  if (!btn) return; // Click wasn't on a button, ignore

  console.log(btn.textContent);
  console.log(btn.dataset.action); // Access data-action attribute
});

// Now even dynamically added buttons will work!
const newBtn = document.createElement("button");
newBtn.classList.add("action-btn");
newBtn.textContent = "New Action";
container.appendChild(newBtn); // Automatically handled by delegation
💡 Pro Tip

Event delegation is essential for todo lists, dynamic cards, table rows, and any UI where elements are added or removed at runtime. Use e.target.closest() to handle nested elements.

Problem 13: Form handling and validation

Validate form data before submission to give users immediate feedback.

Complete Form Handler
const form = document.querySelector("#contact-form");

form.addEventListener("submit", (e) => {
  e.preventDefault(); // Stop the page from refreshing

  // Get form values
  const name = form.querySelector("#name").value.trim();
  const email = form.querySelector("#email").value.trim();
  const message = form.querySelector("#message").value.trim();

  // Validate
  const errors = [];
  if (name.length < 2) errors.push("Name must be at least 2 characters.");
  if (!email.includes("@")) errors.push("Please enter a valid email.");
  if (message.length < 10) errors.push("Message must be at least 10 characters.");

  if (errors.length > 0) {
    const errorDiv = form.querySelector(".error-messages");
    errorDiv.innerHTML = errors.map(err => `<p>${err}</p>`).join("");
    errorDiv.style.display = "block";
    return;
  }

  // Success — submit the data
  console.log({ name, email, message });
  form.reset();
  alert("Message sent successfully!");
});

Module 5 – Arrays & Objects

Master the most important data structures in JavaScript — arrays and objects — and learn the essential methods for manipulating them.

Problem 14: Essential array methods every developer needs

These array methods are used in virtually every JavaScript project. Master them and you'll write cleaner, more expressive code.

Core Array Methods
const products = [
  { name: "Laptop", price: 999, inStock: true },
  { name: "Phone", price: 699, inStock: false },
  { name: "Tablet", price: 449, inStock: true },
  { name: "Watch", price: 299, inStock: true },
];

// .filter() — Keep items that pass a test
const available = products.filter(p => p.inStock);
// [Laptop, Tablet, Watch]

// .map() — Transform each item
const names = products.map(p => p.name);
// ["Laptop", "Phone", "Tablet", "Watch"]

const discounted = products.map(p => ({
  ...p,
  salePrice: Math.round(p.price * 0.9),
}));

// .find() — Get the FIRST item that matches
const phone = products.find(p => p.name === "Phone");
// { name: "Phone", price: 699, inStock: false }

// .reduce() — Accumulate into a single value
const totalValue = products.reduce((sum, p) => sum + p.price, 0);
// 2446

// .some() / .every() — Check conditions
const anyOutOfStock = products.some(p => !p.inStock);  // true
const allAffordable = products.every(p => p.price < 1000); // true

// .sort() — Sort (mutates original array!)
const sorted = [...products].sort((a, b) => a.price - b.price);
// Sorted by price ascending (spread to avoid mutation)

// Chaining methods together
const affordableNames = products
  .filter(p => p.price < 500)
  .map(p => p.name)
  .sort();
// ["Tablet", "Watch"]
Problem 15: Object destructuring and shorthand

Destructuring lets you extract values from objects and arrays into named variables quickly.

Destructuring Patterns
// Object destructuring
const user = { name: "Alice", age: 30, role: "admin", city: "London" };
const { name, age, role } = user;
console.log(name); // "Alice"

// Rename variables
const { name: userName, role: userRole } = user;

// Default values
const { theme = "light", lang = "en" } = user;

// Nested destructuring
const company = {
  name: "TechCorp",
  address: { city: "Manchester", postcode: "M1 1AA" },
};
const { address: { city, postcode } } = company;

// Array destructuring
const [first, second, ...rest] = [10, 20, 30, 40, 50];
// first = 10, second = 20, rest = [30, 40, 50]

// Swap variables without temp
let a = 1, b = 2;
[a, b] = [b, a]; // a = 2, b = 1

// Function parameter destructuring
function displayUser({ name, age, role = "viewer" }) {
  return `${name} (${age}) — ${role}`;
}
displayUser(user); // "Alice (30) — admin"
Problem 16: Working with JSON data

JSON is the standard format for sending data between servers and browsers. Learn to parse and create it.

JSON Operations
// Convert JavaScript object → JSON string
const user = { name: "Alice", age: 30, skills: ["HTML", "CSS", "JS"] };
const jsonString = JSON.stringify(user);
// '{"name":"Alice","age":30,"skills":["HTML","CSS","JS"]}'

// Pretty-printed JSON (for debugging)
const pretty = JSON.stringify(user, null, 2);

// Convert JSON string → JavaScript object
const parsed = JSON.parse(jsonString);
console.log(parsed.name); // "Alice"

// Store data in localStorage (browser storage)
localStorage.setItem("user", JSON.stringify(user));

// Retrieve from localStorage
const saved = JSON.parse(localStorage.getItem("user"));
console.log(saved.skills); // ["HTML", "CSS", "JS"]

// Handle missing data safely
const missing = localStorage.getItem("nonexistent");
const data = missing ? JSON.parse(missing) : { default: true };
⚠️ Common JSON Errors

JSON.parse() throws an error if the string isn't valid JSON. Always wrap it in a try/catch when parsing data from external sources like APIs or localStorage.

Module 6 – Async, Promises & Fetch API

Learn asynchronous JavaScript — how to make API calls, handle responses, manage loading states, and deal with errors gracefully.

Problem 17: Understanding Promises

Promises represent a value that will be available in the future — like ordering food at a restaurant.

Promise Basics
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;
  
  setTimeout(() => {
    if (success) {
      resolve("Data loaded successfully!"); // Fulfilled
    } else {
      reject("Something went wrong.");       // Rejected
    }
  }, 2000);
});

// Consuming a Promise with .then() and .catch()
myPromise
  .then(result => {
    console.log(result); // "Data loaded successfully!"
  })
  .catch(error => {
    console.error(error);
  })
  .finally(() => {
    console.log("Loading complete (runs either way)");
  });

// Promise.all — Wait for ALL promises to complete
const p1 = fetch("/api/users");
const p2 = fetch("/api/posts");
const p3 = fetch("/api/comments");

Promise.all([p1, p2, p3])
  .then(responses => {
    console.log("All 3 requests finished!");
  })
  .catch(error => {
    console.error("At least one request failed");
  });
Problem 18: Fetch API — making HTTP requests

The Fetch API is the modern way to make HTTP requests in JavaScript. It replaces the old XMLHttpRequest.

Fetch with async/await
// GET request — fetching data
async function getUsers() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    
    const users = await response.json(); // Parse JSON body
    console.log(users);
    return users;
  } catch (error) {
    console.error("Failed to fetch users:", error.message);
  }
}

// POST request — sending data
async function createPost(title, body) {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ title, body, userId: 1 }),
    });
    
    const newPost = await response.json();
    console.log("Created:", newPost);
    return newPost;
  } catch (error) {
    console.error("Failed to create post:", error.message);
  }
}

// Call them
getUsers();
createPost("My First Post", "Hello from JavaScript Academy!");
💡 Pro Tip

Always check response.ok — the Fetch API does NOT reject on HTTP errors (404, 500). It only rejects on network failures. You must manually check the status code.

Problem 19: Loading states and error handling patterns

Real-world apps need loading indicators and graceful error handling. Here's a reusable pattern:

Complete Data Loading Pattern
async function loadData(url, container) {
  // Show loading state
  container.innerHTML = `
    <div class="loading">
      <div class="spinner"></div>
      <p>Loading data...</p>
    </div>
  `;

  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`Error ${response.status}`);
    const data = await response.json();

    // Render the data
    container.innerHTML = data.map(item => `
      <div class="card">
        <h3>${item.title}</h3>
        <p>${item.body}</p>
      </div>
    `).join("");

  } catch (error) {
    // Show error state with retry option
    container.innerHTML = `
      <div class="error">
        <p>⚠️ Failed to load: ${error.message}</p>
        <button onclick="loadData('${url}', this.closest('.container'))">
          Try Again
        </button>
      </div>
    `;
  }
}

// Usage
const container = document.querySelector("#posts-container");
loadData("https://jsonplaceholder.typicode.com/posts?_limit=5", container);

Module 7 – ES6+ Modern Features

Level up with modern JavaScript features — optional chaining, nullish coalescing, modules, classes, and other essential ES6+ syntax.

Problem 20: Optional chaining and nullish coalescing

These two operators eliminate the need for verbose null checks in your code.

Old Verbose Way
// Deeply nested property access — old way
const city = user && user.address && user.address.city
  ? user.address.city
  : "Unknown";

// Default value — old way
const theme = userSettings.theme !== null && userSettings.theme !== undefined
  ? userSettings.theme
  : "light";
Modern Optional Chaining & Nullish Coalescing
// Optional chaining (?.) — safely access nested properties
const city = user?.address?.city; // undefined if any part is null/undefined
const firstSkill = user?.skills?.[0]; // Works with arrays too
const result = user?.getProfile?.(); // Works with methods too

// Nullish coalescing (??) — default for null/undefined only
const theme = userSettings.theme ?? "light";
// Only uses "light" if theme is null or undefined
// Unlike ||, it does NOT replace 0, "", or false

// Combining both
const displayName = user?.profile?.name ?? "Anonymous";
const itemCount = cart?.items?.length ?? 0;

// Practical example: API response handling
async function getUserCity(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return data?.address?.city ?? "City not provided";
}
Problem 21: JavaScript classes and constructors

Classes provide a clean syntax for creating reusable object blueprints with methods and inheritance.

ES6 Classes
class Component {
  constructor(selector) {
    this.element = document.querySelector(selector);
    this.isVisible = true;
  }

  hide() {
    this.element.style.display = "none";
    this.isVisible = false;
  }

  show() {
    this.element.style.display = "";
    this.isVisible = true;
  }

  toggle() {
    this.isVisible ? this.hide() : this.show();
  }
}

// Inheritance with extends
class Modal extends Component {
  constructor(selector) {
    super(selector); // Call parent constructor
    this.overlay = document.createElement("div");
    this.overlay.classList.add("modal-overlay");
  }

  open() {
    document.body.appendChild(this.overlay);
    this.show();
    this.overlay.addEventListener("click", () => this.close());
  }

  close() {
    this.overlay.remove();
    this.hide();
  }
}

// Usage
const myModal = new Modal("#signup-modal");
myModal.open();
Problem 22: Modules — import and export

Modules let you split code into separate files and import only what you need. This keeps your codebase organised.

ES Modules
// utils.js — Export functions and variables
export function formatPrice(amount) {
  return `£${amount.toFixed(2)}`;
}

export function slugify(text) {
  return text.toLowerCase().replace(/\s+/g, "-");
}

export const API_BASE = "https://api.example.com";

// Default export (one per file)
export default class App {
  constructor() { /* ... */ }
}

// --------------------------------

// main.js — Import what you need
import App, { formatPrice, slugify, API_BASE } from "./utils.js";

console.log(formatPrice(19.5));  // "£19.50"
console.log(slugify("Hello World")); // "hello-world"

// In HTML — load as module
// <script type="module" src="main.js"></script>
💡 Pro Tip

ES Modules only work when served via HTTP (not file://). Use the VS Code Live Server extension or any local dev server. Also, modules are deferred by default — they don't block HTML parsing.

Module 8 – Real-World Projects

Apply everything you've learned by building practical, real-world features — a theme toggler, todo list, search filter, and more.

Project 1: Build a dark mode toggle

Create a toggle button that switches between light and dark themes, remembering the user's preference.

Complete Dark Mode Implementation
// Dark mode toggle with localStorage persistence
const toggleBtn = document.querySelector("#theme-toggle");

// Check saved preference or system preference
function getPreferredTheme() {
  const saved = localStorage.getItem("theme");
  if (saved) return saved;
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}

function setTheme(theme) {
  document.documentElement.setAttribute("data-theme", theme);
  localStorage.setItem("theme", theme);
  toggleBtn.textContent = theme === "dark" ? "☀️ Light" : "🌙 Dark";
}

// Initialise on page load
setTheme(getPreferredTheme());

// Toggle on click
toggleBtn.addEventListener("click", () => {
  const current = document.documentElement.getAttribute("data-theme");
  setTheme(current === "dark" ? "light" : "dark");
});
CSS Variables for Theming
/* Light theme (default) */
:root {
  --bg-color: #ffffff;
  --text-color: #1a1a1a;
  --card-bg: #f9fafb;
  --border-color: #e5e7eb;
}

/* Dark theme */
[data-theme="dark"] {
  --bg-color: #0f172a;
  --text-color: #e2e8f0;
  --card-bg: #1e293b;
  --border-color: #334155;
}

/* Use variables everywhere */
body {
  background: var(--bg-color);
  color: var(--text-color);
  transition: background 0.3s ease, color 0.3s ease;
}
Project 2: Build a real-time search filter

Create a search input that filters a list of items in real-time as the user types. Uses debouncing for performance.

Search Filter with Debounce
// Debounce utility — prevents excessive function calls
function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Search filter implementation
const searchInput = document.querySelector("#search");
const itemsList = document.querySelector("#items-list");

const items = [
  { title: "Restaurant Template", category: "Food" },
  { title: "Portfolio Design", category: "Creative" },
  { title: "Plumber Landing Page", category: "Trades" },
  { title: "Salon Booking Site", category: "Beauty" },
  { title: "Electrician Website", category: "Trades" },
  { title: "Coffee Shop Template", category: "Food" },
];

function renderItems(filteredItems) {
  if (filteredItems.length === 0) {
    itemsList.innerHTML = `<p class="no-results">No templates found matching your search.</p>`;
    return;
  }
  
  itemsList.innerHTML = filteredItems.map(item => `
    <div class="item-card">
      <h3>${item.title}</h3>
      <span class="badge">${item.category}</span>
    </div>
  `).join("");
}

// Debounced search handler
const handleSearch = debounce((query) => {
  const filtered = items.filter(item =>
    item.title.toLowerCase().includes(query) ||
    item.category.toLowerCase().includes(query)
  );
  renderItems(filtered);
});

searchInput.addEventListener("input", (e) => {
  handleSearch(e.target.value.toLowerCase().trim());
});

// Initial render
renderItems(items);
Project 3: Build a localStorage todo list

Create a complete todo application that saves tasks to localStorage, with add, complete, and delete functionality.

Complete Todo App
class TodoApp {
  constructor(containerSelector) {
    this.container = document.querySelector(containerSelector);
    this.todos = JSON.parse(localStorage.getItem("todos")) || [];
    this.render();
    this.bindEvents();
  }

  save() {
    localStorage.setItem("todos", JSON.stringify(this.todos));
  }

  addTodo(text) {
    this.todos.push({
      id: Date.now(),
      text,
      completed: false,
      createdAt: new Date().toISOString(),
    });
    this.save();
    this.render();
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.completed = !todo.completed;
    this.save();
    this.render();
  }

  deleteTodo(id) {
    this.todos = this.todos.filter(t => t.id !== id);
    this.save();
    this.render();
  }

  render() {
    const pending = this.todos.filter(t => !t.completed).length;
    this.container.innerHTML = `
      <form id="todo-form">
        <input type="text" id="todo-input" placeholder="Add a task..." required>
        <button type="submit">Add</button>
      </form>
      <p>${pending} task${pending !== 1 ? "s" : ""} remaining</p>
      <ul>
        ${this.todos.map(t => `
          <li class="${t.completed ? "completed" : ""}" data-id="${t.id}">
            <input type="checkbox" ${t.completed ? "checked" : ""}>
            <span>${t.text}</span>
            <button class="delete-btn">✕</button>
          </li>
        `).join("")}
      </ul>
    `;
  }

  bindEvents() {
    // Event delegation for dynamic content
    this.container.addEventListener("submit", (e) => {
      e.preventDefault();
      const input = this.container.querySelector("#todo-input");
      if (input.value.trim()) {
        this.addTodo(input.value.trim());
      }
    });

    this.container.addEventListener("change", (e) => {
      if (e.target.type === "checkbox") {
        const id = Number(e.target.closest("li").dataset.id);
        this.toggleTodo(id);
      }
    });

    this.container.addEventListener("click", (e) => {
      if (e.target.classList.contains("delete-btn")) {
        const id = Number(e.target.closest("li").dataset.id);
        this.deleteTodo(id);
      }
    });
  }
}

// Initialise the app
const app = new TodoApp("#todo-app");
💡 What You Practised

This project combines DOM manipulation, event delegation, localStorage, classes, array methods, and template literals — all the core skills from this academy.