// Global variables let albumsData = []; let filteredData = []; let currentPage = 1; const itemsPerPage = 50; let isReversed = false; let wikipediaUrlMappings = {}; let spotifyUrlMappings = {}; // DOM elements const albumsGrid = document.getElementById('albumsGrid'); const searchInput = document.getElementById('searchInput'); const statusFilter = document.getElementById('statusFilter'); const sortBy = document.getElementById('sortBy'); const reverseButton = document.getElementById('reverseButton'); const jumpToRank = document.getElementById('jumpToRank'); const jumpButton = document.getElementById('jumpButton'); const themeSelect = document.getElementById('themeSelect'); const loading = document.getElementById('loading'); const error = document.getElementById('error'); const stats = document.getElementById('stats'); // Initialize the application document.addEventListener('DOMContentLoaded', function() { loadWikipediaMapping(); loadSpotifyMapping(); loadAlbumsData(); setupEventListeners(); loadTheme(); handleUrlParams(); }); // Load Wikipedia URL mappings async function loadWikipediaMapping() { try { const response = await fetch('wikipedia_urls_mapping.json'); if (response.ok) { wikipediaUrlMappings = await response.json(); } } catch (err) { console.warn('Could not load Wikipedia URL mappings:', err); } } // Load Spotify URL mappings async function loadSpotifyMapping() { try { const response = await fetch('spotify_urls_mapping.json'); if (response.ok) { spotifyUrlMappings = await response.json(); } } catch (err) { console.warn('Could not load Spotify URL mappings:', err); } } // Setup event listeners function setupEventListeners() { searchInput.addEventListener('input', debounce(handleSearch, 300)); statusFilter.addEventListener('change', handleFilter); sortBy.addEventListener('change', handleSort); reverseButton.addEventListener('click', handleReverse); jumpButton.addEventListener('click', handleJumpToRank); jumpToRank.addEventListener('keypress', handleRankKeypress); themeSelect.addEventListener('change', handleThemeChange); // Add scroll listener for infinite scroll window.addEventListener('scroll', handleScroll); // Add click listeners to stats cards setupStatsClickHandlers(); } // Setup click handlers for stats cards function setupStatsClickHandlers() { const totalAlbumsCard = document.querySelector('.stat-item:has(#totalAlbums)') || document.querySelector('#totalAlbums').closest('.stat-item'); const newAlbumsCard = document.querySelector('.stat-item:has(#newAlbums)') || document.querySelector('#newAlbums').closest('.stat-item'); const improvedAlbumsCard = document.querySelector('.stat-item:has(#improvedAlbums)') || document.querySelector('#improvedAlbums').closest('.stat-item'); const droppedAlbumsCard = document.querySelector('.stat-item:has(#droppedAlbums)') || document.querySelector('#droppedAlbums').closest('.stat-item'); if (totalAlbumsCard) { totalAlbumsCard.addEventListener('click', () => handleStatsCardClick('')); totalAlbumsCard.style.cursor = 'pointer'; } if (newAlbumsCard) { newAlbumsCard.addEventListener('click', () => handleStatsCardClick('New in 2023')); newAlbumsCard.style.cursor = 'pointer'; } if (improvedAlbumsCard) { improvedAlbumsCard.addEventListener('click', () => handleStatsCardClick('improved')); improvedAlbumsCard.style.cursor = 'pointer'; } if (droppedAlbumsCard) { droppedAlbumsCard.addEventListener('click', () => handleStatsCardClick('dropped')); droppedAlbumsCard.style.cursor = 'pointer'; } } // Handle stats card clicks function handleStatsCardClick(filterValue) { // Update the status filter dropdown statusFilter.value = filterValue; // Trigger the filter change statusFilter.dispatchEvent(new Event('change')); // Scroll to the albums grid albumsGrid.scrollIntoView({ behavior: 'smooth' }); } // Theme handling functions function handleThemeChange(event) { const theme = event.target.value; setTheme(theme); saveTheme(theme); } function setTheme(theme) { if (theme) { document.documentElement.setAttribute('data-theme', theme); } else { document.documentElement.removeAttribute('data-theme'); } } function saveTheme(theme) { try { localStorage.setItem('selectedTheme', theme); } catch (e) { console.warn('Could not save theme to localStorage:', e); } } function loadTheme() { try { const savedTheme = localStorage.getItem('selectedTheme'); if (savedTheme !== null) { setTheme(savedTheme); themeSelect.value = savedTheme; } } catch (e) { console.warn('Could not load theme from localStorage:', e); } } // Load albums data from CSV async function loadAlbumsData() { try { const response = await fetch('top_500_albums_2023.csv'); if (!response.ok) { throw new Error('Failed to load data'); } const csvText = await response.text(); albumsData = parseCSV(csvText); filteredData = [...albumsData]; hideLoading(); renderAlbums(); initializeStats(); } catch (err) { console.error('Error loading data:', err); showError(); } } // Parse CSV data function parseCSV(csvText) { const lines = csvText.split('\n'); const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '')); const albums = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // Handle CSV parsing with proper quote handling const values = parseCSVLine(line); if (values.length < headers.length) continue; const album = {}; headers.forEach((header, index) => { album[header] = values[index] ? values[index].replace(/"/g, '').trim() : ''; }); // Convert rank to number album.Rank = parseInt(album.Rank) || 0; // Extract year from Info field const yearMatch = album.Info ? album.Info.match(/\b(19|20)\d{2}\b/) : null; album.Year = yearMatch ? parseInt(yearMatch[0]) : null; albums.push(album); } return albums; } // Parse a single CSV line handling quotes and commas function parseCSVLine(line) { const values = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { values.push(current); current = ''; } else { current += char; } } values.push(current); return values; } // Render albums function renderAlbums() { const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const albumsToShow = filteredData.slice(0, endIndex); if (currentPage === 1) { albumsGrid.innerHTML = ''; } albumsToShow.slice(startIndex).forEach(album => { const albumCard = createAlbumCard(album); albumsGrid.appendChild(albumCard); }); } // Get cover image path for an album function getCoverImagePath(album) { if (!album.Artist || !album.Album || !album.Rank) return null; // Sanitize artist and album names to match the downloaded filenames const safeArtist = album.Artist.replace(/[<>:"/\\|?*]/g, '').replace(/[^\w\s\-_.]/g, '').replace(/\s+/g, '_').slice(0, 100); const safeAlbum = album.Album.replace(/[<>:"/\\|?*]/g, '').replace(/[^\w\s\-_.]/g, '').replace(/\s+/g, '_').slice(0, 100); const rank = album.Rank.toString().padStart(3, '0'); const filename = `rank_${rank}_${safeArtist}_${safeAlbum}.jpg`; return `covers/${filename}`; } // Create album card element function createAlbumCard(album) { const card = document.createElement('div'); card.className = 'album-card'; card.setAttribute('data-rank', album.Rank); const statusClass = getStatusClass(album.Status); const statusText = formatStatus(album.Status); const coverImagePath = getCoverImagePath(album); card.innerHTML = `
${escapeHtml(p.trim())}
`).join(''); } } return paragraphs.map(p => `${escapeHtml(p)}
`).join(''); } function generateWikipediaUrl(album, artist) { // Clean up album and artist names const albumName = album.trim(); const artistName = artist.trim(); // Check if we have the exact Wikipedia URL from our mapping if (wikipediaUrlMappings[albumName]) { return `https://en.wikipedia.org/wiki/${wikipediaUrlMappings[albumName]}`; } // Remove "The" from the beginning of artist names for URL formatting const artistForUrl = artistName.replace(/^The /, ''); // Format for Wikipedia URL - replace spaces with underscores const albumUrl = albumName.replace(/ /g, '_'); const artistUrl = artistForUrl.replace(/ /g, '_'); // Default pattern: Album_Name_(Artist_Name_album) return `https://en.wikipedia.org/wiki/${encodeURIComponent(albumUrl)}_(${encodeURIComponent(artistUrl)}_album)`; } function generateSpotifyUrl(album, artist) { // Clean up album and artist names const albumName = album.trim(); const artistName = artist.trim(); // Check if we have the exact Spotify URL from our mapping if (spotifyUrlMappings[albumName]) { return spotifyUrlMappings[albumName]; } // If no mapping found, create a Spotify search URL const searchQuery = `${albumName} ${artistName}`; const encodedQuery = encodeURIComponent(searchQuery); return `https://open.spotify.com/search/${encodedQuery}`; } function hideLoading() { loading.style.display = 'none'; albumsGrid.style.display = 'grid'; } function showError() { loading.style.display = 'none'; error.style.display = 'block'; } // Bookmark and navigation functions function handleUrlParams() { const urlParams = new URLSearchParams(window.location.search); const hash = window.location.hash.slice(1); // Handle rank from hash (#rank123) or query parameter (?rank=123) const rankFromHash = hash.match(/^rank(\d+)$/); const rankFromQuery = urlParams.get('rank'); const targetRank = rankFromHash ? parseInt(rankFromHash[1]) : rankFromQuery ? parseInt(rankFromQuery) : null; // Handle other URL parameters for filters/search first const search = urlParams.get('search'); const status = urlParams.get('status'); const sort = urlParams.get('sort'); const reversed = urlParams.get('reversed') === 'true'; // Set up the state based on URL parameters let delay = 1000; if (search) { searchInput.value = search; setTimeout(() => handleSearch({target: searchInput}), delay); delay += 100; } if (status) { statusFilter.value = status; setTimeout(() => handleFilter({target: statusFilter}), delay); delay += 100; } if (sort) { sortBy.value = sort; setTimeout(() => handleSort({target: sortBy}), delay); delay += 100; } if (reversed) { setTimeout(() => handleReverse(), delay); delay += 100; } // Finally, jump to the rank if specified if (targetRank) { setTimeout(() => jumpToRankWithCurrentState(targetRank), delay); } } function handleJumpToRank() { const inputValue = jumpToRank.value.trim(); let rank; if (inputValue === '') { // Default to rank 1 if no input rank = 1; jumpToRank.value = '1'; } else { rank = parseInt(inputValue); if (isNaN(rank) || rank < 1 || rank > 589) { alert('Please enter a valid rank between 1 and 589'); return; } } jumpToRankNumber(rank); } function handleRankKeypress(event) { if (event.key === 'Enter') { handleJumpToRank(); } } function jumpToRankNumber(rank) { if (!albumsData.length) return; // Reset filters to show all albums by rank searchInput.value = ''; statusFilter.value = ''; sortBy.value = 'rank'; isReversed = false; reverseButton.classList.remove('reversed'); // Reset data and apply rank sorting filteredData = [...albumsData]; filteredData.sort((a, b) => a.Rank - b.Rank); // Update URL - clear all params if jumping to rank 1, otherwise set rank if (rank === 1) { clearUrl(); } else { updateUrl({rank}); } // Find the album card and scroll to it currentPage = Math.ceil(rank / itemsPerPage); renderAlbums(); // Wait for render then scroll setTimeout(() => scrollToRank(rank), 100); updateStats(); } function jumpToRankWithCurrentState(rank) { if (!albumsData.length) return; // Don't reset filters - preserve current state // Just ensure the target album is visible in current filtered data // Find the album card and scroll to it // First, make sure enough albums are loaded const albumInFiltered = filteredData.find(album => album.Rank === rank); if (albumInFiltered) { const indexInFiltered = filteredData.indexOf(albumInFiltered); currentPage = Math.ceil((indexInFiltered + 1) / itemsPerPage); renderAlbums(); } // Wait for render then scroll setTimeout(() => scrollToRank(rank), 100); updateStats(); } function scrollToRank(rank) { const albumCard = document.querySelector(`[data-rank="${rank}"]`); if (albumCard) { albumCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Highlight the album briefly albumCard.style.boxShadow = '0 0 20px rgba(102, 126, 234, 0.5)'; albumCard.style.transform = 'scale(1.02)'; setTimeout(() => { albumCard.style.boxShadow = ''; albumCard.style.transform = ''; }, 2000); } } function handleAlbumShare(rank, buttonElement) { const albumUrl = generateAlbumUrl(rank); // Copy to clipboard if (navigator.clipboard) { navigator.clipboard.writeText(albumUrl).then(() => { showAlbumShareFeedback(buttonElement); }).catch(() => { fallbackCopyToClipboard(albumUrl, buttonElement); }); } else { fallbackCopyToClipboard(albumUrl, buttonElement); } } function generateAlbumUrl(rank) { const baseUrl = window.location.origin + window.location.pathname; const params = new URLSearchParams(); // Add current state parameters if (statusFilter.value) { params.set('status', statusFilter.value); } if (sortBy.value !== 'rank') { params.set('sort', sortBy.value); } if (isReversed) { params.set('reversed', 'true'); } // Add search if present if (searchInput.value.trim()) { params.set('search', searchInput.value.trim()); } // Always add the rank for direct navigation params.set('rank', rank); const queryString = params.toString(); return queryString ? `${baseUrl}?${queryString}` : `${baseUrl}#rank${rank}`; } function generateShareableUrl() { const baseUrl = window.location.origin + window.location.pathname; const params = new URLSearchParams(); // Add current filters/search if (searchInput.value.trim()) { params.set('search', searchInput.value.trim()); } if (statusFilter.value) { params.set('status', statusFilter.value); } if (sortBy.value !== 'rank') { params.set('sort', sortBy.value); } if (isReversed) { params.set('reversed', 'true'); } // If we're viewing a specific rank area, add it const visibleCards = document.querySelectorAll('.album-card'); if (visibleCards.length > 0 && sortBy.value === 'rank' && !isReversed) { const firstVisibleRank = visibleCards[0].getAttribute('data-rank'); if (firstVisibleRank) { params.set('rank', firstVisibleRank); } } const queryString = params.toString(); return queryString ? `${baseUrl}?${queryString}` : baseUrl; } function updateUrl(options = {}) { const params = new URLSearchParams(); if (options.rank) { window.location.hash = `rank${options.rank}`; return; } // Update query parameters based on current state if (searchInput.value.trim()) { params.set('search', searchInput.value.trim()); } if (statusFilter.value) { params.set('status', statusFilter.value); } if (sortBy.value !== 'rank') { params.set('sort', sortBy.value); } if (isReversed) { params.set('reversed', 'true'); } const queryString = params.toString(); const newUrl = queryString ? `${window.location.pathname}?${queryString}` : window.location.pathname; window.history.replaceState({}, '', newUrl); } function clearUrl() { // Clear both query parameters and hash window.history.replaceState({}, '', window.location.pathname); } function showAlbumShareFeedback(buttonElement) { const originalText = buttonElement.innerHTML; buttonElement.innerHTML = 'โ'; buttonElement.classList.add('copied'); setTimeout(() => { buttonElement.innerHTML = originalText; buttonElement.classList.remove('copied'); }, 2000); } function fallbackCopyToClipboard(text, buttonElement) { const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); if (buttonElement) { showAlbumShareFeedback(buttonElement); } } catch (err) { alert('Unable to copy to clipboard. URL: ' + text); } document.body.removeChild(textArea); }