diff --git a/index.html b/index.html index 9a400d1..f113dab 100644 --- a/index.html +++ b/index.html @@ -1,21 +1,78 @@ - + - TV Show Project | My Name (My GitHub username) + + + + TV Show Project | Dagim Daniel -
-
+
+ + + + + + + + + +
+ +
+
+ Loading data, please wait... +
+ + +
+ + +
+ + - - + - - + diff --git a/script.js b/script.js index 87a7de8..29240a3 100644 --- a/script.js +++ b/script.js @@ -1,12 +1,279 @@ -//You can edit ALL of the code here -function setup() { - const allEpisodes = getAllEpisodes(); - makePageForEpisodes(allEpisodes); +let state = { + currentView: "SHOWS", + shows: [], + episodes: [], + episodesCache: {}, + selectedShowId: "", + selectedEpisodeId: "ALL", + searchTerm: "", +}; + +async function setup() { + document + .getElementById("show-select") + .addEventListener("change", handleShowSelectChange); + document + .getElementById("episode-select") + .addEventListener("change", handleEpisodeSelectChange); + document + .getElementById("search-input") + .addEventListener("input", handleSearchInput); + document + .getElementById("back-to-shows-btn") + .addEventListener("click", navigateToShowsView); + + try { + showLoading(true); + const response = await fetch("https://api.tvmaze.com/shows"); + if (!response.ok) { + throw new Error(`Failed to fetch shows (HTTP Status ${response.status})`); + } + + const rawShows = await response.json(); + + state.shows = rawShows.sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), + ); + + populateShowDropdown(state.shows); + render(); + } catch (error) { + showError(`Error loading shows: ${error.message}`); + } finally { + showLoading(false); + } +} + +async function loadEpisodesForShow(showId) { + showLoading(true); + hideError(); + + try { + const showIdStr = showId.toString(); + + if (state.episodesCache[showIdStr]) { + state.episodes = state.episodesCache[showIdStr]; + } else { + const response = await fetch( + `https://api.tvmaze.com/shows/${showIdStr}/episodes`, + ); + if (!response.ok) { + throw new Error( + `Failed to fetch episodes (HTTP Status ${response.status})`, + ); + } + const fetchedEpisodes = await response.json(); + state.episodesCache[showIdStr] = fetchedEpisodes; + state.episodes = fetchedEpisodes; + } + + state.selectedShowId = showIdStr; + state.selectedEpisodeId = "ALL"; + state.searchTerm = ""; + state.currentView = "EPISODES"; + + document.getElementById("show-select").value = state.selectedShowId; + document.getElementById("search-input").value = ""; + + populateEpisodeDropdown(state.episodes); + render(); + } catch (error) { + showError(`Error loading episodes: ${error.message}`); + } finally { + showLoading(false); + } +} + +function navigateToShowsView() { + state.currentView = "SHOWS"; + state.selectedShowId = ""; + state.selectedEpisodeId = "ALL"; + state.searchTerm = ""; + + document.getElementById("show-select").value = ""; + document.getElementById("search-input").value = ""; + + render(); +} + +async function handleShowSelectChange(event) { + const showId = event.target.value; + if (!showId) { + navigateToShowsView(); + } else { + await loadEpisodesForShow(showId); + } +} + +function handleEpisodeSelectChange(event) { + state.selectedEpisodeId = event.target.value; + render(); +} + +function handleSearchInput(event) { + state.searchTerm = event.target.value.toLowerCase(); + render(); +} + +function getFilteredShows() { + return state.shows.filter((show) => { + const nameMatch = show.name + ? show.name.toLowerCase().includes(state.searchTerm) + : false; + const summaryMatch = show.summary + ? show.summary.toLowerCase().includes(state.searchTerm) + : false; + const genreMatch = show.genres + ? show.genres.some((genre) => + genre.toLowerCase().includes(state.searchTerm), + ) + : false; + + return nameMatch || summaryMatch || genreMatch; + }); +} + +function getFilteredEpisodes() { + return state.episodes.filter((episode) => { + const matchesSelect = + state.selectedEpisodeId === "ALL" || + episode.id.toString() === state.selectedEpisodeId; + + const titleMatch = episode.name + ? episode.name.toLowerCase().includes(state.searchTerm) + : false; + const summaryMatch = episode.summary + ? episode.summary.toLowerCase().includes(state.searchTerm) + : false; + + return matchesSelect && (titleMatch || summaryMatch); + }); +} + +function render() { + const showsRoot = document.getElementById("shows-root"); + const episodesRoot = document.getElementById("episodes-root"); + const backBtn = document.getElementById("back-to-shows-btn"); + const episodeSelectElem = document.getElementById("episode-select"); + const searchInput = document.getElementById("search-input"); + const matchCountElem = document.getElementById("match-count"); + + if (state.currentView === "SHOWS") { + showsRoot.hidden = false; + episodesRoot.hidden = true; + backBtn.hidden = true; + episodeSelectElem.hidden = true; + searchInput.placeholder = "Search shows..."; + + const filteredShows = getFilteredShows(); + showsRoot.innerHTML = ""; + const showNodes = filteredShows.map(createShowCard); + showsRoot.append(...showNodes); + + matchCountElem.textContent = `Displaying ${filteredShows.length}/${state.shows.length} show(s)`; + } else { + showsRoot.hidden = true; + episodesRoot.hidden = false; + backBtn.hidden = false; + episodeSelectElem.hidden = false; + searchInput.placeholder = "Search episodes..."; + + const filteredEpisodes = getFilteredEpisodes(); + episodesRoot.innerHTML = ""; + const episodeNodes = filteredEpisodes.map(createEpisodeCard); + episodesRoot.append(...episodeNodes); + + matchCountElem.textContent = `Displaying ${filteredEpisodes.length}/${state.episodes.length} episode(s)`; + } +} + +function createShowCard(show) { + const template = document.getElementById("show-template"); + const cardNode = template.content.cloneNode(true); + + const titleElem = cardNode.querySelector(".show-title"); + titleElem.textContent = show.name; + titleElem.addEventListener("click", () => loadEpisodesForShow(show.id)); + + const imgElem = cardNode.querySelector(".show-image"); + imgElem.src = show.image?.medium || ""; + imgElem.alt = show.name || "Show poster"; + imgElem.addEventListener("click", () => loadEpisodesForShow(show.id)); + + cardNode.querySelector(".show-summary").innerHTML = show.summary || ""; + cardNode.querySelector(".show-rating").textContent = + show.rating?.average || "N/A"; + cardNode.querySelector(".show-genres").textContent = show.genres + ? show.genres.join(", ") + : "N/A"; + cardNode.querySelector(".show-status").textContent = show.status || "N/A"; + cardNode.querySelector(".show-runtime").textContent = show.runtime + ? `${show.runtime} mins` + : "N/A"; + + return cardNode; +} + +function createEpisodeCard(episode) { + const template = document.getElementById("episode-template"); + const cardNode = template.content.cloneNode(true); + + const episodeCode = getEpisodeCode(episode); + + cardNode.querySelector(".episode-title").textContent = + `${episode.name} - ${episodeCode}`; + cardNode.querySelector(".episode-image").src = episode.image?.medium || ""; + cardNode.querySelector(".episode-image").alt = + episode.name || "Episode image"; + cardNode.querySelector(".episode-summary").innerHTML = episode.summary || ""; + + return cardNode; +} + +function populateShowDropdown(shows) { + const showSelectElem = document.getElementById("show-select"); + showSelectElem.innerHTML = ``; + + shows.forEach((show) => { + const option = document.createElement("option"); + option.value = show.id; + option.textContent = show.name; + showSelectElem.appendChild(option); + }); +} + +function populateEpisodeDropdown(episodes) { + const episodeSelectElem = document.getElementById("episode-select"); + episodeSelectElem.innerHTML = ``; + + episodes.forEach((episode) => { + const option = document.createElement("option"); + option.value = episode.id; + option.textContent = `${getEpisodeCode(episode)} - ${episode.name}`; + episodeSelectElem.appendChild(option); + }); +} + +function getEpisodeCode(episode) { + const season = episode.season.toString().padStart(2, "0"); + const number = episode.number.toString().padStart(2, "0"); + return `S${season}E${number}`; +} + +function showLoading(isLoading) { + const loadingElem = document.getElementById("loading-indicator"); + loadingElem.hidden = !isLoading; +} + +function showError(message) { + const errorElem = document.getElementById("error-message"); + errorElem.textContent = message; + errorElem.hidden = false; } -function makePageForEpisodes(episodeList) { - const rootElem = document.getElementById("root"); - rootElem.textContent = `Got ${episodeList.length} episode(s)`; +function hideError() { + const errorElem = document.getElementById("error-message"); + errorElem.hidden = true; } window.onload = setup; diff --git a/style.css b/style.css index 77cb8d4..f6d9c22 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,182 @@ -#root { - color: red; +[hidden] { + display: none !important; +} + +body { + background-color: #e0dddd; + margin: 0; + padding-bottom: 60px; + font-family: Arial, Helvetica, sans-serif; +} + +#controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 15px; + padding: 15px 20px; + background-color: #f5f5f5; + border-bottom: 1px solid #ccc; + position: sticky; + top: 0; + z-index: 900; +} + +#controls input, +#controls select, +#controls button { + padding: 8px 12px; + font-size: 1rem; + border: 1px solid #ccc; + border-radius: 5px; +} + +#back-to-shows-btn { + background-color: #0056b3; + color: #ffffff; + border: none; + cursor: pointer; + font-weight: bold; +} + +#back-to-shows-btn:hover { + background-color: #003d80; +} + +#controls input { + min-width: 220px; +} + +#match-count { + font-weight: bold; + color: #333333; +} + +.status-message { + padding: 20px; + margin: 20px; + font-size: 1.25rem; + text-align: center; + background-color: #ffffff; + border-radius: 8px; +} + +.error-state { + color: #721c24; + background-color: #f8d7da; + border: 1px solid #f5c6cb; +} + +#shows-root { + display: flex; + flex-direction: column; + gap: 20px; + padding: 20px; +} + +#episodes-root { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; + padding: 20px; +} + +.show-card { + background-color: #f5f5f5; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 20px; + transition: box-shadow 0.2s ease; +} + +.show-card:hover { + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.show-title { + margin-top: 0; + font-size: 1.5rem; + color: #0056b3; + cursor: pointer; +} + +.show-title:hover { + text-decoration: underline; +} + +.show-body { + display: flex; + flex-wrap: wrap; + gap: 20px; +} + +.show-image { + width: 160px; + height: 225px; + object-fit: cover; + border-radius: 8px; + cursor: pointer; +} + +.show-summary { + flex: 2; + min-width: 250px; + font-size: 15px; + color: #222222; +} + +.show-meta { + flex: 1; + min-width: 180px; + background-color: #eaeaea; + padding: 15px; + border-radius: 8px; + font-size: 14px; +} + +.show-meta p { + margin: 6px 0; +} + +.episode-card { + background-color: #f5f5f5; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 20px; + text-align: left; + transition: box-shadow 0.2s ease; +} + +.episode-card:hover { + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.episode-title { + margin-top: 0; + font-size: 1.25rem; +} + +.episode-image { + width: 100%; + height: auto; + border-radius: 10px; +} + +.episode-summary { + font-size: 16px; + color: #222222; + max-height: 150px; + overflow-y: auto; +} + +#bottomBar { + border-top: 1px solid #000000; + text-align: center; + position: fixed; + bottom: 0; + left: 0; + background-color: #f5f5f5; + width: 100%; + padding: 10px 0; + z-index: 1000; }