-
-
Notifications
You must be signed in to change notification settings - Fork 93
Manchester | ITP-May-26 | Abdu Hassen | Sprint 3 | Project TV Show #80
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
118a073
1f19762
1b6eb24
4f7da4b
89b6f96
09cb001
d3c61c3
37088da
9c7e957
8add72e
6017175
4927cde
a2d9c07
9f59329
b1f2f09
048c076
2976c1b
0fd9bb5
606a8af
6a8d9e8
f49c525
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,21 +1,67 @@ | ||
| <!DOCTYPE html> | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>TV Show Project | My Name (My GitHub username)</title> | ||
| <title> | ||
| TV Show Project | Name:Abdu Hassen (GitHub username:Abduhasen) | ||
| </title> | ||
| <link href="style.css" rel="stylesheet" /> | ||
| <link rel="icon" href="/favicon.ico" type="image/x-icon" /> | ||
| </head> | ||
|
|
||
| <body> | ||
| <div id="root"> | ||
| </div> | ||
|
|
||
| <!-- Loads a provided function called getAllEpisodes() which returns all episodes --> | ||
| <script src="episodes.js"></script> | ||
|
|
||
| <!-- Loads YOUR javascript code --> | ||
| <header id="shows-controls" class="search-container"> | ||
| <input | ||
| type="text" | ||
| id="show-search-input" | ||
| placeholder="Search shows..." | ||
| aria-label="Search shows" | ||
| /> | ||
| <select id="show-select" aria-label="Select a show"> | ||
| <option value="">Select a show</option> | ||
| </select> | ||
| <span id="show-search-count"> Loading shows... </span> | ||
| </header> | ||
| <main id="shows-root"></main> | ||
| <header id="episodes-controls" class="search-container" hidden> | ||
| <button id="back-to-shows" type="button">Back to Shows</button> | ||
| <select id="episode-select" aria-label="Select an episode"></select> | ||
| <input | ||
| type="text" | ||
| id="search-input" | ||
| placeholder="Search episodes..." | ||
| aria-label="Search episodes" | ||
| /> | ||
| <span id="search-count"> Loading episodes... </span> | ||
| </header> | ||
| <main id="root"></main> | ||
| <template id="shows-template"> | ||
| <article class="show-card"> | ||
| <button class="show-button" type="button"> | ||
| <h2 class="show-title"></h2> | ||
| </button> | ||
| <img class="show-image" src="" alt="" /> | ||
| <div class="show-summary"></div> | ||
| <p class="show-genres"></p> | ||
| <p class="show-status"></p> | ||
| <p class="show-rating"></p> | ||
| <p class="show-runtime"></p> | ||
| </article> | ||
| </template> | ||
| <template id="episodes-template"> | ||
| <article class="episode-card"> | ||
| <h2 class="episode-title"></h2> | ||
| <img class="episode-image" src="" alt="" /> | ||
| <div class="episode-summary"></div> | ||
| <button class="episode-link" type="button">View on TVMaze</button> | ||
| </article> | ||
| </template> | ||
| <footer> | ||
| Data sourced from | ||
| <a href="https://tvmaze.com" target="_blank" rel="noopener noreferrer"> | ||
| TVMaze.com | ||
| </a> | ||
| </footer> | ||
| <script src="script.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,254 @@ | ||
| //You can edit ALL of the code here | ||
| function setup() { | ||
| const allEpisodes = getAllEpisodes(); | ||
| makePageForEpisodes(allEpisodes); | ||
| 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) { | ||
| const rootElem = document.getElementById("root"); | ||
| rootElem.textContent = `Got ${episodeList.length} episode(s)`; | ||
| let allShowsCache = null; | ||
| const episodeCache = {}; | ||
|
|
||
| async function getAllShows() { | ||
| if (!allShowsCache) { | ||
| allShowsCache = fetch("https://api.tvmaze.com/shows").then((response) => { | ||
| if (!response.ok) { | ||
| throw new Error("Failed to load shows"); | ||
| } | ||
| return response.json(); | ||
| }); | ||
| } | ||
| return allShowsCache; | ||
| } | ||
|
|
||
| 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]; | ||
| } | ||
| async function setup() { | ||
| const showsRoot = document.getElementById("shows-root"); | ||
| const showsControls = document.getElementById("shows-controls"); | ||
| const showSearchInput = document.getElementById("show-search-input"); | ||
| const showSearchCount = document.getElementById("show-search-count"); | ||
| const showSelect = document.getElementById("show-select"); | ||
| const episodesRoot = document.getElementById("root"); | ||
| const episodesControls = document.getElementById("episodes-controls"); | ||
| const backToShows = document.getElementById("back-to-shows"); | ||
| const episodeDropdown = document.getElementById("episode-select"); | ||
| const searchInput = document.getElementById("search-input"); | ||
| const searchCount = document.getElementById("search-count"); | ||
| const showsTemplate = document.getElementById("shows-template"); | ||
| const episodesTemplate = document.getElementById("episodes-template"); | ||
| let currentEpisodes = []; | ||
| let currentShowName = ""; | ||
| try { | ||
| const allShows = await getAllShows(); | ||
| allShows.sort((a, b) => | ||
| a.name.localeCompare(b.name, undefined, { | ||
| sensitivity: "base", | ||
| }), | ||
| ); | ||
| makePageForShows(allShows); | ||
| updateShowCount(allShows.length); | ||
| fillShowDropdown(allShows); | ||
| function updateShowCount(numberOfShows) { | ||
| showSearchCount.textContent = `Displaying ${numberOfShows}/${allShows.length} shows`; | ||
| } | ||
| showSearchInput.addEventListener("input", (event) => { | ||
| const searchTerm = event.target.value.toLowerCase().trim(); | ||
| const filteredShows = allShows.filter((show) => { | ||
| const nameMatch = show.name.toLowerCase().includes(searchTerm); | ||
| const genresMatch = show.genres | ||
| .join(" ") | ||
| .toLowerCase() | ||
| .includes(searchTerm); | ||
|
|
||
| const summaryMatch = show.summary | ||
| ? show.summary | ||
| .replace(/<[^>]*>/g, "") | ||
| .toLowerCase() | ||
| .includes(searchTerm) | ||
| : false; | ||
| return nameMatch || genresMatch || summaryMatch; | ||
| }); | ||
| makePageForShows(filteredShows); | ||
| updateShowCount(filteredShows.length); | ||
| fillShowDropdown(filteredShows); | ||
| }); | ||
| showSelect.addEventListener("change", (event) => { | ||
| const selectedShowId = event.target.value; | ||
|
|
||
| if (selectedShowId === "") { | ||
| return; | ||
| } | ||
|
|
||
| loadShow(selectedShowId); | ||
| }); | ||
| async function loadShow(showId) { | ||
| try { | ||
| const selectedShow = allShows.find( | ||
| (show) => show.id === Number(showId), | ||
| ); | ||
| if (!selectedShow) { | ||
| return; | ||
| } | ||
| currentShowName = selectedShow.name; | ||
| currentEpisodes = await getEpisodesForShow(showId); | ||
| searchInput.value = ""; | ||
| fillEpisodeDropdown(currentEpisodes); | ||
| showsRoot.hidden = true; | ||
| showsControls.hidden = true; | ||
| episodesRoot.hidden = false; | ||
| episodesControls.hidden = false; | ||
| makePageForEpisodes(currentEpisodes, currentShowName); | ||
| updateCount(currentEpisodes.length); | ||
| window.scrollTo(0, 0); | ||
| } catch (error) { | ||
| console.error(error); | ||
| searchCount.textContent = "Error loading episodes."; | ||
| } | ||
| } | ||
| function fillShowDropdown(shows) { | ||
| showSelect.innerHTML = ""; | ||
|
|
||
| const defaultOption = document.createElement("option"); | ||
|
|
||
| defaultOption.value = ""; | ||
| defaultOption.textContent = "Select a show"; | ||
|
|
||
| showSelect.appendChild(defaultOption); | ||
|
|
||
| shows.forEach((show) => { | ||
| const option = document.createElement("option"); | ||
|
|
||
| option.value = show.id; | ||
| option.textContent = show.name; | ||
|
|
||
| showSelect.appendChild(option); | ||
| }); | ||
| } | ||
| function fillEpisodeDropdown(episodes) { | ||
| episodeDropdown.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}`; | ||
| episodeDropdown.appendChild(option); | ||
| }); | ||
| } | ||
| function updateCount(numberOfEpisodes) { | ||
| searchCount.textContent = `Displaying ${numberOfEpisodes}/${currentEpisodes.length} episodes`; | ||
| } | ||
| searchInput.addEventListener("input", (event) => { | ||
| const searchTerm = event.target.value.toLowerCase().trim(); | ||
| episodeDropdown.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); | ||
| updateCount(filteredEpisodes.length); | ||
| }); | ||
| episodeDropdown.addEventListener("change", (event) => { | ||
| const selectedId = event.target.value; | ||
| searchInput.value = ""; | ||
| if (selectedId === "ALL") { | ||
| makePageForEpisodes(currentEpisodes, currentShowName); | ||
| updateCount(currentEpisodes.length); | ||
| return; | ||
| } | ||
| const selectedEpisode = currentEpisodes.find( | ||
| (episode) => episode.id === Number(selectedId), | ||
| ); | ||
| if (selectedEpisode) { | ||
| makePageForEpisodes([selectedEpisode], currentShowName); | ||
| updateCount(1); | ||
| } | ||
| }); | ||
| backToShows.addEventListener("click", () => { | ||
| episodesRoot.hidden = true; | ||
| episodesControls.hidden = true; | ||
| showsRoot.hidden = false; | ||
| showsControls.hidden = false; | ||
| searchInput.value = ""; | ||
| episodeDropdown.value = "ALL"; | ||
| showSelect.value = ""; | ||
| }); | ||
| function makePageForShows(showList) { | ||
| showsRoot.innerHTML = ""; | ||
| const container = document.createElement("div"); | ||
| container.className = "shows-container"; | ||
| showList.forEach((show) => { | ||
| const card = showsTemplate.content.cloneNode(true); | ||
| const button = card.querySelector(".show-button"); | ||
| const title = card.querySelector(".show-title"); | ||
| const image = card.querySelector(".show-image"); | ||
| const summary = card.querySelector(".show-summary"); | ||
| const genres = card.querySelector(".show-genres"); | ||
| const status = card.querySelector(".show-status"); | ||
| const rating = card.querySelector(".show-rating"); | ||
| const runtime = card.querySelector(".show-runtime"); | ||
| title.textContent = show.name; | ||
| image.src = show.image?.medium || ""; | ||
| image.alt = show.name; | ||
| summary.innerHTML = show.summary || "No summary available."; | ||
| genres.textContent = `Genres: ${show.genres.join(", ")}`; | ||
| status.textContent = `Status: ${show.status}`; | ||
| rating.textContent = `Rating: ${show.rating?.average || "N/A"}`; | ||
| runtime.textContent = `Runtime: ${show.runtime || "N/A"} minutes`; | ||
| button.addEventListener("click", () => { | ||
| loadShow(show.id); | ||
| }); | ||
| container.appendChild(card); | ||
| }); | ||
| showsRoot.appendChild(container); | ||
| } | ||
| function makePageForEpisodes(episodeList, showName) { | ||
| episodesRoot.innerHTML = ""; | ||
| const heading = document.createElement("h1"); | ||
| heading.textContent = `${showName} Episodes`; | ||
| episodesRoot.appendChild(heading); | ||
| const container = document.createElement("div"); | ||
| container.className = "episodes-container"; | ||
| episodeList.forEach((episode) => { | ||
| const card = episodesTemplate.content.cloneNode(true); | ||
| const title = card.querySelector(".episode-title"); | ||
| const image = card.querySelector(".episode-image"); | ||
| const summary = card.querySelector(".episode-summary"); | ||
| const link = card.querySelector(".episode-link"); | ||
| title.textContent = `${episode.name} - ${formatEpisodeCode(episode)}`; | ||
| image.src = episode.image?.medium || ""; | ||
| image.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 |
||
| summary.innerHTML = episode.summary || ""; | ||
|
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? |
||
| if (episode.url) { | ||
| link.addEventListener("click", () => { | ||
|
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. If the purpose of this button is to open a page, is there a more semantic approach you could use than writing js to open said link? |
||
| window.open(episode.url, "_blank"); | ||
| }); | ||
| } else { | ||
| link.style.display = "none"; | ||
| } | ||
| container.appendChild(card); | ||
| }); | ||
| episodesRoot.appendChild(container); | ||
| } | ||
| } catch (error) { | ||
| showSearchCount.textContent = "Failed to load TV shows."; | ||
| console.error(error); | ||
| } | ||
| } | ||
|
|
||
| window.onload = setup; | ||
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.
Unless there's a clear reason for it, having all your functions defined inside setup might not be such a good idea. Do you think this approach is the best for your code?