beatport progress

This commit is contained in:
Broque Thomas 2025-09-27 20:16:18 -07:00
parent 6aa936103c
commit 7e42e20887
5 changed files with 1906 additions and 6 deletions

View file

@ -831,7 +831,7 @@ class BeatportUnifiedScraper:
return tracks
def scrape_genre_charts(self, genre: Dict, limit: int = 100) -> List[Dict]:
"""Scrape charts for a specific genre"""
"""Scrape charts for a specific genre (default: top tracks)"""
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
soup = self.get_page(genre_url)
@ -839,6 +839,214 @@ class BeatportUnifiedScraper:
return tracks
def scrape_genre_top_10(self, genre: Dict) -> List[Dict]:
"""Scrape top 10 tracks for a specific genre"""
return self.scrape_genre_charts(genre, limit=10)
def scrape_genre_releases(self, genre: Dict, limit: int = 100) -> List[Dict]:
"""Scrape top releases for a specific genre"""
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
soup = self.get_page(genre_url)
if not soup:
return []
# Try to find releases section on genre page
releases = self.extract_releases_from_page(soup, f"{genre['name']} Top Releases", limit)
# If no releases found with release extraction, try track extraction
if not releases:
print(f" ⚠️ No releases found with release method, trying track method for {genre['name']}")
releases = self.extract_tracks_from_page(soup, f"{genre['name']} Top Releases", limit)
# Mark these as releases
for release in releases:
release['type'] = 'release'
return releases
def scrape_genre_staff_picks(self, genre: Dict, limit: int = 50) -> List[Dict]:
"""Scrape staff picks for a specific genre"""
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
soup = self.get_page(genre_url)
if not soup:
return []
tracks = []
# Look for staff picks, editorial, or featured sections on genre page
staff_sections = [
'staff pick', 'editorial', 'featured', 'editor', 'hype pick',
'weekend pick', 'best new', 'exclusives'
]
for section_name in staff_sections:
# Find section headings that match staff pick patterns
section_heading = soup.find(['h1', 'h2', 'h3', 'h4'],
string=re.compile(rf'{section_name}', re.I))
if section_heading:
print(f" 📝 Found staff picks section: {section_heading.get_text(strip=True)}")
section_container = section_heading.find_parent()
if section_container:
content_area = section_container.find_next_sibling()
if content_area:
section_tracks = self.extract_tracks_from_page(
content_area, f"{genre['name']} Staff Picks", limit
)
if section_tracks:
tracks.extend(section_tracks)
break # Found staff picks, no need to continue
# If no specific staff picks section found, try to find any editorial content
if not tracks:
print(f" 🔍 No specific staff picks section found, looking for editorial content...")
# Look for DJ charts or featured charts on the genre page
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
for chart_link in chart_links[:10]: # Limit to first 10 charts
chart_name = chart_link.get_text(strip=True)
if chart_name and len(chart_name) > 3:
track_info = {
'position': len(tracks) + 1,
'artist': 'Various Artists',
'title': chart_name,
'list_name': f"{genre['name']} Staff Picks",
'url': urljoin(self.base_url, chart_link.get('href', '')),
'chart_type': 'staff_pick'
}
tracks.append(track_info)
if len(tracks) >= limit:
break
return tracks
def scrape_genre_latest_releases(self, genre: Dict, limit: int = 50) -> List[Dict]:
"""Scrape latest releases for a specific genre"""
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
soup = self.get_page(genre_url)
if not soup:
return []
# Look for latest releases, new releases, or recent sections
latest_sections = ['latest', 'new releases', 'recent', 'newest']
tracks = []
for section_name in latest_sections:
section_heading = soup.find(['h1', 'h2', 'h3', 'h4'],
string=re.compile(rf'{section_name}', re.I))
if section_heading:
print(f" 🕒 Found latest releases section: {section_heading.get_text(strip=True)}")
section_container = section_heading.find_parent()
if section_container:
content_area = section_container.find_next_sibling()
if content_area:
section_tracks = self.extract_tracks_from_page(
content_area, f"Latest {genre['name']} Releases", limit
)
if section_tracks:
tracks.extend(section_tracks)
break
# If no specific latest section found, try releases extraction
if not tracks:
print(f" 🔍 No specific latest releases section found, trying general releases...")
tracks = self.scrape_genre_releases(genre, limit)
return tracks
def scrape_genre_new_charts(self, genre: Dict, limit: int = 50) -> List[Dict]:
"""Scrape new charts (DJ/artist curated) for a specific genre"""
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
soup = self.get_page(genre_url)
if not soup:
return []
tracks = []
# Look for DJ charts, artist charts, or curated content on genre page
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
for chart_link in chart_links[:limit]:
chart_name = chart_link.get_text(strip=True)
chart_href = chart_link.get('href', '')
if chart_name and chart_href and len(chart_name) > 3:
# Extract additional info if available (artist name, etc.)
chart_container = chart_link.find_parent()
artist_name = "Various Artists"
# Try to find artist info near the chart
if chart_container:
# Look for artist links in the same container
artist_link = chart_container.find('a', href=re.compile(r'/artist/'))
if artist_link:
artist_name = artist_link.get_text(strip=True)
chart_info = {
'position': len(tracks) + 1,
'artist': artist_name,
'title': chart_name,
'list_name': f"New {genre['name']} Charts",
'url': urljoin(self.base_url, chart_href),
'chart_type': 'new_chart'
}
tracks.append(chart_info)
return tracks
def discover_genre_page_sections(self, genre: Dict) -> Dict:
"""Analyze a genre page to discover all available sections"""
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
print(f"🔍 Discovering sections for {genre['name']} genre page...")
soup = self.get_page(genre_url)
if not soup:
return {}
sections = {
'top_tracks': [],
'top_releases': [],
'staff_picks': [],
'latest_releases': [],
'new_charts': [],
'other_sections': []
}
# Find all section headings
headings = soup.find_all(['h1', 'h2', 'h3', 'h4'])
for heading in headings:
text = heading.get_text(strip=True).lower()
if any(keyword in text for keyword in ['top 100', 'top 10', 'chart']):
sections['top_tracks'].append(heading.get_text(strip=True))
elif any(keyword in text for keyword in ['release', 'album', 'ep']):
sections['top_releases'].append(heading.get_text(strip=True))
elif any(keyword in text for keyword in ['staff', 'editor', 'pick', 'featured']):
sections['staff_picks'].append(heading.get_text(strip=True))
elif any(keyword in text for keyword in ['latest', 'new', 'recent']):
sections['latest_releases'].append(heading.get_text(strip=True))
elif 'chart' in text:
sections['new_charts'].append(heading.get_text(strip=True))
else:
sections['other_sections'].append(heading.get_text(strip=True))
# Count DJ/artist charts
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
sections['chart_count'] = len(chart_links)
print(f"✅ Discovered sections for {genre['name']}:")
for section_type, items in sections.items():
if items and section_type != 'chart_count':
print(f"{section_type}: {len(items)} sections")
print(f" • Individual charts found: {sections['chart_count']}")
return sections
def scrape_all_genres(self, tracks_per_genre: int = 100, max_workers: int = 5, include_images: bool = False) -> Dict[str, List[Dict]]:
"""Scrape all genres in parallel"""
# Discover genres dynamically if not already done

View file

@ -11942,6 +11942,275 @@ def get_beatport_genre_tracks(genre_slug, genre_id):
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/top-10', methods=['GET'])
def get_beatport_genre_top_10(genre_slug, genre_id):
"""Get top 10 tracks for a specific Beatport genre"""
try:
logger.info(f"🔥 API request for {genre_slug} genre top 10 tracks (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Scrape top 10 tracks for this genre
tracks = scraper.scrape_genre_top_10(genre)
logger.info(f"✅ Successfully scraped {len(tracks)} top 10 tracks for {genre_slug}")
return jsonify({
"success": True,
"tracks": tracks,
"genre": genre,
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching top 10 tracks for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/releases-top-10', methods=['GET'])
def get_beatport_genre_releases_top_10(genre_slug, genre_id):
"""Get top 10 releases for a specific Beatport genre"""
try:
logger.info(f"📊 API request for {genre_slug} genre top 10 releases (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Scrape top 10 releases for this genre
releases = scraper.scrape_genre_releases(genre, limit=10)
logger.info(f"✅ Successfully scraped {len(releases)} top 10 releases for {genre_slug}")
return jsonify({
"success": True,
"tracks": releases,
"genre": genre,
"count": len(releases)
})
except Exception as e:
logger.error(f"❌ Error fetching top 10 releases for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/releases-top-100', methods=['GET'])
def get_beatport_genre_releases_top_100(genre_slug, genre_id):
"""Get top 100 releases for a specific Beatport genre"""
try:
logger.info(f"📊 API request for {genre_slug} genre top 100 releases (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '100'))
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Scrape top releases for this genre
releases = scraper.scrape_genre_releases(genre, limit=limit)
logger.info(f"✅ Successfully scraped {len(releases)} top 100 releases for {genre_slug}")
return jsonify({
"success": True,
"tracks": releases,
"genre": genre,
"count": len(releases)
})
except Exception as e:
logger.error(f"❌ Error fetching top 100 releases for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/staff-picks', methods=['GET'])
def get_beatport_genre_staff_picks(genre_slug, genre_id):
"""Get staff picks for a specific Beatport genre"""
try:
logger.info(f"⭐ API request for {genre_slug} genre staff picks (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '50'))
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Scrape staff picks for this genre
tracks = scraper.scrape_genre_staff_picks(genre, limit=limit)
logger.info(f"✅ Successfully scraped {len(tracks)} staff picks for {genre_slug}")
return jsonify({
"success": True,
"tracks": tracks,
"genre": genre,
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching staff picks for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/latest-releases', methods=['GET'])
def get_beatport_genre_latest_releases(genre_slug, genre_id):
"""Get latest releases for a specific Beatport genre"""
try:
logger.info(f"🕒 API request for {genre_slug} genre latest releases (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '50'))
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Scrape latest releases for this genre
tracks = scraper.scrape_genre_latest_releases(genre, limit=limit)
logger.info(f"✅ Successfully scraped {len(tracks)} latest releases for {genre_slug}")
return jsonify({
"success": True,
"tracks": tracks,
"genre": genre,
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching latest releases for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/new-charts', methods=['GET'])
def get_beatport_genre_new_charts(genre_slug, genre_id):
"""Get new charts for a specific Beatport genre"""
try:
logger.info(f"📈 API request for {genre_slug} genre new charts (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '50'))
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Scrape new charts for this genre
tracks = scraper.scrape_genre_new_charts(genre, limit=limit)
logger.info(f"✅ Successfully scraped {len(tracks)} new charts for {genre_slug}")
return jsonify({
"success": True,
"tracks": tracks,
"genre": genre,
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching new charts for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/sections', methods=['GET'])
def get_beatport_genre_sections(genre_slug, genre_id):
"""Discover all available sections for a specific Beatport genre"""
try:
logger.info(f"🔍 API request for {genre_slug} genre sections discovery (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Discover sections for this genre
sections = scraper.discover_genre_page_sections(genre)
logger.info(f"✅ Successfully discovered sections for {genre_slug}")
return jsonify({
"success": True,
"sections": sections,
"genre": genre
})
except Exception as e:
logger.error(f"❌ Error discovering sections for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"sections": {}
}), 500
@app.route('/api/beatport/top-100', methods=['GET'])
def get_beatport_top_100():
"""Get Beatport Top 100 tracks"""

View file

@ -539,6 +539,128 @@
</div>
</div>
<!-- Genre Detail Sub-View -->
<div class="beatport-sub-view" id="beatport-genre-detail-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back" id="genre-detail-back">← Back to Genre Explorer</button>
<span class="breadcrumb-path" id="genre-detail-breadcrumb">Browse Charts > Genre Explorer > Loading...</span>
</div>
<div class="genre-detail-header">
<div class="genre-detail-info">
<h2 id="genre-detail-title">Loading Genre...</h2>
<p id="genre-detail-description">Explore all chart types for this genre</p>
</div>
</div>
<!-- Main Chart Types Section -->
<div class="genre-main-charts-section">
<h3 class="section-title">📊 Main Charts</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="top-10">
<div class="chart-type-icon">🔥</div>
<div class="chart-type-info">
<h3 id="genre-top-10-title">Top 10</h3>
<p>Current hottest tracks</p>
<span class="track-count">10 tracks</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="top-100">
<div class="chart-type-icon">💯</div>
<div class="chart-type-info">
<h3 id="genre-top-100-title">Top 100</h3>
<p>Complete chart rankings</p>
<span class="track-count">100 tracks</span>
</div>
</div>
</div>
</div>
<!-- Releases Section -->
<div class="genre-releases-section">
<h3 class="section-title">🎵 Releases</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="releases-top-10">
<div class="chart-type-icon">🆕</div>
<div class="chart-type-info">
<h3 id="genre-releases-top-10-title">Top 10 Releases</h3>
<p>Newest releases trending</p>
<span class="track-count">10 releases</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="releases-top-100">
<div class="chart-type-icon">📊</div>
<div class="chart-type-info">
<h3 id="genre-releases-top-100-title">Top 100 Releases</h3>
<p>All trending releases</p>
<span class="track-count">100 releases</span>
</div>
</div>
<div class="genre-chart-type-card" data-chart-type="latest-releases">
<div class="chart-type-icon">🕒</div>
<div class="chart-type-info">
<h3 id="genre-latest-releases-title">Latest Releases</h3>
<p>Recently published</p>
<span class="track-count">~ releases</span>
</div>
</div>
</div>
</div>
<!-- Editorial Section -->
<div class="genre-editorial-section">
<h3 class="section-title">⭐ Editorial</h3>
<div class="genre-chart-types-grid">
<div class="genre-chart-type-card" data-chart-type="staff-picks">
<div class="chart-type-icon"></div>
<div class="chart-type-info">
<h3 id="genre-staff-picks-title">Staff Picks</h3>
<p>Editor curated selection</p>
<span class="track-count">~ tracks</span>
</div>
</div>
</div>
</div>
<!-- New Charts Section (Always Visible) -->
<div class="genre-new-charts-section">
<h3 class="section-title">📈 New Charts Collection</h3>
<p class="section-description">Artist and DJ curated chart collections</p>
<!-- Always Visible Charts List -->
<div class="new-charts-content" id="new-charts-content">
<div class="charts-loading-inline" id="charts-loading-inline">
<div class="loading-spinner-small"></div>
<p>Loading chart collections...</p>
</div>
<div class="new-charts-grid" id="new-charts-grid">
<!-- Charts will be populated here -->
</div>
</div>
</div>
</div>
<!-- Genre Charts List Sub-View -->
<div class="beatport-sub-view" id="beatport-genre-charts-list-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back" id="genre-charts-list-back">← Back to Genre Charts</button>
<span class="breadcrumb-path" id="genre-charts-list-breadcrumb">Browse Charts > Genre Explorer > Genre Charts > New Charts</span>
</div>
<div class="genre-charts-list-header">
<div class="genre-charts-list-info">
<h2 id="genre-charts-list-title">Loading Charts...</h2>
<p id="genre-charts-list-description">Browse all available chart collections for this genre</p>
</div>
</div>
<div class="genre-charts-list-container">
<div class="charts-loading-placeholder" id="charts-loading-placeholder">
<div class="loading-spinner"></div>
<p>🔍 Loading chart collections...</p>
</div>
<div class="genre-charts-grid" id="genre-charts-grid">
<!-- Charts will be populated dynamically -->
</div>
</div>
</div>
<!-- Staff Picks Sub-View -->
<div class="beatport-sub-view" id="beatport-staff-picks-view">
<div class="beatport-breadcrumb">

View file

@ -9746,7 +9746,14 @@ function initializeSyncPage() {
const beatportBackButtons = document.querySelectorAll('.breadcrumb-back');
beatportBackButtons.forEach(button => {
button.addEventListener('click', () => {
showBeatportMainView();
// Handle different back button types
if (button.id === 'genre-detail-back') {
showBeatportGenresView();
} else if (button.id === 'genre-charts-list-back') {
showBeatportGenreDetailViewFromBack();
} else {
showBeatportMainView();
}
});
});
@ -11345,11 +11352,624 @@ async function handleBeatportChartClick(chartType, chartId, chartName, chartEndp
}
}
function handleBeatportGenreClick(genreSlug, genreId) {
console.log(`🎵 Beatport genre clicked: ${genreSlug} (${genreId})`);
function handleBeatportGenreClick(genreSlug, genreId, genreName) {
console.log(`🎵 Beatport genre clicked: ${genreName} (${genreSlug}/${genreId}) - SHOWING GENRE DETAIL VIEW`);
console.log(`📝 Debug: Parameters received - Slug: ${genreSlug}, ID: ${genreId}, Name: ${genreName}`);
// Placeholder for Phase 2 - will open discovery modal
showToast(`🎵 Genre "${genreSlug}" selected - Discovery modal coming in Phase 2!`, 'info');
// Navigate to genre detail view with proper parameters
showBeatportGenreDetailView(genreSlug, genreId, genreName);
}
function showBeatportGenreDetailView(genreSlug, genreId, genreName) {
console.log(`🎯 Showing genre detail view for: ${genreName}`);
console.log(`📝 Debug: Function called with - Slug: ${genreSlug}, ID: ${genreId}, Name: ${genreName}`);
// Hide all other beatport views
document.querySelectorAll('.beatport-sub-view').forEach(view => {
view.classList.remove('active');
});
const mainView = document.getElementById('beatport-main-view');
if (mainView) {
mainView.classList.remove('active');
}
// Show genre detail view
const genreDetailView = document.getElementById('beatport-genre-detail-view');
if (genreDetailView) {
genreDetailView.classList.add('active');
console.log(`📝 Debug: Genre detail view element found and activated`);
// Update view content
const titleElement = document.getElementById('genre-detail-title');
const breadcrumbElement = document.getElementById('genre-detail-breadcrumb');
console.log(`📝 Debug: Title element found: ${!!titleElement}, Breadcrumb element found: ${!!breadcrumbElement}`);
if (titleElement) {
titleElement.textContent = genreName;
console.log(`📝 Debug: Updated title to: ${genreName}`);
}
if (breadcrumbElement) {
breadcrumbElement.textContent = `Browse Charts > Genre Explorer > ${genreName} Charts`;
console.log(`📝 Debug: Updated breadcrumb`);
}
// Update chart type titles with genre name
const chartTitles = [
'genre-top-10-title',
'genre-top-100-title',
'genre-releases-top-10-title',
'genre-releases-top-100-title',
'genre-staff-picks-title',
'genre-latest-releases-title',
'genre-new-charts-title'
];
chartTitles.forEach(titleId => {
const element = document.getElementById(titleId);
if (element) {
console.log(`📝 Debug: Found chart title element: ${titleId}`);
} else {
console.log(`📝 Debug: Missing chart title element: ${titleId}`);
}
});
document.getElementById('genre-top-10-title').textContent = `Top 10 ${genreName}`;
document.getElementById('genre-top-100-title').textContent = `Top 100 ${genreName}`;
document.getElementById('genre-releases-top-10-title').textContent = `Top 10 ${genreName} Releases`;
document.getElementById('genre-releases-top-100-title').textContent = `Top 100 ${genreName} Releases`;
document.getElementById('genre-staff-picks-title').textContent = `${genreName} Staff Picks`;
document.getElementById('genre-latest-releases-title').textContent = `Latest ${genreName} Releases`;
// Load new charts directly (no expansion needed)
console.log(`🔄 Auto-loading new charts for ${genreName}...`);
loadNewChartsInline(genreSlug, genreId, genreName);
// Store current genre data for chart type handlers
genreDetailView.dataset.genreSlug = genreSlug;
genreDetailView.dataset.genreId = genreId;
genreDetailView.dataset.genreName = genreName;
// Add click handlers to chart type cards
setupGenreChartTypeHandlers();
console.log(`✅ Genre detail view shown for ${genreName}`);
} else {
console.error('❌ Genre detail view element not found');
}
}
function setupGenreChartTypeHandlers() {
const chartTypeCards = document.querySelectorAll('#beatport-genre-detail-view .genre-chart-type-card');
chartTypeCards.forEach(card => {
// Remove existing listeners
card.replaceWith(card.cloneNode(true));
});
// Re-select after cloning
const newChartTypeCards = document.querySelectorAll('#beatport-genre-detail-view .genre-chart-type-card');
newChartTypeCards.forEach(card => {
card.addEventListener('click', () => {
const chartType = card.dataset.chartType;
const genreDetailView = document.getElementById('beatport-genre-detail-view');
const genreSlug = genreDetailView.dataset.genreSlug;
const genreId = genreDetailView.dataset.genreId;
const genreName = genreDetailView.dataset.genreName;
// All chart types now go directly to discovery modal
handleGenreChartTypeClick(genreSlug, genreId, genreName, chartType);
});
});
}
function showBeatportGenresView() {
// Hide genre detail view and show genres view
document.querySelectorAll('.beatport-sub-view').forEach(view => {
view.classList.remove('active');
});
const genresView = document.getElementById('beatport-genres-view');
if (genresView) {
genresView.classList.add('active');
}
}
async function toggleNewChartsExpansion(genreSlug, genreId, genreName) {
console.log(`📈 Toggling new charts expansion for: ${genreName}`);
const expandedContent = document.getElementById('new-charts-expanded');
const expandIndicator = document.getElementById('expand-indicator');
const chartsCount = document.getElementById('new-charts-count');
if (!expandedContent || !expandIndicator) {
console.error('❌ New charts expansion elements not found');
return;
}
// Check if already expanded
const isExpanded = expandedContent.style.display !== 'none';
if (isExpanded) {
// Collapse
expandedContent.style.display = 'none';
expandIndicator.classList.remove('expanded');
console.log('📉 Collapsed new charts section');
} else {
// Expand and load charts
expandedContent.style.display = 'block';
expandIndicator.classList.add('expanded');
// Load charts if not already loaded
await loadNewChartsInline(genreSlug, genreId, genreName);
console.log('📈 Expanded new charts section');
}
}
async function loadNewChartsInline(genreSlug, genreId, genreName) {
const chartsGrid = document.getElementById('new-charts-grid');
const loadingInline = document.getElementById('charts-loading-inline');
if (!chartsGrid || !loadingInline) {
console.error('❌ Inline charts elements not found');
return;
}
// Show loading state
loadingInline.style.display = 'block';
chartsGrid.style.display = 'none';
chartsGrid.innerHTML = '';
try {
console.log(`🔍 Loading inline charts for ${genreName}...`);
// Fetch charts from the new-charts endpoint
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/new-charts?limit=20`);
if (!response.ok) {
throw new Error(`Failed to fetch charts: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
// Show empty state
chartsGrid.innerHTML = `
<div class="new-charts-empty">
<h4>No Charts Available</h4>
<p>No curated charts found for ${genreName} at the moment.</p>
</div>
`;
} else {
// Populate charts grid
const chartsHTML = data.tracks.map((chart, index) => {
const chartName = chart.title || 'Untitled Chart';
const artistName = chart.artist || 'Various Artists';
const chartUrl = chart.url || '';
return `
<div class="new-chart-item" data-chart-url="${chartUrl}" data-chart-name="${chartName}" data-chart-artist="${artistName}">
<div class="new-chart-header">
<div class="new-chart-icon">📈</div>
<div class="new-chart-title">
<h5>${chartName}</h5>
<p class="new-chart-artist">by ${artistName}</p>
</div>
</div>
<div class="new-chart-description">
Curated ${genreName} chart collection
</div>
<div class="new-chart-footer">
<div class="new-chart-type">Chart</div>
<div class="new-chart-action">Explore </div>
</div>
</div>
`;
}).join('');
chartsGrid.innerHTML = chartsHTML;
// Add click handlers to chart items
setupNewChartItemHandlers(genreSlug, genreId, genreName);
}
// Hide loading and show grid
loadingInline.style.display = 'none';
chartsGrid.style.display = 'grid';
console.log(`✅ Loaded ${data.tracks?.length || 0} inline charts for ${genreName}`);
showToast(`Found ${data.tracks?.length || 0} chart collections`, 'success');
} catch (error) {
console.error(`❌ Error loading inline charts for ${genreName}:`, error);
// Show error state
chartsGrid.innerHTML = `
<div class="new-charts-empty">
<h4>Error Loading Charts</h4>
<p>Unable to load chart collections for ${genreName}.</p>
</div>
`;
loadingInline.style.display = 'none';
chartsGrid.style.display = 'grid';
showToast(`Error loading charts: ${error.message}`, 'error');
}
}
function setupNewChartItemHandlers(genreSlug, genreId, genreName) {
const chartItems = document.querySelectorAll('#new-charts-grid .new-chart-item');
chartItems.forEach(item => {
item.addEventListener('click', async () => {
const chartName = item.dataset.chartName;
const chartArtist = item.dataset.chartArtist;
const chartUrl = item.dataset.chartUrl;
console.log(`🎵 Chart clicked: ${chartName} by ${chartArtist}`);
try {
// Create a virtual chart data object
const chartHash = `individual_chart_${genreSlug}_${Date.now()}`;
const fullChartName = `${chartName} (${genreName})`;
showToast(`Loading ${chartName}...`, 'info');
// For demonstration, we'll use the genre tracks as chart content
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/tracks?limit=20`);
if (!response.ok) {
throw new Error(`Failed to fetch chart content: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
throw new Error(`No tracks found in chart`);
}
// Create chart data object for playlist card
const chartData = {
hash: chartHash,
name: fullChartName,
chart_type: 'individual_chart',
track_count: data.tracks.length,
tracks: data.tracks.map(track => ({
name: track.title || 'Unknown Title',
artists: [track.artist || 'Unknown Artist'],
album: fullChartName,
duration_ms: 0,
external_urls: { beatport: track.url || chartUrl },
source: 'beatport'
}))
};
// Add card to container (in background, like YouTube does)
console.log(`🃏 Creating Beatport playlist card for: ${fullChartName}`);
addBeatportCardToContainer(chartData);
// Automatically open discovery modal
handleBeatportCardClick(chartHash);
console.log(`✅ Created Beatport card and opened discovery modal for ${fullChartName}`);
} catch (error) {
console.error(`❌ Error loading chart: ${error.message}`);
showToast(`Error loading chart: ${error.message}`, 'error');
}
});
});
}
function showBeatportGenreDetailViewFromBack() {
// Show genre detail view (used by charts list back button)
document.querySelectorAll('.beatport-sub-view').forEach(view => {
view.classList.remove('active');
});
const genreDetailView = document.getElementById('beatport-genre-detail-view');
if (genreDetailView) {
genreDetailView.classList.add('active');
}
}
async function showBeatportGenreChartsListView(genreSlug, genreId, genreName) {
console.log(`📈 Showing charts list for: ${genreName}`);
// Hide all other beatport views
document.querySelectorAll('.beatport-sub-view').forEach(view => {
view.classList.remove('active');
});
const mainView = document.getElementById('beatport-main-view');
if (mainView) {
mainView.classList.remove('active');
}
// Show charts list view
const chartsListView = document.getElementById('beatport-genre-charts-list-view');
if (chartsListView) {
chartsListView.classList.add('active');
// Update view content
document.getElementById('genre-charts-list-title').textContent = `New ${genreName} Charts`;
document.getElementById('genre-charts-list-breadcrumb').textContent = `Browse Charts > Genre Explorer > ${genreName} Charts > New Charts`;
// Store current genre data for individual chart handlers
chartsListView.dataset.genreSlug = genreSlug;
chartsListView.dataset.genreId = genreId;
chartsListView.dataset.genreName = genreName;
// Load charts for this genre
await loadGenreChartsList(genreSlug, genreId, genreName);
console.log(`✅ Charts list view shown for ${genreName}`);
} else {
console.error('❌ Charts list view element not found');
}
}
async function loadGenreChartsList(genreSlug, genreId, genreName) {
const chartsGrid = document.getElementById('genre-charts-grid');
const loadingPlaceholder = document.getElementById('charts-loading-placeholder');
if (!chartsGrid || !loadingPlaceholder) {
console.error('❌ Charts grid or loading placeholder not found');
return;
}
// Show loading state
loadingPlaceholder.style.display = 'block';
chartsGrid.style.display = 'none';
chartsGrid.innerHTML = '';
try {
console.log(`🔍 Loading charts for ${genreName}...`);
// Fetch charts from the new-charts endpoint
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/new-charts?limit=50`);
if (!response.ok) {
throw new Error(`Failed to fetch charts: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
// Show empty state
chartsGrid.innerHTML = `
<div class="genre-charts-empty">
<h3>No Charts Available</h3>
<p>No curated charts found for ${genreName} at the moment.<br>Check back later for new DJ and artist chart collections.</p>
</div>
`;
} else {
// Populate charts grid
const chartsHTML = data.tracks.map((chart, index) => {
const chartName = chart.title || 'Untitled Chart';
const artistName = chart.artist || 'Various Artists';
const chartUrl = chart.url || '';
// Extract chart ID from URL for click handling
const chartId = chartUrl.split('/').pop() || `chart_${index}`;
return `
<div class="genre-chart-item" data-chart-url="${chartUrl}" data-chart-name="${chartName}" data-chart-artist="${artistName}">
<div class="chart-item-header">
<div class="chart-item-icon">📈</div>
<div class="chart-item-title">
<h4>${chartName}</h4>
<p class="chart-item-artist">by ${artistName}</p>
</div>
</div>
<div class="chart-item-description">
Curated chart collection featuring ${genreName} tracks
</div>
<div class="chart-item-footer">
<div class="chart-item-type">Chart</div>
<div class="chart-item-action">Click to explore </div>
</div>
</div>
`;
}).join('');
chartsGrid.innerHTML = chartsHTML;
// Add click handlers to chart items
setupGenreChartItemHandlers(genreSlug, genreId, genreName);
}
// Hide loading and show grid
loadingPlaceholder.style.display = 'none';
chartsGrid.style.display = 'grid';
console.log(`✅ Loaded ${data.tracks?.length || 0} charts for ${genreName}`);
showToast(`Found ${data.tracks?.length || 0} chart collections`, 'success');
} catch (error) {
console.error(`❌ Error loading charts for ${genreName}:`, error);
// Show error state
chartsGrid.innerHTML = `
<div class="genre-charts-empty">
<h3>Error Loading Charts</h3>
<p>Unable to load chart collections for ${genreName}.<br>Please try again later.</p>
</div>
`;
loadingPlaceholder.style.display = 'none';
chartsGrid.style.display = 'grid';
showToast(`Error loading charts: ${error.message}`, 'error');
}
}
function setupGenreChartItemHandlers(genreSlug, genreId, genreName) {
const chartItems = document.querySelectorAll('#genre-charts-grid .genre-chart-item');
chartItems.forEach(item => {
item.addEventListener('click', async () => {
const chartName = item.dataset.chartName;
const chartArtist = item.dataset.chartArtist;
const chartUrl = item.dataset.chartUrl;
console.log(`🎵 Chart clicked: ${chartName} by ${chartArtist}`);
// For now, we'll create a virtual playlist from this chart
// This would eventually fetch the actual chart contents from Beatport
try {
// Create a virtual chart data object
const chartHash = `individual_chart_${genreSlug}_${Date.now()}`;
const fullChartName = `${chartName} (${genreName})`;
showToast(`Loading ${chartName}...`, 'info');
// For demonstration, we'll use the genre tracks as chart content
// In a real implementation, this would fetch the specific chart tracks
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/tracks?limit=20`);
if (!response.ok) {
throw new Error(`Failed to fetch chart content: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
throw new Error(`No tracks found in chart`);
}
// Create chart data object for playlist card
const chartData = {
hash: chartHash,
name: fullChartName,
chart_type: 'individual_chart',
track_count: data.tracks.length,
tracks: data.tracks.map(track => ({
name: track.title || 'Unknown Title',
artists: [track.artist || 'Unknown Artist'],
album: fullChartName,
duration_ms: 0,
external_urls: { beatport: track.url || chartUrl },
source: 'beatport'
}))
};
// Add card to container (in background, like YouTube does)
console.log(`🃏 Creating Beatport playlist card for: ${fullChartName}`);
addBeatportCardToContainer(chartData);
// Automatically open discovery modal
handleBeatportCardClick(chartHash);
console.log(`✅ Created Beatport card and opened discovery modal for ${fullChartName}`);
} catch (error) {
console.error(`❌ Error loading chart: ${error.message}`);
showToast(`Error loading chart: ${error.message}`, 'error');
}
});
});
}
async function handleGenreChartTypeClick(genreSlug, genreId, genreName, chartType) {
console.log(`🎯 Genre chart type clicked: ${chartType} for ${genreName} (${genreSlug}/${genreId})`);
// Map chart types to API endpoints and create descriptive names
const chartTypeMap = {
'top-10': {
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/top-10`,
name: `Top 10 ${genreName}`,
limit: 10
},
'top-100': {
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/tracks`,
name: `Top 100 ${genreName}`,
limit: 100
},
'releases-top-10': {
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/releases-top-10`,
name: `Top 10 ${genreName} Releases`,
limit: 10
},
'releases-top-100': {
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/releases-top-100`,
name: `Top 100 ${genreName} Releases`,
limit: 100
},
'staff-picks': {
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/staff-picks`,
name: `${genreName} Staff Picks`,
limit: 50
},
'latest-releases': {
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/latest-releases`,
name: `Latest ${genreName} Releases`,
limit: 50
},
'new-charts': {
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/new-charts`,
name: `New ${genreName} Charts`,
limit: 50
}
};
const chartConfig = chartTypeMap[chartType];
if (!chartConfig) {
console.error(`❌ Unknown chart type: ${chartType}`);
showToast(`Unknown chart type: ${chartType}`, 'error');
return;
}
try {
// Check if we already have a card for this specific chart type
const existingState = Object.values(beatportChartStates).find(state =>
state.chart && state.chart.name === chartConfig.name && state.chart.chart_type === `genre_${chartType}`
);
if (existingState) {
console.log(`🔄 Found existing Beatport card for ${chartConfig.name}, opening existing modal`);
handleBeatportCardClick(existingState.chart.hash);
return;
}
// Create a chart hash for state management
const chartHash = `genre_${chartType}_${genreSlug}_${genreId}_${Date.now()}`;
showToast(`Loading ${chartConfig.name}...`, 'info');
// Fetch tracks from the specific endpoint
const response = await fetch(`${chartConfig.endpoint}?limit=${chartConfig.limit}`);
if (!response.ok) {
throw new Error(`Failed to fetch ${chartConfig.name}: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
throw new Error(`No tracks found in ${chartConfig.name}`);
}
// Create chart data object for playlist card
const chartData = {
hash: chartHash,
name: chartConfig.name,
chart_type: `genre_${chartType}`,
track_count: data.tracks.length,
tracks: data.tracks.map(track => ({
name: track.title || 'Unknown Title',
artists: [track.artist || 'Unknown Artist'],
album: chartConfig.name,
duration_ms: 0,
external_urls: { beatport: track.url || '' },
source: 'beatport'
}))
};
// Add card to container (in background, like YouTube does)
console.log(`🃏 Creating Beatport playlist card for: ${chartConfig.name}`);
addBeatportCardToContainer(chartData);
// Automatically open discovery modal (like when you click a YouTube or Tidal card in fresh state)
handleBeatportCardClick(chartHash);
console.log(`✅ Created Beatport card and opened discovery modal for ${chartConfig.name}`);
} catch (error) {
console.error(`❌ Error loading ${chartConfig.name}:`, error);
showToast(`Error loading ${chartConfig.name}: ${error.message}`, 'error');
}
}
// ===============================

View file

@ -4862,6 +4862,622 @@ body {
0 0 20px rgba(1, 255, 149, 0.3);
}
/* ================================= */
/* BEATPORT GENRE DETAIL VIEW */
/* ================================= */
.genre-detail-header {
margin-bottom: 30px;
padding: 20px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.genre-detail-info h2 {
font-size: 32px;
font-weight: 700;
color: #ffffff;
margin: 0 0 8px 0;
background: linear-gradient(135deg, #01ff95, #00d4ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.genre-detail-info p {
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
/* Section Styling */
.genre-main-charts-section,
.genre-releases-section,
.genre-editorial-section,
.genre-new-charts-section {
margin-bottom: 40px;
padding: 20px;
background: linear-gradient(135deg,
rgba(20, 20, 20, 0.8) 0%,
rgba(15, 15, 15, 0.9) 100%);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #ffffff;
margin: 0 0 16px 0;
display: flex;
align-items: center;
gap: 8px;
}
.section-description {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
margin: 0 0 20px 0;
font-style: italic;
}
/* Chart Types Grid */
.genre-chart-types-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
/* Chart Type Cards */
.genre-chart-type-card {
background: linear-gradient(135deg,
rgba(25, 25, 25, 0.95) 0%,
rgba(15, 15, 15, 0.98) 100%);
backdrop-filter: blur(20px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.genre-chart-type-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(1, 255, 149, 0.1) 0%,
transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.genre-chart-type-card:hover {
transform: translateY(-3px);
border-color: rgba(1, 255, 149, 0.3);
box-shadow:
0 10px 25px rgba(0, 0, 0, 0.3),
0 0 15px rgba(1, 255, 149, 0.2);
}
.genre-chart-type-card:hover::before {
opacity: 1;
}
.chart-type-icon {
font-size: 32px;
min-width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg,
rgba(1, 255, 149, 0.2) 0%,
rgba(0, 212, 255, 0.2) 100%);
border-radius: 12px;
border: 1px solid rgba(1, 255, 149, 0.3);
position: relative;
z-index: 2;
}
.chart-type-info {
flex: 1;
position: relative;
z-index: 2;
}
.chart-type-info h3 {
font-size: 18px;
font-weight: 600;
color: #ffffff;
margin: 0 0 4px 0;
}
.chart-type-info p {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin: 0 0 8px 0;
}
.track-count {
font-size: 12px;
color: rgba(1, 255, 149, 0.8);
font-weight: 500;
background: rgba(1, 255, 149, 0.1);
padding: 4px 8px;
border-radius: 6px;
display: inline-block;
}
/* Special Chart Card (New Charts) */
.genre-new-charts-container {
display: flex;
justify-content: center;
}
.genre-chart-type-card.special-chart {
background: linear-gradient(135deg,
rgba(30, 20, 40, 0.95) 0%,
rgba(20, 15, 30, 0.98) 100%);
border: 2px solid rgba(138, 43, 226, 0.3);
max-width: 400px;
position: relative;
}
.genre-chart-type-card.special-chart::before {
background: linear-gradient(135deg,
rgba(138, 43, 226, 0.15) 0%,
transparent 50%);
}
.genre-chart-type-card.special-chart:hover {
border-color: rgba(138, 43, 226, 0.5);
box-shadow:
0 12px 30px rgba(0, 0, 0, 0.4),
0 0 20px rgba(138, 43, 226, 0.3);
}
.genre-chart-type-card.special-chart .chart-type-icon {
background: linear-gradient(135deg,
rgba(138, 43, 226, 0.3) 0%,
rgba(75, 0, 130, 0.3) 100%);
border-color: rgba(138, 43, 226, 0.4);
}
.special-badge {
position: absolute;
top: 8px;
right: 8px;
background: linear-gradient(135deg, #8a2be2, #4b0082);
color: #ffffff;
font-size: 10px;
font-weight: 600;
padding: 4px 8px;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.expand-indicator {
position: absolute;
top: 50%;
right: 12px;
transform: translateY(-50%);
font-size: 16px;
color: rgba(138, 43, 226, 0.8);
transition: transform 0.3s ease;
z-index: 5;
background: rgba(0, 0, 0, 0.3);
padding: 4px;
border-radius: 4px;
}
.expand-indicator.expanded {
transform: translateY(-50%) rotate(180deg);
}
/* Always Visible New Charts Content */
.new-charts-content {
margin-top: 20px;
padding: 20px;
background: linear-gradient(135deg,
rgba(35, 25, 45, 0.9) 0%,
rgba(25, 20, 35, 0.95) 100%);
border-radius: 12px;
border: 1px solid rgba(138, 43, 226, 0.15);
}
@keyframes expandDown {
from {
opacity: 0;
max-height: 0;
padding-top: 0;
padding-bottom: 0;
}
to {
opacity: 1;
max-height: 1000px;
padding-top: 20px;
padding-bottom: 20px;
}
}
.charts-loading-inline {
text-align: center;
padding: 40px 20px;
color: rgba(255, 255, 255, 0.6);
}
.charts-loading-inline .loading-spinner-small {
width: 30px;
height: 30px;
border: 3px solid rgba(138, 43, 226, 0.3);
border-top: 3px solid #8a2be2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 12px auto;
}
.charts-loading-inline p {
font-size: 14px;
margin: 0;
}
.new-charts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.new-chart-item {
background: linear-gradient(135deg,
rgba(25, 15, 35, 0.95) 0%,
rgba(15, 10, 25, 0.98) 100%);
backdrop-filter: blur(15px);
border-radius: 10px;
border: 1px solid rgba(138, 43, 226, 0.2);
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.new-chart-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(138, 43, 226, 0.1) 0%,
transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.new-chart-item:hover {
transform: translateY(-3px);
border-color: rgba(138, 43, 226, 0.4);
box-shadow:
0 8px 20px rgba(0, 0, 0, 0.3),
0 0 15px rgba(138, 43, 226, 0.2);
}
.new-chart-item:hover::before {
opacity: 1;
}
.new-chart-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
position: relative;
z-index: 2;
}
.new-chart-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg,
rgba(138, 43, 226, 0.3) 0%,
rgba(75, 0, 130, 0.3) 100%);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
border: 1px solid rgba(138, 43, 226, 0.4);
}
.new-chart-title {
flex: 1;
}
.new-chart-title h5 {
font-size: 14px;
font-weight: 600;
color: #ffffff;
margin: 0 0 2px 0;
line-height: 1.3;
}
.new-chart-artist {
font-size: 12px;
color: rgba(138, 43, 226, 0.8);
margin: 0;
font-weight: 500;
}
.new-chart-description {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
margin: 0 0 10px 0;
line-height: 1.4;
position: relative;
z-index: 2;
}
.new-chart-footer {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 2;
}
.new-chart-type {
font-size: 9px;
background: rgba(138, 43, 226, 0.2);
color: rgba(138, 43, 226, 0.9);
padding: 3px 6px;
border-radius: 4px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
}
.new-chart-action {
font-size: 10px;
color: rgba(1, 255, 149, 0.8);
font-weight: 500;
display: flex;
align-items: center;
gap: 3px;
}
.new-charts-empty {
text-align: center;
padding: 40px 20px;
color: rgba(255, 255, 255, 0.6);
}
.new-charts-empty h4 {
font-size: 16px;
margin: 0 0 8px 0;
color: rgba(255, 255, 255, 0.8);
}
.new-charts-empty p {
font-size: 12px;
margin: 0;
line-height: 1.4;
}
/* ================================= */
/* BEATPORT GENRE CHARTS LIST VIEW */
/* ================================= */
.genre-charts-list-header {
margin-bottom: 30px;
padding: 20px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.genre-charts-list-info h2 {
font-size: 28px;
font-weight: 700;
color: #ffffff;
margin: 0 0 8px 0;
background: linear-gradient(135deg, #8a2be2, #4b0082);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.genre-charts-list-info p {
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
.genre-charts-list-container {
position: relative;
min-height: 400px;
}
.charts-loading-placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: rgba(255, 255, 255, 0.6);
}
.charts-loading-placeholder .loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(138, 43, 226, 0.3);
border-top: 4px solid #8a2be2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.charts-loading-placeholder p {
font-size: 16px;
margin: 0;
}
.genre-charts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
padding: 10px 0;
}
.genre-chart-item {
background: linear-gradient(135deg,
rgba(30, 20, 40, 0.95) 0%,
rgba(20, 15, 30, 0.98) 100%);
backdrop-filter: blur(20px);
border-radius: 12px;
border: 1px solid rgba(138, 43, 226, 0.2);
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.genre-chart-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(138, 43, 226, 0.1) 0%,
transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.genre-chart-item:hover {
transform: translateY(-5px);
border-color: rgba(138, 43, 226, 0.4);
box-shadow:
0 15px 30px rgba(0, 0, 0, 0.3),
0 0 20px rgba(138, 43, 226, 0.3);
}
.genre-chart-item:hover::before {
opacity: 1;
}
.chart-item-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
position: relative;
z-index: 2;
}
.chart-item-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg,
rgba(138, 43, 226, 0.3) 0%,
rgba(75, 0, 130, 0.3) 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 1px solid rgba(138, 43, 226, 0.4);
}
.chart-item-title {
flex: 1;
}
.chart-item-title h4 {
font-size: 16px;
font-weight: 600;
color: #ffffff;
margin: 0 0 4px 0;
line-height: 1.3;
}
.chart-item-artist {
font-size: 14px;
color: rgba(138, 43, 226, 0.8);
margin: 0;
font-weight: 500;
}
.chart-item-description {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
margin: 0 0 12px 0;
line-height: 1.4;
position: relative;
z-index: 2;
}
.chart-item-footer {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 2;
}
.chart-item-type {
font-size: 11px;
background: rgba(138, 43, 226, 0.2);
color: rgba(138, 43, 226, 0.9);
padding: 4px 8px;
border-radius: 6px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.5px;
}
.chart-item-action {
font-size: 12px;
color: rgba(1, 255, 149, 0.8);
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
.genre-charts-empty {
text-align: center;
padding: 60px 20px;
color: rgba(255, 255, 255, 0.6);
}
.genre-charts-empty h3 {
font-size: 20px;
margin: 0 0 12px 0;
color: rgba(255, 255, 255, 0.8);
}
.genre-charts-empty p {
font-size: 14px;
margin: 0;
line-height: 1.5;
}
/* ================================= */
/* RESPONSIVE DESIGN */
/* ================================= */
@ -4872,6 +5488,71 @@ body {
gap: 15px;
}
/* Genre Detail Responsive */
.genre-detail-info h2 {
font-size: 24px;
}
.genre-chart-types-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.genre-chart-type-card {
padding: 16px;
gap: 12px;
}
.chart-type-icon {
font-size: 28px;
min-width: 40px;
height: 40px;
}
.chart-type-info h3 {
font-size: 16px;
}
.section-title {
font-size: 18px;
}
.genre-main-charts-section,
.genre-releases-section,
.genre-editorial-section,
.genre-new-charts-section {
margin-bottom: 24px;
padding: 16px;
}
/* Genre Charts List Responsive */
.genre-charts-list-info h2 {
font-size: 22px;
}
.genre-charts-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.genre-chart-item {
padding: 16px;
}
.chart-item-header {
gap: 10px;
}
.chart-item-icon {
width: 35px;
height: 35px;
font-size: 16px;
}
.chart-item-title h4 {
font-size: 15px;
}
.beatport-chart-item {
flex-direction: column;
text-align: center;