Movie Search App with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #08

Project Overview

A small Movie Search app that queries OMDb using fetch + async/await. Users can type a movie title, filter by type (movie/series/episode), optionally add a year, view results with posters, and paginate through results. Demonstrates API integration, async/await, DOM updates, and basic UX (loading, no-results, errors).

Key Features

  • Search by title (debounced input) using OMDb s= search endpoint.
  • Filter by type (type=movie|series|episode) and year (y=).
  • Display poster, title, year, and type; fallback image for missing posters.
  • Pagination support (OMDb returns max 10 results per page; uses page=).
  • Loading indicator, friendly error / empty-state messages.
  • Clear instructions to insert your OMDb API key.

HTML Code

<!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

: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

/* 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.