-
-
Notifications
You must be signed in to change notification settings - Fork 93
Manchester | ITP-May-26 | Joanne O'Malley | Sprint 3 | Project TV Show #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless there's a clear reason for it, having a lot of your functions defined inside setup might not be such a good idea as it leads to a very long difficult to read function. Do you think this approach is the best for your code? |
||
| const controls = getControlsContainer(); | ||
|
|
||
| // Create the controls once (Added Back Button) | ||
| controls.innerHTML = ` | ||
| <button id="back-to-shows" style="display:none">← Back to Shows</button> | ||
| <select id="show-select"></select> | ||
| <select id="episode-select"></select> | ||
| <input id="search-input" type="text" placeholder="Search..." /> | ||
| <span id="search-count">Loading shows...</span> | ||
| `; | ||
|
|
||
| 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 = `<option value="">Select a show...</option>`; | ||
| 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 = `<option value="ALL">All Episodes</option>`; | ||
|
|
||
| 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) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea to combine this control |
||
| 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 = ` | ||
| <h2 class="show-title" style="cursor:pointer; color:#0066cc;">${show.name}</h2> | ||
| <img src="${show.image?.medium || ""}" alt="${show.name}"> | ||
| <div class="show-summary">${show.summary || "<p>No summary available.</p>"}</div> | ||
| <div class="show-meta"> | ||
| <p><strong>Genres:</strong> ${show.genres?.join(" | ") || "N/A"}</p> | ||
| <p><strong>Status:</strong> ${show.status || "N/A"}</p> | ||
| <p><strong>Rating:</strong> ${show.rating?.average ?? "N/A"}</p> | ||
| <p><strong>Runtime:</strong> ${show.runtime ? `${show.runtime} min` : "N/A"}</p> | ||
| </div> | ||
| `; | ||
|
|
||
| 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 <a href="https://tvmaze.com/" target="_blank" rel="noopener noreferrer">TVMaze.com</a>'; | ||
| 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 = ` | ||
| <h2>${episode.name} - ${formatEpisodeCode(episode)}</h2> | ||
| <img src="${episode.image?.medium || ""}" alt="${episode.name}"> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this an appropriate use of an alt tag? See what https://axesslab.com/alt-texts/ has to say |
||
| <div class="episode-summary">${episode.summary || ""}</div> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't wrong, but can you think of any potential side effects of inserting the summary from the API directly into your HTML like this? |
||
| ${ | ||
| episode.url | ||
| ? `<a href="${episode.url}" target="_blank" rel="noopener noreferrer">View on TVMaze</a>` | ||
| : "" | ||
| } | ||
| `; | ||
|
|
||
| container.appendChild(card); | ||
| }); | ||
|
|
||
| rootElem.appendChild(container); | ||
| } | ||
|
|
||
| window.onload = setup; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of your HTML is created within the Js. What benefits or tradeoffs do you think there are with this approach?