From 25f453e3d95f36441293da2365e80c40e5722388 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Mon, 30 Jun 2025 21:51:10 +0200 Subject: [PATCH] Add comprehensive website functionality with enhanced navigation and sharing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create responsive HTML/CSS/JS website for Top 500 Albums - Add modern design with gradient background and glassmorphism effects - Implement search, filtering, and sorting functionality - Add reverse button for toggling sort order - Create bookmark system with jump-to-rank functionality - Add individual album share buttons with state preservation - Implement URL parameter handling for shareable links - Add favicon with vinyl record design - Create mobile-responsive layout with larger album covers - Add smooth animations and visual feedback throughout - Smart URL management: clean URLs for rank 1, parameterized for others 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- create-favicon.html | 157 ++++++++++ create_favicon.py | 77 +++++ create_simple_favicon.py | 59 ++++ favicon.svg | 24 ++ favicon_generator.html | 41 +++ index.html | 100 ++++++ script.js | 635 +++++++++++++++++++++++++++++++++++++++ styles.css | 556 ++++++++++++++++++++++++++++++++++ 8 files changed, 1649 insertions(+) create mode 100644 create-favicon.html create mode 100644 create_favicon.py create mode 100644 create_simple_favicon.py create mode 100644 favicon.svg create mode 100644 favicon_generator.html create mode 100644 index.html create mode 100644 script.js create mode 100644 styles.css diff --git a/create-favicon.html b/create-favicon.html new file mode 100644 index 0000000..1e7d83d --- /dev/null +++ b/create-favicon.html @@ -0,0 +1,157 @@ + + + + Create Favicon + + + +

+ + + + + \ No newline at end of file diff --git a/create_favicon.py b/create_favicon.py new file mode 100644 index 0000000..af17325 --- /dev/null +++ b/create_favicon.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +Create a favicon for the Top 500 Albums website +""" + +from PIL import Image, ImageDraw, ImageFont +import os + +def create_favicon(): + # Create a new image with transparent background + size = 32 + img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + # Create gradient background (simulate with filled circle) + # Purple/blue gradient colors + colors = [(102, 126, 234), (118, 75, 162)] # #667eea to #764ba2 + + # Draw background circle + margin = 1 + draw.ellipse([margin, margin, size-margin, size-margin], + fill=(102, 126, 234), outline=None) + + # Draw vinyl record + record_margin = 4 + draw.ellipse([record_margin, record_margin, size-record_margin, size-record_margin], + fill=(26, 26, 26), outline=None) + + # Draw center hole + center = size // 2 + hole_radius = 3 + draw.ellipse([center-hole_radius, center-hole_radius, + center+hole_radius, center+hole_radius], + fill=(102, 126, 234), outline=None) + + # Draw grooves + for r in range(6, 12, 2): + draw.ellipse([center-r, center-r, center+r, center+r], + fill=None, outline=(51, 51, 51), width=1) + + # Try to add "500" text + try: + # Use default font, small size + font = ImageFont.load_default() + text = "500" + + # Get text size + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + # Center text at bottom + x = (size - text_width) // 2 + y = size - text_height - 2 + + draw.text((x, y), text, fill=(255, 255, 255), font=font) + except: + # If font fails, just add a music note + draw.text((center-4, size-10), "♫", fill=(255, 255, 255)) + + # Save as PNG first + img.save('favicon-32x32.png', 'PNG') + + # Create 16x16 version + img16 = img.resize((16, 16), Image.Resampling.LANCZOS) + img16.save('favicon-16x16.png', 'PNG') + + # Create ICO file with multiple sizes + img.save('favicon.ico', format='ICO', sizes=[(16, 16), (32, 32)]) + + print("Favicon files created:") + print("- favicon.ico") + print("- favicon-16x16.png") + print("- favicon-32x32.png") + +if __name__ == "__main__": + create_favicon() \ No newline at end of file diff --git a/create_simple_favicon.py b/create_simple_favicon.py new file mode 100644 index 0000000..ffc4838 --- /dev/null +++ b/create_simple_favicon.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Create a simple favicon using basic drawing +""" + +def create_favicon_data(): + """Create a simple 16x16 favicon as a data URL""" + # Create a simple pattern for 16x16 favicon + # This will be a simplified version of our design + + favicon_html = ''' + + +Favicon Creator + + + + +''' + + with open('favicon_generator.html', 'w') as f: + f.write(favicon_html) + + print("Created favicon_generator.html - open this in a browser to download the favicon") + +if __name__ == "__main__": + create_favicon_data() \ No newline at end of file diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..7aa6989 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + 500 + \ No newline at end of file diff --git a/favicon_generator.html b/favicon_generator.html new file mode 100644 index 0000000..b3c4933 --- /dev/null +++ b/favicon_generator.html @@ -0,0 +1,41 @@ + + + +Favicon Creator + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..e71a40a --- /dev/null +++ b/index.html @@ -0,0 +1,100 @@ + + + + + + Top 500 Albums of All Time + + + + + + +
+
+

Top 500 Albums of All Time

+

The Greatest Albums Ever Made - 2023 Edition

+
+
+ +
+
+
+
+ + +
+ +
+ + + + + +
+ +
+
+ + +
+
+
+ +
+
+ 500 + Total Albums +
+
+ 192 + New in 2023 +
+
+ 164 + Improved +
+
+ 113 + Dropped +
+
+ +
+ +
+ +
+
+

Loading albums...

+
+ + +
+
+ +
+
+

© 2023 Top 500 Albums. Data sourced from Rolling Stone and Wikipedia.

+
+
+ + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..68cf140 --- /dev/null +++ b/script.js @@ -0,0 +1,635 @@ +// Global variables +let albumsData = []; +let filteredData = []; +let currentPage = 1; +const itemsPerPage = 50; +let isReversed = false; + +// 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() { + loadAlbumsData(); + setupEventListeners(); + handleUrlParams(); +}); + +// 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); +} + +// 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(); + updateStats(); + + } 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); + }); +} + +// 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); + + card.innerHTML = ` +
#${album.Rank}
+
+
${escapeHtml(album.Album)}
+
${escapeHtml(album.Artist)}
+ ${album.Info ? `
${escapeHtml(album.Info)}
` : ''} +
${statusText}
+ ${album.Description ? `
${escapeHtml(album.Description)}
` : ''} +
+
+
🎵
+
+ + `; + + // Add click handler to expand description + const description = card.querySelector('.album-description'); + if (description) { + description.addEventListener('click', function() { + this.classList.toggle('expanded'); + }); + } + + // 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'; + 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}`; + 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) || + (album.Description && album.Description.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('-')); + } 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 +function updateStats() { + const total = filteredData.length; + const newAlbums = filteredData.filter(album => album.Status === 'New in 2023').length; + const improved = filteredData.filter(album => album.Status.startsWith('+')).length; + const dropped = filteredData.filter(album => album.Status.startsWith('-')).length; + + document.getElementById('totalAlbums').textContent = total; + document.getElementById('newAlbums').textContent = newAlbums; + document.getElementById('improvedAlbums').textContent = improved; + document.getElementById('droppedAlbums').textContent = dropped; +} + +// 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 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 > 500) { + alert('Please enter a valid rank between 1 and 500'); + 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); +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..11f4617 --- /dev/null +++ b/styles.css @@ -0,0 +1,556 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + color: #333; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* Header */ +.header { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + padding: 2rem 0; + text-align: center; +} + +.title { + font-size: 3rem; + font-weight: 700; + color: white; + margin-bottom: 0.5rem; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); +} + +.subtitle { + font-size: 1.2rem; + color: rgba(255, 255, 255, 0.9); + font-weight: 300; +} + +/* Main content */ +.main { + padding: 2rem 0; + flex: 1; +} + +/* Controls */ +.controls { + background: white; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: center; +} + +.search-container { + display: flex; + flex: 1; + min-width: 300px; +} + +.search-input { + flex: 1; + padding: 0.75rem 1rem; + border: 2px solid #e1e5e9; + border-radius: 8px 0 0 8px; + font-size: 1rem; + outline: none; + transition: border-color 0.3s ease; +} + +.search-input:focus { + border-color: #667eea; +} + +.search-button { + padding: 0.75rem 1rem; + background: #667eea; + color: white; + border: none; + border-radius: 0 8px 8px 0; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s ease; +} + +.search-button:hover { + background: #5a6fd8; +} + +.filters { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.filter-select { + padding: 0.75rem 1rem; + border: 2px solid #e1e5e9; + border-radius: 8px; + font-size: 1rem; + background: white; + cursor: pointer; + outline: none; + transition: border-color 0.3s ease; +} + +.filter-select:focus { + border-color: #667eea; +} + +.reverse-button { + padding: 0.75rem 1rem; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + transition: all 0.3s ease; + outline: none; +} + +.reverse-button:hover { + background: #5a6fd8; + transform: translateY(-1px); +} + +.reverse-button:active { + transform: translateY(0); +} + +.reverse-button.reversed { + background: #764ba2; +} + +.reverse-button.reversed:hover { + background: #6a4190; +} + +#reverseIcon { + font-size: 1.2rem; + transition: transform 0.3s ease; +} + +.reverse-button.reversed #reverseIcon { + transform: rotate(180deg); +} + +/* Bookmark controls */ +.bookmark-controls { + display: flex; + gap: 1rem; + align-items: center; + flex-wrap: wrap; +} + +.jump-to-rank { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.rank-input { + width: 140px; + padding: 0.75rem 1rem; + border: 2px solid #e1e5e9; + border-radius: 8px; + font-size: 1rem; + outline: none; + transition: border-color 0.3s ease; +} + +.rank-input:focus { + border-color: #667eea; +} + +.jump-button { + padding: 0.75rem 1.25rem; + background: #667eea; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + transition: all 0.3s ease; + outline: none; +} + +.jump-button:hover { + background: #5a6fd8; + transform: translateY(-1px); +} + +.jump-button:active { + transform: translateY(0); +} + + +/* Stats */ +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-item { + background: white; + padding: 1.5rem; + border-radius: 12px; + text-align: center; + box-shadow: 0 4px 20px rgba(0,0,0,0.1); + transition: transform 0.3s ease; +} + +.stat-item:hover { + transform: translateY(-2px); +} + +.stat-number { + display: block; + font-size: 2.5rem; + font-weight: 700; + color: #667eea; + margin-bottom: 0.5rem; +} + +.stat-label { + font-size: 0.9rem; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Albums grid */ +.albums-grid { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 2rem; +} + +.album-card { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 4px 20px rgba(0,0,0,0.1); + transition: all 0.3s ease; + cursor: pointer; + display: flex; + align-items: center; + gap: 1.5rem; + position: relative; +} + +.album-cover { + width: 180px; + height: 180px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 8px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 2rem; + font-weight: 300; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + position: relative; + overflow: hidden; +} + +.album-cover::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: repeating-linear-gradient( + 45deg, + transparent, + transparent 2px, + rgba(255,255,255,0.1) 2px, + rgba(255,255,255,0.1) 4px + ); + pointer-events: none; +} + +.album-cover-icon { + font-size: 4rem; + opacity: 0.8; + z-index: 1; + position: relative; +} + +.album-share { + position: absolute; + top: 1rem; + right: 1rem; + background: rgba(102, 126, 234, 0.9); + color: white; + border: none; + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s ease; + opacity: 0; + transform: scale(0.8); + backdrop-filter: blur(10px); + box-shadow: 0 2px 8px rgba(0,0,0,0.2); +} + +.album-card:hover .album-share { + opacity: 1; + transform: scale(1); +} + +.album-share:hover { + background: rgba(102, 126, 234, 1); + transform: scale(1.1); +} + +.album-share:active { + transform: scale(0.95); +} + +.album-share.copied { + background: rgba(23, 162, 184, 0.9); +} + +.album-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 32px rgba(0,0,0,0.15); +} + +.album-rank { + font-size: 2.5rem; + font-weight: 700; + color: #667eea; + min-width: 80px; + text-align: center; + flex-shrink: 0; +} + +.album-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.album-title { + font-size: 1.3rem; + font-weight: 600; + color: #333; + line-height: 1.3; + margin: 0; +} + +.album-artist { + font-size: 1.1rem; + color: #666; + margin: 0; +} + +.album-info { + font-size: 0.9rem; + color: #888; + margin: 0; +} + +.album-status { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 500; + align-self: flex-start; +} + +.status-new { + background: #e8f5e8; + color: #2d8f47; +} + +.status-improved { + background: #e3f2fd; + color: #1976d2; +} + +.status-dropped { + background: #ffeaea; + color: #d32f2f; +} + +.status-no-change { + background: #f5f5f5; + color: #666; +} + +.album-description { + font-size: 0.9rem; + color: #555; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.album-description.expanded { + -webkit-line-clamp: unset; +} + +/* Loading and error states */ +.loading { + text-align: center; + padding: 3rem 0; + color: white; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 3px solid rgba(255,255,255,0.3); + border-top: 3px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error { + text-align: center; + padding: 3rem 0; + color: #ff6b6b; + background: white; + border-radius: 12px; + margin: 2rem 0; +} + +/* Footer */ +.footer { + background: rgba(0,0,0,0.1); + color: rgba(255,255,255,0.8); + text-align: center; + padding: 1.5rem 0; + margin-top: 3rem; +} + +/* Responsive design */ +@media (max-width: 768px) { + .title { + font-size: 2rem; + } + + .subtitle { + font-size: 1rem; + } + + .controls { + flex-direction: column; + align-items: stretch; + } + + .search-container { + min-width: auto; + } + + .filters { + justify-content: center; + } + + .reverse-button { + min-width: auto; + padding: 0.75rem; + } + + #reverseText { + display: none; + } + + .bookmark-controls { + width: 100%; + justify-content: center; + } + + .rank-input { + width: 120px; + } + + .album-card { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .album-rank { + min-width: auto; + text-align: left; + } + + .album-cover { + width: 120px; + height: 120px; + align-self: center; + } + + .album-cover-icon { + font-size: 3rem; + } + + .stats { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + .container { + padding: 0 10px; + } + + .controls { + padding: 1rem; + } + + .album-card { + padding: 1rem; + } + + .stats { + grid-template-columns: 1fr; + } +} \ No newline at end of file