![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/soundstudiopro.com/private_html/ |
<?php
session_start();
// Include translation system
require_once 'includes/translations.php';
require_once 'config/database.php';
// Include audio token system for signed URLs
require_once 'utils/audio_token.php';
// Prevent caching of this page to ensure deleted tracks don't show
header('Cache-Control: no-cache, no-store, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
// Set page variables
$current_page = 'community_fixed';
$page_title = t('community.page_title');
$page_description = t('community.page_description');
// Include header
include 'includes/header.php';
$pdo = getDBConnection();
// Ensure wishlist table exists for queries on this page
try {
$pdo->exec("
CREATE TABLE IF NOT EXISTS user_wishlist (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
track_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_wishlist (user_id, track_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (track_id) REFERENCES music_tracks(id) ON DELETE CASCADE
)
");
} catch (Exception $e) {
error_log('Wishlist ensure failed: ' . $e->getMessage());
}
// Get current user info
$user_id = $_SESSION['user_id'] ?? null;
$user_name = 'Guest';
if ($user_id) {
$stmt = $pdo->prepare("SELECT name FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
$user_name = $user['name'] ?? 'User';
}
// Get filter parameters
$sort = $_GET['sort'] ?? 'latest'; // latest, trending, popular, random
$genre = $_GET['genre'] ?? 'all';
// Decode and clean genre parameter (remove quotes if present, decode URL encoding)
if ($genre !== 'all') {
$genre = urldecode($genre);
$genre = trim($genre, '"\'');
$genre = trim($genre);
// Validate genre - reject JSON objects or invalid formats
// Check if it looks like JSON (starts with { or [)
if (preg_match('/^[\s]*[{\[]/', $genre)) {
// This is JSON, not a valid genre - reset to 'all'
error_log("Invalid genre parameter detected (JSON): " . substr($genre, 0, 100));
$genre = 'all';
}
// Check if it contains JSON-like structures (callBackUrl, etc.)
if (stripos($genre, 'callBackUrl') !== false || stripos($genre, 'callback') !== false) {
// This contains callback URL data, not a valid genre - reset to 'all'
error_log("Invalid genre parameter detected (contains callback): " . substr($genre, 0, 100));
$genre = 'all';
}
// Additional validation: genre should be a simple string, not contain JSON characters
if (preg_match('/[{}[\]]/', $genre)) {
// Contains JSON brackets - invalid genre
error_log("Invalid genre parameter detected (contains JSON brackets): " . substr($genre, 0, 100));
$genre = 'all';
}
}
$time_filter = $_GET['time'] ?? 'all'; // all, today, week, month
$search = $_GET['search'] ?? ''; // search keyword
// Decode and clean search parameter (remove quotes if present, decode URL encoding)
if (!empty($search)) {
$search = urldecode($search);
$search = trim($search, '"\'');
$search = trim($search);
}
$page = max(1, intval($_GET['page'] ?? 1));
$per_page = 24; // Efficient grid layout
$offset = ($page - 1) * $per_page;
// Build time filter
$time_condition = '';
switch ($time_filter) {
case 'today':
$time_condition = 'AND mt.created_at >= CURDATE()';
break;
case 'week':
$time_condition = 'AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)';
break;
case 'month':
$time_condition = 'AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)';
break;
}
// Build sort order
$order_by = 'mt.created_at DESC';
switch ($sort) {
case 'trending':
$order_by = '(mt.likes_count * 2 + mt.plays_count + mt.comments_count) DESC, mt.created_at DESC';
break;
case 'popular':
$order_by = 'mt.likes_count DESC, mt.plays_count DESC';
break;
case 'random':
$order_by = 'RAND()';
break;
default:
$order_by = 'mt.created_at DESC';
}
// Build genre filter - check genre column, tags, and metadata JSON
// Use case-insensitive LIKE for all conditions to match partial words (e.g., "deep" in "Deep House")
$genre_condition = '';
if ($genre !== 'all' && !empty($genre)) {
// Normalize genre parameter to lowercase for consistent matching
$genre_lower = strtolower(trim($genre));
$genre_like = '%' . $genre_lower . '%';
// Check genre column, tags, and metadata JSON fields (genre and style)
// All use case-insensitive LIKE to match partial words in compound genres
$genre_condition = "AND (
LOWER(mt.genre) LIKE ?
OR LOWER(mt.tags) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
)";
}
// Build comprehensive search filter - industry-leading search across ALL fields
// Searches: title, artist, genre (all locations), tags (all locations), mood, style, instruments, prompt, lyrics
// This ensures "deep" finds "Deep House", "rap" finds all rap tracks, etc.
$search_condition = '';
if (!empty($search)) {
// Use EXACT same pattern as genre filter for genre fields, then add other fields
$search_condition = "AND (
LOWER(mt.title) LIKE ?
OR LOWER(u.name) LIKE ?
OR LOWER(COALESCE(mt.prompt, '')) LIKE ?
OR LOWER(COALESCE(mt.lyrics, '')) LIKE ?
OR LOWER(mt.genre) LIKE ?
OR LOWER(mt.tags) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
OR LOWER(mt.style) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proGenre') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proGenre'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proSubGenre') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proSubGenre'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.tags') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags'))) LIKE ?
OR LOWER(COALESCE(mt.mood, '')) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.mood') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood'))) LIKE ?
OR LOWER(COALESCE(mt.instruments, '')) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.instruments') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.instruments'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proRhythmSection') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proRhythmSection'))) LIKE ?
OR LOWER(COALESCE(mt.energy, '')) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.energy') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.energy'))) LIKE ?
)";
}
// Get total count for pagination FIRST (before main query) to validate page number
$count_sql = "
SELECT COUNT(*) as total
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND (mt.audio_url IS NOT NULL AND mt.audio_url != '' OR mt.variations_count > 0)
AND mt.user_id IS NOT NULL
AND u.id IS NOT NULL
AND (mt.is_public = 1 OR mt.is_public IS NULL)
$time_condition
$genre_condition
$search_condition
";
$count_stmt = $pdo->prepare($count_sql);
$count_params = [];
if ($genre !== 'all' && !empty($genre)) {
// Normalize genre parameter to lowercase for consistent case-insensitive matching
$genre_lower = strtolower(trim($genre));
$genre_like = '%' . $genre_lower . '%';
// All conditions use case-insensitive LIKE for partial word matching
$count_params = array_merge($count_params, [
$genre_like, // LOWER(mt.genre) LIKE ?
$genre_like, // LOWER(mt.tags) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre'))) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.style'))) LIKE ?
$genre_like, // LOWER(CAST(JSON_EXTRACT(metadata, '$.genre') AS CHAR)) LIKE ?
$genre_like // LOWER(CAST(JSON_EXTRACT(metadata, '$.style') AS CHAR)) LIKE ?
]);
}
if (!empty($search)) {
// Use lowercase search parameter for case-insensitive matching
$search_lower = strtolower(trim($search));
$search_param = '%' . $search_lower . '%';
// Add search parameters for count query - match SQL placeholders exactly (30 total)
$count_params = array_merge($count_params, [
$search_param, // LOWER(mt.title) LIKE ?
$search_param, // LOWER(u.name) LIKE ?
$search_param, // LOWER(COALESCE(mt.prompt, '')) LIKE ?
$search_param, // LOWER(COALESCE(mt.lyrics, '')) LIKE ?
$search_param, // LOWER(mt.genre) LIKE ?
$search_param, // LOWER(mt.tags) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
$search_param, // LOWER(mt.style) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proSubGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proSubGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.tags') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.mood, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.mood') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.instruments, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.instruments') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.instruments'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proRhythmSection') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proRhythmSection'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.energy, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.energy') AS CHAR)) LIKE ?
$search_param // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.energy'))) LIKE ?
]);
}
$count_stmt->execute($count_params);
$total_tracks = $count_stmt->fetchColumn();
$total_pages = $total_tracks > 0 ? ceil($total_tracks / $per_page) : 1; // At least 1 page to show empty state
// Validate page number - redirect if invalid (BEFORE main query)
// Only redirect if page is too high, NOT if there are no results (which is valid)
if ($total_tracks > 0 && $page > $total_pages) {
// Page exceeds total pages - redirect to last valid page
$redirect_url = "?page=" . $total_pages . "&sort=" . urlencode($sort) . "&time=" . urlencode($time_filter) . "&genre=" . urlencode($genre);
if (!empty($search)) {
$redirect_url .= "&search=" . urlencode($search);
}
header("Location: " . $redirect_url);
exit;
}
// If page is 0 or negative, redirect to page 1
if ($page < 1) {
$redirect_url = "?page=1&sort=" . urlencode($sort) . "&time=" . urlencode($time_filter) . "&genre=" . urlencode($genre);
if (!empty($search)) {
$redirect_url .= "&search=" . urlencode($search);
}
header("Location: " . $redirect_url);
exit;
}
// Efficient query - get tracks with all needed data
// IMPORTANT: Only show tracks that actually exist and are complete
// OPTIMIZED: Using JOINs instead of correlated subqueries for better performance
$sql = "
SELECT
mt.id,
mt.title,
mt.audio_url,
mt.duration,
mt.created_at,
mt.user_id,
mt.genre,
mt.tags,
mt.image_url,
mt.task_id,
mt.metadata,
mt.variations_count,
mt.price,
u.name as artist_name,
u.id as artist_id,
u.profile_image,
COALESCE(like_stats.like_count, 0) as like_count,
COALESCE(comment_stats.comment_count, 0) as comment_count,
COALESCE(play_stats.play_count, 0) as play_count,
COALESCE(share_stats.share_count, 0) as share_count,
CASE WHEN user_like_stats.track_id IS NOT NULL THEN 1 ELSE 0 END as user_liked,
COALESCE((SELECT COUNT(*) FROM user_follows WHERE follower_id = ? AND following_id = mt.user_id), 0) as is_following,
CASE WHEN wishlist_stats.track_id IS NOT NULL THEN 1 ELSE 0 END as is_in_wishlist,
COALESCE(vote_stats.vote_count, 0) as vote_count,
COALESCE(rating_stats.average_rating, 0) as average_rating,
COALESCE(rating_stats.rating_count, 0) as rating_count,
user_vote_stats.vote_type as user_vote
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) like_stats ON mt.id = like_stats.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as comment_count FROM track_comments GROUP BY track_id) comment_stats ON mt.id = comment_stats.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) play_stats ON mt.id = play_stats.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as share_count FROM track_shares GROUP BY track_id) share_stats ON mt.id = share_stats.track_id
LEFT JOIN (SELECT track_id, SUM(CASE WHEN vote_type = 'up' THEN 1 ELSE -1 END) as vote_count FROM track_votes GROUP BY track_id) vote_stats ON mt.id = vote_stats.track_id
LEFT JOIN (SELECT track_id, AVG(rating) as average_rating, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) rating_stats ON mt.id = rating_stats.track_id
LEFT JOIN (SELECT track_id FROM track_likes WHERE user_id = ?) user_like_stats ON mt.id = user_like_stats.track_id
LEFT JOIN (SELECT track_id FROM user_wishlist WHERE user_id = ?) wishlist_stats ON mt.id = wishlist_stats.track_id
LEFT JOIN (SELECT track_id, vote_type FROM track_votes WHERE user_id = ?) user_vote_stats ON mt.id = user_vote_stats.track_id
WHERE mt.status = 'complete'
AND (mt.audio_url IS NOT NULL AND mt.audio_url != '' OR mt.variations_count > 0)
AND mt.user_id IS NOT NULL
AND u.id IS NOT NULL
AND (mt.is_public = 1 OR mt.is_public IS NULL)
$time_condition
$genre_condition
$search_condition
ORDER BY $order_by
LIMIT ? OFFSET ?
";
$stmt = $pdo->prepare($sql);
// Parameters: user_like_stats.user_id, wishlist_stats.user_id, user_vote_stats.user_id, is_following user_id (4 total)
$params = [$user_id ?? 0, $user_id ?? 0, $user_id ?? 0, $user_id ?? 0];
if ($genre !== 'all' && !empty($genre)) {
// Normalize genre parameter to lowercase for consistent case-insensitive matching
$genre_lower = strtolower(trim($genre));
$genre_like = '%' . $genre_lower . '%';
// All conditions use case-insensitive LIKE for partial word matching
$params = array_merge($params, [
$genre_like, // LOWER(mt.genre) LIKE ?
$genre_like, // LOWER(mt.tags) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre'))) LIKE ?
$genre_like, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.style'))) LIKE ?
$genre_like, // LOWER(CAST(JSON_EXTRACT(metadata, '$.genre') AS CHAR)) LIKE ?
$genre_like // LOWER(CAST(JSON_EXTRACT(metadata, '$.style') AS CHAR)) LIKE ?
]);
}
if (!empty($search)) {
// Use lowercase search parameter for case-insensitive matching
// This ensures "deep" matches "Deep House", "rap" matches "Rap", etc.
$search_lower = strtolower(trim($search));
$search_param = '%' . $search_lower . '%';
// Add search parameters - match SQL placeholders exactly (30 total)
// Order matches the SQL query above
$params = array_merge($params, [
$search_param, // LOWER(mt.title) LIKE ?
$search_param, // LOWER(u.name) LIKE ?
$search_param, // LOWER(COALESCE(mt.prompt, '')) LIKE ?
$search_param, // LOWER(COALESCE(mt.lyrics, '')) LIKE ?
$search_param, // LOWER(mt.genre) LIKE ?
$search_param, // LOWER(mt.tags) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
$search_param, // LOWER(mt.style) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proSubGenre') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proSubGenre'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.tags') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.mood, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.mood') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.instruments, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.instruments') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.instruments'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proLeadInstrument'))) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.proRhythmSection') AS CHAR)) LIKE ?
$search_param, // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.proRhythmSection'))) LIKE ?
$search_param, // LOWER(COALESCE(mt.energy, '')) LIKE ?
$search_param, // LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.energy') AS CHAR)) LIKE ?
$search_param // LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.energy'))) LIKE ?
]);
}
// Add LIMIT and OFFSET parameters
$params[] = $per_page;
$params[] = $offset;
$stmt->execute($params);
$tracks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Double-check: Filter out any tracks that don't actually exist (safety check)
// This ensures deleted tracks never show up, even if there's a race condition
if (!empty($tracks)) {
$track_ids = array_column($tracks, 'id');
if (!empty($track_ids)) {
$placeholders = implode(',', array_fill(0, count($track_ids), '?'));
$verify_stmt = $pdo->prepare("SELECT id FROM music_tracks WHERE id IN ($placeholders)");
$verify_stmt->execute($track_ids);
$valid_track_ids = array_column($verify_stmt->fetchAll(PDO::FETCH_ASSOC), 'id');
// Filter tracks to only include those that still exist
$tracks = array_filter($tracks, function($track) use ($valid_track_ids) {
return in_array($track['id'], $valid_track_ids);
});
// Re-index array
$tracks = array_values($tracks);
}
} else {
// Ensure tracks is an empty array, not null
$tracks = [];
}
// Get variations for each track
foreach ($tracks as &$track) {
$track['variations'] = [];
if ($track['variations_count'] > 0) {
try {
$var_stmt = $pdo->prepare("
SELECT
variation_index,
audio_url,
duration,
title,
tags,
image_url
FROM audio_variations
WHERE track_id = ?
ORDER BY variation_index ASC
");
$var_stmt->execute([$track['id']]);
$track['variations'] = $var_stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$track['variations'] = [];
}
}
}
unset($track);
// Calculate track rankings for each track
function calculateTrackRankings($pdo, $track) {
$rankings = [];
if ($track['status'] === 'complete') {
// Check if track_votes table exists
$votes_table_exists = false;
try {
$check_stmt = $pdo->query("SHOW TABLES LIKE 'track_votes'");
$votes_table_exists = $check_stmt->rowCount() > 0;
} catch (Exception $e) {
$votes_table_exists = false;
}
// Get total tracks for context
$total_tracks_query = "SELECT COUNT(*) FROM music_tracks WHERE status = 'complete'";
$stmt_total = $pdo->query($total_tracks_query);
$total_tracks = $stmt_total->fetchColumn();
// Overall ranking (weighted score) - among all complete tracks
if ($votes_table_exists) {
$overall_rank_query = "
SELECT
COUNT(*) + 1 as rank
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) tp2 ON mt2.id = tp2.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl2 ON mt2.id = tl2.track_id
LEFT JOIN (
SELECT
track_id,
COALESCE(SUM(CASE WHEN vote_type = 'up' THEN 1 ELSE -1 END), 0) as vote_count
FROM track_votes
GROUP BY track_id
) tv2 ON mt2.id = tv2.track_id
LEFT JOIN (SELECT track_id, AVG(rating) as avg_rating, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr2 ON mt2.id = tr2.track_id
WHERE mt2.status = 'complete'
AND (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tv2.vote_count, 0) * 3.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) >
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * 3.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
OR (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tv2.vote_count, 0) * 3.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) =
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * 3.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
AND mt2.created_at > ?
)
)
";
$stmt_rank = $pdo->prepare($overall_rank_query);
$stmt_rank->execute([
$track['play_count'] ?? 0,
$track['like_count'] ?? 0,
$track['vote_count'] ?? 0,
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['play_count'] ?? 0,
$track['like_count'] ?? 0,
$track['vote_count'] ?? 0,
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['created_at'] ?? date('Y-m-d H:i:s')
]);
} else {
$overall_rank_query = "
SELECT
COUNT(*) + 1 as rank
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) tp2 ON mt2.id = tp2.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl2 ON mt2.id = tl2.track_id
LEFT JOIN (SELECT track_id, AVG(rating) as avg_rating, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr2 ON mt2.id = tr2.track_id
WHERE mt2.status = 'complete'
AND (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) >
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
OR (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) =
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
AND mt2.created_at > ?
)
)
";
$stmt_rank = $pdo->prepare($overall_rank_query);
$stmt_rank->execute([
$track['play_count'] ?? 0,
$track['like_count'] ?? 0,
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['play_count'] ?? 0,
$track['like_count'] ?? 0,
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['created_at'] ?? date('Y-m-d H:i:s')
]);
}
$rankings['overall'] = $stmt_rank->fetchColumn();
// Calculate total score for logging
$total_score = (($track['play_count'] ?? 0) * 1.0) +
(($track['like_count'] ?? 0) * 2.0) +
(($track['vote_count'] ?? 0) * 3.0) +
(($track['average_rating'] ?? 0) * ($track['rating_count'] ?? 0) * 5.0);
// Play count ranking - among all complete tracks
$play_rank_query = "
SELECT COUNT(*) + 1
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) tp2 ON mt2.id = tp2.track_id
WHERE mt2.status = 'complete'
AND COALESCE(tp2.play_count, 0) > ?
";
$stmt_rank = $pdo->prepare($play_rank_query);
$stmt_rank->execute([$track['play_count'] ?? 0]);
$rankings['plays'] = $stmt_rank->fetchColumn();
// Like count ranking - among all complete tracks
$like_rank_query = "
SELECT COUNT(*) + 1
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl2 ON mt2.id = tl2.track_id
WHERE mt2.status = 'complete'
AND COALESCE(tl2.like_count, 0) > ?
";
$stmt_rank = $pdo->prepare($like_rank_query);
$stmt_rank->execute([$track['like_count'] ?? 0]);
$rankings['likes'] = $stmt_rank->fetchColumn();
// Vote count ranking - among all complete tracks (if votes table exists)
if ($votes_table_exists) {
$vote_rank_query = "
SELECT COUNT(*) + 1
FROM music_tracks mt2
LEFT JOIN (
SELECT
track_id,
COALESCE(SUM(CASE WHEN vote_type = 'up' THEN 1 ELSE -1 END), 0) as vote_count
FROM track_votes
GROUP BY track_id
) tv2 ON mt2.id = tv2.track_id
WHERE mt2.status = 'complete'
AND COALESCE(tv2.vote_count, 0) > ?
";
$stmt_rank = $pdo->prepare($vote_rank_query);
$stmt_rank->execute([$track['vote_count'] ?? 0]);
$rankings['votes'] = $stmt_rank->fetchColumn();
}
// Rating ranking (based on average rating with minimum rating count)
if (($track['rating_count'] ?? 0) >= 3) {
$rating_rank_query = "
SELECT COUNT(*) + 1
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, AVG(rating) as avg_rating, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr2 ON mt2.id = tr2.track_id
WHERE mt2.status = 'complete'
AND COALESCE(tr2.rating_count, 0) >= 3
AND COALESCE(tr2.avg_rating, 0) > ?
";
$stmt_rank = $pdo->prepare($rating_rank_query);
$stmt_rank->execute([$track['average_rating'] ?? 0]);
$rankings['rating'] = $stmt_rank->fetchColumn();
}
$rankings['total_tracks'] = $total_tracks;
// Percentile: Top X% means you're in the top X% of tracks
// Formula: ((total - rank + 1) / total) * 100
$rankings['overall_percentile'] = round((($total_tracks - $rankings['overall'] + 1) / max($total_tracks, 1)) * 100, 1);
// Calculate detailed score breakdown for display
$rankings['score_breakdown'] = [
'plays_score' => $track['play_count'] * 1.0,
'likes_score' => $track['like_count'] * 2.0,
'votes_score' => ($track['vote_count'] ?? 0) * 3.0,
'rating_score' => ($track['average_rating'] ?? 0) * ($track['rating_count'] ?? 0) * 5.0,
'total_score' => ($track['play_count'] * 1.0) + ($track['like_count'] * 2.0) + (($track['vote_count'] ?? 0) * 3.0) + (($track['average_rating'] ?? 0) * ($track['rating_count'] ?? 0) * 5.0)
];
}
return $rankings;
}
// Calculate rankings for each track
// Check if track_votes table exists once
$votes_table_exists = false;
try {
$check_stmt = $pdo->query("SHOW TABLES LIKE 'track_votes'");
$votes_table_exists = $check_stmt->rowCount() > 0;
} catch (Exception $e) {
$votes_table_exists = false;
}
foreach ($tracks as &$track) {
$track['status'] = 'complete'; // Ensure status is set
$trackRankings = calculateTrackRankings($pdo, $track);
$track['trackRankings'] = $trackRankings;
}
unset($track);
// Get community stats
$stats_stmt = $pdo->prepare("
SELECT
COUNT(DISTINCT mt.id) as total_tracks,
COUNT(DISTINCT mt.user_id) as total_artists,
COALESCE(SUM(mt.duration), 0) as total_duration
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND (mt.audio_url IS NOT NULL AND mt.audio_url != '' OR mt.variations_count > 0)
AND mt.user_id IS NOT NULL
AND u.id IS NOT NULL
AND (mt.is_public = 1 OR mt.is_public IS NULL)
");
$stats_stmt->execute();
$stats = $stats_stmt->fetch(PDO::FETCH_ASSOC);
?>
<div class="community-page">
<!-- Hero Section -->
<div class="community-hero">
<div class="container">
<div class="hero-header">
<div class="hero-title-section">
<h1><?= t('community.hero_title') ?></h1>
<p class="subtitle"><?= t('community.hero_subtitle') ?></p>
</div>
<button class="share-community-btn" onclick="shareCommunityFeed()" title="Share Community Feed">
<i class="fas fa-share-alt"></i>
<span>Share Feed</span>
</button>
</div>
<!-- Quick Stats -->
<div class="quick-stats">
<div class="stat-item">
<span class="stat-number"><?= number_format($stats['total_tracks']) ?></span>
<span class="stat-label"><?= t('community.tracks') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= number_format($stats['total_artists']) ?></span>
<span class="stat-label"><?= t('community.artists') ?></span>
</div>
<div class="stat-item">
<span class="stat-number"><?= gmdate('H:i:s', $stats['total_duration']) ?></span>
<span class="stat-label"><?= t('community.total_music') ?></span>
</div>
</div>
</div>
</div>
<!-- Filters & Controls -->
<div class="community-filters">
<div class="container">
<div class="filters-row">
<!-- Search Input -->
<div class="filter-group search-group">
<label><?= t('community.search') ?>:</label>
<input type="text"
id="searchInput"
class="search-input"
placeholder="<?= t('community.search_placeholder') ?>"
value="<?= htmlspecialchars($search) ?>"
onkeypress="if(event.key === 'Enter') updateFilters()">
<button class="search-btn" onclick="updateFilters()" title="<?= t('community.search') ?>">
<i class="fas fa-search"></i>
</button>
</div>
<!-- Sort Options -->
<div class="filter-group">
<label><?= t('community.sort') ?>:</label>
<select id="sortSelect" onchange="updateFilters()">
<option value="latest" <?= $sort === 'latest' ? 'selected' : '' ?>><?= t('community.latest') ?></option>
<option value="trending" <?= $sort === 'trending' ? 'selected' : '' ?>><?= t('community.trending') ?></option>
<option value="popular" <?= $sort === 'popular' ? 'selected' : '' ?>><?= t('community.popular') ?></option>
<option value="random" <?= $sort === 'random' ? 'selected' : '' ?>><?= t('community.random') ?></option>
</select>
</div>
<!-- Time Filter -->
<div class="filter-group">
<label><?= t('community.time') ?>:</label>
<select id="timeSelect" onchange="updateFilters()">
<option value="all" <?= $time_filter === 'all' ? 'selected' : '' ?>><?= t('community.all_time') ?></option>
<option value="today" <?= $time_filter === 'today' ? 'selected' : '' ?>><?= t('community.today') ?></option>
<option value="week" <?= $time_filter === 'week' ? 'selected' : '' ?>><?= t('community.this_week') ?></option>
<option value="month" <?= $time_filter === 'month' ? 'selected' : '' ?>><?= t('community.this_month') ?></option>
</select>
</div>
<!-- Genre Filter -->
<div class="filter-group">
<label><?= t('community.genre') ?>:</label>
<select id="genreSelect" onchange="updateFilters()">
<option value="all" <?= $genre === 'all' ? 'selected' : '' ?>><?= t('community.all_genres') ?></option>
<option value="Electronic" <?= $genre === 'Electronic' ? 'selected' : '' ?>>Electronic</option>
<option value="Pop" <?= $genre === 'Pop' ? 'selected' : '' ?>>Pop</option>
<option value="Hip Hop" <?= $genre === 'Hip Hop' ? 'selected' : '' ?>>Hip Hop</option>
<option value="Rock" <?= $genre === 'Rock' ? 'selected' : '' ?>>Rock</option>
<option value="Jazz" <?= $genre === 'Jazz' ? 'selected' : '' ?>>Jazz</option>
<option value="Classical" <?= $genre === 'Classical' ? 'selected' : '' ?>>Classical</option>
<option value="Ambient" <?= $genre === 'Ambient' ? 'selected' : '' ?>>Ambient</option>
</select>
</div>
</div>
</div>
</div>
<!-- Tracks Grid -->
<div class="container">
<?php if (empty($tracks)): ?>
<div class="empty-state">
<i class="fas fa-music"></i>
<h2><?= t('community.no_tracks_found') ?></h2>
<p><?= t('community.no_tracks_desc') ?></p>
</div>
<?php else: ?>
<div class="tracks-grid" id="tracksGrid" data-current-page="1" data-has-more="<?= $page < $total_pages ? 'true' : 'false' ?>">
<?php foreach ($tracks as $track):
$displayTitle = !empty($track['title']) ? htmlspecialchars($track['title']) : t('community.untitled_track');
$duration = $track['duration'] ? gmdate('i:s', $track['duration']) : '0:00';
$isWishlisted = !empty($track['is_in_wishlist']);
// Handle track image - use original image_url from database (should always be local path)
$imageUrl = $track['image_url'] ?? null;
// Trim whitespace and check for empty/null values
if ($imageUrl !== null) {
$imageUrl = trim($imageUrl);
if ($imageUrl === '' || $imageUrl === 'null' || $imageUrl === 'NULL') {
$imageUrl = null;
}
}
// If image_url exists and is not empty/null, normalize and use it
if (!empty($imageUrl)) {
// Normalize local paths (add leading / if missing)
if (!str_starts_with($imageUrl, '/')) {
$imageUrl = '/' . ltrim($imageUrl, '/');
}
} else {
// Only if image_url is empty/null, try fallback sources
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (isset($metadata['image_url']) && !empty($metadata['image_url'])) {
$metaImageUrl = $metadata['image_url'];
// Only use if it's a local path (metadata might have external URLs)
if (strpos($metaImageUrl, 'http://') !== 0 && strpos($metaImageUrl, 'https://') !== 0) {
if (!str_starts_with($metaImageUrl, '/')) {
$metaImageUrl = '/' . ltrim($metaImageUrl, '/');
}
$imageUrl = $metaImageUrl;
}
} elseif (isset($metadata['cover_url']) && !empty($metadata['cover_url'])) {
$metaCoverUrl = $metadata['cover_url'];
// Only use if it's a local path (metadata might have external URLs)
if (strpos($metaCoverUrl, 'http://') !== 0 && strpos($metaCoverUrl, 'https://') !== 0) {
if (!str_starts_with($metaCoverUrl, '/')) {
$metaCoverUrl = '/' . ltrim($metaCoverUrl, '/');
}
$imageUrl = $metaCoverUrl;
}
}
}
// Try to find image file by task_id pattern
if (empty($imageUrl) && !empty($track['task_id'])) {
$uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/track_covers/';
if (is_dir($uploadsDir)) {
$pattern = $uploadsDir . "track_{$track['task_id']}_*";
$files = glob($pattern);
if (!empty($files)) {
$mostRecent = end($files);
$imageUrl = '/uploads/track_covers/' . basename($mostRecent);
}
}
}
// Fallback to default only if no image found
if (empty($imageUrl)) {
$imageUrl = '/assets/images/default-track.jpg';
}
}
// CRITICAL: Check for selected variation and use its audio URL if available
$selectedVariationIndex = null;
$selectedVariation = null;
// Parse metadata to get selected_variation
if (!empty($track['metadata'])) {
$trackMetadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (isset($trackMetadata['selected_variation'])) {
$selectedVariationIndex = (int)$trackMetadata['selected_variation'];
}
}
// Also check the selected_variation column if it exists
if ($selectedVariationIndex === null && isset($track['selected_variation'])) {
$selectedVariationIndex = (int)$track['selected_variation'];
}
// Normalize variation audio URLs first
if (!empty($track['variations'])) {
foreach ($track['variations'] as &$var) {
$varAudioUrl = $var['audio_url'] ?? '';
if ($varAudioUrl && !preg_match('/^https?:\/\//', $varAudioUrl) && !str_starts_with($varAudioUrl, '/')) {
// Fix malformed URLs like "audio_files=filename.mp3" to "/audio_files/filename.mp3"
if (strpos($varAudioUrl, 'audio_files=') === 0) {
$var['audio_url'] = '/' . str_replace('audio_files=', 'audio_files/', $varAudioUrl);
} else {
$var['audio_url'] = '/' . ltrim($varAudioUrl, '/');
}
} else {
$var['audio_url'] = $varAudioUrl;
}
// Find the selected variation
if ($selectedVariationIndex !== null && isset($var['variation_index']) && $var['variation_index'] == $selectedVariationIndex) {
$selectedVariation = $var;
}
}
unset($var); // Break reference
// Fallback: if selected variation not found by index, try array position
if (!$selectedVariation && $selectedVariationIndex !== null && isset($track['variations'][$selectedVariationIndex])) {
$selectedVariation = $track['variations'][$selectedVariationIndex];
}
}
// Use selected variation's audio URL if available, otherwise use main track's audio URL
$mainAudioUrl = '';
if ($selectedVariation && !empty($selectedVariation['audio_url'])) {
$mainAudioUrl = $selectedVariation['audio_url'];
// Also update duration if available
if (!empty($selectedVariation['duration'])) {
$duration = gmdate('i:s', $selectedVariation['duration']);
}
} else {
$mainAudioUrl = $track['audio_url'] ?? '';
// If main track URL is empty/invalid and we have variations, use first variation as fallback
if (empty($mainAudioUrl) && !empty($track['variations']) && is_array($track['variations'])) {
$firstVariation = reset($track['variations']);
if (!empty($firstVariation['audio_url'])) {
$mainAudioUrl = $firstVariation['audio_url'];
if (!empty($firstVariation['duration'])) {
$duration = gmdate('i:s', $firstVariation['duration']);
}
}
}
}
// Use signed proxy URL to hide actual MP3 file paths and prevent URL sharing
// Include variation index if a selected variation exists
// CRITICAL: Pass user/session to bind tokens - prevents token sharing
$sessionId = session_id();
if ($selectedVariationIndex !== null) {
$mainAudioUrl = getSignedAudioUrl($track['id'], $selectedVariationIndex, null, $user_id, $sessionId);
} else {
$mainAudioUrl = getSignedAudioUrl($track['id'], null, null, $user_id, $sessionId);
}
?>
<div class="track-card"
data-track-id="<?= $track['id'] ?>"
itemscope
itemtype="http://schema.org/MusicRecording">
<!-- Track Image -->
<div class="track-image-wrapper">
<img src="<?= htmlspecialchars($imageUrl) ?>" alt="<?= $displayTitle ?>" class="track-image" loading="lazy">
<div class="track-overlay">
<button class="play-btn"
data-track-id="<?= $track['id'] ?>"
data-artist-id="<?= $track['artist_id'] ?>"
data-audio-url="<?= htmlspecialchars($mainAudioUrl ?? '', ENT_QUOTES) ?>"
data-title="<?= htmlspecialchars($displayTitle ?? '', ENT_QUOTES) ?>"
data-artist="<?= htmlspecialchars($track['artist_name'] ?? '', ENT_QUOTES) ?>">
<i class="fas fa-play"></i>
</button>
<div class="track-duration"><?= $duration ?></div>
</div>
</div>
<!-- Track Info -->
<div class="track-info">
<div class="track-title-row">
<div class="track-title-wrapper">
<a href="/track.php?id=<?= $track['id'] ?>"
class="track-title"
title="<?= $displayTitle ?>"
target="_blank"
itemprop="name"><?= $displayTitle ?></a>
<a href="/artist_profile.php?id=<?= $track['artist_id'] ?>"
class="track-artist"
target="_blank"
itemprop="byArtist"
itemscope
itemtype="http://schema.org/MusicGroup">
<span itemprop="name"><?= htmlspecialchars($track['artist_name']) ?></span>
</a>
</div>
<?php if (isset($track['price']) && $track['price'] > 0): ?>
<span class="for-sale-badge" title="<?= t('community.available_for_purchase') ?>">
<i class="fas fa-tag"></i>
<span><?= t('community.for_sale') ?></span>
</span>
<?php endif; ?>
</div>
<?php
// Extract genre and mood like artist_profile.php
$genre = 'Electronic'; // default
$mood = null;
// Try to extract genre/mood from metadata if available
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if ($metadata) {
// Check metadata.genre first, then metadata.style (some tracks store genre in style)
if (!empty($metadata['genre'])) {
$genre = trim($metadata['genre']);
} elseif (!empty($metadata['style'])) {
$genre = trim($metadata['style']);
}
if (!empty($metadata['mood'])) {
$mood = trim($metadata['mood']);
}
}
}
// Use track genre if metadata doesn't have it
if (($genre === 'Electronic' || empty($genre)) && !empty($track['genre'])) {
$genre = trim($track['genre']);
}
// Ensure genre is not empty
if (empty($genre)) {
$genre = 'Electronic';
}
?>
<div class="track-genre">
<a href="?genre=<?= urlencode($genre) ?>" class="genre-tag-link"><?= htmlspecialchars($genre) ?></a>
<?php if ($mood && $mood !== $genre && strtolower($mood) !== 'neutral'): ?>
<a href="?genre=<?= urlencode($mood) ?>" class="genre-tag-link"><?= htmlspecialchars($mood) ?></a>
<?php endif; ?>
</div>
<?php if (!empty($track['created_at'])): ?>
<div class="track-date">
<i class="fas fa-calendar-alt"></i>
<span><?= date('M j, Y', strtotime($track['created_at'])) ?></span>
</div>
<?php endif; ?>
</div>
<!-- Track Ranking Display with Integrated Voting -->
<?php
$trackRankings = $track['trackRankings'] ?? [];
$status = 'complete'; // Tracks shown here are always complete
if ($status === 'complete' && !empty($trackRankings) && isset($trackRankings['overall'])):
// Check if track_votes table exists
$votes_table_exists = false;
try {
$check_stmt = $pdo->query("SHOW TABLES LIKE 'track_votes'");
$votes_table_exists = $check_stmt->rowCount() > 0;
} catch (Exception $e) {
$votes_table_exists = false;
}
?>
<div class="card-track-ranking"
style="cursor: pointer; position: relative; margin-top: 0.75rem;"
data-track-id="<?= $track['id'] ?>"
data-rankings='<?= json_encode($trackRankings, JSON_HEX_APOS | JSON_HEX_QUOT) ?>'
data-track='<?= json_encode([
'id' => $track['id'],
'title' => $displayTitle,
'play_count' => $track['play_count'] ?? 0,
'like_count' => $track['like_count'] ?? 0,
'vote_count' => $track['vote_count'] ?? 0,
'average_rating' => $track['average_rating'] ?? 0,
'rating_count' => $track['rating_count'] ?? 0
], JSON_HEX_APOS | JSON_HEX_QUOT) ?>'
onclick="handleRankingCardClick(this)">
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem;">
<div style="flex: 1;">
<div class="ranking-badge-compact">
<i class="fas fa-trophy"></i>
<span class="ranking-label-compact"><?= t('track.rankings.breakdown.rank') ?></span>
<span class="ranking-value-compact">#<?= number_format($trackRankings['overall']) ?></span>
<span class="ranking-out-of-compact">/ <?= number_format($trackRankings['total_tracks']) ?></span>
</div>
<div class="ranking-percentile-compact">
<span class="percentile-badge-compact">Top <?= $trackRankings['overall_percentile'] ?>%</span>
</div>
<div class="ranking-details-compact">
<div class="ranking-detail-item">
<i class="fas fa-play-circle"></i>
<span>#<?= number_format($trackRankings['plays']) ?></span>
</div>
<div class="ranking-detail-item">
<i class="fas fa-heart"></i>
<span>#<?= number_format($trackRankings['likes']) ?></span>
</div>
<?php if ($votes_table_exists && isset($trackRankings['votes'])): ?>
<div class="ranking-detail-item">
<i class="fas fa-thumbs-up"></i>
<span>#<?= number_format($trackRankings['votes']) ?></span>
</div>
<?php endif; ?>
<?php if (($track['rating_count'] ?? 0) >= 3 && isset($trackRankings['rating'])): ?>
<div class="ranking-detail-item">
<i class="fas fa-star"></i>
<span>#<?= number_format($trackRankings['rating']) ?></span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Integrated Voting Section -->
<div class="voting-section-card" style="display: flex; flex-direction: column; align-items: center; gap: 0.3rem; padding-left: 1rem; border-left: 1px solid rgba(251, 191, 36, 0.3);">
<button class="vote-btn-card vote-up-card <?= ($track['user_vote'] ?? null) === 'up' ? 'active' : '' ?>"
onclick="event.stopPropagation(); voteTrackFromCard(<?= $track['id'] ?>, 'up', this)"
title="<?= t('artist_profile.upvote') ?>">
<i class="fas fa-thumbs-up"></i>
</button>
<div class="vote-count-card" data-track-id="<?= $track['id'] ?>">
<?= number_format($track['vote_count'] ?? 0) ?>
</div>
<button class="vote-btn-card vote-down-card <?= ($track['user_vote'] ?? null) === 'down' ? 'active' : '' ?>"
onclick="event.stopPropagation(); voteTrackFromCard(<?= $track['id'] ?>, 'down', this)"
title="<?= t('artist_profile.downvote') ?>">
<i class="fas fa-thumbs-down"></i>
</button>
</div>
</div>
<div style="text-align: center; margin-top: 0.75rem; font-size: 0.7rem; color: #a0aec0; opacity: 0.7; padding-top: 0.5rem; border-top: 1px solid rgba(251, 191, 36, 0.2);">
<i class="fas fa-info-circle"></i> <?= t('artist_profile.click_ranking_for_details') ?>
</div>
</div>
<?php endif; ?>
<!-- Track Stats -->
<div class="track-stats">
<button class="stat-btn" title="<?= t('preview.plays') ?>">
<i class="fas fa-headphones-alt"></i>
<span><?= number_format($track['play_count']) ?></span>
</button>
<button class="stat-btn like-btn <?= $track['user_liked'] ? 'liked' : '' ?>" onclick="toggleLike(<?= $track['id'] ?>, this)" title="<?= t('library.card.like') ?>">
<i class="fas fa-heart"></i>
<span><?= number_format($track['like_count']) ?></span>
</button>
<button class="stat-btn" onclick="showComments(<?= $track['id'] ?>)" title="<?= t('artist_profile.comments') ?>">
<i class="fas fa-comment"></i>
<span><?= number_format($track['comment_count']) ?></span>
</button>
<button class="stat-btn" onclick="shareTrack(<?= $track['id'] ?>)" title="<?= t('library.card.share') ?>">
<i class="fas fa-share"></i>
<span><?= number_format($track['share_count'] ?? 0) ?></span>
</button>
<?php if ($user_id): ?>
<button class="stat-btn add-to-crate-btn" onclick="openAddToCrateModal(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>')" title="<?= t('library.crates.add_to_crate') ?>">
<i class="fas fa-box"></i>
</button>
<?php endif; ?>
<button class="stat-btn" onclick="showTrackRatingModal(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>')" title="<?= t('artist_profile.rate_track') ?>">
<i class="fas fa-star" style="color: <?= ($track['rating_count'] ?? 0) > 0 ? '#fbbf24' : 'inherit' ?>;"></i>
<?php if (($track['rating_count'] ?? 0) > 0): ?>
<span><?= number_format($track['average_rating'] ?? 0, 1) ?></span>
<?php endif; ?>
</button>
<?php if (!empty($track['variations'])): ?>
<button class="stat-btn variations-btn" onclick="showVariations(<?= $track['id'] ?>, this)" title="<?= count($track['variations']) ?> <?= t('community.variations') ?>">
<i class="fas fa-layer-group"></i>
<span><?= count($track['variations']) ?></span>
</button>
<?php endif; ?>
<?php if ($user_id && $user_id != $track['user_id']): ?>
<button class="stat-btn follow-btn <?= $track['is_following'] ? 'following' : '' ?>"
onclick="toggleFollow(<?= $track['user_id'] ?>, this)"
title="<?= $track['is_following'] ? t('community.unfollow_artist') : t('community.follow_artist') ?>">
<i class="fas fa-<?= $track['is_following'] ? 'user-check' : 'user-plus' ?>"></i>
</button>
<?php endif; ?>
</div>
<!-- Add to Cart Button -->
<div class="track-cart-section">
<button class="add-to-cart-btn"
type="button"
data-track-id="<?= $track['id'] ?>"
data-track-title="<?= htmlspecialchars($displayTitle, ENT_QUOTES | ENT_HTML5, 'UTF-8') ?>"
data-track-price="<?= isset($track['price']) && $track['price'] !== null ? (float)$track['price'] : 1.99 ?>"
onclick="addToCart(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle) ?>', <?= isset($track['price']) && $track['price'] !== null ? (float)$track['price'] : 1.99 ?>, this)">
<i class="fas fa-shopping-cart"></i>
<span><?= t('community.add_to_cart') ?></span>
<?php
// Handle price display: null/not set = 1.99, 0 = free, > 0 = actual price
$displayPrice = 1.99; // default
if (isset($track['price']) && $track['price'] !== null && $track['price'] !== '') {
$displayPrice = (float)$track['price'];
}
?>
<span class="cart-price"><?= $displayPrice > 0 ? '$' . number_format($displayPrice, 2) : t('common.free') ?></span>
</button>
<button class="wishlist-inline-btn <?= $isWishlisted ? 'active' : '' ?>"
onclick="toggleWishlist(<?= $track['id'] ?>, this)"
aria-pressed="<?= $isWishlisted ? 'true' : 'false' ?>"
title="<?= $isWishlisted ? t('artist_profile.remove_from_wishlist') : t('artist_profile.add_to_wishlist') ?>">
<i class="<?= $isWishlisted ? 'fas' : 'far' ?> fa-heart"></i>
</button>
</div>
<!-- Variations Container (hidden by default) -->
<?php if (!empty($track['variations'])): ?>
<div class="variations-container" id="variations-<?= $track['id'] ?>" style="display: none;">
<div class="variations-header">
<span><?= t('community.audio_variations') ?> (<?= count($track['variations']) ?>)</span>
</div>
<div class="variations-grid">
<?php foreach ($track['variations'] as $var):
// Ensure variation_index is set and is an integer
$variationIndex = isset($var['variation_index']) ? (int)$var['variation_index'] : null;
// Use main track title with variation number for consistency
$variationDisplayTitle = $displayTitle . ' - ' . t('community.variation') . ' ' . (($variationIndex !== null) ? ($variationIndex + 1) : 1);
// Generate signed URL with variation index
// CRITICAL: Pass user/session to bind tokens - prevents token sharing
$sessionId = session_id();
$variationAudioUrl = getSignedAudioUrl($track['id'], $variationIndex, null, $user_id, $sessionId);
?>
<div class="variation-item">
<div class="variation-info">
<span class="variation-title"><?= htmlspecialchars($variationDisplayTitle) ?></span>
<span class="variation-duration"><?= $var['duration'] ? gmdate('i:s', $var['duration']) : '0:00' ?></span>
</div>
<button class="variation-play-btn"
data-track-id="<?= $track['id'] ?>"
data-audio-url="<?= htmlspecialchars($variationAudioUrl, ENT_QUOTES) ?>"
data-title="<?= htmlspecialchars($variationDisplayTitle, ENT_QUOTES) ?>"
data-artist="<?= htmlspecialchars($track['artist_name'] ?? '', ENT_QUOTES) ?>"
<?php if ($variationIndex !== null): ?>
data-variation-index="<?= $variationIndex ?>"
<?php endif; ?>>
<i class="fas fa-play"></i>
</button>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Infinite Scroll Loading Indicator -->
<div id="infiniteScrollLoader" style="display: none; text-align: center; padding: 40px 20px; color: white;">
<div class="loading-spinner" style="font-size: 2rem; margin-bottom: 1rem;">âŗ</div>
<p style="font-size: 1.1rem; font-weight: 600;"><?= t('community.loading_more') ?></p>
</div>
<!-- End of Results Message -->
<div id="endOfResults" style="display: none; text-align: center; padding: 40px 20px; color: white;">
<p style="font-size: 1.1rem; font-weight: 600; opacity: 0.8;"><?= t('community.no_more_tracks') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<style>
/* Animated gradient background */
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.community-page {
min-height: 100vh;
background: linear-gradient(-45deg, #667eea, #764ba2, #f093fb, #4facfe);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
padding-bottom: 100px;
position: relative;
overflow-x: hidden;
}
/* Floating particles effect */
.community-page::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 50%, rgba(255,255,255,0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(255,255,255,0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 20%, rgba(255,255,255,0.1) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.community-hero {
position: relative;
z-index: 1;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
padding: 80px 20px;
text-align: center;
color: white;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.hero-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.hero-title-section {
flex: 1;
min-width: 300px;
}
.community-hero h1 {
font-size: 3.5rem;
margin: 0 0 15px 0;
font-weight: 700;
text-shadow: 0 4px 20px rgba(0,0,0,0.3);
animation: fadeInUp 0.8s ease;
}
.community-hero .subtitle {
font-size: 1.2rem;
opacity: 0.95;
margin-bottom: 40px;
font-weight: 400;
animation: fadeInUp 0.8s ease 0.2s both;
}
.share-community-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 32px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.9), rgba(118, 75, 162, 0.9));
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 30px;
color: white;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
white-space: nowrap;
animation: fadeInUp 0.8s ease 0.3s both;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
min-height: 50px;
}
.share-community-btn:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 1), rgba(118, 75, 162, 1));
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.share-community-btn:active {
transform: translateY(-1px);
}
.share-community-btn i {
font-size: 1.3rem;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.quick-stats {
display: flex;
justify-content: center;
gap: 50px;
margin-top: 40px;
flex-wrap: wrap;
animation: fadeInUp 0.8s ease 0.4s both;
}
.stat-item {
text-align: center;
padding: 20px 30px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
cursor: default;
}
.stat-item:hover {
transform: translateY(-5px) scale(1.05);
background: rgba(255, 255, 255, 0.15);
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.stat-number {
display: block;
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 8px;
color: white;
}
.stat-label {
font-size: 0.85rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 600;
}
.community-filters {
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(20px);
padding: 25px;
margin-bottom: 40px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.filters-row {
display: flex;
gap: 25px;
justify-content: center;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
}
.filter-group label {
color: white;
font-weight: 600;
font-size: 0.95rem;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.filter-group select {
padding: 10px 18px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: white;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
outline: none;
}
.search-group {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
max-width: 400px;
}
.search-input {
flex: 1;
padding: 10px 18px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: white;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.3s ease;
outline: none;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search-input:focus {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.search-btn {
padding: 10px 18px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
}
.search-btn:hover {
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.filter-group select:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.filter-group select:focus {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.6);
}
.filter-group select option {
background: #667eea;
color: white;
padding: 10px;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
position: relative;
z-index: 1;
}
.tracks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 30px;
margin-top: 40px;
}
.track-card {
background: rgba(255, 255, 255, 0.98);
border-radius: 20px;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
cursor: pointer;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
animation: cardFadeIn 0.6s ease both;
}
@keyframes cardFadeIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.track-card:nth-child(1) { animation-delay: 0.1s; }
.track-card:nth-child(2) { animation-delay: 0.15s; }
.track-card:nth-child(3) { animation-delay: 0.2s; }
.track-card:nth-child(4) { animation-delay: 0.25s; }
.track-card:nth-child(5) { animation-delay: 0.3s; }
.track-card:nth-child(6) { animation-delay: 0.35s; }
.track-card:hover {
transform: translateY(-10px) scale(1.02);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
border-color: rgba(255, 255, 255, 0.5);
}
.track-image-wrapper {
position: relative;
width: 100%;
padding-top: 100%;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
}
.track-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.track-card:hover .track-image {
transform: scale(1.1) rotate(2deg);
}
.track-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.8) 0%, rgba(118, 75, 162, 0.8) 100%);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.4s ease;
}
.track-card:hover .track-overlay {
opacity: 1;
}
.play-btn {
width: 70px;
height: 70px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
border: 3px solid rgba(255, 255, 255, 0.5);
color: #667eea;
font-size: 1.8rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
transform: scale(0.9);
}
.track-card:hover .play-btn {
transform: scale(1);
}
.play-btn:hover {
transform: scale(1.15);
box-shadow: 0 15px 40px rgba(0,0,0,0.4);
background: white;
}
.play-btn:active {
transform: scale(0.95);
}
.track-duration {
position: absolute;
bottom: 12px;
right: 12px;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(10px);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.5px;
}
.track-info {
padding: 18px 20px 20px;
background: white;
}
.track-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
min-height: 48px;
}
.track-title-wrapper {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
gap: 4px;
}
.track-card .track-title,
.track-info .track-title {
font-size: 1.3rem;
font-weight: 700;
margin: 0;
color: #1a1a2e !important;
line-height: 1.4;
text-decoration: none;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
flex: none;
min-width: 0;
letter-spacing: -0.02em;
word-break: break-word;
width: 100%;
}
.for-sale-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: rgba(46, 204, 113, 0.1);
color: #2ecc71;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
border: 1px solid rgba(46, 204, 113, 0.2);
transition: all 0.2s ease;
}
.for-sale-badge:hover {
background: rgba(46, 204, 113, 0.15);
border-color: rgba(46, 204, 113, 0.3);
}
.for-sale-badge i {
font-size: 0.7rem;
}
.track-card .track-title:hover,
.track-info .track-title:hover {
color: #667eea;
text-decoration: none;
transform: translateX(2px);
}
.track-card .track-artist,
.track-info .track-artist {
color: #4a5568 !important;
text-decoration: none;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
margin-top: 0;
letter-spacing: 0.01em;
}
.track-card .track-artist::before,
.track-info .track-artist::before {
content: 'by';
font-size: 0.85rem;
font-weight: 500;
color: #718096 !important;
opacity: 1;
}
.track-card .track-artist span,
.track-info .track-artist span {
color: #4a5568 !important;
}
.track-card .track-artist:hover,
.track-info .track-artist:hover {
color: #667eea;
transform: translateX(3px);
}
.track-card .track-artist:hover::before,
.track-info .track-artist:hover::before {
color: #a5b4fc;
opacity: 1;
}
.track-genre {
margin-top: 6px;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 6px;
}
.genre-tag-link {
background: rgba(102, 126, 234, 0.15);
color: #a5b4fc;
padding: 6px 12px;
text-decoration: none;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
border: 1px solid rgba(102, 126, 234, 0.2);
display: inline-block;
transition: all 0.2s ease;
text-transform: capitalize;
letter-spacing: 0.3px;
}
.genre-tag-link:hover {
background: rgba(102, 126, 234, 0.25);
color: #8b9aff;
border-color: rgba(102, 126, 234, 0.4);
transform: translateY(-1px);
}
.track-date {
display: flex;
align-items: center;
gap: 6px;
color: #999;
font-size: 0.8rem;
margin-top: 6px;
font-weight: 400;
}
.track-date i {
font-size: 0.75rem;
opacity: 0.7;
}
.track-stats {
display: flex;
flex-wrap: wrap;
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
gap: 8px;
background: #fafafa;
}
.stat-btn {
flex: 0 0 auto;
min-width: 40px;
padding: 10px;
border: none;
background: transparent;
color: #666;
cursor: pointer;
border-radius: 10px;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.stat-btn:hover {
background: linear-gradient(135deg, #667eea15, #764ba215);
color: #667eea;
transform: translateY(-2px);
}
.stat-btn.liked {
color: #e74c3c;
background: rgba(231, 76, 60, 0.1);
}
.stat-btn.following {
color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.stat-btn.variations-btn {
color: #764ba2;
}
.stat-btn.variations-btn:hover {
background: rgba(118, 75, 162, 0.1);
color: #764ba2;
}
.track-cart-section {
padding: 12px 20px;
border-top: 1px solid #f0f0f0;
background: white;
display: flex;
align-items: center;
gap: 12px;
}
.add-to-cart-btn {
width: 100%;
padding: 12px 20px;
border: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.add-to-cart-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
}
.add-to-cart-btn:active {
transform: translateY(0);
}
.add-to-cart-btn i {
font-size: 1rem;
}
.add-to-cart-btn .cart-price {
margin-left: auto;
font-weight: 700;
opacity: 0.95;
}
.track-cart-section .add-to-cart-btn {
flex: 1;
}
.add-to-cart-btn.added {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
box-shadow: 0 4px 15px rgba(72, 187, 120, 0.3);
}
.add-to-cart-btn.added:hover {
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
box-shadow: 0 6px 20px rgba(72, 187, 120, 0.4);
}
.wishlist-inline-btn {
margin-left: 12px;
width: 48px;
height: 48px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.08);
color: #ff7aa2;
font-size: 1.2rem;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.wishlist-inline-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.4);
}
.wishlist-inline-btn.active {
background: rgba(255, 99, 132, 0.2);
border-color: rgba(255, 99, 132, 0.4);
color: #ff6384;
}
.variations-container {
padding: 15px 20px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.variations-header {
font-weight: 600;
color: #333;
margin-bottom: 12px;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.variations-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.variation-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: white;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.2s;
}
.variation-item:hover {
background: #f8f9fa;
border-color: #667eea;
}
.variation-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.variation-title {
font-size: 0.9rem;
font-weight: 500;
color: #333;
}
.variation-duration {
font-size: 0.75rem;
color: #666;
}
.variation-play-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: #667eea;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 0.9rem;
}
.variation-play-btn:hover {
background: #764ba2;
transform: scale(1.1);
}
.empty-state {
text-align: center;
padding: 100px 20px;
color: white;
}
.empty-state i {
font-size: 5rem;
opacity: 0.6;
margin-bottom: 25px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
.empty-state h2 {
margin: 0 0 15px 0;
font-size: 2rem;
font-weight: 700;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 25px;
margin-top: 60px;
padding: 30px 20px;
}
.pagination-btn {
padding: 12px 25px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
color: white;
text-decoration: none;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.pagination-btn:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}
.pagination-info {
color: white;
font-weight: 700;
font-size: 1.1rem;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
@media (max-width: 768px) {
.track-title-row {
min-height: auto !important;
margin-bottom: 8px !important;
gap: 8px !important;
align-items: flex-start !important;
flex-wrap: nowrap !important;
}
.track-title-wrapper {
flex: 1 !important;
min-width: 0 !important;
gap: 2px !important;
display: flex !important;
flex-direction: column !important;
}
.track-card .track-title,
.track-info .track-title {
font-size: 1.1rem !important;
line-height: 1.3 !important;
-webkit-line-clamp: 2 !important;
margin-bottom: 0 !important;
flex: none !important;
width: 100% !important;
}
.track-card .track-artist,
.track-info .track-artist {
font-size: 0.85rem !important;
margin-top: 0 !important;
line-height: 1.2 !important;
display: flex !important;
align-items: center !important;
}
.track-card .track-artist::before,
.track-info .track-artist::before {
font-size: 0.7rem !important;
}
.for-sale-badge {
flex-shrink: 0 !important;
font-size: 0.7rem !important;
padding: 3px 8px !important;
margin-top: 0 !important;
align-self: flex-start !important;
}
.for-sale-badge i {
font-size: 0.65rem !important;
}
.hero-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.hero-title-section {
width: 100%;
min-width: auto;
}
.share-community-btn {
width: 100%;
max-width: 300px;
justify-content: center;
padding: 18px 32px;
font-size: 1.1rem;
}
.tracks-grid {
grid-template-columns: 1fr !important;
gap: 20px;
}
.track-card {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.community-hero h1 {
font-size: 2.2rem;
}
.quick-stats {
gap: 15px;
}
.stat-item {
padding: 15px 20px;
}
.stat-number {
font-size: 1.8rem;
}
.filters-row {
gap: 15px;
}
.genre-tag-link {
font-size: 0.8rem;
padding: 5px 10px;
}
}
@media (max-width: 480px) {
.hero-header {
padding: 1rem;
}
.community-hero h1 {
font-size: 1.8rem;
}
.tracks-grid {
grid-template-columns: 1fr !important;
gap: 15px;
}
.track-card {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.track-info {
padding: 14px 16px 16px;
}
.track-title-row {
min-height: auto !important;
margin-bottom: 8px !important;
gap: 8px !important;
align-items: flex-start !important;
flex-wrap: nowrap !important;
}
.track-title-wrapper {
flex: 1 !important;
min-width: 0 !important;
gap: 2px !important;
display: flex !important;
flex-direction: column !important;
}
.track-card .track-title,
.track-info .track-title {
font-size: 1.1rem !important;
line-height: 1.3 !important;
-webkit-line-clamp: 2 !important;
margin-bottom: 0 !important;
flex: none !important;
width: 100% !important;
}
.track-card .track-artist,
.track-info .track-artist {
font-size: 0.85rem !important;
margin-top: 0 !important;
line-height: 1.2 !important;
display: flex !important;
align-items: center !important;
}
.track-card .track-artist::before,
.track-info .track-artist::before {
font-size: 0.7rem !important;
}
.for-sale-badge {
flex-shrink: 0 !important;
font-size: 0.7rem !important;
padding: 3px 8px !important;
margin-top: 0 !important;
align-self: flex-start !important;
}
.for-sale-badge i {
font-size: 0.65rem !important;
}
.quick-stats {
gap: 10px;
}
.stat-item {
padding: 12px 15px;
}
.stat-number {
font-size: 1.5rem;
}
.filters-row {
flex-direction: column;
gap: 10px;
}
.track-genre {
margin-top: 4px;
}
.genre-tag-link {
font-size: 0.75rem;
padding: 5px 10px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
.track-info {
padding: 16px 18px 18px;
}
.track-title-row {
min-height: 44px;
margin-bottom: 8px;
}
.track-card .track-title,
.track-info .track-title {
font-size: 1.2rem;
line-height: 1.35;
-webkit-line-clamp: 2;
}
.track-card .track-artist,
.track-info .track-artist {
font-size: 0.95rem;
}
.track-artist::before {
font-size: 0.8rem;
}
}
/* Track Ranking Display on Music Cards */
.card-track-ranking {
margin-top: 0.75rem;
padding: 0.75rem;
background: linear-gradient(135deg, rgba(251, 191, 36, 0.08) 0%, rgba(245, 158, 11, 0.08) 100%);
border-radius: 10px;
border: 1px solid rgba(251, 191, 36, 0.2);
transition: all 0.3s ease;
}
.card-track-ranking:hover {
background: linear-gradient(135deg, rgba(251, 191, 36, 0.15) 0%, rgba(245, 158, 11, 0.15) 100%);
border-color: rgba(251, 191, 36, 0.4);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(251, 191, 36, 0.2);
}
.ranking-badge-compact {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.ranking-badge-compact i {
font-size: 1rem;
color: #fbbf24;
}
.ranking-label-compact {
font-size: 0.7rem;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.ranking-value-compact {
font-size: 1.1rem;
font-weight: 700;
color: #fbbf24;
line-height: 1;
}
.ranking-out-of-compact {
font-size: 0.75rem;
color: #a0aec0;
}
.ranking-percentile-compact {
margin-bottom: 0.5rem;
}
.percentile-badge-compact {
display: inline-block;
padding: 0.25rem 0.6rem;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: #000000;
border-radius: 12px;
font-weight: 600;
font-size: 0.7rem;
}
.ranking-details-compact {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.ranking-detail-item {
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0.6rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
font-size: 0.7rem;
color: #ffffff;
border-left: 2px solid #fbbf24;
}
.ranking-detail-item i {
font-size: 0.7rem;
color: #fbbf24;
}
/* Voting buttons on track cards */
.vote-btn-card {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 0.4rem 0.6rem;
color: #a0aec0;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
min-height: 32px;
-webkit-tap-highlight-color: transparent;
}
.vote-btn-card:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.vote-btn-card.vote-up-card.active {
background: rgba(34, 197, 94, 0.2);
border-color: #22c55e;
color: #22c55e;
}
.vote-btn-card.vote-down-card.active {
background: rgba(239, 68, 68, 0.2);
border-color: #ef4444;
color: #ef4444;
}
.vote-count-card {
font-size: 0.9rem;
font-weight: 700;
color: #ffffff;
padding: 0.2rem 0;
min-width: 30px;
text-align: center;
line-height: 1;
}
.voting-section-card {
flex-shrink: 0;
display: flex !important;
}
@media (max-width: 768px) {
.card-track-ranking {
padding: 1rem !important;
margin-top: 1rem !important;
box-sizing: border-box !important;
width: 100% !important;
max-width: 100% !important;
overflow: hidden !important;
}
.card-track-ranking > div:first-child {
flex-direction: column !important;
gap: 1rem !important;
}
.card-track-ranking > div:first-child > div:first-child {
width: 100% !important;
}
.card-track-ranking > div:first-child > div:last-child.voting-section-card {
border-left: none !important;
border-top: 1px solid rgba(251, 191, 36, 0.3) !important;
padding-left: 0 !important;
padding-top: 1rem !important;
padding-bottom: 0.5rem !important;
width: 100% !important;
align-items: center !important;
justify-content: center !important;
flex-direction: row !important;
gap: 1.5rem !important;
}
.ranking-badge-compact {
gap: 0.6rem !important;
margin-bottom: 0.75rem !important;
flex-wrap: wrap !important;
align-items: center !important;
width: 100% !important;
}
.ranking-badge-compact > * {
flex-shrink: 0 !important;
}
.ranking-badge-compact i {
font-size: 1.1rem !important;
}
.ranking-label-compact {
font-size: 0.75rem !important;
}
.ranking-value-compact {
font-size: 1.3rem !important;
}
.ranking-out-of-compact {
font-size: 0.85rem !important;
}
.ranking-percentile-compact {
margin-bottom: 0.75rem !important;
}
.percentile-badge-compact {
font-size: 0.75rem !important;
padding: 0.35rem 0.75rem !important;
}
.ranking-details-compact {
gap: 0.4rem !important;
flex-wrap: wrap !important;
width: 100% !important;
}
.ranking-detail-item {
font-size: 0.75rem !important;
padding: 0.4rem 0.7rem !important;
gap: 0.4rem !important;
flex-shrink: 0 !important;
white-space: nowrap !important;
}
.ranking-detail-item span {
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.ranking-detail-item i {
font-size: 0.75rem !important;
}
.vote-btn-card {
min-width: 44px !important;
min-height: 44px !important;
padding: 0.5rem 0.75rem !important;
font-size: 1rem !important;
}
.vote-count-card {
font-size: 1rem !important;
min-width: 40px !important;
padding: 0.3rem 0 !important;
}
.card-track-ranking > div:last-child {
font-size: 0.75rem !important;
margin-top: 0.75rem !important;
padding-top: 0.75rem !important;
}
}
@media (max-width: 480px) {
.card-track-ranking {
padding: 0.875rem !important;
}
.ranking-badge-compact {
gap: 0.5rem !important;
margin-bottom: 0.6rem !important;
align-items: center !important;
width: 100% !important;
}
.ranking-badge-compact > * {
flex-shrink: 0 !important;
}
.ranking-value-compact {
font-size: 1.2rem !important;
}
.ranking-out-of-compact {
font-size: 0.8rem !important;
}
.ranking-details-compact {
gap: 0.35rem !important;
width: 100% !important;
}
.ranking-detail-item {
font-size: 0.7rem !important;
padding: 0.35rem 0.6rem !important;
flex-shrink: 0 !important;
white-space: nowrap !important;
}
.ranking-detail-item span {
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.card-track-ranking > div:first-child > div:last-child.voting-section-card {
gap: 1.25rem !important;
padding-top: 0.875rem !important;
}
.vote-btn-card {
min-width: 40px !important;
min-height: 40px !important;
padding: 0.45rem 0.65rem !important;
font-size: 0.95rem !important;
}
.vote-count-card {
font-size: 0.95rem !important;
}
}
/* Profile Edit Modal (for ranking breakdown modal) */
.profile-edit-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
animation: fadeIn 0.3s ease;
}
.modal-content {
background: #2a2a2a;
border-radius: 20px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
border: 2px solid #667eea;
animation: slideUp 0.3s ease;
}
.modal-header {
background: #3a3a3a;
padding: 1.5rem 2rem;
border-bottom: 1px solid #4a4a4a;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
color: white;
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: #a0a0a0;
font-size: 2rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.modal-body {
padding: 2rem;
color: #e0e0e0;
overflow-y: auto;
}
.modal-footer {
background: #3a3a3a;
padding: 1.5rem 2rem;
border-top: 1px solid #4a4a4a;
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: transparent;
color: #a0a0a0;
border: 1px solid #4a4a4a;
padding: 0.75rem 1.5rem;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
/* Rating Modal Styles */
.rating-input-section {
text-align: center;
margin-bottom: 1.5rem;
}
.stars-input {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.stars-input i {
font-size: 2rem;
color: #fbbf24;
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease;
}
.stars-input i:hover {
transform: scale(1.2);
}
.stars-input i.fas {
color: #fbbf24;
}
.stars-input i.far {
color: #4a4a4a;
}
.rating-feedback {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.rating-feedback .rating-value {
font-size: 1.5rem;
font-weight: 700;
color: #fbbf24;
}
.rating-feedback .rating-label {
font-size: 0.9rem;
color: #a0a0a0;
}
.rating-comment {
margin-top: 1rem;
}
.rating-comment label {
display: block;
margin-bottom: 0.5rem;
color: #e0e0e0;
font-weight: 500;
}
.rating-comment textarea {
width: 100%;
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 10px;
padding: 1rem;
color: #e0e0e0;
font-size: 1rem;
resize: vertical;
min-height: 100px;
}
.rating-comment textarea:focus {
outline: none;
border-color: #667eea;
}
.rating-comment textarea::placeholder {
color: #6a6a6a;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Mobile optimizations for ranking modal */
@media (max-width: 768px) {
.profile-edit-modal .modal-content {
width: 95% !important;
max-height: 90vh !important;
border-radius: 15px !important;
}
.profile-edit-modal .modal-header {
padding: 1rem !important;
}
.profile-edit-modal .modal-header h3 {
font-size: 1.2rem !important;
line-height: 1.3;
}
.profile-edit-modal .modal-body {
padding: 1rem !important;
font-size: 0.9rem !important;
}
.profile-edit-modal .modal-footer {
padding: 1rem !important;
}
.profile-edit-modal .close-btn {
font-size: 1.5rem !important;
width: 24px !important;
height: 24px !important;
}
.profile-edit-modal .modal-body h4 {
font-size: 1rem !important;
}
}
</style>
<script>
// Ranking Modal Translations
window.rankingTranslations = {
title: '<?= addslashes(t('track.rankings.breakdown.title')) ?>',
overall_ranking: '<?= addslashes(t('track.rankings.breakdown.overall_ranking')) ?>',
rank: '<?= addslashes(t('track.rankings.breakdown.rank')) ?>',
out_of: '<?= addslashes(t('track.rankings.breakdown.out_of')) ?>',
tracks: '<?= addslashes(t('track.rankings.breakdown.tracks')) ?>',
percentile: '<?= addslashes(t('track.rankings.breakdown.percentile')) ?>',
score_calculation: '<?= addslashes(t('track.rankings.breakdown.score_calculation')) ?>',
plays: '<?= addslashes(t('track.rankings.breakdown.plays')) ?>',
likes: '<?= addslashes(t('track.rankings.breakdown.likes')) ?>',
rating: '<?= addslashes(t('track.rankings.breakdown.rating')) ?>',
track_votes: '<?= addslashes(t('track.rankings.breakdown.track_votes')) ?>',
track_votes_label: '<?= addslashes(t('track.rankings.breakdown.track_votes_label')) ?>',
track_rating: '<?= addslashes(t('track.rankings.breakdown.track_rating')) ?>',
track_ratings: '<?= addslashes(t('track.rankings.breakdown.track_ratings')) ?>',
total_score: '<?= addslashes(t('track.rankings.breakdown.total_score')) ?>',
individual_rankings: '<?= addslashes(t('track.rankings.breakdown.individual_rankings')) ?>',
formula: '<?= addslashes(t('track.rankings.breakdown.formula')) ?>',
formula_text: '<?= addslashes(t('track.rankings.breakdown.formula_text')) ?>',
weight: '<?= addslashes(t('track.rankings.breakdown.weight')) ?>',
avg_rating: '<?= addslashes(t('track.rankings.breakdown.avg_rating')) ?>',
ratings: '<?= addslashes(t('track.rankings.breakdown.ratings') ?? 'ratings') ?>',
close: '<?= addslashes(t('track.rankings.breakdown.close') ?? 'Close') ?>',
by_plays: '<?= addslashes(t('track.rankings.by_plays')) ?>',
by_likes: '<?= addslashes(t('track.rankings.by_likes')) ?>',
by_rating: '<?= addslashes(t('track.rankings.by_rating')) ?>',
of: '<?= addslashes(t('track.rankings.of')) ?>',
track: '<?= addslashes(t('track.rankings.breakdown.track') ?? 'Track') ?>',
showing: '<?= addslashes(t('track.rankings.breakdown.showing') ?? 'Showing') ?>',
no_tracks: '<?= addslashes(t('track.rankings.breakdown.no_tracks') ?? 'No tracks found') ?>',
view_all_rankings: '<?= addslashes(t('track.rankings.breakdown.view_all_rankings')) ?>',
all_rankings_title: '<?= addslashes(t('track.rankings.breakdown.all_rankings_title')) ?>',
loading_rankings: '<?= addslashes(t('track.rankings.breakdown.loading_rankings')) ?>',
ratings_for: '<?= addslashes(t('artist_profile.ratings_for')) ?>',
loading_ratings: '<?= addslashes(t('artist_profile.loading_ratings')) ?>',
no_comment: '<?= addslashes(t('artist_profile.no_comment')) ?>',
rate_this_track: '<?= addslashes(t('artist_profile.rate_this_track')) ?>'
};
function updateFilters() {
const sort = document.getElementById('sortSelect').value;
const time = document.getElementById('timeSelect').value;
const genre = document.getElementById('genreSelect').value;
const search = document.getElementById('searchInput').value.trim();
// Build URL for history
let url = `?sort=${sort}&time=${time}&genre=${genre}&page=1`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
// Update URL without reload
window.history.pushState({}, '', url);
// Show loading state
const tracksGrid = document.getElementById('tracksGrid');
const loader = document.getElementById('infiniteScrollLoader');
const endOfResults = document.getElementById('endOfResults');
if (tracksGrid) {
tracksGrid.innerHTML = '<div class="loading-tracks" style="text-align: center; padding: 2rem;"><i class="fas fa-spinner fa-spin"></i> Loading tracks...</div>';
}
if (loader) loader.style.display = 'none';
if (endOfResults) endOfResults.style.display = 'none';
// Fetch filtered tracks via AJAX
const queryParams = new URLSearchParams({
page: 1,
per_page: 24,
sort: sort,
time: time,
genre: genre,
...(search ? { search: search } : {})
});
fetch(`/api/get_community_fixed_tracks.php?${queryParams}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success && data.tracks && data.tracks.length > 0) {
// Clear existing tracks
if (tracksGrid) {
tracksGrid.innerHTML = '';
}
// Render new tracks using exposed function
const fragment = document.createDocumentFragment();
data.tracks.forEach(track => {
if (typeof window.createTrackCard === 'function') {
const trackCard = window.createTrackCard(track);
fragment.appendChild(trackCard);
}
});
if (tracksGrid) {
tracksGrid.appendChild(fragment);
}
// Re-attach play button listeners
if (typeof window.attachPlayButtonListeners === 'function') {
window.attachPlayButtonListeners();
}
// Update infinite scroll state
if (tracksGrid) {
tracksGrid.setAttribute('data-current-page', '1');
tracksGrid.setAttribute('data-has-more', data.pagination.has_more ? 'true' : 'false');
}
// Dispatch event to notify infinite scroll handler of filter change
document.dispatchEvent(new CustomEvent('filtersUpdated', {
detail: {
currentPage: 1,
hasMore: data.pagination.has_more,
totalTracks: data.pagination.total_tracks
}
}));
// Update playlist if global player is active
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer && window._communityPlaylistType === 'community_fixed') {
const buildFn = window.buildCommunityPlaylist;
const updatedPlaylist = buildFn ? buildFn() : [];
if (updatedPlaylist.length > 0 && typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(updatedPlaylist, 'community_fixed', 0);
console.log('đĩ Updated playlist after filter change, total:', updatedPlaylist.length);
}
}
});
}
// Scroll to top of tracks smoothly
if (tracksGrid) {
tracksGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
} else {
// No tracks found
if (tracksGrid) {
tracksGrid.innerHTML = '<div class="no-tracks" style="text-align: center; padding: 3rem; color: #9ca3af;"><i class="fas fa-music"></i><br>No tracks found matching your filters.</div>';
}
}
})
.catch(error => {
console.error('Error loading filtered tracks:', error);
if (tracksGrid) {
tracksGrid.innerHTML = '<div class="error" style="text-align: center; padding: 3rem; color: #ef4444;"><i class="fas fa-exclamation-triangle"></i><br>Error loading tracks. Please try again.</div>';
}
if (typeof showNotification === 'function') {
showNotification('Error loading tracks. Please try again.', 'error');
} else {
alert('Error loading tracks. Please try again.');
}
});
}
// NOTE: playTrack function is now defined AFTER footer.php loads global_player.php (see bottom of file)
// This ensures the global player is loaded before we try to use it
// Record track play count
function recordTrackPlay(trackId) {
// Only record if not already recorded recently
const lastPlayed = sessionStorage.getItem(`played_${trackId}`);
const now = Date.now();
if (!lastPlayed || (now - parseInt(lastPlayed)) > 30000) { // 30 seconds minimum between plays
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'play', track_id: trackId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
sessionStorage.setItem(`played_${trackId}`, now.toString());
console.log('đĩ Play count recorded for track:', trackId);
}
})
.catch(error => {
console.warn('đĩ Play count error:', error);
});
}
}
function toggleLike(trackId, button) {
fetch('/api/toggle_like.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify({track_id: trackId})
})
.then(response => response.json())
.then(data => {
if (!data.success) {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Unable to like this track right now.', 'error');
} else {
alert(data.error || 'Unable to like this track right now.');
}
return;
}
button.classList.toggle('liked', !!data.liked);
const countSpan = button.querySelector('span');
if (countSpan) {
countSpan.textContent = (data.like_count || 0).toLocaleString();
}
})
.catch(error => {
console.error('toggleLike error', error);
if (typeof showNotification === 'function') {
showNotification('Network error while liking track. Please try again.', 'error');
} else {
alert('Network error while liking track. Please try again.');
}
});
}
function toggleFollow(userId, button) {
const isLoggedIn = <?= isset($_SESSION['user_id']) && $_SESSION['user_id'] ? 'true' : 'false' ?>;
if (!isLoggedIn) {
if (typeof showNotification === 'function') {
showNotification(translations.login_required_follow, 'error');
} else {
alert(translations.login_required_follow);
}
setTimeout(() => {
window.location.href = '/auth/login.php';
}, 2000);
return;
}
const isFollowing = button.classList.contains('following');
const icon = button.querySelector('i');
// Disable button during request
button.disabled = true;
const originalHTML = button.innerHTML;
if (icon) {
icon.className = 'fas fa-spinner fa-spin';
}
fetch('/api_social.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'follow', user_id: userId})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const newFollowingState = data.action === 'followed';
button.classList.toggle('following', newFollowingState);
// Update icon
if (icon) {
icon.className = newFollowingState ? 'fas fa-user-check' : 'fas fa-user-plus';
}
// Update title
button.title = newFollowingState ? translations.unfollow_artist : translations.follow_artist;
} else {
// Restore button on error
button.innerHTML = originalHTML;
if (typeof showNotification === 'function') {
showNotification(data.message || 'Failed to update follow status', 'error');
} else {
alert(data.message || 'Failed to update follow status');
}
}
})
.catch(error => {
console.error('Follow error:', error);
button.innerHTML = originalHTML;
if (typeof showNotification === 'function') {
showNotification('Failed to update follow status. Please try again.', 'error');
} else {
alert('Failed to update follow status. Please try again.');
}
})
.finally(() => {
button.disabled = false;
});
}
// Voting functionality for track cards
function voteTrackFromCard(trackId, voteType, button) {
const isLoggedIn = <?= isset($_SESSION['user_id']) && $_SESSION['user_id'] ? 'true' : 'false' ?>;
if (!isLoggedIn) {
if (typeof showNotification === 'function') {
showNotification(translations.login_to_vote, 'error');
} else {
alert(translations.login_to_vote);
}
setTimeout(() => {
window.location.href = '/auth/login.php';
}, 2000);
return;
}
console.log('đĩ Voting on track from card:', trackId, 'vote type:', voteType);
// Disable buttons during request
const card = button.closest('.card-track-ranking');
const upBtn = card ? card.querySelector('.vote-up-card') : null;
const downBtn = card ? card.querySelector('.vote-down-card') : null;
const voteCountElement = card ? card.querySelector('.vote-count-card') : null;
if (upBtn) upBtn.style.pointerEvents = 'none';
if (downBtn) downBtn.style.pointerEvents = 'none';
fetch('/api_social.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
action: 'vote_track',
track_id: trackId,
vote_type: voteType
})
})
.then(response => {
// Check if response is OK
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check if response is JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return response.text().then(text => {
throw new Error('Invalid response format: ' + text.substring(0, 100));
});
}
return response.json();
})
.then(data => {
if (data.success) {
// Update vote buttons state
if (upBtn) upBtn.classList.remove('active');
if (downBtn) downBtn.classList.remove('active');
if (data.user_vote === 'up' && upBtn) {
upBtn.classList.add('active');
} else if (data.user_vote === 'down' && downBtn) {
downBtn.classList.add('active');
}
// Update vote count display
if (voteCountElement) {
const newCount = data.vote_count !== undefined ? parseInt(data.vote_count) || 0 : 0;
voteCountElement.textContent = newCount.toLocaleString();
console.log('â
Vote count updated to:', newCount, 'for track', trackId, 'Action:', data.action);
} else {
console.warn('â ī¸ Vote count element not found for track', trackId);
}
// Refresh ranking modal if it's open for this track
if (window.currentRankingModalTrackId == trackId) {
console.log('đ Refreshing ranking modal after vote...');
// Fetch updated track data and refresh modal
refreshRankingModal(trackId);
}
} else {
const errorMsg = data.message || translations.error_voting;
console.error('Vote failed:', errorMsg);
if (typeof showNotification === 'function') {
showNotification(errorMsg, 'error');
} else {
alert(errorMsg);
}
}
})
.catch(error => {
console.error('Error voting on track:', error);
const errorMsg = error.message || translations.error_voting_retry;
if (typeof showNotification === 'function') {
showNotification(errorMsg, 'error');
} else {
alert(errorMsg);
}
})
.finally(() => {
// Re-enable buttons
if (upBtn) upBtn.style.pointerEvents = '';
if (downBtn) downBtn.style.pointerEvents = '';
});
}
function handleRankingCardClick(element) {
console.log('đ Ranking card clicked');
const trackId = element.getAttribute('data-track-id');
const rankingsJson = element.getAttribute('data-rankings');
const trackJson = element.getAttribute('data-track');
if (!rankingsJson || !trackJson) {
console.error('â Missing data attributes');
alert('Ranking data is not available for this track.');
return;
}
try {
const rankings = JSON.parse(rankingsJson);
const track = JSON.parse(trackJson);
console.log('đ Parsed rankings:', rankings);
console.log('đĩ Parsed track:', track);
openTrackRankingModal(trackId, rankings, track);
} catch (error) {
console.error('â Error parsing JSON:', error);
alert('Error loading ranking data: ' + error.message);
}
}
function openTrackRankingModal(trackId, rankings, track) {
console.log('đ openTrackRankingModal called for track:', trackId);
console.log('đ Rankings data:', rankings);
console.log('đĩ Track data:', track);
if (!rankings || !track) {
console.error('â Missing rankings or track data');
alert('Ranking data is not available for this track.');
return;
}
try {
showRankingBreakdown(rankings, track);
} catch (error) {
console.error('â Error opening ranking breakdown:', error);
alert('Error opening ranking breakdown: ' + error.message);
}
}
function showRankingBreakdown(rankings, track) {
console.log('đ showRankingBreakdown called with:', rankings, track);
if (!rankings || !track) {
console.error('â Missing rankings or track data');
alert('Missing data to display ranking breakdown.');
return;
}
console.log('â
Data validated, creating modal...');
// Remove any existing modal
const existingModal = document.getElementById('rankingBreakdownModal');
if (existingModal) {
existingModal.remove();
}
// Store track ID in modal for refresh functionality
if (track && track.id) {
window.currentRankingModalTrackId = track.id;
}
// Detect mobile
const isMobile = window.innerWidth <= 768;
const modalMaxWidth = isMobile ? '95%' : '600px';
const modalMaxHeight = isMobile ? '90vh' : '80vh';
const breakdown = rankings.score_breakdown || {};
const plays = parseInt(track.play_count) || 0;
const likes = parseInt(track.like_count) || 0;
const votes = parseInt(track.vote_count) || 0;
const avgRating = parseFloat(track.average_rating) || 0;
const ratingCount = parseInt(track.rating_count) || 0;
const t = window.rankingTranslations || {};
const modalHTML = `
<div class="profile-edit-modal" id="rankingBreakdownModal" style="z-index: 10001;">
<div class="modal-content" style="max-width: ${modalMaxWidth}; max-height: ${modalMaxHeight}; display: flex; flex-direction: column; width: ${modalMaxWidth};">
<div class="modal-header" style="flex-shrink: 0; padding: ${isMobile ? '1rem' : '1.5rem 2rem'};">
<h3 style="font-size: ${isMobile ? '1.2rem' : '1.5rem'}; margin: 0;">đ ${t.title || 'Ranking Breakdown'}: ${track.title || t.track || 'Track'}</h3>
<button class="close-btn" onclick="closeRankingBreakdownModal()" style="font-size: ${isMobile ? '1.5rem' : '2rem'}; width: ${isMobile ? '24px' : '30px'}; height: ${isMobile ? '24px' : '30px'};">×</button>
</div>
<div class="modal-body" style="overflow-y: auto; flex: 1; padding: ${isMobile ? '1rem' : '2rem'}; -webkit-overflow-scrolling: touch; font-size: ${isMobile ? '0.9rem' : '1rem'};">
<div style="margin-bottom: ${isMobile ? '1.5rem' : '2rem'};">
<h4 style="color: #667eea; margin-bottom: 1rem; font-size: ${isMobile ? '1rem' : '1.2rem'};">${t.overall_ranking || 'Overall Ranking'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: ${isMobile ? '0.75rem' : '1rem'}; border-radius: 8px; margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.rank || ''}:</span>
<span style="font-size: ${isMobile ? '1.2rem' : '1.5rem'}; font-weight: 700; color: #fbbf24;">#${rankings.overall}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.out_of || 'Out of'}:</span>
<span style="color: #ffffff; font-size: ${isMobile ? '0.9rem' : '1rem'};">${rankings.total_tracks} ${t.tracks || 'tracks'}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.percentile || 'Percentile'}:</span>
<span style="color: #ffffff; font-size: ${isMobile ? '0.9rem' : '1rem'};">Top ${rankings.overall_percentile}%</span>
</div>
</div>
</div>
<div style="margin-bottom: ${isMobile ? '1.5rem' : '2rem'};">
<h4 style="color: #667eea; margin-bottom: 1rem; font-size: ${isMobile ? '1rem' : '1.2rem'};">${t.score_calculation || 'Score Calculation'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: ${isMobile ? '0.75rem' : '1rem'}; border-radius: 8px;">
<div style="margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.plays || 'Plays'} (${plays} Ã 1.0):</span>
<span style="color: #ffffff; font-weight: 600; font-size: ${isMobile ? '0.9rem' : '1rem'};">${breakdown.plays_score || 0}</span>
</div>
<small style="color: #888; font-size: ${isMobile ? '0.75rem' : '0.85rem'};">${plays} ${t.plays || 'plays'} Ã ${t.weight || 'weight'} 1.0</small>
</div>
<div style="margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.likes || 'Likes'} (${likes} Ã 2.0):</span>
<span style="color: #ffffff; font-weight: 600; font-size: ${isMobile ? '0.9rem' : '1rem'};">${breakdown.likes_score || 0}</span>
</div>
<small style="color: #888; font-size: ${isMobile ? '0.75rem' : '0.85rem'};">${likes} ${t.likes || 'likes'} Ã ${t.weight || 'weight'} 2.0</small>
</div>
${votes !== undefined ? `
<div style="margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.track_votes || 'Track Votes'} (${votes} Ã 3.0):</span>
<span style="color: #ffffff; font-weight: 600; font-size: ${isMobile ? '0.9rem' : '1rem'};">${breakdown.votes_score || 0}</span>
</div>
<small style="color: #888; font-size: ${isMobile ? '0.75rem' : '0.85rem'};">${votes} ${t.track_votes_label || 'track votes'} Ã ${t.weight || 'weight'} 3.0</small>
</div>
` : ''}
<div style="margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.track_rating || 'Track Rating'} (${(Number(avgRating) || 0).toFixed(1)} Ã ${(ratingCount || 0)} Ã 5.0):</span>
<span style="color: #ffffff; font-weight: 600; font-size: ${isMobile ? '0.9rem' : '1rem'};">${breakdown.rating_score || 0}</span>
</div>
<small style="color: #888; font-size: ${isMobile ? '0.75rem' : '0.85rem'};">${(Number(avgRating) || 0).toFixed(1)} ${t.avg_rating || 'avg rating'} Ã ${(ratingCount || 0)} ${t.track_ratings || 'track ratings'} Ã ${t.weight || 'weight'} 5.0</small>
</div>
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 2px solid rgba(251, 191, 36, 0.3);">
<div style="display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #fbbf24; font-weight: 700; font-size: ${isMobile ? '1rem' : '1.1rem'};">${t.total_score || 'Total Score'}:</span>
<span style="color: #fbbf24; font-weight: 700; font-size: ${isMobile ? '1.1rem' : '1.3rem'};">${breakdown.total_score || 0}</span>
</div>
</div>
</div>
</div>
<div style="margin-bottom: ${isMobile ? '1.5rem' : '2rem'};">
<h4 style="color: #667eea; margin-bottom: 1rem; font-size: ${isMobile ? '1rem' : '1.2rem'};">${t.individual_rankings || 'Individual Rankings'}</h4>
<div style="display: grid; gap: 0.75rem;">
<div style="background: rgba(0, 0, 0, 0.2); padding: ${isMobile ? '0.6rem' : '0.75rem'}; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.by_plays || 'By Plays'}:</span>
<span style="color: #ffffff; font-weight: 600; font-size: ${isMobile ? '0.9rem' : '1rem'};">#${rankings.plays} ${t.of || 'of'} ${rankings.total_tracks}</span>
</div>
<div style="background: rgba(0, 0, 0, 0.2); padding: ${isMobile ? '0.6rem' : '0.75rem'}; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.by_likes || 'By Likes'}:</span>
<span style="color: #ffffff; font-weight: 600; font-size: ${isMobile ? '0.9rem' : '1rem'};">#${rankings.likes} ${t.of || 'of'} ${rankings.total_tracks}</span>
</div>
${ratingCount >= 3 && rankings.rating ? `
<div style="background: rgba(0, 0, 0, 0.2); padding: ${isMobile ? '0.6rem' : '0.75rem'}; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<span style="color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '1rem'};">${t.by_rating || 'By Rating'}:</span>
<span style="color: #ffffff; font-weight: 600; font-size: ${isMobile ? '0.9rem' : '1rem'};">#${rankings.rating}</span>
</div>
` : ''}
</div>
</div>
<div style="background: rgba(102, 126, 234, 0.1); padding: ${isMobile ? '0.75rem' : '1rem'}; border-radius: 8px; border-left: 3px solid #667eea;">
<small style="color: #a0aec0; font-size: ${isMobile ? '0.75rem' : '0.85rem'}; line-height: 1.5;">
<strong>${t.formula || 'Formula'}:</strong> ${t.formula_text || 'Total Score = (Plays à 1) + (Likes à 2) + (Avg Rating à Rating Count à 5)'}
</small>
</div>
<div style="margin-top: ${isMobile ? '1.5rem' : '2rem'}; padding-top: ${isMobile ? '1.5rem' : '2rem'}; padding-bottom: 1rem; border-top: 2px solid rgba(255, 255, 255, 0.1);">
<button class="btn-primary" onclick="showAllTrackRankings()" style="width: 100%; padding: ${isMobile ? '14px' : '12px'}; font-size: ${isMobile ? '0.9rem' : '1rem'}; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 0.5rem; visibility: visible !important; opacity: 1 !important;">
<i class="fas fa-list"></i> <span>${t.view_all_rankings || 'View All Track Rankings'}</span>
</button>
</div>
</div>
<div class="modal-footer" style="flex-shrink: 0; padding: ${isMobile ? '1rem' : '1.5rem 2rem'}; border-top: 1px solid rgba(255, 255, 255, 0.1);">
<button class="btn-primary" onclick="closeRankingBreakdownModal()" style="width: 100%; padding: ${isMobile ? '12px' : '10px 20px'}; font-size: ${isMobile ? '0.9rem' : '1rem'}; font-weight: 600;">${t.close || 'Close'}</button>
</div>
</div>
</div>
`;
console.log('đ Inserting modal HTML...');
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Wait a bit for DOM to update
setTimeout(() => {
const modal = document.getElementById('rankingBreakdownModal');
console.log('đ Modal element found:', modal);
if (modal) {
// Modal should already have styles from CSS class, but ensure it's visible
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
console.log('â
Modal displayed successfully');
// Add click outside to close
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeRankingBreakdownModal();
}
});
} else {
console.error('â Modal not found after insertion');
alert('Failed to create modal. Please refresh the page.');
}
}, 10);
}
function closeRankingBreakdownModal() {
const modal = document.getElementById('rankingBreakdownModal');
if (modal) {
modal.remove();
document.body.style.overflow = '';
window.currentRankingModalTrackId = null;
}
}
function refreshRankingModal(trackId) {
// Fetch updated track data and refresh the modal
// This is a simplified version - you may want to fetch from an API endpoint
console.log('đ Refreshing ranking modal for track:', trackId);
// For now, just close the modal - user can reopen it to see updated data
closeRankingBreakdownModal();
}
// Helper function to escape HTML for modal content
function escapeHtmlForModal(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showAllTrackRankings() {
// Close the current modal
closeRankingBreakdownModal();
// Detect mobile device
const isMobile = window.innerWidth <= 768;
const modalWidth = isMobile ? '95%' : '900px';
// Create loading modal
const loadingModal = `
<div class="profile-edit-modal" id="allRankingsModal" style="z-index: 10002;">
<div class="modal-content" style="max-width: ${modalWidth}; width: ${modalWidth}; max-height: 90vh;">
<div class="modal-header">
<h3 style="font-size: ${isMobile ? '1.2rem' : '1.5rem'}; margin: 0;">đ ${window.rankingTranslations?.all_rankings_title || 'All Track Rankings'}</h3>
<button class="close-btn" onclick="closeAllRankingsModal()" style="font-size: ${isMobile ? '1.5rem' : '2rem'}; width: ${isMobile ? '24px' : '30px'}; height: ${isMobile ? '24px' : '30px'};">×</button>
</div>
<div class="modal-body" style="text-align: center; padding: 3rem;">
<div class="loading-spinner" style="font-size: 3rem; margin-bottom: 1rem;">âŗ</div>
<p style="color: #a0aec0;">${window.rankingTranslations?.loading_rankings || 'Loading rankings...'}</p>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', loadingModal);
const modal = document.getElementById('allRankingsModal');
if (modal) {
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
// Fetch all rankings
fetch('/api/get_all_track_rankings.php?per_page=500')
.then(response => response.json())
.then(data => {
if (data.success && data.tracks) {
displayAllRankings(data.tracks, data.pagination);
} else {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to load rankings', 'error');
} else {
alert(data.error || 'Failed to load rankings');
}
closeAllRankingsModal();
}
})
.catch(error => {
console.error('Error fetching rankings:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to load rankings. Please try again.', 'error');
} else {
alert('Failed to load rankings. Please try again.');
}
closeAllRankingsModal();
});
}
function displayAllRankings(tracks, pagination) {
const modal = document.getElementById('allRankingsModal');
if (!modal) return;
const t = window.rankingTranslations || {};
const isMobile = window.innerWidth <= 768;
let tracksHTML = '';
if (tracks.length === 0) {
tracksHTML = `
<div style="text-align: center; padding: 3rem; color: #a0aec0;">
<p>${t.no_tracks || 'No tracks found'}</p>
</div>
`;
} else {
// Mobile-friendly table or list view
if (isMobile) {
tracksHTML = `
<div style="overflow-y: auto; max-height: 60vh;">
${tracks.map((track, index) => {
return `
<div style="background: ${index % 2 === 0 ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.02)'}; padding: 1rem; margin-bottom: 0.5rem; border-radius: 8px; border-left: 3px solid #667eea;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<span style="font-weight: 700; color: #fbbf24; font-size: 0.9rem;">#${track.rank}</span>
<span style="font-weight: 600; color: #ffffff; font-size: 0.85rem;">${track.total_score.toLocaleString()}</span>
</div>
<div style="margin-bottom: 0.5rem;">
<a href="/track.php?id=${track.id}" style="color: #667eea; text-decoration: none; font-weight: 500; font-size: 0.9rem;"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">
${escapeHtmlForModal(track.title)}
</a>
<div style="font-size: 0.8rem; color: #a0aec0; margin-top: 2px;">
${escapeHtmlForModal(track.artist_name)}
</div>
</div>
<div style="display: flex; justify-content: space-between; font-size: 0.8rem; color: #a0aec0;">
<span>${t.plays || 'Plays'}: ${track.play_count.toLocaleString()}</span>
<span>${t.likes || 'Likes'}: ${track.like_count.toLocaleString()}</span>
<span>${t.rating || 'Rating'}: ${track.rating_count > 0 ? track.avg_rating.toFixed(1) : '-'}</span>
</div>
</div>
`;
}).join('')}
</div>
`;
} else {
tracksHTML = `
<div style="overflow-x: auto; max-height: 60vh;">
<table style="width: 100%; border-collapse: collapse; color: white;">
<thead>
<tr style="background: rgba(102, 126, 234, 0.2); border-bottom: 2px solid rgba(102, 126, 234, 0.5);">
<th style="padding: 12px; text-align: left; font-weight: 700; color: #667eea;">${t.rank || ''}</th>
<th style="padding: 12px; text-align: left; font-weight: 700; color: #667eea;">${t.track || 'Track'}</th>
<th style="padding: 12px; text-align: center; font-weight: 700; color: #667eea;">${t.plays || 'Plays'}</th>
<th style="padding: 12px; text-align: center; font-weight: 700; color: #667eea;">${t.likes || 'Likes'}</th>
<th style="padding: 12px; text-align: center; font-weight: 700; color: #667eea;">${t.rating || 'Rating'}</th>
<th style="padding: 12px; text-align: right; font-weight: 700; color: #fbbf24;">${t.total_score || 'Score'}</th>
</tr>
</thead>
<tbody>
${tracks.map((track, index) => {
const rowStyle = index % 2 === 0
? 'background: rgba(255, 255, 255, 0.05);'
: 'background: rgba(255, 255, 255, 0.02);';
return `
<tr style="${rowStyle}">
<td style="padding: 12px; font-weight: 600; color: #ffffff;">
#${track.rank}
</td>
<td style="padding: 12px;">
<a href="/track.php?id=${track.id}" style="color: #667eea; text-decoration: none; font-weight: 500;"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">
${escapeHtmlForModal(track.title)}
</a>
<div style="font-size: 0.85rem; color: #a0aec0; margin-top: 2px;">
${escapeHtmlForModal(track.artist_name)}
</div>
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${track.play_count.toLocaleString()}
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${track.like_count.toLocaleString()}
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${track.rating_count > 0 ? track.avg_rating.toFixed(1) + ' (' + track.rating_count + ')' : '-'}
</td>
<td style="padding: 12px; text-align: right; font-weight: 600; color: #ffffff;">
${track.total_score.toLocaleString()}
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
}
if (pagination && pagination.total_tracks > pagination.per_page) {
tracksHTML += `
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255, 255, 255, 0.1); text-align: center; color: #a0aec0; font-size: ${isMobile ? '0.85rem' : '0.9rem'};">
${t.showing || 'Showing'} ${((pagination.page - 1) * pagination.per_page) + 1}-${Math.min(pagination.page * pagination.per_page, pagination.total_tracks)} ${t.of || 'of'} ${pagination.total_tracks.toLocaleString()} ${t.tracks || 'tracks'}
</div>
`;
}
}
modal.querySelector('.modal-body').innerHTML = tracksHTML;
// Add click outside to close
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeAllRankingsModal();
}
});
}
function closeAllRankingsModal() {
const modal = document.getElementById('allRankingsModal');
if (modal) {
modal.remove();
document.body.style.overflow = '';
}
}
function showComments(trackId) {
window.location.href = `/track.php?id=${trackId}#comments`;
}
// Track Rating Functions
const ratingLabelMap = <?= json_encode([
'1' => t('artist_profile.rating_label_1') ?? 'Poor',
'2' => t('artist_profile.rating_label_2') ?? 'Below Average',
'3' => t('artist_profile.rating_label_3') ?? 'Fair',
'4' => t('artist_profile.rating_label_4') ?? 'Decent',
'5' => t('artist_profile.rating_label_5') ?? 'Average',
'6' => t('artist_profile.rating_label_6') ?? 'Good',
'7' => t('artist_profile.rating_label_7') ?? 'Very Good',
'8' => t('artist_profile.rating_label_8') ?? 'Great',
'9' => t('artist_profile.rating_label_9') ?? 'Excellent',
'10' => t('artist_profile.rating_label_10') ?? 'Masterpiece',
'default' => t('artist_profile.rating_label_default') ?? 'Select a rating',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
function getRatingLabel(rating) {
if (ratingLabelMap && ratingLabelMap[rating]) {
return ratingLabelMap[rating];
}
if (ratingLabelMap && ratingLabelMap['default']) {
return ratingLabelMap['default'];
}
return 'Select a rating';
}
function showTrackRatingModal(trackId, trackTitle) {
console.log('đĩ Track rating modal for:', trackId, trackTitle);
// Check if user is logged in
const isLoggedIn = <?= isset($_SESSION['user_id']) && $_SESSION['user_id'] ? 'true' : 'false' ?>;
if (!isLoggedIn) {
if (typeof showNotification === 'function') {
showNotification('Please log in to rate tracks', 'warning');
}
setTimeout(() => {
window.location.href = '/auth/login.php';
}, 2000);
return;
}
// Remove any existing track rating modal first
const existingModal = document.getElementById('trackRatingModal');
if (existingModal) {
existingModal.remove();
}
const modalHTML = `
<div class="profile-edit-modal" id="trackRatingModal" style="z-index: 10001;">
<div class="modal-content">
<div class="modal-header">
<h3><?= htmlspecialchars(t('artist_profile.rate_track_modal_title') ?? 'Rate Track', ENT_QUOTES, 'UTF-8') ?>: ${trackTitle}</h3>
<button class="close-btn" onclick="closeTrackRatingModal()">×</button>
</div>
<div class="modal-body">
<div class="rating-input-section">
<div class="stars-input">
<i class="far fa-star" data-rating="1"></i>
<i class="far fa-star" data-rating="2"></i>
<i class="far fa-star" data-rating="3"></i>
<i class="far fa-star" data-rating="4"></i>
<i class="far fa-star" data-rating="5"></i>
<i class="far fa-star" data-rating="6"></i>
<i class="far fa-star" data-rating="7"></i>
<i class="far fa-star" data-rating="8"></i>
<i class="far fa-star" data-rating="9"></i>
<i class="far fa-star" data-rating="10"></i>
</div>
<div class="rating-feedback">
<span class="rating-value">0/10</span>
<span class="rating-label"><?= htmlspecialchars(t('artist_profile.rate_modal_hint_track') ?? 'Click a star to rate', ENT_QUOTES, 'UTF-8') ?></span>
</div>
</div>
<div class="rating-comment">
<label for="trackRatingComment"><?= htmlspecialchars(t('artist_profile.rate_modal_comment_label') ?? 'Comment (optional)', ENT_QUOTES, 'UTF-8') ?></label>
<textarea id="trackRatingComment" placeholder="<?= htmlspecialchars(t('artist_profile.rate_modal_comment_placeholder_track') ?? 'Share your thoughts about this track...', ENT_QUOTES, 'UTF-8') ?>" rows="4"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeTrackRatingModal()"><?= htmlspecialchars(t('artist_profile.cancel') ?? 'Cancel', ENT_QUOTES, 'UTF-8') ?></button>
<button class="btn-primary" onclick="submitTrackRating(${trackId})"><?= htmlspecialchars(t('artist_profile.submit_rating') ?? 'Submit Rating', ENT_QUOTES, 'UTF-8') ?></button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Make sure modal is visible
const modal = document.getElementById('trackRatingModal');
if (modal) {
modal.style.display = 'flex';
modal.dataset.currentRating = '0';
}
// Fetch user's existing rating for this track
fetch(`/api_social.php?action=get_user_track_rating&track_id=${trackId}`)
.then(response => response.json())
.then(data => {
let existingRating = 0;
let existingComment = '';
if (data.success && data.data && data.data.rating !== null) {
existingRating = parseInt(data.data.rating);
existingComment = data.data.comment || '';
}
setTimeout(() => {
const stars = document.querySelectorAll('#trackRatingModal .stars-input i');
let currentRating = existingRating;
// Initialize with existing rating if found
if (existingRating > 0 && modal) {
modal.dataset.currentRating = existingRating.toString();
stars.forEach((s, index) => {
s.className = index < existingRating ? 'fas fa-star' : 'far fa-star';
});
const ratingValueEl = document.querySelector('#trackRatingModal .rating-value');
const ratingLabelEl = document.querySelector('#trackRatingModal .rating-label');
if (ratingValueEl) ratingValueEl.textContent = `${existingRating}/10`;
if (ratingLabelEl) ratingLabelEl.textContent = getRatingLabel(existingRating);
const commentTextarea = document.getElementById('trackRatingComment');
if (commentTextarea && existingComment) {
commentTextarea.value = existingComment;
}
}
// Add star rating functionality
stars.forEach(star => {
star.addEventListener('click', function() {
const rating = parseInt(this.dataset.rating);
currentRating = rating;
if (modal) modal.dataset.currentRating = rating.toString();
stars.forEach((s, index) => {
s.className = index < rating ? 'fas fa-star' : 'far fa-star';
});
const ratingValueEl = document.querySelector('#trackRatingModal .rating-value');
const ratingLabelEl = document.querySelector('#trackRatingModal .rating-label');
if (ratingValueEl) ratingValueEl.textContent = `${rating}/10`;
if (ratingLabelEl) ratingLabelEl.textContent = getRatingLabel(rating);
});
star.addEventListener('mouseenter', function() {
const rating = parseInt(this.dataset.rating);
stars.forEach((s, index) => {
s.className = index < rating ? 'fas fa-star' : 'far fa-star';
});
});
star.addEventListener('mouseleave', function() {
stars.forEach((s, index) => {
s.className = index < currentRating ? 'fas fa-star' : 'far fa-star';
});
});
});
}, 100);
})
.catch(error => {
console.error('Error fetching user rating:', error);
});
}
function closeTrackRatingModal() {
const modal = document.getElementById('trackRatingModal');
if (modal) modal.remove();
}
function submitTrackRating(trackId) {
const modal = document.getElementById('trackRatingModal');
let rating = 0;
if (modal && modal.dataset.currentRating) {
rating = parseInt(modal.dataset.currentRating, 10);
}
if (!rating || rating < 1 || rating > 10) {
const selectedStars = modal ? modal.querySelectorAll('.stars-input i.fas').length : 0;
if (selectedStars > 0) rating = selectedStars;
}
const comment = document.getElementById('trackRatingComment')?.value || '';
if (!rating || rating < 1 || rating > 10) {
if (typeof showNotification === 'function') {
showNotification('Please select a rating between 1 and 10', 'warning');
}
return;
}
const formData = new FormData();
formData.append('action', 'submit_track_rating');
formData.append('track_id', trackId);
formData.append('rating', rating);
formData.append('comment', comment);
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showNotification === 'function') {
showNotification('Track rating submitted successfully!', 'success');
}
closeTrackRatingModal();
// Update the star display on the page
if (data.data) {
const trackCard = document.querySelector(`.track-card[data-track-id="${trackId}"]`);
if (trackCard) {
const starBtn = trackCard.querySelector('.stat-btn .fa-star');
if (starBtn) {
starBtn.style.color = '#fbbf24';
const countSpan = starBtn.parentElement.querySelector('span');
if (countSpan) {
countSpan.textContent = parseFloat(data.data.average_rating).toFixed(1);
} else if (data.data.average_rating) {
starBtn.parentElement.insertAdjacentHTML('beforeend', `<span>${parseFloat(data.data.average_rating).toFixed(1)}</span>`);
}
}
}
}
} else {
if (typeof showNotification === 'function') {
showNotification(data.message || 'Failed to submit rating', 'error');
}
}
})
.catch(error => {
console.error('Rating submission error:', error);
if (typeof showNotification === 'function') {
showNotification('Error submitting rating', 'error');
}
});
}
function shareTrack(trackId) {
// Find the track card to get title and artist
const trackCard = document.querySelector(`.track-card[data-track-id="${trackId}"]`);
const trackTitleElement = trackCard?.querySelector('.track-title');
const trackArtistElement = trackCard?.querySelector('.track-artist');
const trackTitle = trackTitleElement?.textContent?.trim() || 'Track';
const artistName = trackArtistElement?.textContent?.trim() || 'Artist';
const url = `${window.location.origin}/track.php?id=${trackId}`;
const shareText = `Check out "${trackTitle}" by ${artistName} on SoundStudioPro! đĩ`;
const shareData = {
title: `${trackTitle} by ${artistName}`,
text: shareText,
url: url
};
// Record the share in the database (don't wait for it)
const platform = 'web';
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: platform
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('đ Share recorded for track:', trackId, 'New count:', data.share_count);
// Update share count in UI - use the count from server
const shareButtons = document.querySelectorAll(`button[onclick*="shareTrack(${trackId})"]`);
shareButtons.forEach(button => {
const countSpan = button.querySelector('span');
if (countSpan) {
// Use the count from server if available, otherwise increment
if (data.share_count !== undefined) {
countSpan.textContent = parseInt(data.share_count).toLocaleString();
} else {
const currentCount = parseInt(countSpan.textContent.replace(/,/g, '')) || 0;
countSpan.textContent = (currentCount + 1).toLocaleString();
}
}
});
} else {
console.warn('đ Share recording failed:', data.message || 'Unknown error');
}
})
.catch(error => {
console.error('đ Share recording error:', error);
});
// Try native share API first (mobile devices, especially Android)
if (navigator.share) {
// Check if we can share this data (Android requirement)
if (navigator.canShare && navigator.canShare(shareData)) {
navigator.share(shareData)
.then(() => {
console.log('đ Track shared successfully via native API');
if (typeof showNotification === 'function') {
showNotification('â
Track shared successfully!', 'success');
}
})
.catch((error) => {
// User cancelled or error - fallback to clipboard
if (error.name !== 'AbortError') {
console.log('đ Share API failed, using clipboard:', error);
copyTrackUrlToClipboard(url, shareText);
}
});
} else if (!navigator.canShare) {
// canShare not available, try share anyway
navigator.share(shareData)
.then(() => {
console.log('đ Track shared successfully via Web Share API');
if (typeof showNotification === 'function') {
showNotification('â
Track shared successfully!', 'success');
}
})
.catch((error) => {
// User cancelled or error - fallback to clipboard
if (error.name !== 'AbortError') {
console.log('đ Share API failed, using clipboard:', error);
copyTrackUrlToClipboard(url, shareText);
}
});
} else {
// canShare returned false, use clipboard
console.log('đ Share not available, using clipboard');
copyTrackUrlToClipboard(url, shareText);
}
} else {
// Use clipboard for desktop
copyTrackUrlToClipboard(url, shareText);
}
}
// Copy track URL to clipboard
function copyTrackUrlToClipboard(url, shareText) {
const shareData = `${shareText}\n${url}`;
// Check if we're in a secure context (HTTPS or localhost)
const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
// Try modern clipboard API first (requires secure context)
if (isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareData)
.then(() => {
console.log('đ Clipboard copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!\n\n' + shareData);
}
})
.catch((error) => {
console.error('đ Clipboard API failed:', error);
// Fallback to execCommand method
fallbackCopyTrack(shareData);
});
} else {
// Not in secure context or clipboard API not available, use fallback
console.log('đ Clipboard API not available, using fallback');
fallbackCopyTrack(shareData);
}
}
// Fallback copy method for track sharing
function fallbackCopyTrack(shareData) {
console.log('đ Using fallback copy method for track');
// Use textarea for better mobile compatibility
const textarea = document.createElement('textarea');
textarea.value = shareData;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
textarea.style.opacity = '0';
textarea.setAttribute('readonly', '');
textarea.setAttribute('aria-hidden', 'true');
document.body.appendChild(textarea);
// For Android, we need to ensure the element is visible and focused
if (/Android/i.test(navigator.userAgent)) {
textarea.style.position = 'absolute';
textarea.style.left = '0';
textarea.style.top = '0';
textarea.style.width = '1px';
textarea.style.height = '1px';
textarea.style.opacity = '0.01';
}
// Select the text
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, shareData.length);
try {
const successful = document.execCommand('copy');
// Clean up
setTimeout(() => {
document.body.removeChild(textarea);
}, 100);
if (successful) {
console.log('đ Fallback copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!\n\n' + shareData);
}
} else {
console.log('đ Fallback copy failed, showing prompt');
// Show a more user-friendly prompt
const userCopied = confirm('Please copy this link:\n\n' + shareData + '\n\nClick OK if you copied it.');
if (userCopied && typeof showNotification === 'function') {
showNotification('đ Link copied!', 'success');
}
}
} catch (error) {
console.error('đ Fallback copy error:', error);
document.body.removeChild(textarea);
// Show a more user-friendly prompt
const userCopied = confirm('Please copy this link:\n\n' + shareData + '\n\nClick OK if you copied it.');
if (userCopied && typeof showNotification === 'function') {
showNotification('đ Link copied!', 'success');
}
}
}
// Share Community Feed
function shareCommunityFeed() {
console.log('Share Community Feed button clicked');
const urlToCopy = window.location.href;
console.log('URL to copy:', urlToCopy);
// Try native share API first (mobile devices)
if (navigator.share) {
navigator.share({
title: 'Community Feed - SoundStudioPro',
text: 'Check out the community feed on SoundStudioPro!',
url: urlToCopy
})
.then(() => {
console.log('Share successful');
if (typeof showNotification === 'function') {
showNotification('â
Shared successfully!', 'success');
}
})
.catch((error) => {
// User cancelled or error - fallback to clipboard
if (error.name !== 'AbortError') {
console.log('Share API failed, using clipboard:', error);
copyCommunityUrlToClipboard(urlToCopy);
}
});
} else {
// Use clipboard for desktop
copyCommunityUrlToClipboard(urlToCopy);
}
}
// Copy community URL to clipboard
function copyCommunityUrlToClipboard(urlToCopy) {
// Try modern clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(urlToCopy)
.then(() => {
console.log('Clipboard copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Community Feed link copied to clipboard!', 'success');
} else {
alert('Link copied to clipboard: ' + urlToCopy);
}
})
.catch((error) => {
console.error('Clipboard API failed:', error);
fallbackShareCommunity(urlToCopy);
});
} else {
fallbackShareCommunity(urlToCopy);
}
}
// Fallback share method for community feed
function fallbackShareCommunity(urlToCopy) {
console.log('Using fallback share method');
const input = document.createElement('input');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.style.top = '-9999px';
input.value = urlToCopy;
document.body.appendChild(input);
input.focus();
input.select();
input.setSelectionRange(0, 99999); // For mobile devices
try {
const successful = document.execCommand('copy');
document.body.removeChild(input);
if (successful) {
console.log('Fallback copy successful');
if (typeof showNotification === 'function') {
showNotification('đ Community Feed link copied to clipboard!', 'success');
} else {
alert('Link copied to clipboard: ' + urlToCopy);
}
} else {
console.log('Fallback copy failed, showing prompt');
prompt('Copy this link:', urlToCopy);
}
} catch (error) {
console.error('Fallback copy error:', error);
document.body.removeChild(input);
prompt('Copy this link:', urlToCopy);
}
}
function showVariations(trackId, button) {
const container = document.getElementById(`variations-${trackId}`);
if (!container) return;
const isVisible = container.style.display !== 'none';
container.style.display = isVisible ? 'none' : 'block';
if (button) {
button.classList.toggle('active');
}
}
function addToCart(trackId, title, price, button) {
console.log('đ Adding to cart:', { trackId, title, price, button });
// Store original button HTML and disable button to prevent double-clicks
let originalHTML = null;
if (button) {
originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <span>Adding...</span>';
}
// Use FormData to match cart.php expectations
const formData = new FormData();
formData.append('track_id', trackId);
formData.append('action', 'add');
formData.append('artist_plan', 'free'); // Default to free plan
fetch('/cart.php', {
method: 'POST',
body: formData
})
.then(response => {
console.log('đ Response status:', response.status, response.statusText);
console.log('đ Response headers:', response.headers.get('content-type'));
// Check if response is OK
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check if response is JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return response.text().then(text => {
console.error('đ Non-JSON response:', text);
throw new Error('Server returned non-JSON response');
});
}
return response.json();
})
.then(data => {
console.log('đ Cart response data:', data);
if (data.success) {
// Visual feedback
if (button) {
button.classList.add('added');
button.innerHTML = '<i class="fas fa-check"></i> <span>Added!</span>';
setTimeout(() => {
button.classList.remove('added');
if (originalHTML) {
button.innerHTML = originalHTML;
}
button.disabled = false;
}, 2000);
}
// Show notification if available
if (typeof showNotification === 'function') {
showNotification('đ "' + title + '" added to cart!', 'success');
}
// Update cart counter if it exists
const cartCounts = document.querySelectorAll('.cart-count, .cart-counter');
if (cartCounts.length > 0 && data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
if (data.cart_count > 0) {
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
} else {
count.style.display = 'none';
}
});
} else if (cartCounts.length > 0) {
// Fallback: manually increment if API didn't return count
cartCounts.forEach(count => {
const currentCount = parseInt(count.textContent) || 0;
count.textContent = currentCount + 1;
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
});
}
// Refresh cart modal if it's currently open
const cartModal = document.getElementById('cartModal');
if (cartModal && cartModal.style.display === 'flex') {
if (typeof refreshCartModal === 'function') {
refreshCartModal();
}
}
} else {
// Re-enable button on error
if (button && originalHTML) {
button.disabled = false;
button.innerHTML = originalHTML;
}
// Special handling for "already in cart" - show as warning, not error
const errorMsg = data.message || data.error || 'Failed to add to cart. Please try again.';
if (data.already_in_cart) {
if (typeof showNotification === 'function') {
showNotification('âšī¸ ' + errorMsg, 'warning');
} else {
alert(errorMsg);
}
} else {
if (typeof showNotification === 'function') {
showNotification('â Error: ' + errorMsg, 'error');
} else {
alert('Error: ' + errorMsg);
}
}
}
})
.catch(error => {
console.error('â Cart error:', error);
// Re-enable button on error
if (button && originalHTML) {
button.disabled = false;
button.innerHTML = originalHTML;
}
const errorMsg = 'An error occurred while adding to cart. Please try again.';
if (typeof showNotification === 'function') {
showNotification(errorMsg, 'error');
} else {
alert(errorMsg);
}
});
}
function downloadTrack(trackId) {
window.location.href = `/api/download_track.php?id=${trackId}`;
}
// Store our playTrack function before footer overwrites it
const communityFixedPlayTrack = window.playTrack;
// Ensure playTrack function works after AJAX page loads and restore it after footer loads
document.addEventListener('DOMContentLoaded', function() {
console.log('đĩ Community Fixed page loaded - playTrack function ready');
console.log('đĩ Checking global player availability...');
console.log('window.waitForGlobalPlayer:', typeof window.waitForGlobalPlayer);
console.log('window.enhancedGlobalPlayer:', typeof window.enhancedGlobalPlayer);
console.log('window.globalPlayerReady:', window.globalPlayerReady);
// Restore our function after footer loads (footer loads at end of body)
setTimeout(function() {
if (communityFixedPlayTrack) {
window.playTrack = communityFixedPlayTrack;
console.log('đĩ Restored community_fixed playTrack function after footer load');
}
// Verify global player is ready
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
console.log('â
Global player confirmed ready on page load');
});
} else {
console.error('â waitForGlobalPlayer still not available after footer load');
}
}, 200);
});
// Listen for AJAX page loads
document.addEventListener('ajaxPageLoaded', function() {
console.log('đĩ Community Fixed page loaded via AJAX - playTrack function ready');
// Ensure global player is available
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
console.log('đĩ Global player ready after AJAX load');
});
}
});
</script>
<!-- Infinite Scroll Implementation -->
<script>
(function() {
// Translations for JavaScript
window.wishlistTranslations = {
add_to_wishlist: '<?= addslashes(t('artist_profile.add_to_wishlist')) ?>',
remove_from_wishlist: '<?= addslashes(t('artist_profile.remove_from_wishlist')) ?>'
};
const translations = {
for_sale: '<?= addslashes(t('community.for_sale')) ?>',
available_for_purchase: '<?= addslashes(t('community.available_for_purchase')) ?>',
unknown_artist: '<?= addslashes(t('artists.unknown_artist')) ?>',
untitled_track: '<?= addslashes(t('community.untitled_track')) ?>',
plays: '<?= addslashes(t('preview.plays')) ?>',
likes: '<?= addslashes(t('library.card.like')) ?>',
comments: '<?= addslashes(t('artist_profile.comments')) ?>',
shares: '<?= addslashes(t('library.card.share')) ?>',
rate_track: '<?= addslashes(t('artist_profile.rate_track')) ?>',
add_to_crate: '<?= addslashes(t('library.crates.add_to_crate')) ?>',
variations: '<?= addslashes(t('community.variations')) ?>',
add_to_cart: '<?= addslashes(t('community.add_to_cart')) ?>',
free: '<?= addslashes(t('common.free')) ?>',
error_loading_more: '<?= addslashes(t('community.error_loading_more')) ?>',
login_to_vote: '<?= addslashes(t('artist_profile.login_to_vote')) ?>',
error_voting: '<?= addslashes(t('artist_profile.error_voting')) ?>',
error_voting_retry: '<?= addslashes(t('artist_profile.error_voting_retry')) ?>',
add_to_wishlist: '<?= addslashes(t('artist_profile.add_to_wishlist')) ?>',
remove_from_wishlist: '<?= addslashes(t('artist_profile.remove_from_wishlist')) ?>',
follow_artist: '<?= addslashes(t('community.follow_artist')) ?>',
unfollow_artist: '<?= addslashes(t('community.unfollow_artist')) ?>',
login_required_follow: '<?= addslashes(t('notification.please_log_in_follow')) ?>'
};
let isLoading = false;
let currentPage = 1;
let hasMore = true;
const currentUserId = <?= $user_id ?? 0 ?>;
const tracksGrid = document.getElementById('tracksGrid');
const loader = document.getElementById('infiniteScrollLoader');
const endOfResults = document.getElementById('endOfResults');
if (!tracksGrid) return;
// Get initial state from data attributes
currentPage = parseInt(tracksGrid.getAttribute('data-current-page')) || 1;
hasMore = tracksGrid.getAttribute('data-has-more') === 'true';
// Build community playlist from all visible tracks on the page
function buildCommunityPlaylist() {
const playlist = [];
const grid = document.getElementById('tracksGrid');
if (!grid) {
console.warn('đĩ tracksGrid not found');
return playlist;
}
const trackCards = grid.querySelectorAll('.track-card[data-track-id]');
trackCards.forEach(card => {
const trackId = parseInt(card.getAttribute('data-track-id'));
const playBtn = card.querySelector('.play-btn');
if (playBtn) {
const audioUrl = playBtn.getAttribute('data-audio-url');
const title = playBtn.getAttribute('data-title');
const artist = playBtn.getAttribute('data-artist');
const artistId = playBtn.getAttribute('data-artist-id');
// Only add if we have valid data
if (audioUrl && audioUrl !== 'null' && audioUrl !== '' && title) {
// Convert relative URL to absolute if needed
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
}
playlist.push({
id: trackId,
audio_url: finalAudioUrl,
title: title,
artist_name: artist || translations.unknown_artist,
artist_id: artistId ? parseInt(artistId) : null,
user_id: currentUserId || 0
});
}
}
});
console.log('đĩ Built community playlist:', playlist.length, 'tracks');
return playlist;
}
// Make function globally accessible
window.buildCommunityPlaylist = buildCommunityPlaylist;
// Get current filter values
function getFilterParams() {
return {
sort: document.getElementById('sortSelect')?.value || '<?= $sort ?>',
time: document.getElementById('timeSelect')?.value || '<?= $time_filter ?>',
genre: document.getElementById('genreSelect')?.value || '<?= $genre ?>',
search: document.getElementById('searchInput')?.value.trim() || '<?= htmlspecialchars($search, ENT_QUOTES) ?>'
};
}
// Load more tracks
async function loadMoreTracks() {
if (isLoading || !hasMore) return;
isLoading = true;
loader.style.display = 'block';
const params = getFilterParams();
const queryParams = new URLSearchParams({
page: currentPage + 1,
per_page: 24,
sort: params.sort,
time: params.time,
genre: params.genre,
...(params.search ? { search: params.search } : {})
});
try {
const response = await fetch(`/api/get_community_fixed_tracks.php?${queryParams}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
console.error('API returned error:', data);
throw new Error(data.error || data.message || 'Failed to load tracks');
}
if (data.success && data.tracks && data.tracks.length > 0) {
// Render new tracks
const fragment = document.createDocumentFragment();
data.tracks.forEach(track => {
const trackCard = createTrackCard(track);
fragment.appendChild(trackCard);
});
tracksGrid.appendChild(fragment);
// Update state
currentPage = data.pagination.page;
hasMore = data.pagination.has_more;
tracksGrid.setAttribute('data-current-page', currentPage);
tracksGrid.setAttribute('data-has-more', hasMore ? 'true' : 'false');
// Re-attach play button listeners for new tracks
attachPlayButtonListeners();
// Update playlist if one is currently active (so new tracks are included)
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer && window._communityPlaylistType === 'community_fixed') {
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const updatedPlaylist = buildFn ? buildFn() : [];
if (updatedPlaylist.length > 0) {
const currentIndex = window._communityTrackIndex || 0;
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(updatedPlaylist, 'community_fixed', currentIndex);
console.log('đĩ Updated playlist with new tracks, total:', updatedPlaylist.length);
} else {
window._communityPlaylist = updatedPlaylist;
console.log('đĩ Updated window playlist with new tracks, total:', updatedPlaylist.length);
}
}
}
});
}
// Show end message if no more tracks
if (!hasMore) {
endOfResults.style.display = 'block';
}
} else {
hasMore = false;
endOfResults.style.display = 'block';
}
} catch (error) {
console.error('Error loading more tracks:', error);
console.error('Error details:', {
message: error.message,
stack: error.stack,
response: error.response
});
// Try to get more details from response if available
if (error.response) {
error.response.json().then(data => {
console.error('API Error Response:', data);
}).catch(() => {});
}
if (typeof showNotification === 'function') {
showNotification(translations.error_loading_more, 'error');
} else {
alert(translations.error_loading_more);
}
} finally {
isLoading = false;
loader.style.display = 'none';
}
}
// Create track card HTML (simplified version matching the PHP template)
function createTrackCard(track) {
const card = document.createElement('div');
card.className = 'track-card';
card.setAttribute('data-track-id', track.id);
card.setAttribute('itemscope', '');
card.setAttribute('itemtype', 'http://schema.org/MusicRecording');
const displayTitle = track.title || translations.untitled_track;
const duration = track.duration ? formatDuration(track.duration) : '0:00';
// Use resolved_image_url from API (which handles all fallbacks) or fallback to default
const imageUrl = track.resolved_image_url || track.image_url || '/assets/images/default-track.jpg';
const audioUrl = track.signed_audio_url || '';
const isWishlisted = track.is_in_wishlist > 0;
const displayPrice = track.price && track.price > 0 ? track.price : 1.99;
// Extract genre from metadata
let genre = 'Electronic';
let mood = null;
if (track.metadata) {
const metadata = typeof track.metadata === 'string' ? JSON.parse(track.metadata) : track.metadata;
if (metadata.genre) genre = metadata.genre;
else if (metadata.style) genre = metadata.style;
if (metadata.mood) mood = metadata.mood;
}
if ((genre === 'Electronic' || !genre) && track.genre) {
genre = track.genre;
}
const createdDate = track.created_at ? new Date(track.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '';
card.innerHTML = `
<div class="track-image-wrapper">
<img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(displayTitle)}" class="track-image" loading="lazy">
<div class="track-overlay">
<button class="play-btn"
data-track-id="${track.id}"
data-artist-id="${track.artist_id || ''}"
data-audio-url="${escapeHtml(audioUrl)}"
data-title="${escapeHtml(displayTitle)}"
data-artist="${escapeHtml(track.artist_name || '')}">
<i class="fas fa-play"></i>
</button>
<div class="track-duration">${duration}</div>
</div>
</div>
<div class="track-info">
<div class="track-title-row">
<a href="/track.php?id=${track.id}" class="track-title" title="${escapeHtml(displayTitle)}" target="_blank" itemprop="name">${escapeHtml(displayTitle)}</a>
${track.price > 0 ? `<span class="for-sale-badge" title="${translations.available_for_purchase}"><i class="fas fa-tag"></i><span>${translations.for_sale}</span></span>` : ''}
</div>
<a href="/artist_profile.php?id=${track.artist_id}" class="track-artist" target="_blank" itemprop="byArtist" itemscope itemtype="http://schema.org/MusicGroup">
<span itemprop="name">${escapeHtml(track.artist_name || translations.unknown_artist)}</span>
</a>
<div class="track-genre">
<a href="?genre=${encodeURIComponent(genre)}" class="genre-tag-link">${escapeHtml(genre)}</a>
${mood && mood !== genre && mood.toLowerCase() !== 'neutral' ? `<a href="?genre=${encodeURIComponent(mood)}" class="genre-tag-link">${escapeHtml(mood)}</a>` : ''}
</div>
${createdDate ? `<div class="track-date"><i class="fas fa-calendar-alt"></i><span>${createdDate}</span></div>` : ''}
</div>
<div class="track-stats">
<button class="stat-btn" title="${translations.plays}">
<i class="fas fa-headphones-alt"></i>
<span>${formatNumber(track.play_count || 0)}</span>
</button>
<button class="stat-btn like-btn ${track.user_liked > 0 ? 'liked' : ''}" onclick="toggleLike(${track.id}, this)" title="${translations.likes}">
<i class="fas fa-heart"></i>
<span>${formatNumber(track.like_count || 0)}</span>
</button>
<button class="stat-btn" onclick="showComments(${track.id})" title="${translations.comments}">
<i class="fas fa-comment"></i>
<span>${formatNumber(track.comment_count || 0)}</span>
</button>
<button class="stat-btn" onclick="shareTrack(${track.id})" title="${translations.shares}">
<i class="fas fa-share"></i>
<span>${formatNumber(track.share_count || 0)}</span>
</button>
${currentUserId ? `
<button class="stat-btn add-to-crate-btn" onclick="openAddToCrateModal(${track.id}, '${escapeHtml(displayTitle).replace(/'/g, "\\'")}')" title="${translations.add_to_crate}">
<i class="fas fa-box"></i>
</button>
` : ''}
<button class="stat-btn" onclick="showTrackRatingModal(${track.id}, '${escapeHtml(displayTitle).replace(/'/g, "\\'")}')" title="${translations.rate_track}">
<i class="fas fa-star" style="color: ${(track.rating_count || 0) > 0 ? '#fbbf24' : 'inherit'};"></i>
${(track.rating_count || 0) > 0 ? `<span>${parseFloat(track.average_rating || 0).toFixed(1)}</span>` : ''}
</button>
${track.variations && track.variations.length > 0 ? `
<button class="stat-btn variations-btn" onclick="showVariations(${track.id}, this)" title="${track.variations.length} ${translations.variations}">
<i class="fas fa-layer-group"></i>
<span>${track.variations.length}</span>
</button>
` : ''}
${track.user_id && track.user_id != currentUserId ? `
<button class="stat-btn follow-btn ${track.is_following > 0 ? 'following' : ''}"
onclick="toggleFollow(${track.user_id}, this)"
title="${track.is_following > 0 ? translations.unfollow_artist : translations.follow_artist}">
<i class="fas fa-${track.is_following > 0 ? 'user-check' : 'user-plus'}"></i>
</button>
` : ''}
</div>
<div class="track-cart-section">
<button class="add-to-cart-btn" type="button" data-track-id="${track.id}" data-track-title="${escapeHtml(displayTitle)}" data-track-price="${displayPrice}" onclick="addToCart(${track.id}, '${escapeHtml(displayTitle)}', ${displayPrice}, this)">
<i class="fas fa-shopping-cart"></i>
<span>${translations.add_to_cart}</span>
<span class="cart-price">${displayPrice > 0 ? '$' + displayPrice.toFixed(2) : translations.free}</span>
</button>
<button class="wishlist-inline-btn ${isWishlisted ? 'active' : ''}" onclick="toggleWishlist(${track.id}, this)" aria-pressed="${isWishlisted}" title="${isWishlisted ? translations.remove_from_wishlist : translations.add_to_wishlist}">
<i class="${isWishlisted ? 'fas' : 'far'} fa-heart"></i>
</button>
</div>
`;
return card;
}
// Expose createTrackCard to window for use by updateFilters
window.createTrackCard = createTrackCard;
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDuration(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function formatNumber(num) {
return parseInt(num).toLocaleString();
}
// Attach play button listeners (matches original behavior with pause support)
function attachPlayButtonListeners() {
const playButtons = tracksGrid.querySelectorAll('.play-btn:not([data-listener-attached])');
playButtons.forEach(button => {
button.setAttribute('data-listener-attached', 'true');
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const trackId = this.getAttribute('data-track-id');
const artistId = this.getAttribute('data-artist-id');
const audioUrl = this.getAttribute('data-audio-url');
const title = this.getAttribute('data-title');
const artist = this.getAttribute('data-artist');
const isCurrentlyPlaying = this.classList.contains('playing');
console.log('đĩ Infinite scroll play button clicked:', { trackId, artistId, audioUrl, title, artist, isCurrentlyPlaying });
// Validate audio URL
if (!audioUrl || audioUrl === 'null' || audioUrl === 'NULL' || audioUrl === '') {
console.error('â Invalid audio URL:', audioUrl);
alert('This track is not available for playback.');
return;
}
// If button is already in playing state, pause instead
if (isCurrentlyPlaying) {
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer) {
// Check if global player has togglePlayPause or pause method
if (typeof window.enhancedGlobalPlayer.togglePlayPause === 'function') {
console.log('đĩ Pausing via togglePlayPause');
window.enhancedGlobalPlayer.togglePlayPause();
} else if (typeof window.enhancedGlobalPlayer.pause === 'function') {
console.log('đĩ Pausing via pause method');
window.enhancedGlobalPlayer.pause();
}
// Update button state
button.classList.remove('playing');
const icon = button.querySelector('i');
if (icon) icon.className = 'fas fa-play';
// Clear other playing states
document.querySelectorAll('.play-btn').forEach(btn => {
if (btn !== button) {
btn.classList.remove('playing');
const btnIcon = btn.querySelector('i');
if (btnIcon) btnIcon.className = 'fas fa-play';
}
});
}
});
}
return;
}
// Clear other playing states
document.querySelectorAll('.play-btn').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
});
// Set this button as playing
this.classList.add('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
// Play track using global player
// Ensure audio URL is absolute if it's relative
// Signed URLs are already in format /utils/play_audio.php?id=X&variation=Y&token=Z&expires=W
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
// Preserve query parameters for signed URLs
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('đĩ Converted relative URL to absolute:', finalAudioUrl);
} else if (audioUrl && audioUrl.startsWith('//')) {
// Handle protocol-relative URLs
finalAudioUrl = window.location.protocol + audioUrl;
console.log('đĩ Converted protocol-relative URL:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('â Audio URL is empty!', { audioUrl, finalAudioUrl });
alert('Audio file not available.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
return;
}
// Log the final URL for debugging
console.log('đĩ Final audio URL (infinite scroll):', finalAudioUrl);
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
try {
// Build and load playlist before playing
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const communityPlaylist = buildFn ? buildFn() : [];
if (communityPlaylist.length > 0) {
// Find the index of the clicked track
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
// Load playlist into global player
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
console.log('đĩ Community: Playlist loaded via loadPagePlaylist, index:', startIndex, 'of', communityPlaylist.length);
} else {
// Fallback to window storage
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
console.log('đĩ Community: Playlist stored on window, index:', startIndex, 'of', communityPlaylist.length);
}
}
console.log('đĩ Playing track:', { finalAudioUrl, title, artist, trackId, artistId });
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId, artistId);
if (success) {
console.log('â
Track playing successfully');
if (trackId) recordTrackPlay(trackId);
} else {
console.error('â playTrack returned false');
alert('Failed to play track.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} catch (error) {
console.error('â Error:', error);
alert('Error: ' + error.message);
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} else {
console.error('â Global player not available');
alert('Player not ready. Please refresh.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
});
} else {
// Fallback
setTimeout(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
// Build and load playlist
const communityPlaylist = buildCommunityPlaylist();
if (communityPlaylist.length > 0) {
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
} else {
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
}
}
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId, artistId);
if (trackId) recordTrackPlay(trackId);
} else {
alert('Audio player not ready. Please refresh the page.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
}, 500);
}
});
});
}
// Expose attachPlayButtonListeners to window for use by updateFilters
window.attachPlayButtonListeners = attachPlayButtonListeners;
// Scroll detection
function handleScroll() {
if (isLoading || !hasMore) return;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// Load more when user is 300px from bottom
if (scrollTop + windowHeight >= documentHeight - 300) {
loadMoreTracks();
}
}
// Throttle scroll events
let scrollTimeout;
window.addEventListener('scroll', function() {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(handleScroll, 100);
});
// Listen for filter updates to sync infinite scroll state
document.addEventListener('filtersUpdated', function(e) {
if (e.detail) {
currentPage = e.detail.currentPage || 1;
hasMore = e.detail.hasMore !== undefined ? e.detail.hasMore : true;
isLoading = false;
if (tracksGrid) {
tracksGrid.setAttribute('data-current-page', currentPage.toString());
tracksGrid.setAttribute('data-has-more', hasMore ? 'true' : 'false');
}
if (loader) loader.style.display = 'none';
if (endOfResults) {
endOfResults.style.display = hasMore ? 'none' : 'block';
}
console.log('đĩ Infinite scroll state updated after filter change:', { currentPage, hasMore });
}
});
// Reset on filter change - wrap updateFilters to reset infinite scroll state
const originalUpdateFilters = window.updateFilters;
window.updateFilters = function() {
// Reset infinite scroll state before calling the AJAX update
currentPage = 1;
hasMore = true;
isLoading = false;
if (tracksGrid) {
tracksGrid.setAttribute('data-current-page', '1');
tracksGrid.setAttribute('data-has-more', 'true');
}
if (loader) loader.style.display = 'none';
if (endOfResults) endOfResults.style.display = 'none';
// Call original function (which now uses AJAX)
if (originalUpdateFilters) {
originalUpdateFilters();
}
};
})();
</script>
<?php if ($user_id): ?>
<!-- Add to Crate Modal -->
<div id="addToCrateModal" class="modal-overlay" style="display: none;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3><i class="fas fa-box"></i> <?= t('library.crates.add_to_crate') ?></h3>
<button class="modal-close" onclick="closeAddToCrateModal()">×</button>
</div>
<div class="modal-body">
<p id="addToCrateTrackTitle" style="color: #a5b4fc; margin-bottom: 1rem;"></p>
<div id="cratesList" style="max-height: 300px; overflow-y: auto;">
<div style="text-align: center; padding: 2rem;">
<i class="fas fa-spinner fa-spin"></i> <?= t('library.crates.loading') ?>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeAddToCrateModal()"><?= t('common.cancel') ?></button>
<a href="/library.php?tab=crates" class="btn btn-primary" style="text-decoration: none;">
<i class="fas fa-plus"></i> <?= t('library.crates.create_new') ?>
</a>
</div>
</div>
</div>
<style>
#addToCrateModal.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
#addToCrateModal .modal-content {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 16px;
padding: 0;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
#addToCrateModal .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
#addToCrateModal .modal-header h3 { margin: 0; color: white; font-size: 1.25rem; }
#addToCrateModal .modal-close {
background: none;
border: none;
color: #9ca3af;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
line-height: 1;
}
#addToCrateModal .modal-close:hover { color: white; }
#addToCrateModal .modal-body { padding: 1.5rem; }
#addToCrateModal .modal-footer {
display: flex;
justify-content: flex-end;
gap: 1rem;
padding: 1rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
#addToCrateModal .crate-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: rgba(255,255,255,0.05);
border-radius: 8px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
}
#addToCrateModal .crate-option:hover { background: rgba(102, 126, 234, 0.2); }
#addToCrateModal .crate-option .crate-info { display: flex; align-items: center; gap: 0.75rem; }
#addToCrateModal .crate-option .crate-icon { font-size: 1.5rem; }
#addToCrateModal .crate-option .crate-name { font-weight: 600; color: white; }
#addToCrateModal .crate-option .crate-count { font-size: 0.85rem; color: #9ca3af; }
#addToCrateModal .crate-option.adding { opacity: 0.6; pointer-events: none; }
.add-to-crate-btn { color: #a5b4fc !important; }
.add-to-crate-btn:hover { color: #667eea !important; }
</style>
<script>
let currentTrackIdForCrate = null;
let currentTrackTitleForCrate = null;
function openAddToCrateModal(trackId, trackTitle) {
currentTrackIdForCrate = trackId;
currentTrackTitleForCrate = trackTitle;
const modal = document.getElementById('addToCrateModal');
const titleEl = document.getElementById('addToCrateTrackTitle');
if (!modal) return;
titleEl.textContent = '"' + trackTitle + '"';
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
loadUserCratesForModal();
}
function closeAddToCrateModal() {
const modal = document.getElementById('addToCrateModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
}
currentTrackIdForCrate = null;
}
function loadUserCratesForModal() {
const cratesList = document.getElementById('cratesList');
cratesList.innerHTML = '<div style="text-align: center; padding: 2rem;"><i class="fas fa-spinner fa-spin"></i> <?= t('library.crates.loading') ?></div>';
fetch('/api/get_user_crates.php')
.then(response => response.json())
.then(data => {
if (data.success && data.crates && data.crates.length > 0) {
cratesList.innerHTML = data.crates.map(crate => `
<div class="crate-option" onclick="addTrackToCrateFromModal(${crate.id}, '${(crate.name || '').replace(/'/g, "\\'")}')">
<div class="crate-info">
<span class="crate-icon">đĻ</span>
<div>
<div class="crate-name">${escapeHtml(crate.name)}</div>
<div class="crate-count">${crate.track_count || 0} <?= t('library.crates.tracks') ?></div>
</div>
</div>
<i class="fas fa-plus" style="color: #667eea;"></i>
</div>
`).join('');
} else {
cratesList.innerHTML = '<div style="text-align: center; padding: 2rem; color: #9ca3af;"><?= t('library.crates.no_crates') ?></div>';
}
})
.catch(error => {
console.error('Error loading crates:', error);
cratesList.innerHTML = '<div style="text-align: center; padding: 2rem; color: #ef4444;">Error loading crates</div>';
});
}
function addTrackToCrateFromModal(crateId, crateName) {
const crateOption = event.currentTarget;
crateOption.classList.add('adding');
crateOption.innerHTML = '<div class="crate-info"><i class="fas fa-spinner fa-spin"></i> Adding...</div>';
fetch('/api/add_track_to_crate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ crate_id: crateId, track_id: currentTrackIdForCrate })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showNotification === 'function') {
showNotification('<?= t('library.crates.track_added_to') ?>'.replace(':crate', crateName), 'success');
}
closeAddToCrateModal();
} else {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to add track', 'error');
}
loadUserCratesForModal();
}
})
.catch(error => {
console.error('Error:', error);
loadUserCratesForModal();
});
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAddToCrateModal(); });
document.getElementById('addToCrateModal')?.addEventListener('click', function(e) { if (e.target === this) closeAddToCrateModal(); });
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>
<!-- Play button event listeners - like other working pages -->
<script>
// Setup play button event listeners
document.addEventListener('DOMContentLoaded', function() {
console.log('đĩ Setting up play button listeners');
// Main track play buttons
const playButtons = document.querySelectorAll('.play-btn, .variation-play-btn');
console.log('đĩ Found', playButtons.length, 'play buttons');
playButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const trackId = this.getAttribute('data-track-id');
const artistId = this.getAttribute('data-artist-id');
const audioUrl = this.getAttribute('data-audio-url');
const title = this.getAttribute('data-title');
const artist = this.getAttribute('data-artist');
const isCurrentlyPlaying = this.classList.contains('playing');
console.log('đĩ Play button clicked:', { trackId, artistId, audioUrl, title, artist, isCurrentlyPlaying });
// Validate
if (!audioUrl || audioUrl === 'null' || audioUrl === 'NULL' || audioUrl === '') {
console.error('â Invalid audio URL:', audioUrl);
alert('This track is not available for playback.');
return;
}
// If button is already in playing state, pause instead
if (isCurrentlyPlaying) {
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer) {
// Check if global player has togglePlayPause or pause method
if (typeof window.enhancedGlobalPlayer.togglePlayPause === 'function') {
console.log('đĩ Pausing via togglePlayPause');
window.enhancedGlobalPlayer.togglePlayPause();
} else if (typeof window.enhancedGlobalPlayer.pause === 'function') {
console.log('đĩ Pausing via pause method');
window.enhancedGlobalPlayer.pause();
}
// Update button state
button.classList.remove('playing');
const icon = button.querySelector('i');
if (icon) icon.className = 'fas fa-play';
// Clear other playing states
playButtons.forEach(btn => {
if (btn !== button) {
btn.classList.remove('playing');
const btnIcon = btn.querySelector('i');
if (btnIcon) btnIcon.className = 'fas fa-play';
}
});
}
});
}
return;
}
// Clear other playing states
playButtons.forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) icon.className = 'fas fa-play';
});
// Set this button as playing
this.classList.add('playing');
const icon = this.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
// Play track using global player
// Ensure audio URL is absolute if it's relative
// Signed URLs are already in format /utils/play_audio.php?id=X&variation=Y&token=Z&expires=W
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
// Preserve query parameters for signed URLs
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('đĩ Converted relative URL to absolute:', finalAudioUrl);
} else if (audioUrl && audioUrl.startsWith('//')) {
// Handle protocol-relative URLs
finalAudioUrl = window.location.protocol + audioUrl;
console.log('đĩ Converted protocol-relative URL:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('â Audio URL is empty!', { audioUrl, finalAudioUrl });
alert('Audio file not available.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
return;
}
// Log the final URL for debugging
console.log('đĩ Final audio URL:', finalAudioUrl);
if (window.waitForGlobalPlayer) {
window.waitForGlobalPlayer(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
try {
// Build and load playlist before playing
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const communityPlaylist = buildFn ? buildFn() : [];
if (communityPlaylist.length > 0) {
// Find the index of the clicked track
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
// Load playlist into global player
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
console.log('đĩ Community: Playlist loaded via loadPagePlaylist, index:', startIndex, 'of', communityPlaylist.length);
} else {
// Fallback to window storage
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
console.log('đĩ Community: Playlist stored on window, index:', startIndex, 'of', communityPlaylist.length);
}
}
console.log('đĩ Playing track:', { finalAudioUrl, title, artist, trackId, artistId });
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId, artistId);
if (success) {
console.log('â
Track playing successfully');
if (trackId) recordTrackPlay(trackId);
} else {
console.error('â playTrack returned false');
alert('Failed to play track.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} catch (error) {
console.error('â Error:', error);
alert('Error: ' + error.message);
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
} else {
console.error('â Global player not available');
alert('Player not ready. Please refresh.');
// Reset button
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
});
} else {
// Fallback
setTimeout(function() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
// Build and load playlist
const buildFn = window.buildCommunityPlaylist || buildCommunityPlaylist;
const communityPlaylist = buildFn();
if (communityPlaylist.length > 0) {
const clickedTrackId = parseInt(trackId);
const trackIndex = communityPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(communityPlaylist, 'community_fixed', startIndex);
} else {
window._communityPlaylist = communityPlaylist;
window._communityPlaylistType = 'community_fixed';
window._communityTrackIndex = startIndex;
}
}
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId, artistId);
if (trackId) recordTrackPlay(trackId);
} else {
alert('Audio player not ready. Please refresh the page.');
button.classList.remove('playing');
if (icon) icon.className = 'fas fa-play';
}
}, 500);
}
});
});
console.log('â
Play button listeners setup complete');
});
</script>