diff --git a/index.css b/index.css new file mode 100644 index 00000000..c8a4c5ff --- /dev/null +++ b/index.css @@ -0,0 +1,439 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Futura, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; +} + +figure { + margin: auto; +} + +ul { + margin: 0; + padding: 0; + list-style: none; +} + +input, +button { + padding: 0; + border: none; + outline: none; + font: inherit; + background: none; +} + +button, +label { + cursor: pointer; +} + +a { + text-decoration: none; + color: inherit; +} + +.visuallyhidden { + overflow: hidden; + position: absolute; + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + border: 0; + clip: rect(0 0 0 0); +} + +.closebtn::before { + content: "×"; + transition: opacity 0.25s; + + display: flex; + justify-content: center; + align-content: center; + + width: 20px; + height: 20px; + line-height: 20px; + border-radius: 100%; +} + +.app { + display: grid; + grid-template-rows: auto 1fr auto; + + overflow: hidden; + height: 100vh; + background: #222; + color: #eee; +} + +.app__header { + display: flex; + justify-content: space-around; + + position: relative; + padding: 5px; +} + +.app__content { + overflow-y: auto; + padding: 10px 0; + background: #000; + color: #666; +} + +.app__content.loading { + content: "loading"; +} + +.app__footer { + padding: 10px; +} + +.search { + display: flex; + align-items: center; + + margin: auto; + padding: 5px 5px 5px 10px; + border-radius: 50px; + line-height: 30px; + background: #444; +} + +.search__label { + padding-right: 5px; + line-height: inherit; + cursor: pointer; +} +.search__field { + padding: 0 5px; + border: 1px solid #333; + border-right: none; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + background: #eee; +} + +.search__controls { + display: flex; + align-items: center; + + position: relative; + line-height: 1; +} + +.search__controls__resetbtn { + position: absolute; + left: -25px; +} + +.search__controls__resetbtn::before { + background: #999; + color: #fff; + opacity: 0; +} + +.search__controls__resetbtn:hover::before, +.search__controls__resetbtn:focus::before { + opacity: 1; +} + +.search__controls__submitbtn { + position: relative; + color: #eee; +} + +.search__controls__submitbtn::before { + transition: background-color 0.25s, color 0.25s; + content: "►"; + display: block; + width: 30px; + height: 30px; + line-height: 30px; +} + +.search__controls__submitbtn::before { + border: 1px solid #333; + border-top-right-radius: 30px; + border-bottom-right-radius: 30px; + background-color: #333; +} + +.search__controls__submitbtn:focus::before { + background-color: #999; + color: #222; +} + +.suggestions { + overflow: hidden; + position: absolute; + left: 0; + top: 100%; + width: 100%; + background: #000000cc; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + z-index: 1; +} + +.suggestions__link { + display: block; + padding: 10px; + outline: none; +} + +.suggestions__link:focus { + background: #222; + color: #ccc; +} + +.movies { + display: grid; + grid-template-columns: repeat(auto-fill, 200px); + grid-gap: 10px; + place-content: center; + + max-width: 1040px; + min-height: 100%; + margin: auto; +} + +.movie { + position: relative; + overflow: hidden; + border: none; + border: 1px solid #333; + border-radius: 5px; + background: #333; + color: #bbb; +} + +.movie__poster { + pointer-events: none; + + display: flex; + place-content: center; + + width: 100%; + height: 100%; + min-width: 200px; + min-height: 280px; + object-fit: cover; +} + +.movie__title { + pointer-events: none; + + position: absolute; + left: 0; + bottom: 0; + width: 100%; + margin: 0; + padding: 10px; + background: #000000cc; +} + +.movie__title::before { + content: ""; + display: block; + position: absolute; + left: 0; + bottom: 100%; + width: 100%; + height: 30px; + background: linear-gradient(to bottom, #00000000, #000000cc); +} + +.pagination { + display: grid; + grid-template-columns: repeat(auto-fill, 2em); + grid-gap: 5px; + justify-content: center; + + /* background: hotpink; */ +} + +.pagination__link { + width: 2em; + height: 2em; + line-height: 2em; + text-align: center; + border-radius: 2em; + font-size: 0.8em; + background: #000; + color: #666; +} + +.pagination__link--current { + color: #fff; +} + +.pagination__link--more { + width: 5em; +} + +.error { + display: flex; + + height: 100%; +} + +.error__msg { + margin: auto; + text-align: center; + color: red; +} + +.details { + display: none; + justify-content: center; + align-items: center; + + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; +} + +.details.is-displayed { + display: flex; +} + +.details__bg, +.details__bg::after { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} + +.details__bg img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center top; + filter: blur(5px); +} + +.details__bg::after { + content: ""; + background-position: center top; + background-repeat: no-repeat; + background-size: cover; + background-image: repeating-linear-gradient( + 180deg, + rgba(0, 0, 0, 0.85), + rgba(0, 0, 0, 0.85) 1px, + rgba(0, 0, 0, 0.6) 1px, + rgba(0, 0, 0, 0.6) 2px + ); + background-size: cover; +} + +.details__content { + display: flex; + flex-direction: column; + + position: relative; + width: 640px; + background: #000000aa; +} + +.details__poster { + display: none; + flex: 1 0 200px; +} + +.details__poster__img { + object-fit: cover; +} + +.details__info { + display: flex; + flex-direction: column; + + padding: 20px; + color: #ccc; +} + +.details__title { + margin: 0; + font-weight: 100; +} + +.details__closebtn { + position: absolute; + right: 10px; + top: 10px; +} + +.details__closebtn::before { + border: 1px solid currentColor; + background: #000; + color: #fff; +} + +.details__imdb { + display: flex; + align-items: center; + + margin-top: auto; + margin-left: auto; + padding: 5px 10px; + border: 1px solid #666; + border-radius: 50px; +} + +.imdb__stars { + display: grid; + grid-template-columns: repeat(10, 1em); + + position: relative; + margin-left: 5px; + font-family: monospace; +} + +.imdb__stars::before { + content: "★★★★★★★★★★"; + position: absolute; + color: #999; +} + +.imdb__stars span { + display: block; + mix-blend-mode: overlay; + background: yellow; +} + +@media (min-width: 760px) { + .suggestions { + left: 50%; + width: 380px; + margin-left: -190px; + } + .movie { + font-size: 14px; + } + + .details__content { + flex-direction: initial; + box-shadow: 0 15px 25px #00000088; + } + + .details__poster { + display: block; + } + + .details__closebtn { + right: -10px; + top: -10px; + } +} diff --git a/index.html b/index.html new file mode 100644 index 00000000..3d70a1e8 --- /dev/null +++ b/index.html @@ -0,0 +1,43 @@ + + + + + + + + Project Cinema + + + + +
+
+ +
+ +
+ + +
+ +
+ + + + + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 00000000..c710c030 --- /dev/null +++ b/index.js @@ -0,0 +1,232 @@ +const API_KEY = "898d3609"; +const API_URL = `http://www.omdbapi.com/?apikey=${API_KEY}`; +const ITEMS_PER_PAGE = 10; +const MAX_PAGES = 19; + +const searchForm = document.querySelector("#search-form"); +const searchField = document.querySelector("#search-field"); +const suggestions = document.querySelector("#suggestions"); +const content = document.querySelector("#content"); +const pagination = document.querySelector("#pagination"); +const details = document.querySelector("#details"); + +const getSearchUrl = (s, page = 1) => { + return `${API_URL}&s=${s}&page=${page}`; +}; + +const getInfoUrl = i => `${API_URL}&i=${i}`; + +const onSearchReset = () => { + suggestions.innerHTML = ""; + searchField.focus(); +}; + +function resetUI() { + content.innerHTML = ""; + pagination.innerHTML = " "; + suggestions.innerHTML = ""; + searchField.focus(); +} + +function resetDetails() { + document.removeEventListener("keyup", onEscapeDetails); + details.classList.remove("is-displayed"); + details.innerHTML = ""; +} + +// Search form is submitted +//-------------------------------------- +function onSearchSubmit(e) { + e.preventDefault(); + + resetUI(); + fetchSearchResults(1); +} + +function fetchSearchResults(pageNum) { + const searchTerm = searchField.value; + + if (searchTerm.length === 0) return; + + suggestions.innerHTML = ""; + content.innerHTML = ""; + content.classList.add("loading"); + updatePagination(pageNum); + + fetch(getSearchUrl(searchTerm, pageNum)) + .then(res => (res.ok ? res.json() : Promise.reject(res.statusText))) + .then(data => displaySearchResults(data, searchTerm, pageNum)) + .catch(err => displayError(err)); +} + +const displaySearchResults = (data, searchTerm, pageNum) => { + content.classList.remove("loading"); + + if (data.Error) { + return displayError(data.Error); + } + + const pageCount = Math.ceil(data.totalResults / ITEMS_PER_PAGE); + suggestions.innerHTML = ""; + content.innerHTML = getMovieHTML(data.Search); + pagination.innerHTML = getPaginationHTML(searchTerm, pageCount); + updatePagination(pageNum); +}; + +function getMovieHTML(movies) { + if (!movies) return ""; + + function makePoster({ Poster, Title, Year, imdbID }) { + return ` + + Movie Poster +

${Title} (${Year})

+
+ `; + } + + const html = movies.map(makePoster).join(""); + return `
${html}
`; +} + +function getPaginationHTML(searchTerm, pageCount) { + function makePageBtn(_, i) { + const n = i + 1; + const url = getSearchUrl(searchTerm, n); + + return ` + ${n} + `; + } + + const more = pageCount > MAX_PAGES + const pageNum = more ? MAX_PAGES : pageCount; + const links = Array.from({ length: pageNum }, makePageBtn); + + if(more) links.push(`MORE`) + + return links.join(""); +} + +// Display additional movie info +//-------------------------------------- +function onMovieClick(e) { + e.preventDefault(); + + if (e.target.matches("a.movie")) { + fetchMovieInfo(e.target.href); + } +} + +function fetchMovieInfo(url) { + fetch(url) + .then(res => (res.ok ? res.json() : Promise.reject(res.statusText))) + .then(data => displayMovieInfo(data)) + .catch(err => displayError(err)); +} + +function displayMovieInfo(data) { + // console.log("onMovieInfo:", data); + function makeInfo(m) { + return ` +
+ +
+
+ + +
+

${m.Title}

+

${m.Plot}

+
+ IMDB: +
+   +
+
+
+
+ `; + } + + document.addEventListener("keyup", onEscapeDetails); + details.innerHTML = makeInfo(data); + details.classList.add("is-displayed"); +} + +function onEscapeDetails(e) { + if (e.keyCode === 27) resetDetails(); +} + +function onCloseDetailsClick(e) { + if (e.target.matches("button.details__closebtn")) { + resetDetails(); + } +} + +// Typeahead support +//-------------------------------------- +function onSearchInput(e) { + const key = String.fromCharCode(e.keyCode); + const term = searchField.value; + + // Return if key pressed was no alphanumeric or term is too short + if (!/[a-zA-Z0-9-_ ]/.test(key)) return; + if (term.length < 3) return; + + fetch(getSearchUrl(term + "*")) + .then(res => (res.ok ? res.json() : Promise.reject(res.statusText))) + .then(data => displaySuggestions(data.Search)) + .catch(err => displayError(err)); +} + +function displaySuggestions(data = []) { + const cb = m => `${m.Title}`; + const html = data.map(cb).join(""); + suggestions.innerHTML = html; +} + +function onSuggestionClick(e) { + e.preventDefault(); + + searchField.value = e.target.textContent; + fetchSearchResults(); +} + +// Pagination click handler +//-------------------------------------- +function onPageClick(e) { + e.preventDefault(); + + if (e.target.matches("a.pagination__link")) { + fetchSearchResults(e.target.dataset.page); + } +} + +function updatePagination(pageNum) { + console.log("updatePagination", pageNum); + [...pagination.children].forEach(el => { + +el.dataset.page === pageNum + ? el.classList.add("pagination__link--current") + : el.classList.remove("pagination__link--current"); + }); +} + +// Handle errors gracefully +//-------------------------------------- +function displayError(err) { + const html = `

${err}

`; + content.innerHTML = html; +} + +// Bind event handlers +//------------------------------------------------------------------------------ +searchForm.addEventListener("reset", onSearchReset); +searchForm.addEventListener("submit", onSearchSubmit); +searchField.addEventListener("keyup", debounce(onSearchInput)); +content.addEventListener("click", onMovieClick); +pagination.addEventListener("click", onPageClick); +suggestions.addEventListener("click", onSuggestionClick); +details.addEventListener("click", onCloseDetailsClick); diff --git a/utils.js b/utils.js new file mode 100644 index 00000000..8f49eac8 --- /dev/null +++ b/utils.js @@ -0,0 +1,18 @@ +function debounce(fn, wait = 250, callFirst = true) { + let timeout; + + return function() { + let args = arguments; + if (!wait) return fn(...args); + + let callNow = callFirst && !timeout; + clearTimeout(timeout); + + timeout = setTimeout(function() { + timeout = null; + if (!callNow) return fn(...args); + }, wait); + + if (callNow) return fn(...args); + }; +}