diff --git a/index.html b/index.html index 9a400d1..035aeab 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - TV Show Project | My Name (My GitHub username) + TV Show Project | Joanne O'Malley (joanne342) diff --git a/script.js b/script.js index 87a7de8..01cbdf4 100644 --- a/script.js +++ b/script.js @@ -1,12 +1,297 @@ -//You can edit ALL of the code here -function setup() { - const allEpisodes = getAllEpisodes(); - makePageForEpisodes(allEpisodes); +// Helper to format season and episode numbers into "S01E01" format +function formatEpisodeCode(episode) { + const season = String(episode.season).padStart(2, "0"); + const number = String(episode.number).padStart(2, "0"); + return `S${season}E${number}`; } -function makePageForEpisodes(episodeList) { +// Cache episode requests so each episode URL is fetched only once +const episodeCache = {}; + +function getEpisodesForShow(showId) { + if (!episodeCache[showId]) { + episodeCache[showId] = fetch( + `https://api.tvmaze.com/shows/${showId}/episodes` + ).then((response) => { + if (!response.ok) { + throw new Error(`Failed to load episodes for show ID ${showId}`); + } + + return response.json(); + }); + } + + return episodeCache[showId]; +} + +// Creates or gets the .controls container +function getControlsContainer() { + let controls = document.querySelector(".controls"); + + if (!controls) { + controls = document.createElement("div"); + controls.className = "controls"; + + const root = document.getElementById("root"); + root.parentNode.insertBefore(controls, root); + } + + return controls; +} + +async function setup() { + const controls = getControlsContainer(); + + // Create the controls once (Added Back Button) + controls.innerHTML = ` + + + + + Loading shows... + `; + + const backToShowsBtn = document.getElementById("back-to-shows"); + const showSelect = document.getElementById("show-select"); + const episodeSelect = document.getElementById("episode-select"); + const searchInput = document.getElementById("search-input"); + const searchCount = document.getElementById("search-count"); + + let currentEpisodes = []; + let currentShowName = ""; + let allShows = []; + let currentView = "shows"; // View state tracker ("shows" | "episodes") + + try { + // Fetch all shows + const showsResponse = await fetch("https://api.tvmaze.com/shows"); + + if (!showsResponse.ok) { + throw new Error("Failed to load shows"); + } + + allShows = await showsResponse.json(); + + // Sort alphabetically, ignoring case + allShows.sort((a, b) => + a.name.localeCompare(b.name, undefined, { + sensitivity: "base", + }) + ); + + // Populate show dropdown selector + showSelect.innerHTML = ``; + allShows.forEach((show) => { + const option = document.createElement("option"); + option.value = show.id; + option.textContent = show.name; + showSelect.appendChild(option); + }); + + // Renders the main shows front-page view + function renderShowsView(showsToDisplay) { + currentView = "shows"; + backToShowsBtn.style.display = "none"; + episodeSelect.style.display = "none"; + showSelect.value = ""; + searchInput.value = ""; + searchInput.placeholder = "Search shows by name, genre, summary..."; + + makePageForShows(showsToDisplay, loadShow); + searchCount.textContent = `Displaying ${showsToDisplay.length}/${allShows.length} shows`; + } + + // Populate the episode dropdown for the current show + function populateEpisodeSelect(episodes) { + episodeSelect.innerHTML = ``; + + episodes.forEach((episode) => { + const option = document.createElement("option"); + option.value = episode.id; + option.textContent = `${formatEpisodeCode(episode)} - ${episode.name}`; + episodeSelect.appendChild(option); + }); + } + + // Load episodes for a show + async function loadShow(showId) { + searchCount.textContent = "Loading episodes..."; + + try { + const selectedShow = allShows.find( + (show) => show.id === Number(showId) + ); + + if (!selectedShow) return; + + currentShowName = selectedShow.name; + currentEpisodes = await getEpisodesForShow(showId); + + currentView = "episodes"; + backToShowsBtn.style.display = "inline-block"; + episodeSelect.style.display = "inline-block"; + showSelect.value = showId; + searchInput.value = ""; + searchInput.placeholder = "Search episodes..."; + + populateEpisodeSelect(currentEpisodes); + makePageForEpisodes(currentEpisodes, currentShowName); + searchCount.textContent = `Displaying ${currentEpisodes.length}/${currentEpisodes.length} episodes`; + } catch (error) { + searchCount.textContent = "Error loading episodes."; + console.error(error); + } + } + + // Display shows listing on app load + renderShowsView(allShows); + + // Navigation: Back to shows button + backToShowsBtn.addEventListener("click", () => { + renderShowsView(allShows); + }); + + // Show dropdown selector + showSelect.addEventListener("change", async (event) => { + if (event.target.value) { + await loadShow(event.target.value); + } else { + renderShowsView(allShows); + } + }); + + // Dual-purpose search (Shows or Episodes based on view) + searchInput.addEventListener("input", (event) => { + const searchTerm = event.target.value.toLowerCase().trim(); + + if (currentView === "shows") { + const filteredShows = allShows.filter((show) => { + const nameMatch = show.name.toLowerCase().includes(searchTerm); + const genreMatch = show.genres.some((g) => + g.toLowerCase().includes(searchTerm) + ); + const summaryMatch = show.summary + ? show.summary.replace(/<[^>]*>/g, "").toLowerCase().includes(searchTerm) + : false; + + return nameMatch || genreMatch || summaryMatch; + }); + + makePageForShows(filteredShows, loadShow); + searchCount.textContent = `Displaying ${filteredShows.length}/${allShows.length} shows`; + } else { + episodeSelect.value = "ALL"; + + const filteredEpisodes = currentEpisodes.filter((episode) => { + const nameMatch = episode.name.toLowerCase().includes(searchTerm); + const summaryMatch = episode.summary + ? episode.summary.replace(/<[^>]*>/g, "").toLowerCase().includes(searchTerm) + : false; + + return nameMatch || summaryMatch; + }); + + makePageForEpisodes(filteredEpisodes, currentShowName); + searchCount.textContent = `Displaying ${filteredEpisodes.length}/${currentEpisodes.length} episodes`; + } + }); + + // Episode dropdown selector + episodeSelect.addEventListener("change", (event) => { + const selectedId = event.target.value; + searchInput.value = ""; + + if (selectedId === "ALL") { + makePageForEpisodes(currentEpisodes, currentShowName); + searchCount.textContent = `Displaying ${currentEpisodes.length}/${currentEpisodes.length} episodes`; + return; + } + + const selectedEpisode = currentEpisodes.find( + (episode) => episode.id === Number(selectedId) + ); + + if (selectedEpisode) { + makePageForEpisodes([selectedEpisode], currentShowName); + searchCount.textContent = `Displaying 1/${currentEpisodes.length} episodes`; + } + }); + } catch (error) { + searchCount.textContent = "Failed to load TV shows."; + console.error(error); + } +} + +// Render show cards for front page listing +function makePageForShows(showsList, onShowClick) { + const rootElem = document.getElementById("root"); + rootElem.innerHTML = ""; + + const container = document.createElement("div"); + container.className = "shows-container"; + + showsList.forEach((show) => { + const card = document.createElement("article"); + card.className = "show-card"; + + card.innerHTML = ` +

${show.name}

+ ${show.name} +
${show.summary || "

No summary available.

"}
+
+

Genres: ${show.genres?.join(" | ") || "N/A"}

+

Status: ${show.status || "N/A"}

+

Rating: ${show.rating?.average ?? "N/A"}

+

Runtime: ${show.runtime ? `${show.runtime} min` : "N/A"}

+
+ `; + + card.querySelector(".show-title").addEventListener("click", () => { + onShowClick(show.id); + }); + + container.appendChild(card); + }); + + rootElem.appendChild(container); +} + +// Render episode cards +function makePageForEpisodes(episodeList, showName) { const rootElem = document.getElementById("root"); - rootElem.textContent = `Got ${episodeList.length} episode(s)`; + rootElem.innerHTML = ""; + + const heading = document.createElement("h1"); + heading.textContent = `${showName} Episodes`; + rootElem.appendChild(heading); + + const credit = document.createElement("p"); + credit.innerHTML = + 'Data originally from TVMaze.com'; + rootElem.appendChild(credit); + + const container = document.createElement("div"); + container.className = "episodes-container"; + + episodeList.forEach((episode) => { + const card = document.createElement("article"); + card.className = "episode-card"; + + card.innerHTML = ` +

${episode.name} - ${formatEpisodeCode(episode)}

+ ${episode.name} +
${episode.summary || ""}
+ ${ + episode.url + ? `View on TVMaze` + : "" + } + `; + + container.appendChild(card); + }); + + rootElem.appendChild(container); } window.onload = setup; diff --git a/style.css b/style.css index 77cb8d4..89451ea 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,59 @@ +body { + font-family: system-ui, sans-serif; + background: #fafafa; + padding: 2rem; +} + #root { - color: red; + width: min(1100px, 100%); + margin-inline: auto; +} + +.episodes-container { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; +} + +.episode-card { + flex: 1 1 300px; + padding: 1.25rem; + background: #fff; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.episode-card img { + display: block; + aspect-ratio: 16 / 9; + object-fit: cover; + border-radius: 10px; +} + +.episode-card h2 { + margin: 1rem 0 0.75rem; +} + +.episode-summary { + color: #666; + line-height: 1.6; +} + +.episode-card a { + display: inline-block; + margin-top: 0.5rem; + color: #145dbf; +} + +.episode-card a:hover { + opacity: 0.75; +} +.controls { + display: flex; + gap: 20px; +} + +.controls input, +.controls select { + padding: 10px; }