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)
diff --git a/index.html b/index.html
index f5ebbb9..f9fc57e 100644
--- a/index.html
+++ b/index.html
@@ -8,17 +8,22 @@
-
BB API
+
+ Breaking Bad Characters
-
+
+
+
+
-
+
+
@@ -27,6 +32,11 @@
+
+
diff --git a/main.js b/main.js
index fccaf1c..53b7006 100644
--- a/main.js
+++ b/main.js
@@ -1,12 +1,38 @@
-// `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"),
+ 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();
} )
@@ -15,52 +41,89 @@ function loader (){
output.innerHTML=
`
-
+ Loading...
`
}
-inpsearch.addEventListener("change",()=>{
- let searchquery = inpsearch.value;
- loader();
- fetchcharcters(searchquery)
+// 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));
})
-async function fetchcharcters(query){
+async function fetchcharcters(){
+ 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.");
+ }
+}
- let res;
+function showMessage(text){
+ output.innerHTML = "";
+ const p = document.createElement("p");
+ p.className = "col text-white text-center";
+ p.textContent = text;
+ output.appendChild(p);
+}
- 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){
+ if(!results.length){
+ showMessage("No characters found.");
+ return;
+ }
output.innerHTML=""
- results.map(result =>{
- const htmlstring =
- `
-
-