Movie Search App with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #08

Project Overview

The Movie Search App is a beginner-friendly project built using HTML, CSS, and JavaScript. It enables users to search for movies by title and view relevant results using the OMDb API. Users can also specify the type of content, including Movie, Series, or Episode, along with the year of release. Each result displays the movie poster, title, year of release, and content type. In addition, the search results can be viewed across multiple pages with 5 movies displayed per page.

It uses the JavaScript Fetch API, async/await, APIs, DOM manipulation, events, and several other features. Additionally, the app displays messages for loading states, errors, and unavailable posters. As a result, beginners can learn how to create a working application that fetches data from an external source and displays it correctly.

Key Features

  • Search Movies by Title: Users can enter a movie title in the search field to search for matching movies. The application requests search results from the OMDb API server and displays them on the interface.
  • Movie Type Filter: You can filter movies by selecting one of the three types: Movie, Series, Episode. It is useful when you want to exclude one or two types when searching (for example, when multiple content types have the same name).
  • Year Filter: You can specify the year of release of the film or other media. This field helps to refine the search to a specific year.
  • Movie Posters: The search results contain posters for the selected movies. However, if a poster is unavailable, the application displays a default image instead.
  • Movie Information: Each result card contains important information, including the movie or series title, year of release, and media type. Therefore, users can quickly identify and choose the result they want to explore.
  • Live Search: Using JavaScript, users can perform a search when the search input or filter changes. To avoid unnecessary API requests, a debounce function delays the search while the user is typing.
  • Pagination: The OMDb API has a limitation on the number of results per page. Therefore, users can use the Previous and Next buttons to move across multiple pages of search results. Previous and Next buttons you can move across multiple pages of search results.
  • Loading State: It displays a loading message while waiting for the API response. This provides feedback to users while the search request is being processed.
  • Empty State, Error Handling: it also shows message about the absence of movies if the search was performed incorrectly or no results were found. In addition, there is a warning message if an error occurs during the search.
  • Movie Links: If a search result has an imdbID, clicking on the card opens the selected movie’s page on the IMDb website.
  • Adaptive Layout: The movie results are displayed in a responsive grid layout that adjusts according to the available screen size. This allows the application to work comfortably on desktops, tablets, and mobile devices.

What You’ll Learn

  • Learn how to fetch data from an external API using JavaScript.
  • Know how async and await handle asynchronous API requests.
  • Practice working with JSON data.
  • Understand how query parameters are added to an API URL.
  • Learn how to dynamically create HTML elements using JavaScript.
  • Practice displaying API results in a responsive layout.
  • You will know how error is handled with try and catch.
  • Learn how to create a loading state for an application.
  • Explore how debounce functions reduce unnecessary API requests.
  • Understand how pagination works with API results.
  • Practice updating the DOM based on user input.

HTML Code

HTML creates the complete structure of the Movie Search App. The main content is placed inside the .card container, which holds the application heading, search controls, movie results, and pagination buttons. Users can enter a movie title through the #query input. Meanwhile, the #type select element provides options for filtering results by Movie, Series, or Episode, and the #year input allows users to optionally enter a release year.

The #searchBtn button starts the movie search, while the #status element provides space for messages such as loading information and search instructions. Next, the #results section acts as the main container where JavaScript dynamically creates and displays movie result cards. At the bottom, the #prevBtn and #nextBtn buttons allow users to move between result pages, while #pageInfo displays the current page information. Finally, script.js is loaded at the bottom of the <body> so JavaScript can access the HTML elements and provide the application’s functionality.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Day 8 — Movie Search App</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="card" role="main" aria-labelledby="title">
    <header>
      <div class="logo">MS</div>
      <div>
        <h1 id="title">Day 8: Movie Search App</h1>
        <p class="lead">Search movies using the OMDb API. Built with async/await and fetch.</p>
      </div>
    </header>

    <section class="search-panel">
      <div class="row">
        <input id="query" type="search" placeholder="Search movies, e.g. The Matrix" autocomplete="off" />
        <select id="type">
          <option value="">All</option>
          <option value="movie">Movie</option>
          <option value="series">Series</option>
          <option value="episode">Episode</option>
        </select>
        <input id="year" type="number" placeholder="Year (optional)" min="1888" max="2100" />
        <button id="searchBtn" class="btn">Search</button>
      </div>
      <div class="meta-row">
        <div id="status" class="muted">Enter a title to begin.</div>
      </div>
    </section>

    <section id="results" class="results-grid" aria-live="polite">
      <!-- results inserted here -->
    </section>

    <nav class="pagination" aria-label="Search results pagination">
      <button id="prevBtn" class="btn secondary" disabled>Prev</button>
      <div id="pageInfo" class="muted">Page 0 of 0</div>
      <button id="nextBtn" class="btn secondary" disabled>Next</button>
    </nav>

    <details style="margin-top:14px">
      <summary>How it works (short)</summary>
      <p class="small">JS calls OMDb search endpoint with query and optional filters. Responses include up to 10 results per page; use the `page` parameter to paginate. Replace `YOUR_OMDB_API_KEY` in `script.js` with your key from the OMDb site. See the OMDb docs for full parameter list. :contentReference[oaicite:3]{index=3}</p>
    </details>
  </main>

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

CSS Code

CSS controls the appearance and layout of the Movie Search App. First, the:root section defines reusable values for colors such as the background, card, accent color, muted text, and main text color. Next, the.card class styles the main application container by adding a background, padding, rounded corners, border, and shadow. The .search-panel .row uses Flexbox to arrange the search input, filters, and Search button in a row.

The .results-grid class uses CSS Grid to display movie cards in multiple columns. The repeat(auto-fill, minmax(...)) layout allows the number of columns to adjust automatically based on the available screen width. In addition, each movie result uses the .card-item class, while .poster styles the movie image. The .title and .subtitle classes display the movie title, year, and content type. The .pagination section styles the Previous and Next buttons, while .loading and .err provide different styles for loading and error messages.

:root {
  --bg: #071026;
  --card: #0b1220;
  --accent: #7c3aed;
  --muted: #9aa4b2;
  --white: #e6eef6;
  font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: #002252;
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
}

.card {
  width: min(980px, 96%);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
  border-radius: 12px;
  padding: 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.03);
}

header {
  display: flex;
  gap: 12px;
  align-items: center;
}

.logo {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--accent), #22c1c3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}

h1 {
  margin: 0;
  font-size: 18px;
}

.lead {
  margin: 4px 0 12px;
  color: var(--muted);
  font-size: 14px;
}

.search-panel .row {
  display: flex;
  gap: 10px;
  align-items: center;
}

.search-panel input[type="search"],
.search-panel input[type="number"],
.search-panel select {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: white;
  color: black;
  outline: none;
}

.search-panel input:focus {
  box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.08);
  border-color: rgba(124, 58, 237, 0.6);
}

.btn {
  padding: 10px 14px;
  border-radius: 10px;
  border: 0;
  background: linear-gradient(90deg, var(--accent), #22c1c3);
  color: white;
  font-weight: 600;
  cursor: pointer
}

.btn.secondary {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--muted)
}

.meta-row {
  margin-top: 8px;
}

.muted {
  color: var(--muted);
  font-size: 13px
}

.small {
  font-size: 13px;
  color: var(--muted)
}

.results-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 14px;
  margin-top: 18px;
  min-height: 140px
}

.card-item {
  background: rgba(255, 255, 255, 0.02);
  border-radius: 8px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  border: 1px solid rgba(255, 255, 255, 0.03)
}

.poster {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 6px;
  background: linear-gradient(180deg, #0a0a0a, #111);
}

.title {
  font-size: 14px;
  font-weight: 600;
  margin-top: 8px;
  text-align: center;
}

.subtitle {
  font-size: 12px;
  color: var(--muted);
  margin-top: 4px;
}

.pagination {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: center;
  margin-top: 16px
}

.pagination .muted {
  min-width: 110px;
  text-align: center
}

.loading {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--muted)
}

.err {
  color: #ffbaba;
  background: rgba(255, 20, 20, 0.05);
  padding: 8px;
  border-radius: 8px;
  color: #ffdede;
  border: 1px solid rgba(255, 20, 20, 0.06)
}

Javascript Code

JavaScript handles the main functionality of the Movie Search App. First, it defines the OMDb API URL and API key, then selects the required search fields, buttons, results section, and pagination elements from the HTML. The buildUrl() function creates the API request URL by adding the search title, page number, movie type, year, and API key as query parameters. The debounce() function delays the search for a short time while the user is typing, which helps avoid sending too many API requests.

The searchMovies() function uses async/await and fetch() to request movie data from the OMDb API. After receiving the response, the code converts the data into JSON and checks whether matching results are available. The renderResults() function dynamically creates movie cards containing the poster, title, year, and type. It also provides a fallback image when a movie poster is unavailable. The JavaScript code then calculates the total number of pages and enables or disables the Previous and Next buttons.

Finally, event listeners connect the search input, filters, Search button, and pagination buttons to the appropriate functions. This allows users to search for movies and browse results without refreshing the webpage.

/* Day 8 — Movie Search App (OMDb)
   Replace YOUR_OMDB_API_KEY with your OMDb API key.
   Docs: http://www.omdbapi.com/ (see parameters: s=, type=, y=, page=). :contentReference[oaicite:4]{index=4}
*/

const API_KEY = '581144cc'; // <-- REPLACE with your key
const BASE_URL = 'https://www.omdbapi.com/'; // OMDb base URL. :contentReference[oaicite:5]{index=5}

/* DOM */
const qInput = document.getElementById('query');
const typeSelect = document.getElementById('type');
const yearInput = document.getElementById('year');
const searchBtn = document.getElementById('searchBtn');
const resultsEl = document.getElementById('results');
const statusEl = document.getElementById('status');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const pageInfo = document.getElementById('pageInfo');

let currentPage = 1;
let totalPages = 0;
let lastQuery = '';

/* helper: build URL */
function buildUrl(params = {}) {
  const url = new URL(BASE_URL);
  url.searchParams.set('apikey', API_KEY);
  Object.keys(params).forEach(k => {
    if (params[k] !== '' && params[k] != null) url.searchParams.set(k, params[k]);
  });
  return url.toString();
}

/* debounce helper to avoid too many requests while typing */
function debounce(fn, wait = 300) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), wait);
  };
}

/* show loading */
function setLoading(loading=true) {
  if (loading) {
    statusEl.innerHTML = `<span class="loading">Searching…</span>`;
  } else {
    statusEl.textContent = '';
  }
}

/* render empty / error */
function showMessage(msg, isError=false) {
  resultsEl.innerHTML = `<div class="${isError ? 'err' : 'muted'}">${msg}</div>`;
  pageInfo.textContent = `Page 0 of 0`;
  prevBtn.disabled = nextBtn.disabled = true;
}

/* render result cards */
function renderResults(list) {
  if (!Array.isArray(list) || list.length === 0) {
    showMessage('No results found.');
    return;
  }
  resultsEl.innerHTML = '';
  list.forEach(item => {
    const el = document.createElement('article');
    el.className = 'card-item';
    // poster fallback
    const poster = (item.Poster && item.Poster !== 'N/A') ? item.Poster : '';
    const img = document.createElement('img');
    img.className = 'poster';
    img.alt = `${item.Title} poster`;
    img.src = poster || 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"><rect width="100%" height="100%" fill="%230a0a0a"/><text x="50%" y="50%" font-size="18" fill="%239aa4b2" alignment-baseline="middle" text-anchor="middle">No Poster</text></svg>';
    el.appendChild(img);
    const title = document.createElement('div');
    title.className = 'title';
    title.textContent = item.Title;
    el.appendChild(title);
    const subtitle = document.createElement('div');
    subtitle.className = 'subtitle';
    subtitle.textContent = `${item.Year} • ${item.Type}`;
    el.appendChild(subtitle);

    // optional: clicking opens IMDb page (if imdbID present)
    if (item.imdbID) {
      el.style.cursor = 'pointer';
      el.addEventListener('click', () => {
        window.open(`https://www.imdb.com/title/${item.imdbID}/`, '_blank');
      });
    }

    resultsEl.appendChild(el);
  });
}

/* perform search using OMDb 's' endpoint (search) */
async function searchMovies(query, options = {}) {
  if (!query || query.trim().length < 1) {
    showMessage('Please enter a movie title to search.');
    return;
  }

  const page = options.page || 1;
  setLoading(true);
  try {
    const params = { s: query.trim(), page: page };
    if (options.type) params.type = options.type;
    if (options.year) params.y = options.year;
    const url = buildUrl(params);

    const res = await fetch(url);
    if (!res.ok) throw new Error(`Network error: ${res.status}`);
    const data = await res.json();

    // OMDb returns { Response: "False", Error: "Movie not found!" } when no results
    if (data.Response === 'False') {
      showMessage(data.Error || 'No results found.');
      setLoading(false);
      return;
    }

    // data.Search is an array and data.totalResults is string number
    const list = data.Search || [];
    const totalResults = parseInt(data.totalResults || (list.length), 10);
    totalPages = Math.ceil(totalResults / 10);

    renderResults(list);
    pageInfo.textContent = `Page ${page} of ${totalPages}`;
    prevBtn.disabled = page <= 1;
    nextBtn.disabled = page >= totalPages;

    currentPage = page;
    lastQuery = query;
    setLoading(false);
  } catch (err) {
    console.error(err);
    showMessage('Request failed. Check console for details.', true);
    setLoading(false);
  }
}

/* wire up UI */
const doSearch = debounce(() => {
  currentPage = 1;
  const q = qInput.value;
  const t = typeSelect.value;
  const y = yearInput.value;
  searchMovies(q, { page: 1, type: t || undefined, year: y || undefined });
}, 350);

qInput.addEventListener('input', doSearch);
typeSelect.addEventListener('change', doSearch);
yearInput.addEventListener('input', doSearch);

searchBtn.addEventListener('click', (e) => {
  e.preventDefault();
  doSearch();
});

prevBtn.addEventListener('click', () => {
  if (currentPage > 1) {
    const q = lastQuery;
    const t = typeSelect.value;
    const y = yearInput.value;
    searchMovies(q, { page: currentPage - 1, type: t || undefined, year: y || undefined });
  }
});

nextBtn.addEventListener('click', () => {
  if (currentPage < totalPages) {
    const q = lastQuery;
    const t = typeSelect.value;
    const y = yearInput.value;
    searchMovies(q, { page: currentPage + 1, type: t || undefined, year: y || undefined });
  }
});

/* initial hint */
showMessage('Enter a title and press Search (or type to search).');
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Related Projects

Day 6 : Tip Calculator

Calculates tip amount and total per person based on bill and tip %.

Concepts: Form inputs, math logic.

Day 10 : Typing Speed Test

Measures typing speed and accuracy.

Concepts: Timers, string comparison, event listeners.

Day 11 : Drum Kit

Play drum sounds when clicking buttons or pressing keys.

Concepts: Keyboard events, Audio API.