diff --git a/index.html b/index.html index 9a400d1..52ac867 100644 --- a/index.html +++ b/index.html @@ -1,21 +1,78 @@ - - - - TV Show Project | My Name (My GitHub username) - - - - -
+ + + + TV Show Project | Martin Mwaka (temceo) + + + + + +
+ +
+ + +

+ + +
+ +
+ + +
+
+
+ + +
+
+

+ Film data source: + tvmaze.com +

- - + + + - - - - + \ No newline at end of file diff --git a/script.js b/script.js index 87a7de8..440f4ea 100644 --- a/script.js +++ b/script.js @@ -1,12 +1,355 @@ -//You can edit ALL of the code here -function setup() { - const allEpisodes = getAllEpisodes(); - makePageForEpisodes(allEpisodes); +// You can edit ALL of the code here + +const filmGrid = document.getElementById('film-grid'); +const showGrid = document.getElementById('show-grid'); +const singleFilmContainer = document.querySelector('.single-film-grid'); +const filterDisplay = document.querySelector('.filter-display'); +const showFilterDisplay = document.querySelector('.show-filter-display'); +const searchArea = document.querySelector('.search-area'); +const filmSelect = document.getElementById('film-select'); +const showSelect = document.getElementById('show-select'); +const searchInput = document.getElementById('film-search'); +const showSearchInput = document.getElementById('show-search'); +const filmControls = document.getElementById('film-controls'); +const showControls = document.getElementById('show-controls'); +const returnToShowsButton = document.querySelector('.return-to-shows'); +const returnToFilmsButton = document.querySelector('.exit'); +const API_SHOW_URL = 'https://api.tvmaze.com/shows'; + +let showsCache = null; +let showsPromise = null; +const filmsCache = new Map(); +const filmsPromises = new Map(); + +const state = { + filmQuery: '', + films: [], + filteredFilms: [], + showQuery: '', + shows: [], + filteredShows: [], + episodeId: 1, + selectedFilm: {}, +}; + +async function setup() { + showMessage('Loading shows...', 1000); + + try { + const fetchedShows = await fetchShows(); + // sort shows alphabetically by show name on fetching + if (!fetchedShows) return; + const sortedShows = fetchedShows + .map((show) => show) + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) + ); + state.shows = sortedShows; + renderFilms(); + renderShows(); + populateShowSelect(); + clearMessage(); + showMessage('Shows loaded', 1500); + } catch (error) { + console.error('Failed to load shows:', error); + showMessage('Sorry, we could not load the shows right now.'); + } +} + +async function fetchJson(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); +} + +async function fetchShows() { + if (showsCache) return showsCache; + + if (!showsPromise) { + showsPromise = fetchJson(API_SHOW_URL).then((data) => { + showsCache = data; + return showsCache; + }); + } + + return showsPromise; +} + +function createShowCard(show) { + const showCard = document + .getElementById('show-card-template') + .content.cloneNode(true); + + const showTitle = showCard.querySelector('h2'); + showTitle.dataset.showId = show.id; + showTitle.innerText = `${show.name}`; + showTitle.addEventListener('click', () => { + const normalisedId = showTitle.dataset.showId.trim(); + state.episodeId = Number(normalisedId); + showFilmsView(); + getFilms(state.episodeId); + }); + + const showImage = showCard.querySelector('img'); + showImage.src = show.image?.medium || ''; + showImage.alt = show.name || 'image from show'; + + const showSummary = showCard.querySelector('.show-summary'); + showSummary.innerHTML = show.summary || ''; + + const showRating = showCard.querySelector('.show-ratings'); + showRating.innerText = show.rating.average; + + const showGenres = showCard.querySelector('.show-genres'); + showGenres.innerText = show.genres.join(' | '); + + const showStatus = showCard.querySelector('.show-status'); + showStatus.innerText = show.status; + + const showRunTime = showCard.querySelector('.show-runtime'); + showRunTime.innerText = show.runtime; + + return showCard; +} + +function populateShowOption(show) { + const option = document.createElement('option'); + const { id, name } = show; + option.value = String(id); + option.textContent = name; + return option; +} + +function populateShowSelect() { + // clear showSelect before populating it + showSelect.innerHTML = ''; + const showsToDisplay = state.showQuery.trim() + ? state.filteredShows + : state.shows; + + showSelect.append(...showsToDisplay.map(populateShowOption)); +} + +function renderShows() { + const showDisplayElem = showGrid; + showDisplayElem.innerHTML = ''; + + const { showQuery, shows } = state; + const normalisedQuery = showQuery.trim().toLowerCase(); + + state.filteredShows = shows.filter((show) => { + const name = show.name?.toLowerCase() || ''; + const summary = show.summary?.toLowerCase() || ''; + const genres = show.genres?.join(',').toLowerCase() || ''; + return ( + name.includes(normalisedQuery) || + summary.includes(normalisedQuery) || + genres.includes(normalisedQuery) + ); + }); + + const showList = normalisedQuery === '' ? shows : state.filteredShows; + + document.querySelector('.show-label').innerText = + `Found ${showList.length} shows`; + populateShowSelect(); + showDisplayElem.append(...showList.map(createShowCard)); +} + +function setShowControlsEnabled(isEnabled) { + showSelect.disabled = !isEnabled; + showSearchInput.disabled = !isEnabled; + showControls.classList.toggle('hidden', !isEnabled); +} + +function showShowsView() { + showGrid.classList.remove('hidden'); + filmGrid.classList.add('hidden'); + singleFilmContainer.classList.add('hidden'); + filmControls.classList.add('hidden'); + returnToShowsButton.classList.add('hidden'); + searchArea.classList.remove('hidden'); + setShowControlsEnabled(true); +} + +async function fetchFilms(showId = state.episodeId) { + if (filmsCache.has(showId)) { + return filmsCache.get(showId); + } + + if (!filmsPromises.has(showId)) { + const promise = fetchJson( + `https://api.tvmaze.com/shows/${showId}/episodes` + ).then((data) => { + const normalisedData = Array.isArray(data) ? data : []; + filmsCache.set(showId, normalisedData); + return normalisedData; + }); + + filmsPromises.set(showId, promise); + } + + return filmsPromises.get(showId); +} + +function createFilmCard(film) { + const filmCard = document + .getElementById('film-card-template') + .content.cloneNode(true); + const title = filmCard.querySelector('h3'); + const filmImage = filmCard.querySelector('img'); + const filmSummary = filmCard.querySelector('p'); + + title.innerText = `${film.name} - ${formatFilmEpisodeCode('S', film.season)}${formatFilmEpisodeCode('E', film.number)}`; + filmImage.src = film.image?.medium || ''; + filmImage.alt = film.name || 'image from film'; + filmSummary.innerHTML = film.summary || ''; + + return filmCard; +} + +function populateFilmOption(film) { + const option = document.createElement('option'); + const { id, season, number, name } = film; + const seasonEpisodeDetails = `${formatFilmEpisodeCode('S', season)}${formatFilmEpisodeCode('E', number)}`; + option.value = String(id); + option.textContent = `${seasonEpisodeDetails} - ${name}`; + return option; +} + +async function getFilms(showId = state.episodeId) { + showMessage('Loading films...', 1000); + try { + const fetchedFilms = await fetchFilms(showId); + state.films = Array.isArray(fetchedFilms) ? fetchedFilms : []; + renderFilms(); + populateFilmSelect(); + clearMessage(); + if (state.films.length === 0) { + showMessage('No episodes available for this show.', 2000); + } else { + showMessage('Films loaded', 1500); + } + } catch (error) { + console.error('Failed to load films:', error); + showMessage('Sorry, we could not load the films right now.'); + } +} + +function formatFilmEpisodeCode(prefix, value) { + return `${prefix}${String(value).padStart(2, '0')}`; +} + +function populateFilmSelect() { + filmSelect.innerHTML = ''; + filmSelect.append(...state.films.map(populateFilmOption)); +} + +function displaySelectedFilm() { + const singleFilmContent = document.querySelector('.show-single-film'); + const chosenFilm = createFilmCard(state.selectedFilm); + + singleFilmContent.innerHTML = ''; + state.selectedFilm = {}; + singleFilmContent.append(chosenFilm); + + showSingleFilmView(); +} + +function renderFilms() { + const rootElem = filmGrid; + rootElem.innerHTML = ''; + + const filmsToRender = Array.isArray(state.films) ? state.films : []; + const { filmQuery } = state; + const normalisedQuery = filmQuery.trim().toLowerCase(); + + state.filteredFilms = filmsToRender.filter((film) => { + const name = film.name?.toLowerCase() || ''; + const summary = film.summary?.toLowerCase() || ''; + return name.includes(normalisedQuery) || summary.includes(normalisedQuery); + }); + + const episodeList = + normalisedQuery === '' ? filmsToRender : state.filteredFilms; + + filterDisplay.innerText = `Displaying ${episodeList.length}/${filmsToRender.length}`; + + rootElem.append(...episodeList.map(createFilmCard)); +} + +function showSingleFilmView() { + showGrid.classList.add('hidden'); + filmGrid.classList.add('hidden'); + singleFilmContainer.classList.remove('hidden'); + filmControls.classList.add('hidden'); + returnToShowsButton.classList.add('hidden'); + searchArea.classList.add('hidden'); +} + +function showFilmsView() { + showGrid.classList.add('hidden'); + filmGrid.classList.remove('hidden'); + singleFilmContainer.classList.add('hidden'); + filmControls.classList.remove('hidden'); + returnToShowsButton.classList.remove('hidden'); + searchArea.classList.remove('hidden'); + setShowControlsEnabled(false); +} + +function showMessage(message, duration = 3000) { + const existingMessage = document.querySelector('.app-message'); + if (existingMessage) existingMessage.remove(); + + const messageBox = document.createElement('div'); + messageBox.className = 'app-message'; + messageBox.textContent = message; + document.body.appendChild(messageBox); + + setTimeout(() => { + messageBox.remove(); + }, duration); } -function makePageForEpisodes(episodeList) { - const rootElem = document.getElementById("root"); - rootElem.textContent = `Got ${episodeList.length} episode(s)`; +function clearMessage() { + document.querySelector('.app-message')?.remove(); } -window.onload = setup; +// EVENT HANDLERS +searchInput.addEventListener('input', (event) => { + state.filmQuery = event.target.value; + renderFilms(); +}); + +showSearchInput.addEventListener('input', (event) => { + state.showQuery = event.target.value; + renderShows(); +}); + +filmSelect.addEventListener('change', (event) => { + const selectedValue = event.target.value.trim(); + if (!selectedValue) return; + + state.selectedFilm = + state.films.find((film) => film.id === Number(selectedValue)) || {}; + event.target.value = ''; + displaySelectedFilm(); +}); + +showSelect.addEventListener('change', async (event) => { + const selectedValue = event.target.value.trim(); + if (!selectedValue) return; + + state.episodeId = Number(selectedValue); +}); + +returnToFilmsButton.addEventListener('click', showFilmsView); + +returnToShowsButton.addEventListener('click', showShowsView); + +window.onload = () => { + showShowsView(); + setup(); +}; diff --git a/style.css b/style.css index 77cb8d4..7e26efe 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,251 @@ +/* CSS Reset */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +* { + margin: 0; + padding: 0; +} + +html { + font-size: 100%; + line-height: 1.5; +} + +img { + display: block; + width: 100%; + height: auto; +} + +body { + font-family: Arial, sans-serif; + background: #f4f4f4; + color: #222; +} + +.source { + text-align: center; +} + #root { - color: red; + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +.film-grid { + width: 100%; + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} + +.card, +.show-card { + width: 100%; + background: white; + padding: 1rem; +} + +.card { + border-radius: 8px; +} + +header { + margin-bottom: 0.8rem; +} + +.summary { + margin-top: 0.8rem; +} + +.search-area, +.show-search-area { + margin: 1.5rem auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; +} + +select, +input { + height: 1.5rem; + width: 300px; + border-radius: 3px; +} + +main { + margin: 0 auto; + width: 90%; + max-width: 1200px; +} + +/* single film css */ + +#film-controls { + margin: 1rem auto; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.single-film-grid { + margin: 2rem 0; + display: flex; + flex-direction: column; + align-items: center; +} +.show-single-film { + max-width: 500px; + width: 90%; +} + +button.exit { + padding: 0.7rem 0.5rem; + border: none; + border-radius: 5px; + margin: 2rem 0; + background-color: black; + color: white; + font-weight: 700; +} + +.app-message { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #222; + color: white; + padding: 0.8rem 1rem; + border-radius: 6px; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +/* css to hide divs as required */ +.hidden { + display: none !important; +} + +.film-view { + display: flex; + flex-direction: column; + align-items: center; +} + +.return-to-shows { + display: block; + margin: 1rem auto 1.5rem; + padding: 0.7rem 1rem; + background-color: black; + color: white; + border: none; + border-radius: 5px; + font-weight: 700; +} + +/* show area css*/ +.show-grid { + margin: 1.5rem 0; +} + +.show-title { + margin-bottom: 1rem; + font-size: 2rem; +} + +.show-title:hover { + cursor: pointer; + color: blue; +} +.show-content { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + padding: 1rem; + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; +} + +/* Image */ +.img-container { + width: 100%; + max-width: 300px; + margin: 0 auto; +} + +.show-image { + width: 100%; + height: auto; + aspect-ratio: 2 / 3; + object-fit: cover; + display: block; + border-radius: 6px; +} + +/* Summary */ +.show-summary { + line-height: 1.6; + font-size: 1rem; +} + +/* Details */ +.show-details { + display: grid; + gap: 0.75rem; +} + +.show-details p { + margin: 0; +} + +/* show-controls (input and select) */ +#show-controls { + margin: 1rem auto; +} + +#show-controls input { + margin-bottom: 1rem; +} + +/* show grid media queries */ +@media (min-width: 900px) { + .show-content { + grid-template-columns: 200px 1fr 250px; + align-items: start; + gap: 2rem; + } + + .img-container { + max-width: none; + } +} + +@media (min-width: 1200px) { + .show-content { + grid-template-columns: 250px minmax(300px, 1fr) 300px; + padding: 2rem; + } +} + +/* general page media queries */ +@media (min-width: 700px) { + .film-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + } +} + +@media (min-width: 1000px) { + .film-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } }