Source: script.js

// script.js

/**
 * @fileoverview Main application logic for the Pokédex web app.
 *
 * This file handles initial setup, loading and rendering of Pokémon cards,
 * search functionality, event handling, and interaction with the PokéAPI.
 */

let matches = [];
let currentPokemon;
let startPokemon = 1;
let endPokemon = 21;
let searchPool;

const missingno = {
  name: "missingNo.",
  url: "https://en.wikipedia.org/wiki/MissingNo.",
};

const typeColors = {
  normal: "#919AA2",
  fighting: "#CE416B",
  flying: "#8FA9DE",
  poison: "#AA6BC8",
  ground: "#D97845",
  rock: "#C5B78C",
  bug: "#91C12F",
  ghost: "#5269AD",
  steel: "#5A8EA2",
  fire: "#FF9D54",
  water: "#5090D6",
  grass: "#63BC5A",
  electric: "#F4D23C",
  psychic: "#FA7179",
  ice: "#73CEC0",
  dragon: "#0B6DC3",
  dark: "#5A5465",
  fairy: "#EC8FE6",
  unknown: "#FFFFFF",
};

/**
 * Initializes the application on page load.
 *
 * Shows the loading screen, renders the initial batch of Pokémon,
 * and prepares the search pool for user input.
 *
 * @returns {Promise<void>}
 */
async function init() {
  showLoadingScreen();
  await renderOverview(startPokemon, endPokemon);
  loadSearchPool();
}

/**
 * Fetches data for a specific Pokémon by ID from the PokéAPI.
 *
 * @param {number} id - The ID of the Pokémon to fetch.
 * @returns {Promise<object>} A promise that resolves to the Pokémon data.
 */
async function getPokemonData(id) {
  let url = `https://pokeapi.co/api/v2/pokemon/${id}`;

  try {
    let response = await fetch(url);
    currentPokemon = await response.json();
    return currentPokemon;
  } catch (error) {
    console.error(error);
  }
}

/**
 * Renders the MissingNo. fallback view when a Pokémon or its data could not be found.
 *
 * This function replaces the overview content with a MissingNo. template
 * and adjusts the UI by hiding or showing relevant elements.
 */
function renderMissingNo() {
  let container = getElementHelper("overview-container");
  container.innerHTML = renderMissingNoTemplate();
  hideLoadBtn();
  showBackBtn();
  hideLoadingScreen();
  emptySearchInput();
  disableSearchBtn();
}

/**
 * Renders an overview of multiple Pokémon by ID range.
 *
 * This function fetches and renders individual Pokémon cards from start to end ID.
 * It also includes a condition to hide the load button when the 151st Pokémon is rendered.
 *
 * @param {number} startPokemon - The starting Pokémon ID.
 * @param {number} endPokemon - The ending Pokémon ID.
 */
async function renderOverview(startPokemon, endPokemon) {
  let container = getElementHelper("overview-container");

  for (let id = startPokemon; id <= endPokemon; id++) {
    await renderOverviewSingleCard(id, container);
  }
  hideLoadingScreen();
}

/**
 * Renders a single Pokémon overview card into the given container element.
 *
 * @param {number} id - The ID of the Pokémon to render.
 * @param {HTMLElement} container - The HTML element where the card will be appended.
 * @returns {Promise<void>}
 */
async function renderOverviewSingleCard(id, container) {
  currentPokemon = await getPokemonData(id);
  const backgroundColor = typeColors[currentPokemon.types[0].type.name] || "#66aed7";

  try {
    container.innerHTML += renderOverviewTemplate(currentPokemon, backgroundColor);
  } catch (error) {
    console.error(error);
    renderMissingNo();
    return;
  }

  renderTypes(`types-container-${currentPokemon.id}`);

  if (currentPokemon.id >= 151) {
    hideLoadBtn();
  }
}

/**
 * Renders one or two type icons of the current Pokémon into the given container.
 *
 * Works for both overview cards and detail views.
 *
 * @param {string} containerId - The ID of the HTML container element to render the types into.
 */
function renderTypes(containerId) {
  let container = getElementHelper(containerId);
  container.innerHTML = "";

  for (let i = 0; i < currentPokemon.types.length; i++) {
    container.innerHTML += renderTypesTemplate(i);
  }
}

/**
 * Starts the rendering process to load more Pokémon into the overview.
 *
 * Increases the current start and end range, while ensuring it stays within the bounds
 * of the 1st generation (max ID 151). Also prevents loading if the start point
 * exceeds the available Pokémon.
 *
 * @returns {void}
 */
function loadMorePokemon() {
  showLoadingScreen();
  startPokemon = endPokemon + 1;
  endPokemon = endPokemon + 21;

  if (endPokemon > 151) {
    endPokemon = 151;
  }

  if (startPokemon >= 151) {
    hideLoadBtn();
    hideLoadingScreen();
    return;
  }

  renderOverview(startPokemon, endPokemon);
}

/**
 * Loads the search pool containing the first 151 Pokémon and prepends MissingNo at index 0.
 *
 * This ensures that the Pokémon array index matches the Pokémon ID,
 * simplifying ID-based access in the search functionality.
 *
 * @returns {Promise<void>}
 */
async function loadSearchPool() {
  let Url = "https://pokeapi.co/api/v2/pokemon?limit=151&offset=0";
  let response = await fetch(Url);
  let unfinishedSearchPool = await response.json();
  searchPool = [missingno, ...unfinishedSearchPool.results];
}

/**
 * Searches the pool of 152 Pokémon (including MissingNo) for name matches based on the user input.
 *
 * Prevents searching if the input has fewer than 3 characters.
 * Updates the UI by enabling/disabling the search button and rendering or closing suggestions.
 */
function searchInPool() {
  let inputField = getElementHelper("search-input");
  let inputValue = inputField.value.trim();
  emptyMatches();

  if (inputValue.length >= 3) {
    enableSearchBtn();
    let inputRef = inputValue.toLowerCase();
    pushMatchesIntoArray(inputRef);
    renderSuggestions();
  } else {
    disableSearchBtn();
    closeSuggestions();
  }
}

/**
 * Renders the overview for all Pokémon found by the search function.
 *
 * Uses the index from the `matches` array to determine the ID of each Pokémon,
 * and displays their overview cards in the main container.
 * Also updates UI elements like the load and back buttons.
 *
 * @returns {Promise<void>}
 */
async function renderOverviewMatches() {
  showLoadingScreen();
  let container = getElementHelper("overview-container");
  container.innerHTML = "";

  for (let iMatches = 0; iMatches < matches.length; iMatches++) {
    renderOverviewSingleMatch(iMatches, container);
  }

  hideLoadingScreen();
  hideLoadBtn();
  showBackBtn();
}

/**
 * Renders a single Pokémon card from the matches array into the given container.
 *
 * Retrieves the Pokémon by its index in the `matches` array, fetches its data,
 * and appends the rendered card including its type icons.
 *
 * @param {number} iMatches - The index in the `matches` array to render.
 * @param {HTMLElement} container - The HTML element where the card will be appended.
 * @returns {Promise<void>}
 */
async function renderOverviewSingleMatch(iMatches, container) {
  iMatchesToCurrentPokemonId = matches[iMatches].index;
  currentPokemon = await getPokemonData(iMatchesToCurrentPokemonId);
  const backgroundColor = typeColors[currentPokemon.types[0].type.name] || "#66aed7";
  container.innerHTML += renderOverviewTemplate(currentPokemon, backgroundColor);
  renderTypes(`types-container-${currentPokemon.id}`);
}

/**
 * Pushes all Pokémon from the search pool whose name includes the input string into the `matches` array.
 *
 * Performs a case-sensitive substring match and stores both the index and the Pokémon data
 * for later use in the search result rendering.
 *
 * @param {string} inputRef - The input string to match against Pokémon names.
 */
function pushMatchesIntoArray(inputRef) {
  for (let i = 0; i < searchPool.length; i++) {
    if (searchPool[i].name.includes(inputRef)) {
      matches.push({ index: i, pokemon: searchPool[i] });
    }
  }
}

/**
 * Renders search suggestions into the dropdown container.
 *
 * Iterates over the `matches` array and appends each Pokémon as a clickable
 * list item. Each suggestion triggers a handler on click to load the selected Pokémon.
 */
function renderSuggestions() {
  let container = document.getElementById("dropdown-suggestions");
  container.innerHTML = "";

  for (let i = 0; i < matches.length; i++) {
    container.innerHTML += /*html*/ `
      <p onclick="handleClickOnSuggestion(${matches[i].index})">${capitalizeFirstLetter(matches[i].pokemon.name)}</p>
      `;
  }
}

/**
 * Handles the logic when a search suggestion is clicked.
 *
 * Opens the overlay for the selected Pokémon and resets the search UI.
 *
 * @param {number} matchId - The ID of the Pokémon selected from the search suggestions.
 */
function handleClickOnSuggestion(matchId) {
  openOverlay(matchId);
  emptySearchInput();
  closeSuggestions();
  disableSearchBtn();
}

/**
 * Handles the logic when the search button is clicked.
 *
 * If no matches are found, it renders MissingNo as a fallback.
 * Otherwise, it renders the matching Pokémon and resets the search UI.
 *
 * @returns {void}
 */
function handleClickOnSearchBtn() {
  if (matches.length === 0) {
    renderMissingNo();
    return;
  }

  renderOverviewMatches();
  emptySearchInput();
  closeSuggestions();
  disableSearchBtn();
}

/**
 * Closes the dropdown list of search suggestions when the input field loses focus.
 *
 * Uses a slight delay to ensure that `onclick` events on suggestions can still register
 * before the dropdown is removed.
 */
window.addEventListener("DOMContentLoaded", () => {
  getElementHelper("search-input").addEventListener("blur", () => {
    setTimeout(function () {
      closeSuggestions();
    }, 200);
  });
});

/**
 * Resets the view to the start of the Pokédex after a search.
 *
 * Clears the current content and restores the default state,
 * starting from Pokémon ID 1 to 21. Also updates UI elements accordingly.
 */
function backToStart() {
  let container = document.getElementById("overview-container");
  showLoadingScreen();
  container.innerHTML = "";
  startPokemon = 1;
  endPokemon = 21;
  hideBackBtn();
  showLoadButton();
  renderOverview(startPokemon, endPokemon);
}