|
Balloni Albums is a website with a single web page, and a single data file.
The data file is in states/albums.json,
a JSON document listing the albums of music, manually maintained by me on the server.
The single web page contains the styling; there are no separate stylesheets.
The page contains all the JavaScript; there are no separate script files.
The page is driven by script; there is very little page layout not rendered by script.
When the site is loaded, the page loads the list of albums from the server
into a global variable called allAlbums using a function called getAllAlbums.
allAlbums is an array, each element of which is an object with fields:
{
"added": 1775158095,
"year": 2023,
"genre": "Electronic",
"artist": "Miss Grit",
"album": "Follow the Cyborg"
}
Then the page loads the albums into the UI using a function called loadAlbums.
Once the albums are loaded, the page computes the distinct list of genres
and populates the UI for picking genres, then the page is displayed.
When the user changes the genre or enters a key in the search box,
a function called omniboxChange is called. It picks apart what the
user is asking for with genre and search screens, filters allAlbums into a smaller list, and
calls loadAlbums with that smaller list to display the albums the user wants to see.
Let's have a look at the script.
Download the page to see the actual script as some things may get cut off or misrepresented in the samples below.
The only global variable that the script tracks is allAlbums,
the global list of all albums that were downloaded from the server when the page was loaded.
The page keeps this list in memory so that if you change the genre or your search that it does not need to access the server;
all the albums are already in memory.
The functions are:
getAllAlbums
// Fetch all albums from the server
async function getAllAlbums() {
const response = await fetch("states/albums.json");
if (!response.ok) {
alert("Loading albums failed");
return null;
}
const data = await response.json();
const albums = data.albums;
if (!albums) {
alert("No albums found");
return null;
}
return albums;
}
omniboxChange
// Handle changes to the search field (or genre picker)
function omniboxChange() {
// Get the search string
const omnibox = document.getElementById("omnibox");
const search_string = omnibox.value.toLowerCase();
console.log("Search: " + search_string);
// Break the search string into search terms
const search_terms = [];
let coll = "";
for (let s = 0; s < search_string.length; ++s) {
const c = search_string[s];
if (c == ' ') {
if (coll) {
search_terms.push(coll);
coll = "";
}
}
else
coll += c;
}
if (coll)
search_terms.push(coll);
// Get the chosen genre
const genre_picker = document.getElementById("genrePicker");
const genre = genre_picker.value;
console.log("Genre: " + search_string);
// Filter the albums
const filtered_albums = [];
for (let a = 0; a < allAlbums.length; ++a) {
const album = allAlbums[a];
let to_match = (album.artist + " " + album.album).toLowerCase();
let missed = false;
for (let s = 0; s < search_terms.length; ++s) {
if (to_match.indexOf(search_terms[s]) < 0) {
missed = true;
break;
}
}
if (!missed) {
if (!genre || genre == "Genre:" || album.genre == genre)
filtered_albums.push(album);
}
}
// Load the filtered albums
loadAlbums(filtered_albums);
}
loadAlbums
// Load a list of albums into the UI
function loadAlbums(albums) {
const albums_table = document.getElementById("albumList");
albums_table.innerHTML = "";
const header_row = document.createElement("TR");
albums_table.appendChild(header_row);
let header_cell = null;
header_cell = document.createElement("TH");
header_cell.innerText = "Added";
header_row.appendChild(header_cell);
header_cell = document.createElement("TH");
header_cell.innerText = "Album";
header_row.appendChild(header_cell);
albums.forEach(album => {
const album_row = document.createElement("TR");
albums_table.appendChild(album_row);
const added_cell = document.createElement("TD");
added_cell.className = "added";
added_cell.innerText = new Date(album.added * 1000).toLocaleDateString();
album_row.appendChild(added_cell);
const album_cell = document.createElement("TD");
album_row.appendChild(album_cell);
const album_table = document.createElement("TABLE");
album_table.className = "albumTable";
album_cell.appendChild(album_table);
const artist_album_row = document.createElement("TR");
album_table.appendChild(artist_album_row);
const artist_cell = document.createElement("TD");
artist_cell.className = "artistAlbum";
artist_cell.style.border = "none";
artist_cell.innerHTML =
"" + encodeHtml(album.genre) + " - " + encodeHtml(album.year) + " "
+
"" + encodeHtml(album.artist) + " "
+
"" + encodeHtml(album.album) + " ";
artist_album_row.appendChild(artist_cell);
const browse_cell = document.createElement("TD");
browse_cell.className = "browse";
browse_cell.innerText = "Browse";
browse_cell.addEventListener("click", () => {
const search_string = "\"" + album.artist + "\" \"" + album.album + "\"";
const search_url = "https://www.google.com/search?q=" + encodeURIComponent(search_string);
window.open(search_url, "_blank");
});
artist_album_row.appendChild(browse_cell);
});
albums_table.style.display = "";
}
Back to the list
Does not use cookies or collect user data.
Copyright © 2026 Michael Balloni.
All rights reserved.
|