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
- + Breaking Bad +
+ +
- + +
@@ -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 = - ` - -
-
Name : ${result.portrayed}
-
-
Actor : ${result.name}
-
NickName : ${result.nickname}
-
Status : ${result.status}
-
Birthday : ${result.birthday}
-
occupation : ${result.occupation}
- -
- ` - - let outstring = document.createElement("div") - outstring.classList.add("col-md-3" , "mb-3" , "img-info") - outstring.innerHTML=htmlstring - output.appendChild(outstring) - }) + 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"); + img.className = "img"; + img.src = image ? image.medium : "img/bb-logo.png"; + img.alt = character.name; + + 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) + }) +} 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; }