Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<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 | Joanne O'Malley (joanne342)</title>
<link href="style.css" rel="stylesheet" />
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
</head>

Copy link
Copy Markdown
Contributor

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?

Expand Down
297 changes: 291 additions & 6 deletions script.js
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() {

Copy link
Copy Markdown
Contributor

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 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) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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}">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
58 changes: 57 additions & 1 deletion style.css
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;
}