Balloni Album List
This page describes how Balloni Album List website works.
|
|
The site is 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 album list is loaded, the page computes the distinct list of genres and populates the UI for picking a genre of albums to view, 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. getAllAlbums// Fetch all albums from the server
async function getAllAlbums() {
const response = await fetch("states/albums.json");
if (!response.ok) {
alert("Loading album list 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 =
"Does not use cookies or collect user data. Copyright © 2026 Michael Balloni. All rights reserved. |