Tired of tutorial hell? This is where your real frontend journey begins.

Whether you're:
βœ‹ A total beginner looking to build your first real projects
πŸ’Ό A job seeker needing portfolio pieces that impress employers
πŸ“ˆ An aspiring freelancer wanting practical, client-ready skills

These 12 carefully crafted projects will give you the hands-on experience most courses only talk about. Each one:

βœ… Solves real problems (no "foo/bar" examples)
βœ… Grows with your skills (from basic DOM to complex APIs)
βœ… Looks portfolio-ready (modern UI, clean code)
βœ… Teaches professional workflows (debugging, optimization, deployment)

All projects use vanilla JavaScript (no frameworks) with clean code.

Todo List App

A dynamic task manager with add, edit, delete, and prioritization features. Practice CRUD operations and localStorage for data persistence while mastering DOM manipulation.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>TODO LIST</title>
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
      <div class="container">
        <h1>Todo List</h1>
        <div class="input-container">
          <input class="todo-input" placeholder="Add a new task...">
          <button class="add-button">
            <i class="fa fa-plus-circle"></i>
          </button>
        </div>
        <div class="filters">
          <div class="filter" data-filter="completed">Complete</div>
          <div class="filter" data-filter="pending">Incomplete</div>
          <div class="delete-all">Delete All</div>
        </div>
        <div class="todos-container">
          <ul class="todos"></ul>
          <img class="empty-image" src="./empty.svg">
        </div>
      </div>
      <script src="./script.js"></script>
    </body>
    </html>
  • style.css

    *,
    *::before,
    *::after {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: 'Roboto', sans-serif;
      height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      background: url(./background.jpg) no-repeat;
      background-position: center;
      background-size: cover;
    }
    
    .container {
      width: 400px;
      height: auto;
      min-height: 400px;
      padding: 30px;
      background: transparent;
      border: 2px solid #e6b7eca1;
      border-radius: 10px;
      backdrop-filter: blur(15px);
    }
    
    h1 {
      color: #eee;
      text-align: center;
      margin-bottom: 36px;
    }
    
    .input-container {
      display: flex;
      justify-content: space-between;
      margin-bottom: 25px;
    }
    
    .todo-input {
      flex: 1;
      outline: none;
      padding: 10px 10px 10px 20px;
      background-color: transparent;
      border: 2px solid #e6b7eca1;
      border-radius: 30px;
      color: #eee;
      font-size: 16px;
      margin-right: 10px;
    }
    
    .todo-input::placeholder {
      color: #bfbfbf;
    }
    
    .add-button {
      border: none;
      outline: none;
      background: #e6b7eca1;
      color: #fff;
      font-size: 35px;
      cursor: pointer;
      border-radius: 40px;
      width: 40px;
      height: 40px;
    }
    
    .empty-image {
      margin: 55px auto 0;
      display: block;
    }
    
    .todo {
      list-style: none;
      display: flex;
      align-items: center;
      justify-content: space-between;
      background-color: #463c7b;
      border-radius: 5px;
      margin: 10px 0;
      padding: 10px 12px;
      border: 2px solid #e6b7eca1;
      transition: all 0.2s ease;
    }
    
    .todo:first-child {
      margin-top: 0;
    }
    
    .todo:last-child {
      margin-bottom: 0;
    }
    
    .todo:hover {
      background-color:#e6b7eca1;
    }
    
    .todo label {
      cursor: pointer;
      width: fit-content;
      display: flex;
      align-items: center;
      font-family: 'Roboto', sans-serif;
      color: #eee;
    }
    
    .todo span {
      padding-left: 20px;
      position: relative;
      cursor: pointer;
    }
    
    span::before {
      content: "";
      height: 20px;
      width: 20px;
      position: absolute;
      margin-left: -30px;
      border-radius: 100px;
      border: 2px solid #e6b7eca1;
    }
    
    input[type='checkbox'] {
      visibility: hidden;
    }
    
    input:checked + span {
      text-decoration: line-through
    }
    
    .todo:hover input:checked + span::before, input:checked + span::before {
      background: url(./check.svg) 50% 50% no-repeat #09bb21;
      border-color: #09bb21;
    }
    
    .todo:hover span::before {
      border-color: #eee;
    }
    
    .todo .delete-btn  {
      background-color: transparent;
      border: none;
      cursor: pointer;
      color: #eee;
      font-size: 24px;
    }
    
    .todos-container  {
      height: 300px;
      overflow: overlay;
    }
    
    .todos-container::-webkit-scrollbar-track  {
      background: rgb(247, 247, 247);
      border-radius: 20px
    }
    
    .todos-container::-webkit-scrollbar  {
      width: 0;
    }
    
    .todos-container:hover::-webkit-scrollbar  {
      width: 7px;
    }
    
    .todos-container::-webkit-scrollbar-thumb  {
      background: #d5d5d5;
      border-radius: 20px;
    }
    
    .filters {
      display: flex;
      justify-content: space-between;
      margin-bottom: 25px;
    }
    
    .filter {
      color: #eee;
      padding: 5px 15px;
      border-radius: 100px;
      border: 2px solid #e6b7eca1;
      transition: all 0.2s ease;
      cursor: pointer;
    }
    
    .filter.active, .filter:hover {
      background-color: #e6b7eca1;
    }
    
    .delete-all {
      display: flex;
      color: #eee;
      padding: 7px 15px;
      cursor: pointer;
      transition: all 0.2s ease;
    }
    
    .delete-all:hover {
      border-radius: 5px;
      background-color: #e6b7eca1;
    }
  • script.js

    const input = document.querySelector("input");
    const addButton = document.querySelector(".add-button");
    const todosHtml = document.querySelector(".todos");
    const emptyImage = document.querySelector(".empty-image");
    let todosJson = JSON.parse(localStorage.getItem("todos")) || [];
    const deleteAllButton = document.querySelector(".delete-all");
    const filters = document.querySelectorAll(".filter");
    let filter = '';
    
    showTodos();
    
    function getTodoHtml(todo, index) {
      if (filter && filter != todo.status) {
        return '';
      }
      let checked = todo.status == "completed" ? "checked" : "";
      return /* html */ `
        <li class="todo">
          <label for="${index}">
            <input id="${index}" onclick="updateStatus(this)" type="checkbox" ${checked}>
            <span class="${checked}">${todo.name}</span>
          </label>
          <button class="delete-btn" data-index="${index}" onclick="remove(this)"><i class="fa fa-times"></i></button>
        </li>
      `; 
    }
    
    function showTodos() {
      if (todosJson.length == 0) {
        todosHtml.innerHTML = '';
        emptyImage.style.display = 'block';
      } else {
        todosHtml.innerHTML = todosJson.map(getTodoHtml).join('');
        emptyImage.style.display = 'none';
      }
    }
    
    function addTodo(todo)  {
      input.value = "";
      todosJson.unshift({ name: todo, status: "pending" });
      localStorage.setItem("todos", JSON.stringify(todosJson));
      showTodos();
    }
    
    input.addEventListener("keyup", e => {
      let todo = input.value.trim();
      if (!todo || e.key != "Enter") {
        return;
      }
      addTodo(todo);
    });
    
    addButton.addEventListener("click", () => {
      let todo = input.value.trim();
      if (!todo) {
        return;
      }
      addTodo(todo);
    });
    
    function updateStatus(todo) {
      let todoName = todo.parentElement.lastElementChild;
      if (todo.checked) {
        todoName.classList.add("checked");
        todosJson[todo.id].status = "completed";
      } else {
        todoName.classList.remove("checked");
        todosJson[todo.id].status = "pending";
      }
      localStorage.setItem("todos", JSON.stringify(todosJson));
    }
    
    function remove(todo) {
      const index = todo.dataset.index;
      todosJson.splice(index, 1);
      showTodos();
      localStorage.setItem("todos", JSON.stringify(todosJson));
    }
    
    filters.forEach(function (el) {
      el.addEventListener("click", (e) => {
        if (el.classList.contains('active')) {
          el.classList.remove('active');
          filter = '';
        } else {
          filters.forEach(tag => tag.classList.remove('active'));
          el.classList.add('active');
          filter = e.target.dataset.filter;
        }
        showTodos();
      });
    });
    
    deleteAllButton.addEventListener("click", () => {
      todosJson = [];
      localStorage.setItem("todos", JSON.stringify(todosJson));
      showTodos();
    });
πŸ“ Ressources

Music Player

A sleek audio player with playlist management. Dive into the Web Audio API and media controls.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜…

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Music player</title>
        <link rel="stylesheet" href="./style.css">
        <script src="https://code.iconify.design/iconify-icon/1.0.8/iconify-icon.min.js"></script>
    </head>
    <body>
      <div class="player">
          <div class="thumbnail">
              <img src="" id="thumb">
          </div>
          <div class="details">
              <h3 id="title"></h3>
              <span id="artist"></span>
          </div>
          <audio id="song">
              <source src="" type="audio/mp3"></source>
          </audio>
          <div class="time">
              <span id="start">00:00</span>
              <span id="end">00:00</span>
          </div>
          <input type="range" class="progress-bar" id="progress" value="0" max="0">
          <div class="action">
              <button class="prev-play">
                  <iconify-icon icon="iconoir:skip-prev-solid"></iconify-icon>
              </button>
              <button class="play" id="play">
                  <iconify-icon icon="iconoir:play-solid" class="icon-play"></iconify-icon>
                  <iconify-icon icon="iconoir:pause-solid" class="icon-pause hidden"></iconify-icon>
              </button>
              <button class="next-play">
                  <iconify-icon icon="iconoir:skip-next-solid"></iconify-icon>
              </button>
          </div>
      </div>
      <script type="module" src="script.js"></script>
    </body>
    </html>
  • style.css

    @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
    
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    :root {
      --primary-color: #0170fe;
      --secondary-color: #11d2d9;
      --white-color: #fff;
    }
    
    body {
      font-family: Poppins, sans-serif;
      min-height: 100vh;
      background-image: url("background.jpg");
      background-position: center;
      background-size: cover;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .player {
      width: 100%;
      max-width: 350px;
      backdrop-filter: blur(40px);
      box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
      padding: 60px 40px;
      border-radius: 10px;
    }
    
    img {
      width: 150px;
      height: 150px;
      border-radius: 50%;
    }
    
    .thumbnail {
      background-image: linear-gradient(120deg, var(--secondary-color),var(--primary-color));
      width: 160px;
      height: 160px;
      border-radius: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
      margin: auto;
    }
    
    img.play {
      animation: 15s rotate linear infinite;
    }
    
    @keyframes rotate {
      0% {
        transform: rotate(0deg);
      }
      100% {
        transform: rotate(360deg);
      }
    }
    
    .details {
      text-align: center;
      margin-top: 30px;
    }
    
    .details span {
      font-size: 15px;
      margin-top: 5px;
    }
    
    .time {
      margin-top: 30px;
      display: flex;
      justify-content: space-between;
    }
    
    .progress-bar {
      margin: 10px 0 30px;
      width: 100%;
      appearance: none;
      height: 6px;
      cursor: pointer;
    }
    
    .progress-bar::-webkit-slider-thumb {
      appearance: none;
      width: 12px;
      height: 12px;
      border-radius: 3px;
      background-image: linear-gradient(120deg, var(--secondary-color),var(--primary-color));
      transition: 0.2s ease-in-out;
    }
    
    .action {
      display: flex;
      justify-content: space-evenly;
    }
    
    .action button {
      width: 60px;
      height: 60px;
      background-color: var(--white-color);
      border-radius: 50%;
      border: 2px solid var(--white-color);
      cursor: pointer;
    }
    
    .action button.prev-play:hover,
    .action button.next-play:hover {
      background-color: #EEE;
    }
    
    .action button iconify-icon {
      font-size: 20px;
    }
    
    .action button.play {
      background-image: linear-gradient(120deg, var(--secondary-color),var(--primary-color));
      color: var(--white-color);
    }
    
    .action button.prev-play iconify-icon,
    .action button.next-play iconify-icon {
      color: var(--primary-color);
    }
    
    .hidden {
      display: none;
    }
  • script.js

    const songs = await fetch("songs.json").then((res) => res.json());
    
    const playBtn = document.querySelector(".icon-play");
    const pauseBtn = document.querySelector(".icon-pause");
    const song = document.querySelector("#song");
    const title = document.querySelector("#title");
    const artist = document.querySelector("#artist");
    const thumb = document.querySelector("#thumb");
    const progress = document.querySelector("#progress");
    const start = document.querySelector("#start");
    const end = document.querySelector("#end");
    
    const playButton = document.querySelector(".play");
    const prevPlayButton = document.querySelector(".prev-play");
    const nextPlayButton = document.querySelector(".next-play");
    
    playButton.addEventListener("click", playPause);
    prevPlayButton.addEventListener("click", () => changeSong(-1));
    nextPlayButton.addEventListener("click", () => changeSong(1));
    progress.addEventListener("change", updateSongProgress);
    
    let index = 0;
    let interval;
    let songProgressTrackerInterval;
    
    setSongDetails(index);
    
    function setSongDetails(songIndex) {
      song.src = songs[songIndex].link;
      title.innerHTML = songs[songIndex].name;
      artist.innerHTML = songs[songIndex].artists;
      thumb.src = songs[songIndex].image;
      start.innerHTML = "00:00";
      end.innerHTML = "00:00";
      clearInterval(interval);
      song.onloadedmetadata = loadMetadata;
    }
    
    function loadMetadata() {
      progress.max = song.duration;
      progress.value = song.currentTime;
      updateSongTimeDisplay();
      interval = setInterval(updateSongTimeDisplay, 1000);
    }
    
    function updateSongTimeDisplay() {
      let min = Math.floor(song.duration / 60);
      let sec = Math.floor(song.duration % 60);
    
      let curMin = Math.floor(song.currentTime / 60);
      let curSec = Math.floor(song.currentTime % 60);
    
      if (sec < 10) {
        sec = "0" + sec;
      }
      if (curSec < 10) {
        curSec = "0" + curSec;
      }
      if (min < 10) {
        min = "0" + min;
      }
      if (curMin < 10) {
        curMin = "0" + curMin;
      }
      end.innerHTML = min + ":" + sec;
      start.innerHTML = curMin + ":" + curSec;
    }
    
    function changeSong(increment) {
      index = (index + increment + songs.length) % songs.length;
      setSongDetails(index);
      song.play();
    }
    
    function playPause() {
      if (!pauseBtn.classList.contains("hidden")) {
        song.pause();
        pauseBtn.classList.add("hidden");
        playBtn.classList.remove("hidden");
        thumb.classList.remove("play");
        return;
      }
      song.play();
      startSongProgressTracker();
      playBtn.classList.add("hidden");
      pauseBtn.classList.remove("hidden");
      thumb.classList.add("play");
    }
    
    function updateSongProgress() {
      clearInterval(songProgressTrackerInterval);
      song.currentTime = progress.value;
      startSongProgressTracker();
    };
    
    function startSongProgressTracker() {
      clearInterval(songProgressTrackerInterval);
      songProgressTrackerInterval = setInterval(() => {
        progress.value = song.currentTime;
        if (song.currentTime == song.duration) {
          changeSong(1);
        }
      }, 1000);
    }
  • songs.json

    [
        {
            "name": "Happier",
            "link": "https://www.dropbox.com/s/zp1xfir101y4sc3/happier.mp3?raw=1",
            "artists": "Marshmello",
            "image": "https://www.dropbox.com/s/xxmwcz14hkn7iwl/happier.png?raw=1"
        },
        {
            "name": "Stay",
            "link": "https://www.dropbox.com/s/umam9olakop001d/stay.mp3?raw=1",
            "artists": "Justin Bieber",
            "image": "https://www.dropbox.com/s/kierj5lzst1yx9n/stay.jpg?raw=1"
        },
        {
            "name": "Perfect",
            "link": "https://www.dropbox.com/s/3mjzj73400sxovk/perfect.mp3?raw=1",
            "artists": "Ed Sheeran",
            "image": "https://www.dropbox.com/s/crlthbozdznb13g/perfect.jpeg?raw=1"
        },
        {
            "name": "7 Rings",
            "link": "https://www.dropbox.com/s/yo5tcfdjoz95ozf/7-rings.mp3?raw=1",
            "artists": "Ariana Grande",
            "image": "https://www.dropbox.com/s/gobvfxj4r0t053v/7-rings.jpg?raw=1"
        },
        {
            "name": "Girls Like You",
            "link": "https://www.dropbox.com/s/yi1cpg16snrl3fc/girls-like-you.mp3?raw=1",
            "artists": "Maroon 5",
            "image": "https://www.dropbox.com/s/ouq5zzgbqsk9zx0/girls-like-you.png?raw=1"
        }
    ]
πŸ“ Ressources

Weather App

Get real-time forecasts using geolocation. Connect to weather APIs.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜…

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Weather App</title>
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
      <div class="container">
        <div class="input-container">
          <input class="search-input" id="searchInput" placeholder="Search...">
          <button class="search-button" id="searchButton">
            <i class="fa fa-search"></i>
          </button>
        </div>
        <div class="weather-container">
          <div id="cityWeather" class="city-weather hidden">
            <img class="icon" id="icon" alt>
            <h1 class="temperature" id="temperature"></h1>
            <h2 class="city" id="city"></h2>
            <div class="details">
              <div class="detail">
                <img src="./images/icons/humidity.png">
                <div class="description">
                  <p class="value" id="humidity">%</p>
                  <p>Humidity</p>
                </div>
              </div>
              <div class="detail">
                <img src="./images/icons/wind.png">
                <div class="description">
                  <p class="value" id="wind">km/h</p>
                  <p>Wind</p>
                </div>
              </div>
            </div>
          </div>
          <div id="noCityFound" class="no-city-found hidden">
            <img src="images/location-not-found.png" alt>
            <p>Oops! Location not found!</p>
          </div>
        </div>
      </div>
      <script src="./script.js"></script>
    </body>
    </html>
  • style.css

    *,
    *::before,
    *::after {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    :root {
      --primary-color: #4c77b6;
      --white-color: #fff;
    }
    
    body {
      font-family: "Roboto", sans-serif;
      min-height: 100vh;
      background: url(images/background.jpg) no-repeat;
      background-position: center;
      background-size: cover;
      color: var(--white-color);
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .container {
      width: 400px;
      border: 2px solid var(--white-color);
      padding: 30px;
      border-radius: 10px;
      backdrop-filter: blur(40px);
      text-align: center;
    }
    
    .city-weather,.no-city-found {
      height: 0px;
      overflow: hidden;
      transition: height 0.7s ease;
    }
    
    .no-city-found {
      height: 300px;
    }
    
    .city-weather  {
      height: 410px;
    }
    
    .hidden {
      height: 0;
    }
    
    .input-container {
      display: flex;
      justify-content: space-between;
    }
    
    .search-input {
      flex: 1;
      padding: 10px 10px 10px 20px;
      border-radius: 30px;
      border: 2px solid transparent;
      font-size: 16px;
      margin-right: 10px;
      outline: none;
    }
    
    .search-button {
      width: 40px;
      height: 40px;
      background: var(--primary-color);
      color: var(--white-color);
      border: none;
      border-radius: 40px;
      font-size: 24px;
      cursor: pointer;
      outline: none;
    }
    
    .no-city-found img {
      width: 100%;
      margin-top: 20px;
    } 
    
    .no-city-found p {
      font-size: 24px;
    }
    
    .city-weather .icon {
      width: 170px;
    }
    
    .city-weather .temperature {
      font-size: 75px;
      font-weight: 500;
    }
    
    .city-weather .city {
      font-size: 40px;
      font-weight: 400;
    }
    
    .city-weather .details {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-top: 48px;
    }
    
    .details .detail {
      display: flex;
      align-items: center;
      column-gap: 8px;
      text-align: left;
    }
    
    .detail img {
      width: 40px;
    }
    
    .detail .value {
      font-size: 24px;
    }
  • script.js

    const API_KEY = "REPLACE_IT_WITH_YOUR_API_KEY: https://api.openweathermap.org";
    
    const searchInput = document.getElementById("searchInput");
    const searchButton = document.getElementById("searchButton");
    const city = document.getElementById("city");
    const temperature = document.getElementById("temperature");
    const humidity = document.getElementById("humidity");
    const wind = document.getElementById("wind");
    const icon = document.getElementById("icon");
    const cityWeather = document.getElementById("cityWeather");
    const noCityFound = document.getElementById("noCityFound");
    
    searchButton.addEventListener("click", updateWeatherData);
    searchInput.addEventListener("keydown",(event) => event.key === "Enter" && updateWeatherData());
    
    async function updateWeatherData() {
      try {
        if (!searchInput.value.trim()) {
          alert("Please enter the city");
          return;
        }
        const weatherData = await getWeatherData(searchInput.value);
        noCityFound.classList.add("hidden");
        cityWeather.classList.remove("hidden");
        city.textContent = weatherData.city;
        temperature.textContent = weatherData.temperature + "Β°C";
        humidity.textContent = weatherData.humidity + "%";
        wind.textContent = weatherData.wind + " km/h";
        icon.src = weatherData.icon;
      } catch (error) {
        if (error?.code === "404") {
          cityWeather.classList.add("hidden");
          noCityFound.classList.remove("hidden");
          console.error(error.message);
        } else {
          alert("Unknown error: " + error);
        }
      }
    }
    
    function getIconUrl(icon) {
      return `./images/icons/${icon.toLowerCase()}.png`;
    }
    
    async function getWeatherData(city) {
      const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${API_KEY}`;
      const apiResponse = await fetch(apiUrl);
      const apiResponseBody = await apiResponse.json();
    
      if (apiResponseBody.cod == "404" || apiResponseBody.cod == "400") {
        throw { code: "404", message: "City not found" };
      }
    
      return {
        city: apiResponseBody.name,
        temperature: Math.round(apiResponseBody?.main?.temp),
        humidity: apiResponseBody.main.humidity,
        wind: Math.round(apiResponseBody.wind.speed),
        icon: getIconUrl(apiResponseBody.weather[0].main),
      };
    }
πŸ“ Ressources

QR Code Generator

Convert URLs/text to scannable QR codes. Integrate with third-party APIs.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜…

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>QR Code Generator</title>
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
      <div class="container">
        <div class="header-container mb-35">
          <h1>QR Code Generator</h1>
          <h2>
            Online tool to generate QR Codes from texts, urls and much more.
          </h2>
        </div>
        <input type="text" class="text-input mb-35" placeholder="Enter text or a link">
        <button class="generate-button">Generate QR Code</button>
        <div class="qrcode-container">
          <div class="qrcode"></div>
        </div>
        <button class="download-button hidden">Download QR Code</button>
      </div>
      <script src="https://cdn.rawgit.com/davidshimjs/qrcodejs/gh-pages/qrcode.min.js"></script>
      <script src="script.js"></script>
    </body>
    </html>
  • style.css

    @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
    
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    :root {
      --primary-color: #662d72;
      --secondary-color: #eb4e9b; 
      --background-color: #f3f2f245;
      --hover-1-color: #783885;
      --hover-2-color: #f15ea6;
      --text-color: #bb408c;
    }
    
    body {
      font-family: Poppins, sans-serif;
      min-height: 100vh;
      background: url(background.jpg) no-repeat;
      background-position: center;
      background-size: cover;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .hidden {
      display: none;
    }
    
    .container {
      background: var(--background-color);
      width: 500px;
      padding: 50px;
      backdrop-filter: blur(40px);
      border-radius: 10px;
      display: flex;
      flex-direction: column;
    }
    
    .header-container {
      text-align: center;
    }
    
    .mb-35 {
      margin-bottom: 35px;
    }
    
    h1 {
      color: var(--primary-color);
      font-size: 36px;
      margin-bottom: 20px;
    }
    
    h2 {
      color: var(--text-color);
      font-size: 22px;
    }
    
    input {
      font-size: 18px;
      padding: 10px;
      border: none;
      border-radius: 5px;
      outline: none;
    }
    
    button {
      font-size: 18px;
      padding: 12px 0;
      border: none;
      border-radius: 5px;
      color: white;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    
    .generate-button {
      background-color: var(--primary-color);
    }
    
    .generate-button:hover {
      background-color: var(--hover-1-color);
    }
    
    .download-button {
      background-color:var(--secondary-color)
    }
    
    .download-button:hover {
      background-color:var(--hover-2-color);
    }
    
    .qrcode {
      display: flex;
      justify-content: center;
      height: 0;
      overflow: hidden;
      transition: height 0.35s linear;
    }
    
    .qrcode img {
      padding: 10px;
      background: white;
      border-radius: 3px;
      height: 250px;
      width: 250px;
    }
  • script.js

    const textInput = document.querySelector(".text-input");
    const generateButton = document.querySelector(".generate-button");
    const qrCodeContainer = document.querySelector(".qrcode-container");
    const qrCodeElement = document.querySelector(".qrcode");
    const downloadButton = document.querySelector(".download-button");
    
    generateButton.addEventListener("click", generateQRCode);
    textInput.addEventListener("keydown",(event) => event.key === "Enter" && generateQRCode());
    downloadButton.addEventListener("click", download);
    
    function generateQRCode() {
      const text = textInput.value.trim();
      if (!text) {
        alert("Please enter some text to generate a QR code.");
        return;
      }
      
      const firstGeneration = qrCodeElement.innerHTML == "";
      qrCodeElement.innerHTML = "";
      qrCodeElement.style.height = "0";
      
      if (!firstGeneration) {
        qrCodeContainer.style.height = "250px";
      }
    
      new QRCode(qrCodeElement, {
        text: text,
        width: 250,
        height: 250,
      });
    
      setTimeout(() => {
        qrCodeElement.style.height = "250px";
        downloadButton.classList.remove("hidden");
        generateButton.classList.add("mb-35");
        qrCodeContainer.classList.add("mb-35");
      }, firstGeneration ? 0 : 750);
    }
    
    function download() {
      const qrCodeImage = document.querySelector(".qrcode img");
      const downloadLink = document.createElement("a");
      downloadLink.href = qrCodeImage.src;
      downloadLink.download = "qrcode.png";
      downloadLink.click();
    }
πŸ“ Ressources

Calculator

A fully functional calculator with keyboard support. Perfect for understanding mathematical operations and event handling in JavaScript.

πŸ“Œ Difficulty: β˜…β˜…β˜†β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Calculator</title>
        <link rel="stylesheet" href="./style.css" />
    </head>
    <body>
      <div class="container">
        <h1>Calculator</h1>
        <div class="display">
          <div class="expression">0</div>
          <div class="result hidden"></div>
        </div>
        <div class="buttons">
          <div class="button">C</div>
          <div class="button">(</div>
          <div class="button">)</div>
          <div class="button">/</div>
          <div class="button">7</div>
          <div class="button">8</div>
          <div class="button">9</div>
          <div class="button">*</div>
          <div class="button">4</div>
          <div class="button">5</div>
          <div class="button">6</div>
          <div class="button">-</div>
          <div class="button">1</div>
          <div class="button">2</div>
          <div class="button">3</div>
          <div class="button">+</div>
          <div class="button">0</div>
          <div class="button valuse">00</div>
          <div class="button">.</div>
          <div class="button equals">=</div>
        </div>
      </div>
      <script src="script.js"></script>
    </body>
    
    </html>
  • style.css

    @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
    
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    :root {
      --primary-color: #7552a0;
      --secondary-color: #43377f;
      --text-color: #fff;
      --border-color: #7552a0A0;
    }
    
    body {
      font-family: Poppins, sans-serif;
      min-height: 100vh;
      background: url(background.jpg) no-repeat;
      background-position: center;
      background-size: cover;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .hidden {
      display: none;
    }
    
    .container {
      width: 300px;
      padding: 30px;
      border: 2px var(--border-color) solid;
      border-radius: 10px;
      backdrop-filter: blur(20px);
    }
    
    @media only screen and (min-width: 768px) {
      .container {
        width: 400px
      }
    }
    
    h1 {
      text-align: center;
      color: var(--text-color);
      font-size: 30px;
      margin-bottom: 16px;
    }
    
    @media only screen and (min-width: 576px) {
      h1 {
        font-size: 40px;
      }
    }
    
    .display {
      height: 200px;
      border: 2px var(--border-color) solid;
      border-radius: 5px;
      margin-bottom: 25px;
      display: flex;
      flex-direction: column;
      justify-content: end;
      align-items: end;
    }
    
    .buttons {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      grid-gap: 20px;
    }
    
    .display .expression {
      font-size: 36px;
      padding: 0 10px;
    }
    
    .display .result {
      font-size: 46px;
      font-weight: 500;
      padding: 0 10px;
    }
    
    .display .result::before {
      content: '=';
      padding-right: 5px;
    }
    
    
    .button {
      border: 2px solid var(--primary-color);
      border-radius: 100px;
      width: 45px;
      height: 45px;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 20px;
      cursor: pointer;
      user-select: none;
      transition: all .2s ease-in;
    }
    
    @media only screen and (min-width: 768px) {
      .button {
        width: 60px;
        height: 60px;
        font-size: 24px;
      }
    }
    
    .button:hover {
      background-color: var(--secondary-color);
      color: var(--text-color);
      border-color: transparent;
    }
    
    .button:nth-child(1),
    .button:nth-child(2),
    .button:nth-child(3),
    .button:nth-child(4),
    .button:nth-child(8),
    .button:nth-child(12),
    .button:nth-child(16),
    .button:nth-child(20) {
      border: none;
      background-color: var(--primary-color);
      color: var(--text-color);
      font-weight: 600;
    }
    
    :is(
      .button:nth-child(1),
      .button:nth-child(2),
      .button:nth-child(3),
      .button:nth-child(4),
      .button:nth-child(8),
      .button:nth-child(12),
      .button:nth-child(16),
      .button:nth-child(20)
      ):hover {
        opacity: .85;
    } 
  • script.js

    const displayContainer = document.querySelector('.display');
    const displayExpression = document.querySelector('.display .expression');
    const displayResult = document.querySelector('.display .result');
    
    const isResultHidden = () => displayResult.classList.contains('hidden');
    const isResultShown = () => !isResultHidden();
    const getResultValue = () => displayResult.innerText == 'Error' ? '0' : displayResult.innerText;
    const isSymbol = (button) => /[\+\-\*\/]$/.test(button.innerHTML);
    
    document.querySelectorAll('.button').forEach(button => 
      button.addEventListener('click', () => handleButtonClick(button)));
    
    function handleButtonClick(button) {
      switch(button.innerHTML) {
        case 'C': {
          displayExpression.innerHTML = '0';
          displayResult.classList.add('hidden');
          break;
        }
        case '=': {
          try {
            displayResult.classList.remove('hidden');
            displayResult.innerHTML = eval(displayExpression.innerHTML);
          } catch {
            displayResult.innerHTML = "Error";
          }
          break;
        }
        default: {
          if (isResultShown()) {
            displayExpression.innerHTML = isSymbol(button) ? getResultValue() : '0';
            displayResult.classList.add('hidden');
          }
          if (displayExpression.innerHTML == '0') {
            displayExpression.innerHTML = button.innerHTML == '00' ? '0' : button.innerHTML;
          } else {
            displayExpression.innerHTML += button.innerHTML;
          }
        }
      }
    }
πŸ“ Ressources

BMI Calculator

A health tool that calculates Body Mass Index with visual feedback. Learn form validation and dynamic DOM updates.

πŸ“Œ Difficulty: β˜…β˜…β˜†β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>BMI Calculator</title>
        <link rel="stylesheet" href="./style.css" />
    </head>
    <body>
      <div class="container">
        <div class="header-container">
          <h1>BMI Calculator</h1>
          <h2>
            Calculate your Body Mass Index easily and accurately
          </h2>
        </div>
        <form autocomplete="off">
          <div class="inputs-container">
            <div class="input-container height-container">
              <input id="height" placeholder=" ">
              <p class="error"></p>
              <label for="height">Your height in cm</label>
            </div>
            <div class="input-container weight-container">
              <input id="weight" placeholder=" ">
              <p class="error"></p>
              <label for="weight">Your weight in kg</label>
            </div>
          </div>
          <button>Calculate your BMI</button>
        </form>
        <div class="bmi-result">
          <p class="bmi-value"></p>
          <p class="bmi-description"></p>
        </div>
      </div>
      <div class="container description-container">
        <h2>Description</h2>
        <p class="description">
          Let's calculate the Body Mass Index (BMI) to 
          assess the risks associated with overweight based on the person's height and weight. 
          According to the World Health Organization (WHO), the formula is <b>BMI = W/HΒ²</b>, 
          where <b>W</b> is the person's weight in kilograms and <b>H</b> is the height in meters
        </p>
        <div class="bmi-ranges">
          <div class="bmi-range-container underweight">
            <p class="bmi-range"><18,5</p>
            <p class="bmi-range-label">Underweight</p>
          </div>
          <div class="bmi-range-container normal">
            <p class="bmi-range">18,5-25</p>
            <p class="bmi-range-label">Normal</p>
          </div>
          <div class="bmi-range-container overweight">
            <p class="bmi-range">25-30</p>
            <p class="bmi-range-label">Overweight</p>
          </div>
          <div class="bmi-range-container obesity">
            <p class="bmi-range">30-40</p>
            <p class="bmi-range-label">Obese</p>
          </div>
          <div class="bmi-range-container morbid-obesity">
            <p class="bmi-range">>40</p>
            <p class="bmi-range-label">Extremely obesity</p>
          </div>
        </div>
      </div>
      <script src="script.js"></script>
    </body>
    </html>
  • style.css

    @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
    
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    :root {
      --primary-color: #b34083;
      --primary-text-color: #fff;
      --secondary-color: #f0b23a; 
      --secondary-text-color: #ea96c7;
      --hover-color: #983979; 
      --border-color: #d78fb980;
      --error-color: #f94a4a;
    }
    
    body {
      font-family: Poppins, sans-serif;
      min-height: 100vh;
      background: url(background.jpg) no-repeat;
      background-position: center;
      background-size: cover;
      color: var(--primary-text-color);
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      padding: 100px 0;
    }
    
    .container {
      max-width: 700px;
      border: 2px var(--border-color) solid;
      padding: 50px;
      border-radius: 10px;
      backdrop-filter: blur(10px);
    }
    
    .header-container {
      text-align: center;
      margin-bottom: 45px;
    }
    
    h1 {
      font-size: 30px;
      display: inline-block;
      background: linear-gradient(to right, var(--secondary-color), var(--primary-color));
      -webkit-text-fill-color: transparent;
      background-clip: text;
      -webkit-background-clip: text;
      margin-bottom: 12px;
    }
    
    @media only screen and (min-width: 576px) {
      h1 {
        font-size: 40px;
      }
    }
    
    h2 {
      font-size: 18px;
      font-weight: 400;
      color: var(--secondary-text-color);
    }
    
    @media only screen and (min-width: 576px) {
      h2 {
        font-size: 22px;
      }
    }
    
    form {
      display: flex;
      flex-direction: column;
      row-gap: 20px;
    }
    
    .inputs-container {
      display: flex;
      flex-direction: column;
      row-gap: 20px;
    }
    
    .input-container {
      display: flex;
      flex-direction: column;
      flex: 1;
      position: relative;
    }
    
    label {
      font-size: 18px;
      font-weight: 500;
      position: absolute;
      left: 0px;
      top: 0px;
      transition: all 0.2s ease;
      pointer-events: none;
    }
    
    input {
      background: transparent;
      padding: 8px 0;
      border: none;
      border-bottom: 2px solid var(--primary-text-color);
      margin-top: 5px;
      font-size: 18px;
      outline: none;
      font-weight: 600;
      color: var(--secondary-color);
    }
    
    input:focus ~ label,
    input:not(:placeholder-shown) ~ label {
      top: -16px;
      font-size: 13px;
    }
    
    .input-container.error input {
      color: var(--error-color);
      border-color: var(--error-color);
      outline-color: transparent;
    }
    
    .input-container .error {
      color: var(--error-color);
      font-size: 12px;
      font-weight: 600;
      height: 20px;
    }
    
    button {
      background-color: var(--primary-color);
      color: var(--primary-text-color);
      font-size: 20px;
      padding: 12px;
      border: none;
      border-radius: 3px;
      cursor: pointer;
      transition: background-color 0.2s ease-in;
    }
    
    button:hover, button:focus {
      background-color: var(--hover-color);
    }
    
    .bmi-result {
      margin-top: 20px;
      height: 0;
      transition: height 0.2s ease-in;
    }
    
    .bmi-result .bmi-value,
    .bmi-result .bmi-description {
      text-align: center;
    }
    
    .bmi-result .bmi-value {
      font-size: 64px;
      font-weight: 600;
    }
    
    .bmi-result .bmi-description {
      font-size: 20px;
      font-weight: 500;
    }
    
    .container.description-container {
      margin-top: 60px;
    }
    
    .description {
      margin-top: 20px;
      font-size: 15px;
      text-align: justify;
    }
    
    .bmi-ranges {
      margin-top: 30px;
      display: flex;
      column-gap: 5px;
      row-gap: 5px;
      flex-wrap: wrap;
    }
    
    .bmi-range-container {
      --color: #eee;
      flex: 1;
      border: 2px solid var(--color);
      display: flex;
      flex-direction: column;
      border-radius: 3px;
      min-width: 82px;
    }
    
    .bmi-range {
      text-align: center;
      padding: 6px 10px;
      background-color: var(--color);
      font-weight: 600;
    }
    
    .bmi-range-label {
      padding: 6px 10px;
      height: 60px;
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      font-weight: 600;
      font-size: 14px;
    }
    
    .underweight {
      --color: #5BC8F3;
    }
    .normal {
      --color: #9DB63B;
    }
    .overweight {
      --color: #FAC90E;
    }
    .obesity {
      --color: #F48A20;
    }
    .morbid-obesity {
      --color: #FF0000;
    }
  • script.js

    const BMIRanges = [
      { description: "Underweight", color: "#5BC8F3", range: [0, 18.5] },
      { description: "Normal", color: "#9DB63B", range: [18.5, 25] },
      { description: "Overweight", color: "#FAC90E", range: [25, 30] },
      { description: "Obese", color: "#F48A20", range: [30, 40] },
      { description: "Extremely obesity", color: "#FF0000",range: [40, Number.MAX_VALUE]}
    ];
    
    const form = document.querySelector("form");
    const heightInput = document.querySelector("#height");
    const heightInputError = document.querySelector("#height + .error");
    const weightInput = document.querySelector("#weight");
    const weightInputError = document.querySelector("#weight + .error");
    const bmiValue = document.querySelector(".bmi-value");
    const bmiDescription = document.querySelector(".bmi-description");
    const bmiResult = document.querySelector(".bmi-result");
    
    form.addEventListener("submit", handleForm);
    heightInput.addEventListener("keyup", validateInput);
    weightInput.addEventListener("keyup", validateInput);
    
    function handleForm(event) {
      event.preventDefault();
    
      resetErrors();
      let containsErrors = false;
      if (isNotNumber(heightInput)) {
        containsErrors = true;
        showError(heightInput);
      }
      if (isNotNumber(weightInput)) {
        containsErrors = true;
        showError(weightInput);
      }
      if (containsErrors) {
        return;
      }
    
      const height = parseInt(heightInput.value);
      const weight = parseInt(weightInput.value);
      showBmi(calculateBmi(weight, height));
    }
    
    function validateInput(event) {
      if (event.key == "Tab") return;
      const input = event.target;
      isNotNumber(input) ? showError(input) : resetError(input);
    }
    
    const isNumber = (input) => /^[0-9]+$/.test(input.value);
    
    const isNotNumber = (input) => !isNumber(input);
    
    function showBmi(BMI) {
      const bmiRange = getBmiRange(BMI);
      bmiValue.innerHTML = BMI;
      bmiValue.style.color = bmiRange.color;
      bmiDescription.innerHTML = bmiRange.description;
      bmiDescription.style.color = bmiRange.color;
      bmiResult.style.height = "115px";
    }
    
    function calculateBmi(weight, height) {
      return (weight / Math.pow(height / 100, 2)).toFixed(1);
    }
    
    function resetError(input) {
      input.parentElement.classList.remove("error");
      input.nextElementSibling.innerHTML = "";
    }
    
    function showError(input) {
      bmiValue.innerHTML = "";
      bmiDescription.innerHTML = "";
      bmiResult.style.height = "0";
      input.parentElement.classList.add("error");
      input.nextElementSibling.innerHTML = "Value invalide";
    }
    
    function resetErrors() {
      heightInput.parentElement.classList.remove("error");
      weightInput.parentElement.classList.remove("error");
      heightInputError.innerHTML = "";
      weightInputError.innerHTML = "";
    }
    
    function getBmiRange(BMI) {
      return BMIRanges.find((data) => BMI >= data.range[0] && BMI < data.range[1]);
    }
πŸ“ Ressources

Countdown Timer

An interactive timer with pause/resume functionality. Master date handling and UI animations.

πŸ“Œ Difficulty: β˜…β˜…β˜†β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>COUNT DOWN</title>
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
      <main>
        <h1>Comming Soon</h1>
        <div class="container">
          <div class="counter-container">
            <span id="days" class="counter">0</span>
            <span>Days</span>
          </div>
          <div class="counter-container">
            <span id="hours" class="counter">0</span>
            <span>Hours</span>
          </div>
          <div class="counter-container">
            <span id="minutes" class="counter">0</span>
            <span>Minutes</span>
          </div>
          <div class="counter-container">
            <span id="seconds" class="counter">0</span>
            <span>Seconds</span>
          </div>
        </div>
      </main>
      <script src="./script.js"></script>
      <script src="./snow.js"></script>
    </body>
    </html>
  • style.css

    *,
    *::before,
    *::after {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: 'Roboto', sans-serif;
      height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      background: url(./background.png) no-repeat;
      background-position: center;
      background-size: cover;
    }
    
    h1 {
      text-align: center;
      color: #C75263;
      font-size: 50px;
      margin-bottom: 30px;
      text-transform: uppercase;
    }
    
    .container {
      display: flex;
    }
    
    .counter-container {
      width: 120px;
      height: 120px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      text-align: center;
      background: transparent;
      border: 2px solid #8c1c44;
      border-radius: 10px;
      backdrop-filter: blur(15px);
      color: #8c1c44;
      margin: 0 5px;
      text-transform: uppercase;
    }
    
    .counter {
      display: block;
      font-size: 35px;
      font-weight: 600;
    }
  • script.js

    const countDownDate = new Date("Jan 1, 2024 00:00:00").getTime();
    const days = document.getElementById("days");
    const hours = document.getElementById("hours");
    const minutes = document.getElementById("minutes");
    const seconds = document.getElementById("seconds");
    
    const interval = setInterval(() => {
      const now = new Date().getTime();
      const duration = countDownDate - now;
      
      if (duration < 0) {
        clearInterval(interval);
        updateDuration(0);
        return;
      }
      
      updateDuration(duration);
      }, 1000);
    
    function updateDuration(duration)  {
      const days = Math.floor(duration / (1000 * 60 * 60 * 24));
      const hours = Math.floor((duration % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60));
      const seconds = Math.floor((duration % (1000 * 60)) / 1000);
      
      days.innerHTML = days;
      hours.innerHTML = hours;
      minutes.innerHTML = minutes;
      seconds.innerHTML = seconds;
    }
πŸ“ Ressources

Password Generator

Create secure passwords with customizable rules. Learn randomization and strength validation.

πŸ“Œ Difficulty: β˜…β˜…β˜†β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Password generator</title>
      <script src="https://code.iconify.design/iconify-icon/1.0.8/iconify-icon.min.js"></script>
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
      <div class="container">
        <div class="header-container">
          <h1 class="gradient-font">Password Generator</h1>
          <h2>
            Online tool to generate strong and secure password difficult to crack
          </h2>
        </div>
        <div class="generated-password-container" title="Copy Password">
          <span class="password" id="password"></span>
          <button class="icon-button copy-button" id="copyButton" title="Copy Password">
            <iconify-icon icon="ci:copy" class="icon"></iconify-icon>
          </button>
        </div>
        <div class="options-container">
          <div class="length-container">
            <p class="label">
              LENGTH:
              <span id="lengthValue">20</span>
            </p>
            <div class="length">
              <span>5</span>
              <div class="length-container">
                <input type="range" min="5" max="30" value="20" id="lengthInput">
              </div>
              <span>30</span>
            </div>
          </div>
          <div class="error-message" id="errorMessage">
            At least one option must be selected
          </div>
          <p class="label">SETTINGS</p>
          <div class="boolean-options-container">
            <label for="includeUppercase">
              Include Uppercase
              <input type="checkbox" id="includeUppercase" checked>
              <div class="toggle-switch"></div>
            </label>
            <label for="includeLowercase">
              Include Lowercase
              <input type="checkbox" id="includeLowercase" checked>
              <div class="toggle-switch"></div>
            </label>
            <label for="includeNumber">
              Include Numbers
              <input type="checkbox" id="includeNumber" checked>
              <div class="toggle-switch"></div>
            </label>
            <label for="includeSymbols">
              Include Symbols
              <input type="checkbox" id="includeSymbols" checked>
              <div class="toggle-switch"></div>
            </label>
          </div>
        </div>
        <button class="primary" type="button" id="generatePasswordButton">
          Generate Password
        </button>
      </div>
      <script src="script.js"></script>
    </body>
    
    </html>
  • style.css

    @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
    @import url("https://fonts.googleapis.com/css?family=Roboto Mono");
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    :root {
      --primary-color: #ee4339;
      --secondary-color: #ffdbc5e3;
      --brick-red: #820233;
      --text-color: #f6dbce;
      --white-color: #fff;
      --border-color: #ffe3e299;
    }
    
    body {
      font-family: Poppins, sans-serif;
      min-height: 100vh;
      background: url(background.jpg) no-repeat;
      background-position: center;
      background-size: cover;
      color: var(--brick-red);
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .hidden {
      display: none;
    }
    
    .container {
      max-width: 850px;
      border: 2px var(--border-color) solid;
      padding: 50px;
      border-radius: 10px;
      box-shadow: 0px 0px 7px 0 rgba(0, 0, 0, 0.1);
      backdrop-filter: blur(40px);
    }
    
    .header-container {
      text-align: center;
      margin-bottom: 30px;
    }
    
    h1 {
      font-size: 30px;
      margin-bottom: 12px;
      color: var(--primary-color);
    }
    
    @media only screen and (min-width: 576px) {
      h1 {
        font-size: 40px;
      }
    }
    
    h2 {
      font-size: 14px;
      font-weight: 500;
    }
    
    @media only screen and (min-width: 576px) {
      h2 {
        font-size: 17px;
      }
    }
    
    .generated-password-container {
      background: var(--secondary-color);
      border: var(--border-color) solid 2px;
      height: 53px;
      border-radius: 5px;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-bottom: 32px;
      cursor: pointer;
      position: relative;
    }
    
    .password {
      font-size: 14px;
      font-weight: 600;
      font-family: "Roboto Mono";
    }
    
    @media only screen and (min-width: 576px) {
      .password {
        font-size: 18px;
      }
    }
    
    .icon-button {
      all: initial;
      position: absolute;
      right: 8px;
      height: 20px;
      width: 20px;
      border: 2px solid transparent;
      border-radius: 3px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      background-color: transparent;
      transition: background-color 0.25s ease;
    }
    
    @media only screen and (min-width: 576px) {
      .icon-button {
        height: 30px;
        width: 30px;
      }
    }
    
    .icon-button:hover {
      background-color: var(--brick-red);
    }
    
    .icon {
      font-size: 25px;
      color: var(--brick-red);
      transition: color 0.25s ease;
    }
    
    .icon-button:hover .icon {
      color: var(--text-color);
    }
    
    .length {
      background: var(--secondary-color);
      padding: 13px;
      margin-bottom: 20px;
      border-radius: 5px;
      display: flex;
      align-items: center;
      justify-content: center;
      column-gap: 15px;
    }
    
    .length-container {
      width: 100%;
    }
    
    input[type="range"] {
      all: initial;
      background: var(--brick-red);
      width: 100%;
      height: 7px;
      background-image: linear-gradient(var(--primary-color), var(--primary-color));
      background-size: 60% 100%;
      background-repeat: no-repeat;
      border-radius: 5px;
      cursor: pointer;
    }
    
    input[type="range"]::-webkit-slider-thumb {
      appearance: none;
      width: 20px;
      height: 20px;
      background-color: var(--primary-color);
      border-radius: 50%;
    }
    
    .options-container .label {
      color: var(--text-color);
    }
    
    .boolean-options-container {
      display: grid;
      grid-template-columns: 1fr;
      row-gap: 15px;
      column-gap: 15px;
    }
    
    @media only screen and (min-width: 576px) {
      .boolean-options-container {
        grid-template-columns: 1fr 1fr;
      }
    }
    
    label {
      background: var(--secondary-color);
      padding: 11px;
      border-radius: 5px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      font-size: 12px;
      font-weight: 600;
      cursor: pointer;
    }
    
    .toggle-switch {
      background-color: var(--brick-red);
      width: 40px;
      height: 22px;
      border-radius: 100px;
      position: relative;
      cursor: pointer;
      transition: background-color 0.25s ease;
    }
    
    .toggle-switch:before {
      content: "";
      background-color: var(--white-color);
      height: 18px;
      width: 18px;
      position: absolute;
      top: 2px;
      left: 4px;
      border-radius: 100px;
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
      transition: left 0.25s ease;
    }
    
    input {
      display: none;
    }
    
    input:checked + .toggle-switch {
      background-color: var(--primary-color);
    }
    
    input:checked + .toggle-switch:before {
      left: 20px;
    }
    
    button {
      background-color: var(--primary-color);
      color: var(--text-color);
      width: 100%;
      margin-top: 40px;
      padding: 15px 0;
      border: none;
      border-radius: 5px;
      text-transform: uppercase;
      font-weight: 600;
      letter-spacing: 1.6px;
      cursor: pointer;
    }
    
    .error-message {
      color: var(--primary-color);
      font-size: 20px;
      font-weight: 600;
      text-align: center;
      margin-top: 10px;
      display: none;
    }
  • script.js

    const generatedPassword = document.getElementById("password");
    const lengthValue = document.getElementById("lengthValue");
    const lengthInput = document.getElementById("lengthInput");
    const options = document.querySelector(".boolean-options-container")
    const includeLowercase = document.getElementById("includeLowercase");
    const includeUppercase = document.getElementById("includeUppercase");
    const includeNumber = document.getElementById("includeNumber");
    const includeSymbols = document.getElementById("includeSymbols");
    const errorMessage = document.getElementById("errorMessage");
    const generatePasswordButton = document.getElementById("generatePasswordButton");
    const copyButton = document.getElementById("copyButton");
    
    lengthInput.addEventListener("input", handleLengthChange);
    includeLowercase.addEventListener("change", handleCheckboxChange);
    includeUppercase.addEventListener("change", handleCheckboxChange);
    includeNumber.addEventListener("change", handleCheckboxChange);
    includeSymbols.addEventListener("change", handleCheckboxChange);
    generatePasswordButton.addEventListener("click", generatePassword);
    copyButton.addEventListener("click", copyPasswordToClipboard);
    
    generatePassword();
    
    function generatePassword() {
      let chars = buildCharacterString();
      let passwordLength = lengthValue.textContent;
      let password = "";
      for (let i = 1; i <= passwordLength; i++) {
        password += generateRandomCharacter(chars);
      }
      generatedPassword.innerText = password;
    }
    
    function buildCharacterString() {
      let chars = "";
    
      if (includeLowercase.checked) {
        chars += "abcdefghijklmnopqrstuvwxyz";
      }
    
      if (includeUppercase.checked) {
        chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
      }
    
      if (includeNumber.checked) {
        chars += "0123456789";
      }
    
      if (includeSymbols.checked) {
        chars += "!@#$%^&*()";
      }
    
      return chars;
    }
    
    function generateRandomCharacter(chars) {
      let randomNumber = Math.floor(Math.random() * chars.length);
      return chars.substring(randomNumber, randomNumber + 1);
    }
    
    function handleLengthChange(event) {
      lengthValue.textContent = event.target.value;
      lengthInput.style.backgroundSize = ((event.target.value - 5) / 25) * 100 + "% 100%";
      generatePassword();
    }
    
    function handleCheckboxChange(event) {
      errorMessage.style.display = "none";
      if (isNoOptionChecked()) {
        errorMessage.style.display = "block";
        event.target.checked = true;
        return;
      }
      generatePassword();
    }
    
    const isAtLeastOneOptionChecked = () => options.querySelector("input[type=checkbox]:checked") ? true : false;
    
    const isNoOptionChecked = () => !isAtLeastOneOptionChecked();
    
    async function copyPasswordToClipboard() {
      try {
        if (!navigator?.clipboard?.writeText) return;
    
        await navigator.clipboard.writeText(generatedPassword.textContent);
        const generatedPasswordValue = generatedPassword.textContent;
        generatedPassword.textContent = 'Copied!';
        setTimeout(() => generatedPassword.textContent = generatedPasswordValue, "1000");
      
      } catch (err) {
        console.error(err);
      }
    }
πŸ“ Ressources

Quiz App

A timed trivia game with scoring. Perfect for array manipulation practice.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Quiz App</title>
        <link rel="stylesheet" href="./style.css">
      </head>
      <body>
        <div class="container" id="quiz">
          <div class="header-container">
            <h1>Quiz App</h1>
            <h2 id="question">Question text</h2>
          </div>
          <div class="options">
          </div>
          <button id="submit">Submit</button>
        </div>
        <script type="module" src="script.js"></script>
      </body>
    </html>
  • style.css

    @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
    
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    :root {
      --primary-color: #a5183b;
      --secondary-color: #ff6e3b;
      --text-color: #680131;
      --white-color: #fff;
      --border-color: #a5183b21;
      --option-background-color :#f7efdca8;
      --option-hover-background-color :#f7efdc;
    }
    
    body {
      font-family: "Poppins", sans-serif;
      min-height: 100vh;
      background-image: url("background.jpg");
      background-position: center;
      background-size: cover;
      color: var(--text-color);
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .container {
      backdrop-filter: blur(100px);
      padding: 50px;
      border: 2px var(--border-color) solid;
      border-radius: 10px;
    }
    
    @media(min-width: 768px) {
      .container {
        width: 700px;
      }
    }
    
    @media(min-width: 992px) {
      .container {
        width: 900px;
      }
    }
    
    .header-container {
      text-align: center;
      margin-bottom: 30px;
    }
    
    h1 {
      color: var(--primary-color);
      font-size: 30px;
      margin-bottom: 12px;
    }
    
    @media only screen and (min-width: 576px) {
      h1 {
        font-size: 40px;
      }
    }
    
    h2 {
      font-size: 18px;
      font-weight: 600;
    }
    
    @media only screen and (min-width: 576px) {
      h2 {
        font-size: 22px;
      }
    }
    
    button {
      width: 100%;
      font-size: 20px;
      background-color: var(--primary-color);
      color: var(--white-color);
      padding: 14px 0;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: opacity 0.5s ease;
    }
    
    button:hover, button:focus {
      opacity: 0.90;
    }
    
    .options {
      display: grid;
      grid-template-columns: 1fr;
      column-gap: 15px;
      row-gap: 15px;
      margin: 0 0 35px;
    }
    
    @media(min-width: 768px) {
      .options {
        grid-template-columns: repeat(2, 1fr);
      }
    }
    
    .option {
      background-color: var(--option-background-color);
      display: flex;
      align-items: center;
      padding: 10px 20px;
      border-radius: 5px;
      font-size: 18px;
      cursor: pointer;
      transition: background-color 0.5s ease;
    }
    
    .option:hover {
      background-color: var(--option-hover-background-color);
    }
    
    .option.selected {
      background-color: var(--secondary-color);
    }
    
    .option.selected span {
      color: var(--white-color); 
    }
    
    .index {
      background-color: var(--secondary-color);
      color: var(--white-color);
      width: 40px;
      height: 40px;
      min-width: 40px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 10px;
      font-size: 22px;
      text-transform: uppercase;
      transition: background-color 1s ease;
    }
    
    .option.selected .index {
      color: var(--primary-color);
      background-color: var(--white-color);
    }
    
    .answer {
      text-align: center;
      margin-bottom: 40px;
    }
    
    .score {
      color: var(--primary-color);
      font-size: 30px;
      font-weight: 900;
    }
  • script.js

    const quizData = await fetch("quizzes.json").then((res) => res.json());
    const container = document.querySelector(".container");
    const options = document.querySelector(".options");
    const questionEl = document.getElementById("question");
    const submitBtn = document.getElementById("submit");
    
    let currentQuiz = 0;
    let score = 0;
    
    loadQuiz();
    options.addEventListener("click", selectAnswer);
    submitBtn.addEventListener("click", answerQuestion);
    
    function loadQuiz() {
      const currentQuizData = quizData[currentQuiz];
      const optionIndices = ["a", "b", "c", "d"];
    
      questionEl.innerText = currentQuizData.question;
      options.innerHTML = "";
      currentQuizData.options.forEach((option, index) => {
        options.innerHTML += `
            <div class="option" data-option="${option}">
              <div class="index">${optionIndices[index]}</div>
              <span class="option-answer">${option}</span>
            </div>`;
      });
    }
    
    function selectAnswer(event) {
      const selectedOption = event.target.closest('.option');
      if (!selectedOption) {
        return;
      }
      
      const previouslySelectedOption = options.querySelector('.option.selected');
      if (previouslySelectedOption) {
        previouslySelectedOption.classList.remove("selected");
      }
    
      selectedOption.classList.add("selected");
    }
    
    function answerQuestion() {
      const selectedOption = getSelected();
      if (isAnswerCorrect(selectedOption)) {
        score++;
      }
      currentQuiz++;
      if (!isQuizComplete()) {
        loadQuiz();
      } else {
        showResults();
      }
    }
    
    function getSelected() {
      const selectedOption = [...options.querySelectorAll(".option")].find(
        (option) => option.classList.contains("selected")
      );
      return selectedOption ? selectedOption.getAttribute("data-option") : null;
    }
    
    function isAnswerCorrect(selectedOption) {
      return selectedOption && selectedOption === getCurrentCorrectAnswer();
    }
    
    function getCurrentCorrectAnswer() {
      return quizData[currentQuiz].correct;
    }
    
    function isQuizComplete() {
      return currentQuiz >= quizData.length;
    }
    
    function showResults() {
      container.innerHTML = `
        <h2 class="answer">You answered correctly at <span class="score">${score}/${quizData.length}</span> questions</h2>
        <button onclick="location.reload()">Reload</button>
      `;
    }
  • quizzes.json

    [
        {
            "question": "Which language runs in a web browser?",
            "options": [
                "Java",
                "C",
                "Python",
                "JavaScript"
            ],
            "correct": "JavaScript"
        },
        {
            "question": "Which attribute is used to specify the URL of the page that the link goes to?",
            "options": [
                "src",
                "href",
                "url",
                "link"
            ],
            "correct": "href"
        },    
        {
            "question": "Which CSS property is used to change the font size of an element?",
            "options": [
                "font-style",
                "font-family",
                "font-size",
                "text-size"
            ],
            "correct": "font-size"
        },
        {
            "question": "What keyword is used to declare a function in JavaScript?",
            "options": [
                "func",
                "def",
                "function",
                "method"
            ],
            "correct": "function"
        }    
    ]
πŸ“ Ressources

Tic Tac Toe Game

The classic Tic Tac Toe game. Master game state management.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>tic tac toe</title>
            <link rel="stylesheet" href="./style.css">
        </head>
        <body>
            <div class="container">
                <h1>Tic Tac Toe</h1>
                <div class="game-message current-player">Who's Turn : 
                    <span>X</span>
                </div>
                <div class="game-board">
                    <div class="row">
                        <div class="cell" id="cell-1"></div>
                        <div class="cell" id="cell-2"></div>
                        <div class="cell" id="cell-3"></div>
                    </div>
                    <div class="row">
                        <div class="cell" id="cell-4"></div>
                        <div class="cell" id="cell-5"></div>
                        <div class="cell" id="cell-6"></div>
                    </div>
                    <div class="row">
                        <div class="cell" id="cell-7"></div>
                        <div class="cell" id="cell-8"></div>
                        <div class="cell" id="cell-9"></div>
                    </div>
                </div>
                <button id="resetButton">Reset Game</button>
            </div>
            <script src="script.js"></script>
        </body>
    </html>
  • style.css

    @import url("https://fonts.googleapis.com/css2?family=Varela+Round&display=swap");
    
    *,
    *::before,
    *::after {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    :root {
      --primary-color: #f8a126;
      --secondary-color: #8d185e;
      --white-color: #fff;
      --hover-color: #e07b23;
      --border-color: #f8a1267a;
      --bright-blue: #2f76ff;
      --raspberry-red: #e12472;
    }
    
    body {
      font-family: "Varela Round", sans-serif;
      min-height: 100vh;
      background: url(background.jpg) no-repeat;
      background-position: center;
      background-size: cover;
      color: var(--white-color);
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .container {
      border: 2px solid var(--border-color);
      padding: 40px;
      border-radius: 10px;
      backdrop-filter: blur(15px);
      display: flex;
      flex-direction: column;
    }
    
    h1 {
      text-align: center;
      margin-bottom: 20px;
    }
    
    .current-player {
      color: var(--primary-color);
      font-size: 20px;
      font-weight: 600;
      height: 56px;
    }
    
    .current-player span {
      color: var(--white-color);
      background: var(--primary-color);
      display: inline-flex;
      width: 30px;
      height: 30px;
      justify-content: center;
      align-items: center;
      border-radius: 3px;
    }
    
    .game-board {
      display: grid;
      grid-template-columns: repeat(3, 100px);
      grid-template-rows: repeat(3, 100px);
      gap: 10px;
    }
    
    .cell {
      font-size: 90px;
      background-color: #00000045;
      height: 100px;
      margin-bottom: 10px;
      border-radius: 5px;
      display: flex;
      justify-content: center;
      align-items: center;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }
    
    .cell:hover {
      background-color: #00000060;
    }
    
    .cell-O {
      text-shadow: 0 0 10px var(--raspberry-red), 0 0 20px var(--raspberry-red), 0 0 30px var(--raspberry-red);
    }
    
    .cell-X {
      text-shadow: 0 0 10px var(--bright-blue), 0 0 20px var(--bright-blue), 0 0 30px var(--bright-blue);
    }
    
    :is(.cell-O, .cell-X):hover {
      background-color: #00000045;
    }
    
    .cell.correct {
      background-color: var(--secondary-color);
    }
    
    .cell.correct:hover {
      background-color: var(--secondary-color);
    }
    
    button {
      background-color: var(--primary-color);
      color: var(--white-color);
      font-size: 20px;
      margin: 40px auto 0;
      padding: 10px 20px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }
    
    button:hover {
      background-color: var(--hover-color);
    }
    
    @media (max-width: 600px) {
      .game-board {
        grid-template-columns: repeat(3, 60px);
        grid-template-rows: repeat(3, 60px);
      }
    
      .cell {
        width: 60px;
        height: 60px;
        font-size: 60px;
      }
    
      button {
        font-size: 16px;
      }
    }
  • script.js

    const EGameStatus = {
      NOT_STARTED: "NOT_STARTED",
      IN_PROGRESS: "IN_PROGRESS",
      FINISHED_WITH_WINNER: "FINISHED_WITH_WINNER",
      FINISHED_WITH_TIE: "FINISHED_WITH_TIE",
    };
    
    let gameStatus = EGameStatus.NOT_STARTED;
    let currentPlayer = "X";
    const cells = document.querySelectorAll(".cell");
    const resetButton = document.getElementById("resetButton");
    const gameMessage = document.querySelector(".game-message");
    
    updateGameMessage();
    resetButton.addEventListener("click", reset);
    cells.forEach((cell, index) =>
      cell.addEventListener("click", () => handleCellClick(index))
    );
    
    function handleCellClick(index) {
      if (isGameFinished() || cells[index].innerHTML !== "") {
        return;
      }
    
      gameStatus = EGameStatus.IN_PROGRESS;
      cells[index].innerHTML = currentPlayer;
      cells[index].classList.add("cell-" + currentPlayer);
    
      if (checkWinner()) {
        gameStatus = EGameStatus.FINISHED_WITH_WINNER;
        updateGameMessage();
        return;
      }
    
      if (checkTie()) {
        gameStatus = EGameStatus.FINISHED_WITH_TIE;
        updateGameMessage();
        return;
      }
    
      currentPlayer = currentPlayer === "X" ? "O" : "X";
      updateGameMessage();
    }
    
    function checkWinner() {
      const winPatterns = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
        [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
        [0, 4, 8], [2, 4, 6], // Diagonals
      ];
    
      for (const pattern of winPatterns) {
        const [a, b, c] = pattern;
        if (
          cells[a].innerHTML !== "" &&
          cells[a].innerHTML === cells[b].innerHTML &&
          cells[a].innerHTML === cells[c].innerHTML
        ) {
          updateCellsWinners(cells[a], cells[b], cells[c]);
          return true;
        }
      }
    
      return false;
    }
    
    function updateCellsWinners(a, b, c) {
      a.classList.add("correct");
      b.classList.add("correct");
      c.classList.add("correct");
    }
    
    function checkTie() {
      return Array.from(cells).every((cell) => cell.innerHTML !== "");
    }
    
    function reset() {
      gameStatus = EGameStatus.NOT_STARTED;
      currentPlayer = "X";
      updateGameMessage();
      cells.forEach((cell) => {
        cell.innerText = "";
        cell.className = "cell";
      });
    }
    
    function updateGameMessage() {
      if (gameStatus === EGameStatus.FINISHED_WITH_TIE) {
        gameMessage.innerHTML = "It's a tie!";
      } else if (gameStatus === EGameStatus.FINISHED_WITH_WINNER) {
        gameMessage.innerHTML = `Winner : <span>${currentPlayer}</span>`;
      } else {
        gameMessage.innerHTML = `Who's Turn : <span>${currentPlayer}</span>`;
      }
    }
    
    function isGameFinished() {
      return gameStatus == EGameStatus.FINISHED_WITH_TIE || gameStatus == EGameStatus.FINISHED_WITH_TIE;
    }
πŸ“ Ressources

Typing Speed Test

Measure words-per-minute with accuracy tracking. Practice keyboard events.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜…

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible"
        content="IE=edge">
      <meta name="viewport"
        content="width=device-width, initial-scale=1.0">
      <title>Typing Speed Test</title>
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
      <div class="container">
        <div class="header-container">
          <h1>Typing Speed Test</h1>
          <h2>
            Online tool to help you improve your typing speed and accuracy.
          </h2>
        </div>
        <div class="text-to-type-container">
          <div class="text-to-type">
          </div>
          <div class="footer-container">
            <div class="typed-text-stat-container">
              <div class="typed-text-stat-value-container">
                <span class="typed-text-stat-value"
                  id="nbWord">
                  0
                </span>
                <span class="typed-text-stat-label">Word/min</span>
              </div>
              <div class="typed-text-stat-value-container">
                <span class="typed-text-stat-value"
                  id="errors">
                  0
                </span>
                <span class="typed-text-stat-label">Errors</span>
              </div>
            </div>
            <button id="tryAgainBtn">Try again</button>
          </div>
        </div>
      </div>
      <script src="lorem-ipsum.js"></script>
      <script src="script.js"></script>
    </body>
    </html>
  • style.css

    @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
    @import url("https://fonts.googleapis.com/css?family=Roboto Mono");
    
    *,
    *::before,
    *::after {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    :root {
      --primary-color: #f7933d;
      --text-color: #424242;
      --white-color: #fff;
      --red-color: #f57173;
      --green-color: #34a055;
      --background-color: #ffffff95;
      --cursor-color: #222;
      --border-color-1: #f8a3a269;
      --border-color-2: #74334799;
    }
    
    body {
      font-family: "Roboto", sans-serif;
      min-height: 100vh;
      background-image: url("background.jpg");
      background-position: center;
      background-size: cover;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .container {
      width: 800px;
      height: auto;
      text-align: center;
      padding: 50px;
      border: 2px solid var(--border-color-1);
      border-radius: 10px;
      backdrop-filter: blur(30px);
    }
    
    .header-container {
      font-family: Poppins, sans-serif;
      text-align: center;
      margin-bottom: 30px;
    }
    
    h1 {
      color: var(--primary-color);
      font-size: 36px;
      margin-bottom: 16px;
    }
    
    h2 {
      color: var(--white-color);
      font-size: 14px;
      font-weight: 400;
    }
    
    @media only screen and (min-width: 576px) {
      h2 {
        font-size: 17px;
      }
    }
    
    .text-to-type-container {
      background: var(--background-color);
      border-radius: 5px;
      padding: 32px;
    }
    
    .text-to-type-container .footer-container {
      display: flex;
      justify-content: space-between;
      border-top: 1px var(--border-color-2) solid;
      padding-top: 14px;
    }
    
    .footer-container .typed-text-stat-container {
      display: flex;
    }
    
    .typed-text-stat-value-container {
      padding: 8px 16px;
      border-right: 1px var(--border-color-2) solid;
    }
    
    .typed-text-stat-value {
      font-weight: 600;
    }
    
    .typed-text-stat-value-container:first-child {
      padding-left: 0;
    }
    
    button {
      background-color: var(--primary-color);
      color: var(--white-color);
      padding: 8px;
      font-size: 16px;
      border: 0;
      border-radius: 5px;
      outline: none;
    }
    
    .text-to-type-container .text-to-type {
      font-family: "Roboto Mono";
      font-size: 22px;
      word-break: break-all;
      padding-bottom: 32px;
    }
    
    .text-to-type span {
      color: var(--text-color);
    }
    
    .text-to-type-container .text-to-type.blink span.cursor {
      color: var(--white-color);
      background-color: var(--cursor-color);
    }
    
    .text-to-type-container span.NOK {
      color: var(--red-color);
    }
    
    .text-to-type-container span.OK {
      color: var(--green-color);
    }
  • script.js

    const textToType = document.querySelector(".text-to-type");
    const tryAgainBtn = document.querySelector("#tryAgainBtn");
    const errorsElement = document.querySelector("#errors");
    const nbWordElement = document.querySelector("#nbWord");
    const loremIpsum = new LoremIpsum();
    
    let caractersToType;
    let caracterToTypeIndex;
    let typedCaractersErrors;
    let typingStartTime;
    let finished;
    
    initState();
    toggleCursor();
    document.addEventListener("keydown", handleKeyPress);
    tryAgainBtn.addEventListener("click", tryAgain);
    document.addEventListener("keydown",(event) => event.key === "Enter" && tryAgain());
    
    function tryAgain(){
      initState();
    }
    
    function initState() {
      caracterToTypeIndex = 0;
      typedCaractersErrors = 0;
      typingStartTime = new Date().getTime();
      finished = false;
      setTextToType();
    }
    
    function setTextToType() {
      textToType.innerHTML = getRandomText()
        .split("")
        .map((char, index) => `<span class="${index === 0 ? "cursor" : ""}">${char}</span>`)
        .join("");
      caractersToType = textToType.querySelectorAll("span");
    }
    
    function getRandomText() {
      return loremIpsum.sentence(20).toLowerCase().replace(/[^a-z ]/g, "");
    }
    
    function toggleCursor() {
      setInterval(() => textToType.classList.toggle("blink"), 500);
    }
    
    function isLastcaracter() {
      return caracterToTypeIndex >= caractersToType.length - 1;
    }
    
    function handleKeyPress(event) {
      if (isNotValidTypedKey(event.key) || finished) {
        return;
      }
    
      handleTypedKey(event.key);
    
      if (isLastcaracter()) {
        finishTyping();
        return;
      }
    
      moveCursor();
    }
    
    function isNotValidTypedKey(key) {
      return !/^[a-z ]$/.test(key);
    }
    
    function finishTyping() {
      finished = true;
      updateTypingStats();
      removeCursor();
    }
    
    function isTypedCharacterCorrect(key) {
      return key === caractersToType[caracterToTypeIndex].innerText;
    }
    
    function handleTypedKey(key) {
      if (isTypedCharacterCorrect(key)) {
        caractersToType[caracterToTypeIndex].classList.add("OK");
      } else {
        caractersToType[caracterToTypeIndex].classList.add("NOK");
        typedCaractersErrors++;
      }
    }
    
    function updateTypingStats() {
      errorsElement.innerHTML = typedCaractersErrors;
      nbWordElement.innerHTML = calculateNbWord(typingStartTime);
    }
    
    function calculateNbWord(startTime){
      const elapsedTimeInMinutes = (new Date().getTime() - startTime) / (1000 * 60); // Convert milliseconds to minutes
      const totalWordsTyped = Math.ceil(caracterToTypeIndex / 5); // Assuming an average word length of 5 characters
      return Math.round(totalWordsTyped / elapsedTimeInMinutes);
    }
    
    function moveCursor() {
      caractersToType[caracterToTypeIndex++].classList.remove("cursor");
      caractersToType[caracterToTypeIndex].classList.add("cursor");
    }
    
    function removeCursor() {
      caractersToType[caracterToTypeIndex].classList.remove("cursor");
    }
  • lorem-ipsum.js

    ;(function() {
    
      "use strict";
    
      /**
      * LoremIpsum constructor
      *
      * @type {Function}
      */
      window.LoremIpsum = function() {
        // pass
      }
    
      /**
      * LoremIpsum prototype
      *
      * @type {Object}
      */
      window.LoremIpsum.prototype = {
    
        /**
        * Possible words
        *
        * @type {Array}
        */
        _words: [ "a", "ac", "accumsan", "ad", "adipiscing", "aenean", "aenean", "aliquam", "aliquam", "aliquet", "amet", "ante", "aptent", "arcu", "at", "auctor", "augue", "bibendum", "blandit", "class", "commodo", "condimentum", "congue", "consectetur", "consequat", "conubia", "convallis", "cras", "cubilia", "curabitur", "curabitur", "curae", "cursus", "dapibus", "diam", "dictum", "dictumst", "dolor", "donec", "donec", "dui", "duis", "egestas", "eget", "eleifend", "elementum", "elit", "enim", "erat", "eros", "est", "et", "etiam", "etiam", "eu", "euismod", "facilisis", "fames", "faucibus", "felis", "fermentum", "feugiat", "fringilla", "fusce", "gravida", "habitant", "habitasse", "hac", "hendrerit", "himenaeos", "iaculis", "id", "imperdiet", "in", "inceptos", "integer", "interdum", "ipsum", "justo", "lacinia", "lacus", "laoreet", "lectus", "leo", "libero", "ligula", "litora", "lobortis", "lorem", "luctus", "maecenas", "magna", "malesuada", "massa", "mattis", "mauris", "metus", "mi", "molestie", "mollis", "morbi", "nam", "nec", "neque", "netus", "nibh", "nisi", "nisl", "non", "nostra", "nulla", "nullam", "nunc", "odio", "orci", "ornare", "pellentesque", "per", "pharetra", "phasellus", "placerat", "platea", "porta", "porttitor", "posuere", "potenti", "praesent", "pretium", "primis", "proin", "pulvinar", "purus", "quam", "quis", "quisque", "quisque", "rhoncus", "risus", "rutrum", "sagittis", "sapien", "scelerisque", "sed", "sem", "semper", "senectus", "sit", "sociosqu", "sodales", "sollicitudin", "suscipit", "suspendisse", "taciti", "tellus", "tempor", "tempus", "tincidunt", "torquent", "tortor", "tristique", "turpis", "ullamcorper", "ultrices", "ultricies", "urna", "ut", "ut", "varius", "vehicula", "vel", "velit", "venenatis", "vestibulum", "vitae", "vivamus", "viverra", "volutpat", "vulputate" ],
    
        /**
        * Get random number
        *
        * @param  {Number} x
        * @param  {Number} y
        * @return {Number}
        */
        _random: function(x, y) {
          var rnd = (Math.random() * 2 - 1) + (Math.random() * 2 - 1) + (Math.random() * 2 - 1);
          return Math.round(Math.abs(rnd) * x + y);
        },
    
        /**
        * Get random number between min and max
        *
        * @param  {Number} min (optional) lower result limit
        * @param  {Number} max (optional) upper result limit
        * @return {Number}     random number
        */
        _count: function(min, max) {
          var result;
          if (min && max) result = Math.floor(Math.random() * (max - min + 1) + min);
          else if (min) result = min;
          else if (max) result = max;
          else result = this._random(8, 2);
    
          return result;
        },
    
        /**
        * Get random words
        *
        * @param  {Number} min (optional) minimal words count
        * @param  {Number} max (optional) maximal words count
        * @return {Object}     array of random words
        */
        words: function(min, max) {
          var result = [];
          var count = this._count(min, max);
    
          // get random words
          while (result.length < count) {
            var pos = Math.floor(Math.random() * this._words.length);
            var rnd = this._words[pos];
    
            // do not allow same word twice in a row
            if (result.length && result[result.length - 1] === rnd) {
              continue;
            }
    
            result.push(rnd);
          }
    
          return result;
        },
    
        /**
        * Generate sentence
        *
        * @param  {Number} min (optional) minimal words count
        * @param  {Number} max (optional) maximal words count
        * @return {String}     sentence
        */
        sentence: function(min, max) {
          var words = this.words(min, max);
    
          // add comma(s) to sentence
          var index = this._random(6, 2);
          while (index < words.length - 2) {
            words[index] += ",";
            index += this._random(6, 2);
          }
    
          // append puctation on end
          var punct = "...!?"
          words[words.length - 1] += punct.charAt(Math.floor(Math.random() * punct.length));
    
          // uppercase first letter
          words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
    
          return words.join(" ");
        },
    
        /**
        * Generate paragraph
        *
        * @param  {Number} min (optional) minimal words count
        * @param  {Number} max (optional) maximal words count
        * @return {String}     paragraph
        */
        paragraph: function(min, max) {
          if (!min && !max) {
            min = 20;
            max = 60;
          }
    
          var result = "";
          var count = this._count(min, max);
    
          // append sentences until limit is reached
          while (result.slice(0, -1).split(" ").length < count) {
            result += this.sentence() + " ";
          }
          result = result.slice(0, -1)
    
          // remove words
          if (result.split(" ").length > count) {
            var punct = result.slice(-1);
            result = result.split(" ").slice(0, count).join(" ");
            result = result.replace(/,$/, "");
            result += punct;
          }
    
          return result;
        }
    
      }
    
    })();
πŸ“ Ressources

Hash Generator

Generate cryptographic hashes (MD5, SHA-1) from user input. Explore web cryptography APIs.

πŸ“Œ Difficulty: β˜…β˜…β˜…β˜†

πŸ“ Source Code

code-tabs

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Hash generator</title>
      <script src="https://code.iconify.design/iconify-icon/1.0.8/iconify-icon.min.js"></script>
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
      <div class="container">
        <div class="header-container">
          <h1 class="gradient-font">Hash Generator</h1>
          <h2>
            Online tool to instantly generate the hash value of an input text
          </h2>
        </div>
        <div class="generated-hash-container">
          <textarea id="inputText">Text example</textarea>
          <div class="calculated-hashes-container">
            <div class="calculated-hash-container">
              <div class="left">
                <span class="hash-code-algo">SHA-1</span>
                <span class="hash-code-value" id="SHA1"></span>
              </div>
              <button class="icon-button copy-button" title="Copy">
                <iconify-icon icon="ci:copy" class="icon"></iconify-icon>
              </button>
            </div>
            <div class="calculated-hash-container">
              <div class="left">
                <span class="hash-code-algo">SHA-256</span>
                <span class="hash-code-value" id="SHA256"></span>
              </div>
              <button class="icon-button copy-button" title="Copy">
                <iconify-icon icon="ci:copy" class="icon"></iconify-icon>
              </button>
            </div>
            <div class="calculated-hash-container">
              <div class="left">
                <span class="hash-code-algo">SHA-384</span>
                <span class="hash-code-value" id="SHA384"></span>
              </div>
              <button class="icon-button copy-button" title="Copy">
                <iconify-icon icon="ci:copy" class="icon"></iconify-icon>
              </button>
            </div>
            <div class="calculated-hash-container">
              <div class="left">
                <span class="hash-code-algo">SHA-512</span>
                <span class="hash-code-value" id="SHA512"></span>
              </div>
              <button class="icon-button copy-button" title="Copy">
                <iconify-icon icon="ci:copy" class="icon"></iconify-icon>
              </button>
            </div>
          </div>
        </div>
      </div>
      <script src="script.js"></script>
    </body>
    </html>
  • style.css

    @import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
    
    *,
    *::before,
    *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    :root {
      --primary-color: #c3e3ee;
      --secondary-color: #26688b;
      --white-color: #fff;
      --text-color: #424242;
      --border-color: #c3e3ee73;
      --hover-color: #ddd;
    }
    
    body {
      font-family: "Poppins", sans-serif;
      min-height: 100vh;
      background-image: url("background.jpg");
      background-position: center;
      background-size: cover;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    .container {
      backdrop-filter: blur(30px);
      max-width: 950px;
      border: 2px var(--border-color) solid;
      padding: 50px;
      border-radius: 10px;
      box-shadow: 0px 0px 7px 0 rgba(0, 0, 0, 0.1);
    }
    
    @media only screen and (min-width: 576px) {
      .container {
        padding: 50px 100px;
      }
    }
    
    .header-container {
      text-align: center;
      margin-bottom: 30px;
    }
    
    h1 {
      color: var(--secondary-color);
      font-size: 30px;
    }
    
    @media only screen and (min-width: 576px) {
      h1 {
        font-size: 40px;
      }
    }
    
    h2 {
      font-size: 14px;
      font-weight: 400;
    }
    
    @media only screen and (min-width: 576px) {
      h2 {
        font-size: 17px;
      }
    }
    
    textarea {
      width: 100%;
      min-height: 150px;
      background-color: var(--primary-color);
      padding: 8px 16px;
      font-size: 18px;
      border-radius: 5px;
      border: none;
      outline: none;
      transition: background-color 1s ease;
    }
    
    textarea:hover {
      background-color: var(--hover-color);
    }
    
    .calculated-hashes-container {
      display: grid;
      grid-template-columns: 1fr;
      row-gap: 10px;
      margin-top: 10px;
    }
    
    .calculated-hashes-container .calculated-hash-container {
      background-color: var(--primary-color);
      min-width: 0;
      padding: 8px 16px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      border-radius: 5px;
    }
    
    .calculated-hash-container .left {
      min-width: 0;
      display: flex;
      align-items: center;
      column-gap: 10px;
    }
    
    .calculated-hash-container .left .hash-code-algo {
      background-color: var(--secondary-color);
      color: var(--white-color);
      flex-shrink: 0;
      font-size: 14px;
      font-weight: 600;
      width: 85px;
      text-align: center;
      border-radius: 5px;
    }
    
    .calculated-hash-container .left .hash-code-value {
      overflow: hidden;
      text-overflow: ellipsis;
      letter-spacing: 1px;
      font-size: 14px;
      white-space: nowrap;
    }
    
    .icon-button {
      all: initial;
      height: 20px;
      width: 20px;
      border-radius: 3px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      transition: background-color 1s ease;
    }
    
    @media only screen and (min-width: 576px) {
      .icon-button {
        height: 30px;
        width: 30px;
      }
    }
    
    .icon-button:hover {
      background-color: var(--secondary-color);
    }
    
    .icon {
      font-size: 25px;
      color: var(--text-color);
    }
    
    .icon-button:hover .icon {
      color: var(--white-color);
    }
  • script.js

    const SHA1 = document.getElementById("SHA1");
    const SHA256 = document.getElementById("SHA256");
    const SHA384 = document.getElementById("SHA384");
    const SHA512 = document.getElementById("SHA512");
    const inputText = document.getElementById("inputText");
    const copyButtons = document.querySelectorAll(".copy-button");
    
    handleKeyPress();
    document.addEventListener("keyup", handleKeyPress);
    copyButtons.forEach((copyButton) =>
      copyButton.addEventListener("click", (event) => copyHashCodeToClipboard(event))
    );
    
    function handleKeyPress() {
      if (inputText.value == "") {
        SHA1.innerHTML = "";
        SHA256.innerHTML = "";
        SHA384.innerHTML = "";
        SHA512.innerHTML = "";
        return;
      }
    
      encode(inputText.value, "SHA-1").then((hash) => {
        SHA1.innerHTML = hash;
      });
    
      encode(inputText.value, "SHA-256").then((hash) => {
        SHA256.innerHTML = hash;
      });
    
      encode(inputText.value, "SHA-384").then((hash) => {
        SHA384.innerHTML = hash;
      });
    
      encode(inputText.value, "SHA-512").then((hash) => {
        SHA512.innerHTML = hash;
      });
    }
    
    async function encode(text, algo) {
      const encoder = new TextEncoder();
      const data = encoder.encode(text);
      const hashBuffer = await crypto.subtle.digest(algo, data);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      const hashHex = hashArray
        .map((byte) => byte.toString(16).padStart(2, "0"))
        .join("");
      return hashHex;
    }
    
    async function copyHashCodeToClipboard(event) {
      try {
        if (!navigator?.clipboard?.writeText) {
          return;
        }
        const hashCodeElement = event.target.closest(".calculated-hash-container").querySelector(".hash-code-value");
    
        await navigator.clipboard.writeText(hashCodeElement.textContent);
        const hashCodeValue = hashCodeElement.textContent;
        hashCodeElement.textContent = 'Copied!';
        setTimeout(() => hashCodeElement.textContent = hashCodeValue, "1000");
      } catch (err) {
        console.error(err);
      }
    }
πŸ“ Ressources

πŸš€ Ready to Build Your Portfolio?

You now have 12 practical web projects to level up your frontend skillsβ€”from beginner-friendly calculators to expert-level weather apps. Each project is designed to:

βœ… Teach real-world skills (APIs, DOM manipulation, algorithms)
βœ… Boost your portfolio with interactive, deployable apps
βœ… Prepare you for interviews with common technical challenges