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