From 9fd0c9c366d31af885d7d7fe4042972794d467a1 Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:20:33 +0300 Subject: [PATCH 1/9] fix: replace defunct breakingbadapi.com with the TVmaze cast API breakingbadapi.com no longer resolves (DNS lookup fails), so the page was stuck on 'Loading...' forever with an unhandled fetch rejection. Load the Breaking Bad cast (show 169) from TVmaze, which is free, keyless and CORS-enabled, and filter it locally by character or actor name. TVmaze doesn't provide nickname/status/occupation, so cards show character, actor and actor's birthday. This also fixes the Name/Actor labels, which were swapped. Cards are built with textContent instead of interpolating API data into innerHTML. --- main.js | 83 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/main.js b/main.js index fccaf1c..eee33af 100644 --- a/main.js +++ b/main.js @@ -1,9 +1,13 @@ -// `https://www.breakingbadapi.com/api/characters?name=` +// breakingbadapi.com has shut down (its domain no longer resolves), so the +// cast list is loaded from the TVmaze API instead: https://www.tvmaze.com/api +// Breaking Bad is show 169 on TVmaze. +const CAST_URL = "https://api.tvmaze.com/shows/169/cast"; const inpsearch = document.getElementById("inp-search"), output = document.getElementById("output"), audio = new Audio("theme.m4a") +let cast = []; window.addEventListener("load" , ()=>{ audio.play(); @@ -22,45 +26,58 @@ function loader (){ } inpsearch.addEventListener("change",()=>{ - let searchquery = inpsearch.value; - loader(); - fetchcharcters(searchquery) + render(filterCast(inpsearch.value)); }) -async function fetchcharcters(query){ - - let res; +async function fetchcharcters(){ + const res = await fetch(CAST_URL); + cast = await res.json(); + render(filterCast(inpsearch.value)); +} - if(query){ - res = await fetch(`https://www.breakingbadapi.com/api/characters?name=${query}`) - }else{ - res = await fetch(`https://www.breakingbadapi.com/api/characters`) - } +// Match the search text against both the character and the actor name. +function filterCast(query){ + const q = query.trim().toLowerCase(); + if(!q) return cast; + return cast.filter(({ person, character }) => + character.name.toLowerCase().includes(q) || + person.name.toLowerCase().includes(q) + ); +} - let results = await res.json(); +function createField(tag, label, value){ + const el = document.createElement(tag); + const span = document.createElement("span"); + span.textContent = ` ${value || "Unknown"} `; + el.append(`${label} : `, span); + return el; +} +// Build cards with DOM APIs / textContent rather than innerHTML so that +// data from the API is never interpreted as markup. +function render(results){ output.innerHTML="" - results.map(result =>{ - const htmlstring = - ` - -
-
Name : ${result.portrayed}
-
-
Actor : ${result.name}
-
NickName : ${result.nickname}
-
Status : ${result.status}
-
Birthday : ${result.birthday}
-
occupation : ${result.occupation}
+ results.forEach(({ person, character }) =>{ + const card = document.createElement("div") + card.classList.add("col-md-3" , "mb-3" , "img-info") -
- ` + const image = character.image || person.image; + const img = document.createElement("img"); + img.className = "img"; + img.src = image ? image.medium : "img/bb-logo.png"; + img.alt = character.name; - let outstring = document.createElement("div") - outstring.classList.add("col-md-3" , "mb-3" , "img-info") - outstring.innerHTML=htmlstring - output.appendChild(outstring) - }) + const info = document.createElement("div"); + info.className = "info-display"; + info.append( + createField("h5", "Name", character.name), + document.createElement("hr"), + createField("h6", "Actor", person.name), + createField("h6", "Actor's birthday", person.birthday) + ); -} \ No newline at end of file + card.append(img, info); + output.appendChild(card) + }) +} From b822ffe6cfa097430dcc852c4ac2943e4c872b42 Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:20:58 +0300 Subject: [PATCH 2/9] fix: show an error message when characters fail to load Network or HTTP errors previously left the loading spinner on screen indefinitely. --- main.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/main.js b/main.js index eee33af..5db5e95 100644 --- a/main.js +++ b/main.js @@ -30,9 +30,23 @@ inpsearch.addEventListener("change",()=>{ }) async function fetchcharcters(){ - const res = await fetch(CAST_URL); - cast = await res.json(); - render(filterCast(inpsearch.value)); + try{ + const res = await fetch(CAST_URL); + if(!res.ok) throw new Error(`HTTP ${res.status}`); + cast = await res.json(); + render(filterCast(inpsearch.value)); + }catch(err){ + console.error(err); + showMessage("Couldn't load characters. Please try again later."); + } +} + +function showMessage(text){ + output.innerHTML = ""; + const p = document.createElement("p"); + p.className = "col text-white text-center"; + p.textContent = text; + output.appendChild(p); } // Match the search text against both the character and the actor name. From a23b3060f0816de5236a33fb2258ab112d74b01a Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:21:08 +0300 Subject: [PATCH 3/9] feat: filter characters as you type and show an empty-results message Search previously only ran on the 'change' event (blur/Enter) and a query with no matches rendered a blank area. Since filtering is now local, update on 'input' and tell the user when nothing matches. --- main.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index 5db5e95..6e7a23b 100644 --- a/main.js +++ b/main.js @@ -25,7 +25,8 @@ function loader (){ ` } -inpsearch.addEventListener("change",()=>{ +// Filtering is local, so update results on every keystroke. +inpsearch.addEventListener("input",()=>{ render(filterCast(inpsearch.value)); }) @@ -70,6 +71,11 @@ function createField(tag, label, value){ // Build cards with DOM APIs / textContent rather than innerHTML so that // data from the API is never interpreted as markup. function render(results){ + if(!results.length){ + showMessage("No characters found."); + return; + } + output.innerHTML="" results.forEach(({ person, character }) =>{ From 6a085f4487ac83c19d869b89b0d2f42f242cac29 Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:22:07 +0300 Subject: [PATCH 4/9] fix: handle blocked autoplay and add a button to play/pause the theme music Browsers reject audio.play() without a user gesture, which produced an unhandled promise rejection and meant the music never played. When autoplay was allowed there was no way to stop it (WCAG 1.4.2 Audio Control). Catch the rejection and add a toggle button with aria-pressed state. --- index.html | 3 +++ main.js | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index f5ebbb9..7f3cec6 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,9 @@
+
+ +
diff --git a/main.js b/main.js index 6e7a23b..33baf1d 100644 --- a/main.js +++ b/main.js @@ -5,12 +5,34 @@ const CAST_URL = "https://api.tvmaze.com/shows/169/cast"; const inpsearch = document.getElementById("inp-search"), output = document.getElementById("output"), + musicToggle = document.getElementById("music-toggle"), audio = new Audio("theme.m4a") let cast = []; +function updateMusicToggle(){ + const playing = !audio.paused; + musicToggle.textContent = playing ? "Pause theme music" : "Play theme music"; + musicToggle.setAttribute("aria-pressed", String(playing)); +} + +function playMusic(){ + // Browsers block autoplay with sound until the user interacts with the + // page, in which case play() rejects; the toggle button stays available. + return audio.play().catch(() => {}).finally(updateMusicToggle); +} + +musicToggle.addEventListener("click", ()=>{ + if(audio.paused){ + playMusic(); + }else{ + audio.pause(); + updateMusicToggle(); + } +}) + window.addEventListener("load" , ()=>{ - audio.play(); + playMusic(); loader(); fetchcharcters(); } ) From 518c9489152da93c58719284bc164be494397a6d Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:22:30 +0300 Subject: [PATCH 5/9] fix(a11y): add alt text, a search label and keyboard access to character details The logo had no alt text and the search box relied on its placeholder only. Character details were revealed on :hover alone, so keyboard and touch users couldn't reach them; cards are now focusable and show details on :focus. The decorative loader image gets an empty alt. --- index.html | 5 +++-- main.js | 4 +++- style.css | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 7f3cec6..eee31d7 100644 --- a/index.html +++ b/index.html @@ -14,14 +14,15 @@
- + Breaking Bad
- + +
diff --git a/main.js b/main.js index 33baf1d..fa88de6 100644 --- a/main.js +++ b/main.js @@ -41,7 +41,7 @@ function loader (){ output.innerHTML= `
- + Loading...
` @@ -103,6 +103,8 @@ function render(results){ results.forEach(({ person, character }) =>{ const card = document.createElement("div") card.classList.add("col-md-3" , "mb-3" , "img-info") + // Focusable so keyboard and touch users can reveal the details too. + card.tabIndex = 0 const image = character.image || person.image; const img = document.createElement("img"); diff --git a/style.css b/style.css index 5c6ccd7..633a1ea 100644 --- a/style.css +++ b/style.css @@ -27,7 +27,8 @@ header img { color: white; } -.img-info:hover>.info-display { +.img-info:hover>.info-display, +.img-info:focus>.info-display { display: block; } From fe5f17cd8224cbd969532582c51af50d326e5b82 Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:22:40 +0300 Subject: [PATCH 6/9] fix: give the page a descriptive title and meta description --- index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index eee31d7..47f5e93 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,8 @@ - BB API + + Breaking Bad Characters From e18aa6471d628a258ac58bd22cbf5c1d279b7659 Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:22:54 +0300 Subject: [PATCH 7/9] fix: credit TVmaze as the data source The TVmaze API license (CC BY-SA) requires crediting TVmaze with a link back from the site. --- index.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.html b/index.html index 47f5e93..f9fc57e 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,11 @@
+ +
From b721d0d079a0cc059799a8d980b2355e47b50941 Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:23:03 +0300 Subject: [PATCH 8/9] docs: document the TVmaze data source and how to run the site locally --- README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bfbf291..7a3e182 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,27 @@ # Breaking Bad API -> This apps shows all characters of Breaking Bad Show & also search for any character +> This app shows the main cast of the Breaking Bad show and lets you search for any character or actor. + +Live demo: https://breakingbadapp.vercel.app/ + ### Made with -* Bootstrap -* Vanilla Javascript -* Breaking Bad API +* Bootstrap 4 +* Vanilla JavaScript +* [TVmaze API](https://www.tvmaze.com/api) (cast of show [169](https://www.tvmaze.com/shows/169/breaking-bad)) + +> The app originally used `breakingbadapi.com`, which has since shut down. Data now comes from TVmaze and is used under its CC BY-SA license. + +### Running locally +It's a static site with no build step or dependencies. Serve the repository root with any static file server, for example: + +``` +npx serve . +# or +python3 -m http.server 8000 +``` + +Then open the printed URL (e.g. http://localhost:8000). You can open `index.html` directly from disk too, but some browsers restrict `fetch` and audio for `file://` pages. + +### Features +* Lists each main character with the actor and the actor's birthday (hover over a card, or focus it with the keyboard) +* Filters by character or actor name as you type +* Theme music with a play/pause button (browsers may block autoplay until you interact with the page) From 4595e33fa68773f9f4d00c89517ad178e25aa0a0 Mon Sep 17 00:00:00 2001 From: fadyehabamer Date: Thu, 24 Sep 2026 14:23:49 +0300 Subject: [PATCH 9/9] fix: don't replace the loading or error message while typing before data loads Typing into the search box before the cast arrived (or after it failed to load) replaced the spinner/error with a misleading 'No characters found.' --- main.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.js b/main.js index fa88de6..53b7006 100644 --- a/main.js +++ b/main.js @@ -49,6 +49,8 @@ function loader (){ // Filtering is local, so update results on every keystroke. inpsearch.addEventListener("input",()=>{ + // Keep the loading/error message until the cast has actually loaded. + if(!cast.length) return; render(filterCast(inpsearch.value)); })