- Redesigned album card layout with grid-based header (rank + details) - Added Wikipedia links for all albums with accurate URL mappings - Improved description formatting with proper paragraphs and typography - Made stats cards clickable filters that maintain static numbers - Significantly increased album art size on mobile devices (280px/240px vs 120px) - Enhanced visual feedback for interactive elements 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
771 lines
No EOL
24 KiB
JavaScript
771 lines
No EOL
24 KiB
JavaScript
// Global variables
|
|
let albumsData = [];
|
|
let filteredData = [];
|
|
let currentPage = 1;
|
|
const itemsPerPage = 50;
|
|
let isReversed = false;
|
|
let wikipediaUrlMappings = {};
|
|
|
|
// 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 loading = document.getElementById('loading');
|
|
const error = document.getElementById('error');
|
|
const stats = document.getElementById('stats');
|
|
|
|
// Initialize the application
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadWikipediaMapping();
|
|
loadAlbumsData();
|
|
setupEventListeners();
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
// 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' });
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="album-header-grid">
|
|
<div class="album-rank">#${album.Rank}</div>
|
|
<div class="album-details">
|
|
<div class="album-title">${escapeHtml(album.Album)}</div>
|
|
<div class="album-artist">${escapeHtml(album.Artist)}</div>
|
|
${album.Info ? `<div class="album-info">${escapeHtml(album.Info)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="album-cover">
|
|
${coverImagePath ?
|
|
`<img src="${coverImagePath}" alt="${escapeHtml(album.Album)} by ${escapeHtml(album.Artist)}" class="album-cover-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
|
<div class="album-cover-icon" style="display: none;">🎵</div>` :
|
|
`<div class="album-cover-icon">🎵</div>`
|
|
}
|
|
</div>
|
|
<div class="album-status ${statusClass}">${statusText}</div>
|
|
${album.Description ? `<div class="album-description">${formatDescription(album.Description)}</div>` : ''}
|
|
<div class="album-links">
|
|
<a href="${generateWikipediaUrl(album.Album, album.Artist)}" target="_blank" rel="noopener noreferrer" class="wikipedia-link">
|
|
View on Wikipedia →
|
|
</a>
|
|
</div>
|
|
<button class="album-share" title="Share this album" data-rank="${album.Rank}">🔗</button>
|
|
`;
|
|
|
|
// Description is now always fully visible
|
|
|
|
// Add click handler for share button
|
|
const shareBtn = card.querySelector('.album-share');
|
|
shareBtn.addEventListener('click', function(e) {
|
|
e.stopPropagation(); // Prevent card click
|
|
const rank = this.getAttribute('data-rank');
|
|
handleAlbumShare(rank, this);
|
|
});
|
|
|
|
return card;
|
|
}
|
|
|
|
// Get CSS class for status
|
|
function getStatusClass(status) {
|
|
if (status === 'New in 2023') return 'status-new';
|
|
if (status === 'No change') return 'status-no-change';
|
|
if (status.startsWith('+')) return 'status-improved';
|
|
if (status.startsWith('-')) return 'status-dropped';
|
|
if (status.startsWith('Dropped')) return 'status-dropped';
|
|
return 'status-no-change';
|
|
}
|
|
|
|
// Format status text
|
|
function formatStatus(status) {
|
|
if (status === 'New in 2023') return 'New in 2023';
|
|
if (status === 'No change') return 'No Change';
|
|
if (status.startsWith('+')) return `↗ ${status}`;
|
|
if (status.startsWith('-')) return `↘ ${status}`;
|
|
if (status.startsWith('Dropped')) return `⤵ ${status}`;
|
|
return status;
|
|
}
|
|
|
|
// Handle search
|
|
function handleSearch(event) {
|
|
const query = event.target.value.toLowerCase().trim();
|
|
|
|
if (!query) {
|
|
filteredData = [...albumsData];
|
|
} else {
|
|
filteredData = albumsData.filter(album =>
|
|
album.Artist.toLowerCase().includes(query) ||
|
|
album.Album.toLowerCase().includes(query)
|
|
);
|
|
}
|
|
|
|
// Reset reverse state when searching
|
|
resetReverse();
|
|
|
|
currentPage = 1;
|
|
renderAlbums();
|
|
updateStats();
|
|
}
|
|
|
|
// Handle filter
|
|
function handleFilter(event) {
|
|
const filterValue = event.target.value;
|
|
|
|
if (!filterValue) {
|
|
filteredData = [...albumsData];
|
|
} else if (filterValue === 'improved') {
|
|
filteredData = albumsData.filter(album => album.Status.startsWith('+'));
|
|
} else if (filterValue === 'dropped') {
|
|
filteredData = albumsData.filter(album => album.Status.startsWith('-') || album.Status.startsWith('Dropped'));
|
|
} else {
|
|
filteredData = albumsData.filter(album => album.Status === filterValue);
|
|
}
|
|
|
|
// Reset reverse state when filtering
|
|
resetReverse();
|
|
|
|
currentPage = 1;
|
|
renderAlbums();
|
|
updateStats();
|
|
}
|
|
|
|
// Handle sort
|
|
function handleSort(event) {
|
|
const sortValue = event.target.value;
|
|
|
|
filteredData.sort((a, b) => {
|
|
let result;
|
|
switch (sortValue) {
|
|
case 'rank':
|
|
result = a.Rank - b.Rank;
|
|
break;
|
|
case 'artist':
|
|
result = a.Artist.localeCompare(b.Artist);
|
|
break;
|
|
case 'album':
|
|
result = a.Album.localeCompare(b.Album);
|
|
break;
|
|
case 'year':
|
|
const yearA = a.Year || 0;
|
|
const yearB = b.Year || 0;
|
|
result = yearB - yearA; // Most recent first
|
|
break;
|
|
default:
|
|
result = 0;
|
|
}
|
|
|
|
// Apply reverse if needed
|
|
return isReversed ? -result : result;
|
|
});
|
|
|
|
currentPage = 1;
|
|
renderAlbums();
|
|
}
|
|
|
|
// Handle reverse
|
|
function handleReverse() {
|
|
isReversed = !isReversed;
|
|
|
|
// Update button appearance
|
|
reverseButton.classList.toggle('reversed', isReversed);
|
|
|
|
// Reverse the current filtered data
|
|
filteredData.reverse();
|
|
|
|
currentPage = 1;
|
|
renderAlbums();
|
|
}
|
|
|
|
// Reset reverse state
|
|
function resetReverse() {
|
|
if (isReversed) {
|
|
isReversed = false;
|
|
reverseButton.classList.remove('reversed');
|
|
}
|
|
}
|
|
|
|
// Handle infinite scroll
|
|
function handleScroll() {
|
|
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000) {
|
|
if (currentPage * itemsPerPage < filteredData.length) {
|
|
currentPage++;
|
|
renderAlbums();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update statistics
|
|
// Initialize stats with static values from the full dataset (called only once)
|
|
function initializeStats() {
|
|
const total = albumsData.length;
|
|
const newAlbums = albumsData.filter(album => album.Status === 'New in 2023').length;
|
|
const improved = albumsData.filter(album => album.Status.startsWith('+')).length;
|
|
const dropped = albumsData.filter(album => album.Status.startsWith('-') || album.Status.startsWith('Dropped')).length;
|
|
|
|
document.getElementById('totalAlbums').textContent = total;
|
|
document.getElementById('newAlbums').textContent = newAlbums;
|
|
document.getElementById('improvedAlbums').textContent = improved;
|
|
document.getElementById('droppedAlbums').textContent = dropped;
|
|
}
|
|
|
|
// Update stats function (now empty but kept for compatibility)
|
|
function updateStats() {
|
|
// Stats remain static and don't update when filtering
|
|
}
|
|
|
|
// Utility functions
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDescription(text) {
|
|
if (!text) return '';
|
|
|
|
// Split by double line breaks or periods followed by capital letters
|
|
const paragraphs = text
|
|
.split(/\n\n|\. (?=[A-Z])/)
|
|
.map(p => p.trim())
|
|
.filter(p => p.length > 0)
|
|
.map(p => p.endsWith('.') ? p : p + '.');
|
|
|
|
// If we only have one paragraph, try to split by sentences for better formatting
|
|
if (paragraphs.length === 1 && text.length > 200) {
|
|
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
|
|
if (sentences.length > 3) {
|
|
// Group sentences into 2-3 paragraphs
|
|
const third = Math.ceil(sentences.length / 3);
|
|
return [
|
|
sentences.slice(0, third).join(' '),
|
|
sentences.slice(third, third * 2).join(' '),
|
|
sentences.slice(third * 2).join(' ')
|
|
].filter(p => p.trim()).map(p => `<p>${escapeHtml(p.trim())}</p>`).join('');
|
|
}
|
|
}
|
|
|
|
return paragraphs.map(p => `<p>${escapeHtml(p)}</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 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: 'center'
|
|
});
|
|
|
|
// 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);
|
|
} |